Add the chain package and its buckets: builds, rollouts and assignments with history 64 (issue 1089)
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.
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user