A read could start a state's watch before its follower joined, and a change landing in that gap woke nothing (review of ADR 0580 §2): a joining follower now runs over the copy's current values before any later change. The watched reader keeps each archive once per URL, and both caches are bounded. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
245 lines
7.9 KiB
Go
245 lines
7.9 KiB
Go
package stdio
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// WatchedStates is a copy of the states a module reads, each filled by its own watch and kept by its changes (novox/hq
|
|
// ADR 0580 clause 1): a read of a whole state is one watch's current values, never a get per key, which took a
|
|
// composition past 30 seconds and a compare past its call bound. A state is watched on its first read, or on Follow,
|
|
// and the watch hands every current value over before the read is answered, so the copy is complete from the first
|
|
// answer. A key whose value never changes once written (a manifest, by its digest) is read once with Once and kept.
|
|
// A state helper, no rule: what a module reads is its own.
|
|
type WatchedStates struct {
|
|
watch func(state string, handle func(StateChange) error) error
|
|
get func(state, key string) ([]byte, bool, error)
|
|
|
|
mu sync.Mutex
|
|
states map[string]*watchedState
|
|
// once holds each value read with Once, by state and key, for the process's life: a few kilobytes per manifest
|
|
// digest. Past onceAtMost keys it is emptied, which costs only a read again.
|
|
once map[string][]byte
|
|
}
|
|
|
|
// onceAtMost bounds the values Once keeps.
|
|
const onceAtMost = 4096
|
|
|
|
type watchedState struct {
|
|
ready chan struct{} // closed once the watch's current values are all in the copy, or it was refused
|
|
err error
|
|
|
|
mu sync.Mutex
|
|
values map[string][]byte
|
|
then []func(StateChange) error
|
|
started bool // the current values are in, so a change runs then at once
|
|
pending []StateChange // the current values, run through then once all are in
|
|
thenMu sync.Mutex // then runs one change at a time
|
|
}
|
|
|
|
// NewWatchedStates is the copy over this bundle's runtime: each state by KeptState.Watch, Once by KeptState.Get.
|
|
func NewWatchedStates() *WatchedStates {
|
|
return WatchedStatesOver(
|
|
func(state string, handle func(StateChange) error) error { return State(state).Watch("", handle) },
|
|
func(state, key string) ([]byte, bool, error) {
|
|
e, err := State(state).Get(key)
|
|
if err != nil || e == nil {
|
|
return nil, false, err
|
|
}
|
|
return e.Value, true, nil
|
|
})
|
|
}
|
|
|
|
// WatchedStatesOver is the copy over a watch and a get of one's own (a process's bus, a test's): watch hands handle
|
|
// every current value and returns once they are handled, then hands every change; get reads one key.
|
|
func WatchedStatesOver(watch func(state string, handle func(StateChange) error) error,
|
|
get func(state, key string) ([]byte, bool, error)) *WatchedStates {
|
|
return &WatchedStates{watch: watch, get: get, states: map[string]*watchedState{}, once: map[string][]byte{}}
|
|
}
|
|
|
|
// watched is the state's copy, its watch started now when it has none, the caller waiting until its current values
|
|
// are in. then, when given, runs after the copy took each change: the current values once they are all in (a then
|
|
// that reads the copy reads it whole), each later change as it comes. No lock is held while the watch starts, so a
|
|
// then may read any state of the copy.
|
|
func (w *WatchedStates) watched(state string, then func(StateChange) error) (*watchedState, error) {
|
|
w.mu.Lock()
|
|
s, ok := w.states[state]
|
|
if ok {
|
|
w.mu.Unlock()
|
|
<-s.ready
|
|
if s.err != nil {
|
|
return nil, s.err
|
|
}
|
|
if then != nil {
|
|
s.join(state, then)
|
|
}
|
|
return s, nil
|
|
}
|
|
s = &watchedState{ready: make(chan struct{}), values: map[string][]byte{}}
|
|
if then != nil {
|
|
s.then = []func(StateChange) error{then}
|
|
}
|
|
w.states[state] = s
|
|
w.mu.Unlock()
|
|
err := w.watch(state, func(c StateChange) error {
|
|
s.mu.Lock()
|
|
if c.Op == OpPut {
|
|
s.values[c.Key] = append([]byte(nil), c.Value...)
|
|
} else {
|
|
delete(s.values, c.Key)
|
|
}
|
|
if !s.started {
|
|
s.pending = append(s.pending, c)
|
|
s.mu.Unlock()
|
|
return nil
|
|
}
|
|
s.mu.Unlock()
|
|
return s.run(state, c)
|
|
})
|
|
if err != nil {
|
|
s.err = fmt.Errorf("the state %s is not watched: %w", state, err)
|
|
w.mu.Lock()
|
|
delete(w.states, state) // asked again on the next read
|
|
w.mu.Unlock()
|
|
close(s.ready)
|
|
return nil, s.err
|
|
}
|
|
s.mu.Lock()
|
|
s.started = true
|
|
pending := s.pending
|
|
s.pending = nil
|
|
s.mu.Unlock()
|
|
close(s.ready)
|
|
for _, c := range pending {
|
|
if err := s.run(state, c); err != nil {
|
|
sayf("%v", err)
|
|
}
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
// join adds then to a state already watched (a read started its watch first) and runs it over the copy's current values
|
|
// before any later change, so no change that landed before it joined is lost to it (review of ADR 0580 §2: a read
|
|
// beat the follower to the watch at start-up). Under thenMu, so a later change waits for these; a change the copy took
|
|
// just before the snapshot may also be handed again, which a then that wakes work takes as a second wake.
|
|
func (s *watchedState) join(state string, then func(StateChange) error) {
|
|
s.thenMu.Lock()
|
|
defer s.thenMu.Unlock()
|
|
s.mu.Lock()
|
|
s.then = append(s.then, then)
|
|
current := make([]StateChange, 0, len(s.values))
|
|
for k, v := range s.values {
|
|
current = append(current, StateChange{State: state, Key: k, Op: OpPut, Value: v, Current: true})
|
|
}
|
|
s.mu.Unlock()
|
|
sort.Slice(current, func(i, j int) bool { return current[i].Key < current[j].Key })
|
|
for _, c := range current {
|
|
if err := then(c); err != nil {
|
|
sayf("%s %s: %v", state, c.Key, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// run hands one change, already in the copy, to every then.
|
|
func (s *watchedState) run(state string, c StateChange) error {
|
|
s.thenMu.Lock()
|
|
defer s.thenMu.Unlock()
|
|
s.mu.Lock()
|
|
then := append([]func(StateChange) error(nil), s.then...)
|
|
s.mu.Unlock()
|
|
var errs []error
|
|
for _, f := range then {
|
|
if err := f(c); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
if len(errs) > 0 {
|
|
return fmt.Errorf("%s %s: %v", state, c.Key, errs)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Follow watches the state into the copy and runs then on each change after the copy took it, so a reader then wakes
|
|
// never reads a copy older than its change; it asks again until the watch is taken (2 s, doubled to a minute), as
|
|
// KeptState.Follow does. On a state already watched then hears the changes from now on.
|
|
func (w *WatchedStates) Follow(state string, then func(StateChange) error) {
|
|
for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) {
|
|
if _, err := w.watched(state, then); err == nil {
|
|
return
|
|
} else {
|
|
sayf("%v; asking again in %s", err, wait)
|
|
}
|
|
time.Sleep(wait)
|
|
}
|
|
}
|
|
|
|
// Get is a key's value in the copy, false when it has none.
|
|
func (w *WatchedStates) Get(state, key string) ([]byte, bool, error) {
|
|
s, err := w.watched(state, nil)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
v, ok := s.values[key]
|
|
return v, ok, nil
|
|
}
|
|
|
|
// Keys are the state's keys in the copy, sorted.
|
|
func (w *WatchedStates) Keys(state string) ([]string, error) {
|
|
s, err := w.watched(state, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
keys := make([]string, 0, len(s.values))
|
|
for k := range s.values {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
return keys, nil
|
|
}
|
|
|
|
// Values is every key's value in the copy.
|
|
func (w *WatchedStates) Values(state string) (map[string][]byte, error) {
|
|
s, err := w.watched(state, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
out := make(map[string][]byte, len(s.values))
|
|
for k, v := range s.values {
|
|
out[k] = v
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// Once is a key whose value never changes once written, read by key the first time it is found and kept after: a
|
|
// manifest by its digest. A key not found is not kept, so it is read again on the next ask.
|
|
func (w *WatchedStates) Once(state, key string) ([]byte, bool, error) {
|
|
w.mu.Lock()
|
|
v, ok := w.once[state+" "+key]
|
|
w.mu.Unlock()
|
|
if ok {
|
|
return v, true, nil
|
|
}
|
|
v, found, err := w.get(state, key)
|
|
if err != nil || !found {
|
|
return nil, false, err
|
|
}
|
|
w.mu.Lock()
|
|
if len(w.once) >= onceAtMost {
|
|
clear(w.once)
|
|
}
|
|
w.once[state+" "+key] = v
|
|
w.mu.Unlock()
|
|
return v, true, nil
|
|
}
|
|
|
|
// Direct is a key read by key now, not from the copy: a value that changes and is read one at a time (a build).
|
|
func (w *WatchedStates) Direct(state, key string) ([]byte, bool, error) { return w.get(state, key) }
|