corago/codex-keep/store.go
ms2k e4b84d9802 Add daemon-attached goal recovery operator
Resume goals after transient API failures with persisted exponential backoff while respecting pauses and interruptions.
2026-10-18 14:01:07 +09:00

78 lines
1.6 KiB
Go

package main
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"syscall"
)
type store struct {
path string
lock *os.File
Watches map[string]*watch `json:"watches"`
}
func openStore(path string) (*store, error) {
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
return nil, err
}
lock, err := os.OpenFile(path+".lock", os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
return nil, err
}
if err := syscall.Flock(int(lock.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil {
return nil, fmt.Errorf("checkpoint is already in use: %w", errors.Join(err, lock.Close()))
}
s := &store{path: path, lock: lock, Watches: make(map[string]*watch)}
data, err := os.ReadFile(path)
if errors.Is(err, os.ErrNotExist) {
return s, nil
}
if err == nil {
err = json.Unmarshal(data, s)
}
if err == nil && s.Watches == nil {
err = errors.New("checkpoint has no watches")
}
if err != nil {
return nil, errors.Join(err, s.close())
}
return s, nil
}
func (s *store) close() error { return s.lock.Close() }
func (s *store) save() error {
data, err := json.Marshal(s)
if err != nil {
return err
}
f, err := os.CreateTemp(filepath.Dir(s.path), ".state-*")
if err != nil {
return err
}
defer func() { _ = os.Remove(f.Name()) }()
if _, err = f.Write(data); err == nil {
err = f.Sync()
}
closeErr := f.Close()
if err != nil {
return err
}
if closeErr != nil {
return closeErr
}
return os.Rename(f.Name(), s.path)
}
func (s *store) watch(id string) *watch {
w := s.Watches[id]
if w == nil {
w = &watch{}
s.Watches[id] = w
}
return w
}