package swoosh import ( "encoding/json" "errors" "fmt" "os" "path/filepath" ) // Snapshot captures a serialized view of the orchestrator state and replay cursor. type Snapshot struct { State OrchestratorState `json:"state"` LastAppliedHLC string `json:"last_applied_hlc"` LastAppliedIndex uint64 `json:"last_applied_index"` } // SnapshotStore persists and loads orchestrator snapshots. type SnapshotStore interface { Save(s Snapshot) error LoadLatest() (Snapshot, error) } // ErrSnapshotNotFound indicates there is no stored snapshot. var ErrSnapshotNotFound = errors.New("snapshot not found") // FileSnapshotStore stores snapshots using atomic file replacement. type FileSnapshotStore struct { path string } // NewFileSnapshotStore creates a snapshot store at the supplied path. func NewFileSnapshotStore(path string) *FileSnapshotStore { return &FileSnapshotStore{path: path} } // Save writes the snapshot with atomic replace semantics. func (s *FileSnapshotStore) Save(snapshot Snapshot) error { dir := filepath.Dir(s.path) if err := os.MkdirAll(dir, 0o755); err != nil { return fmt.Errorf("create snapshot directory: %w", err) } payload, err := canonicalJSON(snapshot) if err != nil { return fmt.Errorf("marshal snapshot: %w", err) } temp, err := os.CreateTemp(dir, "snapshot-*.tmp") if err != nil { return fmt.Errorf("create temp snapshot: %w", err) } tempName := temp.Name() if _, err := temp.Write(payload); err != nil { temp.Close() os.Remove(tempName) return fmt.Errorf("write snapshot: %w", err) } if err := temp.Sync(); err != nil { temp.Close() os.Remove(tempName) return fmt.Errorf("sync snapshot: %w", err) } if err := temp.Close(); err != nil { os.Remove(tempName) return fmt.Errorf("close snapshot temp file: %w", err) } if err := os.Rename(tempName, s.path); err != nil { os.Remove(tempName) return fmt.Errorf("rename snapshot: %w", err) } return nil } // LoadLatest returns the persisted snapshot or ErrSnapshotNotFound if absent. func (s *FileSnapshotStore) LoadLatest() (Snapshot, error) { payload, err := os.ReadFile(s.path) if err != nil { if errors.Is(err, os.ErrNotExist) { return Snapshot{}, ErrSnapshotNotFound } return Snapshot{}, fmt.Errorf("read snapshot: %w", err) } var snapshot Snapshot if err := json.Unmarshal(payload, &snapshot); err != nil { return Snapshot{}, fmt.Errorf("decode snapshot: %w", err) } return snapshot, nil } // InMemorySnapshotStore fulfills SnapshotStore in memory for testing. type InMemorySnapshotStore struct { snapshot Snapshot has bool } // Save stores the snapshot in memory. func (s *InMemorySnapshotStore) Save(snapshot Snapshot) error { s.snapshot = snapshot s.has = true return nil } // LoadLatest retrieves the stored snapshot or ErrSnapshotNotFound. func (s *InMemorySnapshotStore) LoadLatest() (Snapshot, error) { if !s.has { return Snapshot{}, ErrSnapshotNotFound } return s.snapshot, nil }