Bind the have bucket once per pass, bound by the queue's timeout, and stop the pass at the first error (issue 1132)

This commit is contained in:
2026-10-18 19:18:08 +02:00
parent 0b391f49a2
commit 9d9e3a46c4
4 changed files with 129 additions and 50 deletions
+57 -7
View File
@@ -18,11 +18,14 @@ import (
// haveLink is a link with a bucket behind it that keeps each key's revision as the bus does.
type haveLink struct {
*quietLink
hmu sync.Mutex
kv map[string]haveEntry
sets []string // keys written, in order
events []string // "report" and "have <key>", in the order they happened
failing bool
hmu sync.Mutex
kv map[string]haveEntry
sets []string // keys written, in order
events []string // "report" and "have <key>", in the order they happened
failing bool
binds int
setTries int
bindFails, bindStuck bool
}
type haveEntry struct {
@@ -44,7 +47,22 @@ func (l *haveLink) Report(ctx context.Context, node string, body []byte) error {
func (l *haveLink) Health(context.Context, string, []byte) error { return nil }
func (l *haveLink) HaveGet(_ context.Context, key string) ([]byte, uint64, error) {
func (l *haveLink) HaveBind(ctx context.Context) (HaveBucket, error) {
l.hmu.Lock()
l.binds++
stuck, refuse := l.bindStuck, l.bindFails
l.hmu.Unlock()
if stuck {
<-ctx.Done()
return nil, ctx.Err()
}
if refuse {
return nil, errors.New("no grant for the have bucket")
}
return l, nil
}
func (l *haveLink) Get(_ context.Context, key string) ([]byte, uint64, error) {
l.hmu.Lock()
defer l.hmu.Unlock()
e, ok := l.kv[key]
@@ -54,9 +72,10 @@ func (l *haveLink) HaveGet(_ context.Context, key string) ([]byte, uint64, error
return e.value, e.rev, nil
}
func (l *haveLink) HaveSet(_ context.Context, key string, value []byte, revision uint64) error {
func (l *haveLink) Set(_ context.Context, key string, value []byte, revision uint64) error {
l.hmu.Lock()
defer l.hmu.Unlock()
l.setTries++
if l.failing {
return errors.New("the bucket refused")
}
@@ -289,3 +308,34 @@ func TestASilentReconcileStillWritesTheHaves(t *testing.T) {
t.Errorf("an unchanged reconcile wrote %d keys", l.setCount()-before)
}
}
// 1132/AC7: a bind that fails, or never answers, writes nothing and returns within the queue's timeout; a write that
// fails ends the pass, and is said once.
func TestAFailedBindOrWriteEndsThePassAndReturnsInTime(t *testing.T) {
for name, set := range map[string]func(*haveLink){
"a bind refused": func(l *haveLink) { l.bindFails = true },
"a bind that never answers": func(l *haveLink) { l.bindStuck = true },
} {
l := newHaveLink()
set(l)
q, said := aHaveQueue(l)
q.Timeout = 100 * time.Millisecond
start := time.Now()
if !q.tell(context.Background(), appliedModules(), applied) {
t.Fatalf("%s: the report did not go out", name)
}
if took := time.Since(start); took > time.Second {
t.Errorf("%s: the pass took %s", name, took)
}
if l.binds != 1 || l.setTries != 0 || len(*said) != 1 {
t.Errorf("%s: binds %d, writes tried %d, said %v; want one bind, no write, one line", name, l.binds, l.setTries, *said)
}
}
l := newHaveLink()
l.failing = true
q, said := aHaveQueue(l)
q.tell(context.Background(), appliedModules(), applied)
if l.binds != 1 || l.setTries != 1 || len(*said) != 1 {
t.Errorf("a failed write: binds %d, writes tried %d, said %v; want one of each and one line", l.binds, l.setTries, *said)
}
}