Resume goals after transient API failures with persisted exponential backoff while respecting pauses and interruptions.
78 lines
1.6 KiB
Go
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
|
|
}
|