package chain import ( "context" "encoding/json" "errors" "fmt" "regexp" "strings" "github.com/novox/mesh-controller/internal/broker" ) // Bucket is one kind of record of the chain, typed. Reads need nothing; **every write asks first whether this // controller holds the lease** and writes nothing when it does not (ADR 0408 ยง2). type Bucket[T any] struct { b Backend holds Holds } // Holds says whether this controller may write now: nil when it holds the lease, and why not when it does not. The // serving controller answers from its lease, and a command from the holder's (the epoch gate). type Holds func(ctx context.Context) error // Kept is a record as the bucket holds it: with its revision, the one a write must find. type Kept[T any] struct { Key string Value T Revision uint64 } // validKey is what the bus takes as a key: letters, digits and - _ / = . with no dot at either end. var validKey = regexp.MustCompile(`^[-/_=.a-zA-Z0-9]+$`) // ErrBadKey is a key the bus does not take: refused loudly, never rewritten into another. var ErrBadKey = errors.New("the bus takes no key of that spelling") // ValidKey says the bus takes the key: letters, digits and - _ / = . with no dot at either end. func ValidKey(key string) bool { return validKey.MatchString(key) && !strings.HasPrefix(key, ".") && !strings.HasSuffix(key, ".") } func (s Bucket[T]) guard(ctx context.Context, key string, value []byte) error { if s.holds == nil { return ErrNotHeld } if err := s.holds(ctx); err != nil { return fmt.Errorf("%w: %v", ErrNotHeld, err) } if !ValidKey(key) { return fmt.Errorf("%w: %q", ErrBadKey, key) } if len(value) > broker.ChainValueBytes { return fmt.Errorf("%w: %d bytes, the most is %d", ErrTooLarge, len(value), broker.ChainValueBytes) } return nil } func decode[T any](e Entry) (Kept[T], error) { var v T if err := json.Unmarshal(e.Value, &v); err != nil { return Kept[T]{}, fmt.Errorf("the value of %s cannot be read: %w", e.Key, err) } return Kept[T]{Key: e.Key, Value: v, Revision: e.Revision}, nil } // Get is one record; false when the key holds none. func (s Bucket[T]) Get(ctx context.Context, key string) (Kept[T], bool, error) { e, ok, err := s.b.Get(ctx, key) if err != nil || !ok { return Kept[T]{}, false, err } e.Key = key k, err := decode[T](e) return k, err == nil, err } // Create writes a key that holds nothing: ErrExists when it holds something. It returns the revision. func (s Bucket[T]) Create(ctx context.Context, key string, v T) (uint64, error) { body, err := json.Marshal(v) if err != nil { return 0, err } if err := s.guard(ctx, key, body); err != nil { return 0, err } return s.b.Create(ctx, key, body) } // Update writes a key at the revision it was read at: ErrMoved when it moved, the key unchanged. func (s Bucket[T]) Update(ctx context.Context, key string, v T, revision uint64) (uint64, error) { body, err := json.Marshal(v) if err != nil { return 0, err } if err := s.guard(ctx, key, body); err != nil { return 0, err } return s.b.Update(ctx, key, body, revision) } // All is every record, by key, read in one pass of a watch. An error is an error: never an empty bucket. func (s Bucket[T]) All(ctx context.Context) (map[string]Kept[T], error) { ctx, stop := context.WithCancel(ctx) defer stop() w, err := s.b.Watch(ctx) if err != nil { return nil, err } out := map[string]Kept[T]{} for { select { case <-ctx.Done(): return nil, fmt.Errorf("reading the bucket: %w", ctx.Err()) case e, open := <-w: if !open { return nil, errors.New("the bucket stopped answering before it was read whole") } if e.Caught { return out, nil } k, err := decode[T](e) if err != nil { return nil, err } out[e.Key] = k } } } // History is a key's earlier records and its current one, oldest first. func (s Bucket[T]) History(ctx context.Context, key string) ([]Kept[T], error) { all, err := s.b.History(ctx, key) if err != nil { return nil, err } out := make([]Kept[T], 0, len(all)) for _, e := range all { e.Key = key k, err := decode[T](e) if err != nil { return nil, err } out = append(out, k) } return out, nil } // Keys is every key of the bucket. func (s Bucket[T]) Keys(ctx context.Context) ([]string, error) { return s.b.Keys(ctx) } // Watch is the bucket's raw entries: its current values, a Caught mark, then each write. func (s Bucket[T]) Watch(ctx context.Context) (<-chan Entry, error) { return s.b.Watch(ctx) } // GetRaw is one key's value as the bucket holds it, for a writer that changes a field of a record and must keep the rest // exactly as it is. func (s Bucket[T]) GetRaw(ctx context.Context, key string) (Entry, bool, error) { e, ok, err := s.b.Get(ctx, key) e.Key = key return e, ok, err } // CreateRaw writes a key's first value as bytes, with the same guards as Create. func (s Bucket[T]) CreateRaw(ctx context.Context, key string, value []byte) (uint64, error) { if err := s.guard(ctx, key, value); err != nil { return 0, err } return s.b.Create(ctx, key, value) } // UpdateRaw writes a key's value as bytes, at the revision it was read at, with the same guards as Update. func (s Bucket[T]) UpdateRaw(ctx context.Context, key string, value []byte, revision uint64) (uint64, error) { if err := s.guard(ctx, key, value); err != nil { return 0, err } return s.b.Update(ctx, key, value, revision) } // Chain is the three buckets of the chain. type Chain struct { Builds Bucket[Build] Rollouts Bucket[Rollout] Assignments Bucket[Assignment] rollouts Backend holds Holds } // RolloutsOf is the rollouts bucket typed as the controller's plan, which carries more than the chain's Rollout names: // the same keys and values, read and written as the plan. func RolloutsOf[T any](c *Chain) Bucket[T] { return Bucket[T]{c.rollouts, c.holds} } // New is a chain over three backends, written only while holds says this controller may write. A nil holds writes nothing. func New(builds, rollouts, assignments Backend, holds Holds) *Chain { return &Chain{ Builds: Bucket[Build]{builds, holds}, Rollouts: Bucket[Rollout]{rollouts, holds}, Assignments: Bucket[Assignment]{assignments, holds}, rollouts: rollouts, holds: holds, } } // InMemory is a chain held in this process, for tests, written whenever holds says so; nil always holds. func InMemory(holds Holds) *Chain { if holds == nil { holds = func(context.Context) error { return nil } } return New(NewMemory(), NewMemory(), NewMemory(), holds) }