Each log line and each node goes to the operating system with one write
and no buffer before the call returns, so a crash of the process loses
nothing. view.json stays atomic through a temporary file and a rename.
An fsync on each write cost about 96 ms per message on a typical disk,
which made a long replay take hours. Durability against a power loss is
a later group commit, with one background sync that makes all earlier
writes durable.
Issue:
beb9a70
-// Write appends line to the file of day and flushes it to the disk.
+// Write appends line to the file of day with one write to the operating system,
+// without a buffer. A crash of the process then loses nothing.
func (j *journal) Write(day string, line []byte) error {
if day != j.day {
err := j.Close()
func (j *journal) Write(day string, line []byte) error {
if day != j.day {
err := j.Close()
return fmt.Errorf("write line: %w", err)
}
return fmt.Errorf("write line: %w", err)
}
- err = j.file.Sync()
- if err != nil {
- return fmt.Errorf("flush line: %w", err)
- }
-
// Log is the append-only message log of one chat. It writes one JSON line for
// each message to main/YYYY-MM-DD.jsonl, named for the local day of the write.
// Log is the append-only message log of one chat. It writes one JSON line for
// each message to main/YYYY-MM-DD.jsonl, named for the local day of the write.
-// It never edits or deletes a line. Each write goes to the file and to the disk
-// before Append returns. An exclusive lock on the lock file makes sure that
-// only one process writes the log. After a failed write, the end of the file is
-// not known, so the log refuses all writes until it is opened again. The caller
-// serializes all calls.
+// It never edits or deletes a line. Each line goes to the operating system
+// before Append returns, so a crash of the process loses no message. An
+// exclusive lock on the lock file makes sure that only one process writes the
+// log. After a failed write, the end of the file is not known, so the log
+// refuses all writes until it is opened again. The caller serializes all calls.
type Log struct {
clock func() time.Time
lock *os.File
type Log struct {
clock func() time.Time
lock *os.File
return m.cview.Context(m.tree, end)
}
return m.cview.Context(m.tree, end)
}
-// writeFileAtomic replaces the file at path with data. A reader or a crash sees
-// the old file or the new file, never a part of one.
+// writeFileAtomic replaces the file at path with data through a temporary file
+// and a rename. A reader or a crash of the process sees the old file or the new
+// file, never a part of one.
func writeFileAtomic(path string, data []byte) error {
func writeFileAtomic(path string, data []byte) error {
- dir := filepath.Dir(path)
- file, err := os.CreateTemp(dir, "."+filepath.Base(path)+".*")
+ file, err := os.CreateTemp(filepath.Dir(path), "."+filepath.Base(path)+".*")
if err != nil {
return fmt.Errorf("create temporary file: %w", err)
}
_, err = file.Write(data)
if err != nil {
return fmt.Errorf("create temporary file: %w", err)
}
_, err = file.Write(data)
- if err == nil {
- err = file.Sync()
- }
-
err = errors.Join(err, file.Close())
if err == nil {
err = os.Rename(file.Name(), path)
err = errors.Join(err, file.Close())
if err == nil {
err = os.Rename(file.Name(), path)
return errors.Join(fmt.Errorf("replace file: %w", err), os.Remove(file.Name()))
}
return errors.Join(fmt.Errorf("replace file: %w", err), os.Remove(file.Name()))
}
- return syncDir(dir)
-}
-
-// syncDir flushes the entries of dir to the disk.
-func syncDir(dir string) (err error) {
- handle, err := os.Open(dir)
- if err != nil {
- return fmt.Errorf("open directory: %w", err)
- }
-
- defer func() {
- err = errors.Join(err, handle.Close())
- }()
-
- err = handle.Sync()
- if err != nil {
- return fmt.Errorf("flush directory: %w", err)
- }
-