internal/chain holds typed Build, Rollout and Assignment and a Backend with no delete or purge, in memory and on the bus. The builds and rollouts buckets are asserted with history 64, no age and values up to just under a mebibyte; the assignments bucket gets the same, keeping its marker age because a stream cannot be made to disallow message ages. Migration 0104 makes the one-row chain_moved switch. The controller keeps no consolidated schema, so 0104 is the whole schema change. Nothing reads the buckets yet.
144 lines
3.9 KiB
Go
144 lines
3.9 KiB
Go
package chain
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"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 func() bool
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
func (s Bucket[T]) guard(value []byte) error {
|
|
if s.holds == nil || !s.holds() {
|
|
return ErrNotHeld
|
|
}
|
|
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(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(body); err != nil {
|
|
return 0, err
|
|
}
|
|
return s.b.Update(ctx, key, body, revision)
|
|
}
|
|
|
|
// All is every record, by key. An error is an error: never an empty bucket.
|
|
func (s Bucket[T]) All(ctx context.Context) (map[string]Kept[T], error) {
|
|
keys, err := s.b.Keys(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := make(map[string]Kept[T], len(keys))
|
|
for _, key := range keys {
|
|
k, ok, err := s.Get(ctx, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if ok {
|
|
out[key] = k
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// 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) }
|
|
|
|
// Chain is the three buckets of the chain.
|
|
type Chain struct {
|
|
Builds Bucket[Build]
|
|
Rollouts Bucket[Rollout]
|
|
Assignments Bucket[Assignment]
|
|
}
|
|
|
|
// New is a chain over three backends, written only while holds says this controller holds the lease.
|
|
func New(builds, rollouts, assignments Backend, holds func() bool) *Chain {
|
|
return &Chain{
|
|
Builds: Bucket[Build]{builds, holds},
|
|
Rollouts: Bucket[Rollout]{rollouts, holds},
|
|
Assignments: Bucket[Assignment]{assignments, holds},
|
|
}
|
|
}
|
|
|
|
// InMemory is a chain held in this process, for tests, written whenever holds says so; nil always holds.
|
|
func InMemory(holds func() bool) *Chain {
|
|
if holds == nil {
|
|
holds = func() bool { return true }
|
|
}
|
|
return New(NewMemory(), NewMemory(), NewMemory(), holds)
|
|
}
|