Make atomic file replacement crash durable
This commit is contained in:
@@ -63,7 +63,7 @@ func (s *LocalStore) Load(ctx context.Context, path string) (*Manifest, error) {
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
// Save writes the manifest to path atomically via temp file + rename.
|
||||
// Save writes the manifest to path through the durable file replacement primitive.
|
||||
func (s *LocalStore) Save(ctx context.Context, path string, m *Manifest) error {
|
||||
if err := checkContext(ctx); err != nil {
|
||||
return err
|
||||
@@ -95,46 +95,9 @@ func (s *LocalStore) Save(ctx context.Context, path string, m *Manifest) error {
|
||||
}
|
||||
data = append(data, '\n')
|
||||
|
||||
dir := filepath.Dir(path)
|
||||
if err := fileops.EnsureWorkspaceDirectory(dir); err != nil {
|
||||
return fmt.Errorf("save manifest: create directory %q: %w", dir, err)
|
||||
if err := writeJSONAtomically(ctx, path, data); err != nil {
|
||||
return fmt.Errorf("save manifest: %w", err)
|
||||
}
|
||||
|
||||
tmp, err := os.CreateTemp(dir, ".manifest.json.tmp-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("save manifest: create temp file: %w", err)
|
||||
}
|
||||
tmpName := tmp.Name()
|
||||
removeTmp := true
|
||||
defer func() {
|
||||
if removeTmp {
|
||||
_ = os.Remove(tmpName)
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err := tmp.Write(data); err != nil {
|
||||
_ = tmp.Close()
|
||||
return fmt.Errorf("save manifest: write temp file: %w", err)
|
||||
}
|
||||
if err := tmp.Sync(); err != nil {
|
||||
_ = tmp.Close()
|
||||
return fmt.Errorf("save manifest: sync temp file: %w", err)
|
||||
}
|
||||
if err := tmp.Close(); err != nil {
|
||||
return fmt.Errorf("save manifest: close temp file: %w", err)
|
||||
}
|
||||
if err := os.Chmod(tmpName, fileops.WorkspaceFileMode); err != nil {
|
||||
return fmt.Errorf("save manifest: set temp file permissions: %w", err)
|
||||
}
|
||||
if err := checkContext(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Rename(tmpName, path); err != nil {
|
||||
return fmt.Errorf("save manifest: rename temp file: %w", err)
|
||||
}
|
||||
removeTmp = false
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -221,7 +184,7 @@ func (s *LocalStore) SaveRun(ctx context.Context, path string, m *RunManifest) e
|
||||
}
|
||||
data = append(data, '\n')
|
||||
|
||||
return writeJSONAtomically(ctx, path, ".run-manifest.json.tmp-*", data)
|
||||
return writeJSONAtomically(ctx, path, data)
|
||||
}
|
||||
|
||||
func validateLoadedManifest(m *Manifest) error {
|
||||
@@ -330,48 +293,17 @@ func normalizeRunManifest(m *RunManifest) {
|
||||
}
|
||||
}
|
||||
|
||||
func writeJSONAtomically(ctx context.Context, path, tempPattern string, data []byte) error {
|
||||
func writeJSONAtomically(ctx context.Context, path string, data []byte) error {
|
||||
dir := filepath.Dir(path)
|
||||
if err := fileops.EnsureWorkspaceDirectory(dir); err != nil {
|
||||
return fmt.Errorf("create directory %q: %w", dir, err)
|
||||
}
|
||||
|
||||
tmp, err := os.CreateTemp(dir, tempPattern)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create temp file: %w", err)
|
||||
}
|
||||
tmpName := tmp.Name()
|
||||
removeTmp := true
|
||||
defer func() {
|
||||
if removeTmp {
|
||||
_ = os.Remove(tmpName)
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err := tmp.Write(data); err != nil {
|
||||
_ = tmp.Close()
|
||||
return fmt.Errorf("write temp file: %w", err)
|
||||
}
|
||||
if err := tmp.Sync(); err != nil {
|
||||
_ = tmp.Close()
|
||||
return fmt.Errorf("sync temp file: %w", err)
|
||||
}
|
||||
if err := tmp.Close(); err != nil {
|
||||
return fmt.Errorf("close temp file: %w", err)
|
||||
}
|
||||
if err := os.Chmod(tmpName, fileops.WorkspaceFileMode); err != nil {
|
||||
return fmt.Errorf("set temp file permissions: %w", err)
|
||||
}
|
||||
if err := checkContext(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Rename(tmpName, path); err != nil {
|
||||
return fmt.Errorf("rename temp file: %w", err)
|
||||
}
|
||||
removeTmp = false
|
||||
|
||||
return nil
|
||||
return fileops.ReplaceFileAtomic(path, data, fileops.ReplaceFileOptions{
|
||||
Mode: fileops.WorkspaceFileMode,
|
||||
BeforeRename: func() error {
|
||||
return checkContext(ctx)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func checkContext(ctx context.Context) error {
|
||||
|
||||
Reference in New Issue
Block a user