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:
+53
-22
@@ -55,7 +55,7 @@ type Memory struct {
|
||||
mu sync.Mutex
|
||||
values map[string][]Entry
|
||||
revision uint64
|
||||
watchers []chan Entry
|
||||
watchers []*watcher
|
||||
// Now is the clock the entries are dated by; nil is time.Now.
|
||||
Now func() time.Time
|
||||
// writes counts the creates and updates taken.
|
||||
@@ -144,10 +144,7 @@ func (m *Memory) write(key string, value []byte) uint64 {
|
||||
}
|
||||
m.values[key] = h
|
||||
for _, w := range m.watchers {
|
||||
select {
|
||||
case w <- clone(e):
|
||||
default:
|
||||
}
|
||||
w.push(clone(e))
|
||||
}
|
||||
return e.Revision
|
||||
}
|
||||
@@ -166,14 +163,41 @@ func (m *Memory) Keys(_ context.Context) ([]string, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// watcher is one watch of a Memory: an unbounded queue, so a watch that is slow is behind and never misses a write.
|
||||
type watcher struct {
|
||||
mu sync.Mutex
|
||||
q []Entry
|
||||
wake chan struct{}
|
||||
}
|
||||
|
||||
func (w *watcher) push(e Entry) {
|
||||
w.mu.Lock()
|
||||
w.q = append(w.q, e)
|
||||
w.mu.Unlock()
|
||||
select {
|
||||
case w.wake <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (w *watcher) pop() (Entry, bool) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
if len(w.q) == 0 {
|
||||
return Entry{}, false
|
||||
}
|
||||
e := w.q[0]
|
||||
w.q = w.q[1:]
|
||||
return e, true
|
||||
}
|
||||
|
||||
func (m *Memory) Watch(ctx context.Context) (<-chan Entry, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.fail != nil {
|
||||
return nil, m.fail
|
||||
}
|
||||
// Room for the current values, the caught mark, and a burst of later writes.
|
||||
w := make(chan Entry, len(m.values)+1+1024)
|
||||
w := &watcher{wake: make(chan struct{}, 1)}
|
||||
keys := make([]string, 0, len(m.values))
|
||||
for k := range m.values {
|
||||
keys = append(keys, k)
|
||||
@@ -181,30 +205,37 @@ func (m *Memory) Watch(ctx context.Context) (<-chan Entry, error) {
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
h := m.values[k]
|
||||
w <- clone(h[len(h)-1])
|
||||
w.q = append(w.q, clone(h[len(h)-1]))
|
||||
}
|
||||
w <- Entry{Caught: true}
|
||||
w.q = append(w.q, Entry{Caught: true})
|
||||
m.watchers = append(m.watchers, w)
|
||||
out := make(chan Entry)
|
||||
go func() {
|
||||
defer close(out)
|
||||
defer func() {
|
||||
m.mu.Lock()
|
||||
for i, x := range m.watchers {
|
||||
if x == w {
|
||||
m.watchers = append(m.watchers[:i], m.watchers[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
m.mu.Unlock()
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
m.mu.Lock()
|
||||
for i, x := range m.watchers {
|
||||
if x == w {
|
||||
m.watchers = append(m.watchers[:i], m.watchers[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
m.mu.Unlock()
|
||||
return
|
||||
case e := <-w:
|
||||
e, ok := w.pop()
|
||||
if !ok {
|
||||
select {
|
||||
case out <- e:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-w.wake:
|
||||
}
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case out <- e:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
+39
-1
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -143,6 +143,22 @@ type Links struct {
|
||||
Changed []Change `json:"changed,omitempty"`
|
||||
}
|
||||
|
||||
// HeldMerge is a merge a rollout holds: the columns the controller's table of batched merges had, kept in the record of the
|
||||
// batch or walk that answers it (novox/hq ADR 0276, issue 1092). A merge no plan answers (Waiting) stays in the record
|
||||
// that held it last. Placed is when it entered this record, so one found in two records after a crash between the two
|
||||
// writes is read from the one it entered last.
|
||||
type HeldMerge struct {
|
||||
Repository string `json:"repository"`
|
||||
Branch string `json:"branch,omitempty"`
|
||||
Commit string `json:"commit"`
|
||||
Merged time.Time `json:"merged,omitzero"`
|
||||
Heard time.Time `json:"heard,omitzero"`
|
||||
Event json.RawMessage `json:"event,omitempty"`
|
||||
Alone bool `json:"alone,omitempty"`
|
||||
Waiting bool `json:"waiting,omitempty"`
|
||||
Placed time.Time `json:"placed,omitzero"`
|
||||
}
|
||||
|
||||
// Rollout is the controller's record of one batch's delivery, kept under the batch's id: the commit it fixed per
|
||||
// repository, its tiers, the builds it carried and the assignments it changed (ADR 0408 §1). The controller's plan
|
||||
// carries more (its modules, its times, its backups) beside these, under the same key.
|
||||
|
||||
Reference in New Issue
Block a user