Initial SWOOSH executor and reducer implementation
This commit is contained in:
117
snapshot.go
Normal file
117
snapshot.go
Normal file
@@ -0,0 +1,117 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user