Keep rollouts and the merges they answer in the rollouts bucket (issue 1092)

Once chain_moved records the kind, a plan is a key of the rollouts bucket under its id and the open batch is the same key in state assembling. A save refuses a copy another save moved since (the plan's own Revision, kept in its value, counts saves) and keeps the merges the record holds, so a copy read before a merge moved cannot lay it back. The merges a batch or walk answers are the record's held field, with the columns the table had; a merge no plan answers stays, flagged, in the record that held it last, or in the record kept for those. A merge moved to a plan is written to the receiving record first and then taken from the giving one; one found in both is read from the one it entered last and repaired at the next cut. Plans and merges are read from a view kept by one watch of the bucket. A bus that does not answer, a lease lost and a key moved are now not-now like the store being away, so a merge whose plan could not be written is held and handed over again, never acknowledged. The start moves release_plan and batched_merge into the bucket, one kind, counted against both tables.
This commit is contained in:
2026-10-18 03:06:50 +02:00
parent e0fb241263
commit 275ddd76e8
21 changed files with 1939 additions and 40 deletions
+39 -1
View File
@@ -35,6 +35,11 @@ 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
@@ -42,7 +47,7 @@ func (s Bucket[T]) guard(ctx context.Context, key string, value []byte) error {
if err := s.holds(ctx); err != nil {
return fmt.Errorf("%w: %v", ErrNotHeld, err)
}
if !validKey.MatchString(key) || strings.HasPrefix(key, ".") || strings.HasSuffix(key, ".") {
if !ValidKey(key) {
return fmt.Errorf("%w: %q", ErrBadKey, key)
}
if len(value) > broker.ChainValueBytes {
@@ -147,19 +152,52 @@ func (s Bucket[T]) Keys(ctx context.Context) ([]string, error) { return s.b.Keys
// 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,
}
}