Give each caller of a pass its own nodes' errors and write the grants whenever asked; a wake during a take-in makes it look once more (issue 1945)
mesh/delivery closed: a newer head of the same pull request
mesh/check pass 8114cee2af47 on 0fff3efd4e0b | unit 959 run, 172 kept, 2 skipped | integration 1615 run, 236 kept, 7 skipped | over bound: integration…
mesh/delivery closed: a newer head of the same pull request
mesh/check pass 8114cee2af47 on 0fff3efd4e0b | unit 959 run, 172 kept, 2 skipped | integration 1615 run, 236 kept, 7 skipped | over bound: integration…
This commit is contained in:
@@ -120,6 +120,9 @@ var (
|
|||||||
doneKeysSay = func(s string) { fmt.Println(s) }
|
doneKeysSay = func(s string) { fmt.Println(s) }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// doneKeysLookAgain marks a module woken while a take-in of it ran: that take-in goes round once more.
|
||||||
|
var doneKeysLookAgain sync.Map
|
||||||
|
|
||||||
// doneKeysMu keeps one take-in of a module's done key at a time in this process, per module.
|
// doneKeysMu keeps one take-in of a module's done key at a time in this process, per module.
|
||||||
var doneKeysMu sync.Map
|
var doneKeysMu sync.Map
|
||||||
|
|
||||||
@@ -130,17 +133,25 @@ var doneKeysMu sync.Map
|
|||||||
func takeDoneKey(ctx context.Context, open *stores, module string) {
|
func takeDoneKey(ctx context.Context, open *stores, module string) {
|
||||||
mu, _ := doneKeysMu.LoadOrStore(module, &sync.Mutex{})
|
mu, _ := doneKeysMu.LoadOrStore(module, &sync.Mutex{})
|
||||||
if !mu.(*sync.Mutex).TryLock() {
|
if !mu.(*sync.Mutex).TryLock() {
|
||||||
return // a take-in of the module already follows it, and looks again within doneKeyRetry
|
// A take-in of the module already follows it: it is told to look again once it is done, so a newer key written
|
||||||
|
// meanwhile is never lost (the review of issue 1945).
|
||||||
|
doneKeysLookAgain.Store(module, true)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
defer mu.(*sync.Mutex).Unlock()
|
defer mu.(*sync.Mutex).Unlock()
|
||||||
failed := false
|
failed := false
|
||||||
saidWait := ""
|
saidWait := ""
|
||||||
for {
|
for {
|
||||||
|
doneKeysLookAgain.Delete(module) // what this look reads covers every wake before it
|
||||||
err := doneKeyTakeOnce(ctx, open, module)
|
err := doneKeyTakeOnce(ctx, open, module)
|
||||||
o := doneKeyObservation(module)
|
o := doneKeyObservation(module)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if keeper := conditionsFrom; keeper != nil && failed {
|
if keeper := conditionsFrom; keeper != nil && failed {
|
||||||
_, _ = keeper.Clear(ctx, o.Key(), "composed and granted")
|
_, _ = keeper.Clear(ctx, o.Key(), "composed and granted")
|
||||||
|
failed = false
|
||||||
|
}
|
||||||
|
if _, again := doneKeysLookAgain.LoadAndDelete(module); again {
|
||||||
|
continue // a wake came while this one ran: once more, for the key written meanwhile
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -357,9 +368,9 @@ func composerFor(open *stores) *composer {
|
|||||||
if c, ok := doneKeysComposers.Load(open); ok {
|
if c, ok := doneKeysComposers.Load(open); ok {
|
||||||
return c.(*composer)
|
return c.(*composer)
|
||||||
}
|
}
|
||||||
c := newComposer(func(ctx context.Context, nodes []string) error {
|
c := newComposer(func(ctx context.Context, nodes []string) map[string]error {
|
||||||
_, err := composeModules(ctx, open, nodes)
|
_, failed := composeModulesByNode(ctx, open, nodes)
|
||||||
return err
|
return failed
|
||||||
}, func(ctx context.Context) error {
|
}, func(ctx context.Context) error {
|
||||||
bus, err := aBus()
|
bus, err := aBus()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -86,3 +86,38 @@ func TestAWakeWhileATakeInFollowsAddsNone(t *testing.T) {
|
|||||||
close(release)
|
close(release)
|
||||||
<-follower
|
<-follower
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A wake while a take-in of the module follows it is not lost: the follower goes round once more after it succeeds, so
|
||||||
|
// a newer build's key written meanwhile is taken in (the review of issue 1945).
|
||||||
|
func TestAWakeDuringATakeInIsTakenAfterIt(t *testing.T) {
|
||||||
|
was, wasRetry, wasSay := doneKeyTakeOnce, doneKeyRetry, doneKeysSay
|
||||||
|
t.Cleanup(func() { doneKeyTakeOnce, doneKeyRetry, doneKeysSay = was, wasRetry, wasSay })
|
||||||
|
doneKeyRetry = 20 * time.Millisecond
|
||||||
|
doneKeysSay = func(string) {}
|
||||||
|
var mu sync.Mutex
|
||||||
|
calls := 0
|
||||||
|
inFirst := make(chan struct{})
|
||||||
|
release := make(chan struct{})
|
||||||
|
doneKeyTakeOnce = func(context.Context, *stores, string) error {
|
||||||
|
mu.Lock()
|
||||||
|
calls++
|
||||||
|
n := calls
|
||||||
|
mu.Unlock()
|
||||||
|
if n == 1 {
|
||||||
|
close(inFirst)
|
||||||
|
<-release
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
follower := make(chan struct{})
|
||||||
|
go func() { takeDoneKey(context.Background(), nil, "mesh-host"); close(follower) }()
|
||||||
|
<-inFirst
|
||||||
|
takeDoneKey(context.Background(), nil, "mesh-host") // the newer key's wake, while the first runs
|
||||||
|
close(release)
|
||||||
|
<-follower
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
if calls != 2 {
|
||||||
|
t.Fatalf("taken %d times: the wake during the take-in was lost", calls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,29 +26,52 @@ import (
|
|||||||
// every refusal is named in the error. It answers the keys it wrote, by node. Under the lease only: a controller that
|
// every refusal is named in the error. It answers the keys it wrote, by node. Under the lease only: a controller that
|
||||||
// does not hold it writes nothing and says so.
|
// does not hold it writes nothing and says so.
|
||||||
func composeModules(ctx context.Context, open *stores, nodes []string) (map[string][]string, error) {
|
func composeModules(ctx context.Context, open *stores, nodes []string) (map[string][]string, error) {
|
||||||
|
out, byNode := composeModulesByNode(ctx, open, nodes)
|
||||||
|
names := make([]string, 0, len(byNode))
|
||||||
|
for n := range byNode {
|
||||||
|
names = append(names, n)
|
||||||
|
}
|
||||||
|
sort.Strings(names)
|
||||||
|
var errs []error
|
||||||
|
for _, n := range names {
|
||||||
|
errs = append(errs, byNode[n])
|
||||||
|
}
|
||||||
|
return out, errors.Join(errs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// composeModulesByNode is composeModules with each node's error apart: a failure before any node is composed is every
|
||||||
|
// node's (the composer gives each caller only the errors of the nodes it asked for, issue 1945).
|
||||||
|
func composeModulesByNode(ctx context.Context, open *stores, nodes []string) (map[string][]string, map[string]error) {
|
||||||
|
all := func(err error) (map[string][]string, map[string]error) {
|
||||||
|
byNode := map[string]error{}
|
||||||
|
for _, n := range nodes {
|
||||||
|
byNode[n] = err
|
||||||
|
}
|
||||||
|
return nil, byNode
|
||||||
|
}
|
||||||
if _, err := theLease.epoch(ctx); err != nil {
|
if _, err := theLease.epoch(ctx); err != nil {
|
||||||
return nil, fmt.Errorf("nothing is composed: %w", err)
|
return all(fmt.Errorf("nothing is composed: %w", err))
|
||||||
}
|
}
|
||||||
bus, err := aBus()
|
bus, err := aBus()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("nothing is composed: the bus: %w", err)
|
return all(fmt.Errorf("nothing is composed: the bus: %w", err))
|
||||||
}
|
}
|
||||||
defer bus.Close()
|
defer bus.Close()
|
||||||
js, err := jetstream.New(bus.Conn())
|
js, err := jetstream.New(bus.Conn())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return all(err)
|
||||||
}
|
}
|
||||||
sign, err := controllerSigner(ctx, open)
|
sign, err := controllerSigner(ctx, open)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("nothing is composed: the controller's signing key: %w", err)
|
return all(fmt.Errorf("nothing is composed: the controller's signing key: %w", err))
|
||||||
}
|
}
|
||||||
fps, err := currentFingerprints(ctx, open.inventory)
|
fps, err := currentFingerprints(ctx, open.inventory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return all(err)
|
||||||
}
|
}
|
||||||
entries, err := open.inventory.Catalogued(ctx)
|
entries, err := open.inventory.Catalogued(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return all(err)
|
||||||
}
|
}
|
||||||
manifests := make(map[string]catalogue.Manifest, len(entries))
|
manifests := make(map[string]catalogue.Manifest, len(entries))
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
@@ -56,7 +79,7 @@ func composeModules(ctx context.Context, open *stores, nodes []string) (map[stri
|
|||||||
}
|
}
|
||||||
upgrades, err := open.inventory.Upgrades(ctx)
|
upgrades, err := open.inventory.Upgrades(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return all(err)
|
||||||
}
|
}
|
||||||
policy := func(module string) (string, string, string) {
|
policy := func(module string) (string, string, string) {
|
||||||
u, ok := upgrades[module]
|
u, ok := upgrades[module]
|
||||||
@@ -67,7 +90,7 @@ func composeModules(ctx context.Context, open *stores, nodes []string) (map[stri
|
|||||||
}
|
}
|
||||||
sort.Strings(nodes)
|
sort.Strings(nodes)
|
||||||
out := map[string][]string{}
|
out := map[string][]string{}
|
||||||
var errs []error
|
errsOf := map[string][]error{}
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
// One composition of a node at a time in this process: two at once collided on its keys' revisions (issue 1945).
|
// One composition of a node at a time in this process: two at once collided on its keys' revisions (issue 1945).
|
||||||
unlock := lockComposing(node)
|
unlock := lockComposing(node)
|
||||||
@@ -75,7 +98,7 @@ func composeModules(ctx context.Context, open *stores, nodes []string) (map[stri
|
|||||||
defer unlock()
|
defer unlock()
|
||||||
at, err := openComposedPlace(ctx, js, bus.Conn(), node)
|
at, err := openComposedPlace(ctx, js, bus.Conn(), node)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, fmt.Errorf("%s: %w", node, err))
|
errsOf[node] = append(errsOf[node], fmt.Errorf("%s: %w", node, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
at.sign, at.policy = sign, policy
|
at.sign, at.policy = sign, policy
|
||||||
@@ -84,21 +107,21 @@ func composeModules(ctx context.Context, open *stores, nodes []string) (map[stri
|
|||||||
}
|
}
|
||||||
declared, err := composeSend(ctx, open, node)
|
declared, err := composeSend(ctx, open, node)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, fmt.Errorf("%s: %w", node, err))
|
errsOf[node] = append(errsOf[node], fmt.Errorf("%s: %w", node, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
body, err := compositionBody(declared)
|
body, err := compositionBody(declared)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, fmt.Errorf("%s: %w", node, err))
|
errsOf[node] = append(errsOf[node], fmt.Errorf("%s: %w", node, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// The provisioner's plans of the modules the node's composition hands to it, for the assignments mirror.
|
// The provisioner's plans of the modules the node's composition hands to it, for the assignments mirror.
|
||||||
if err := open.inventory.RecordComposedPlans(ctx, node, plansKept(declared)); err != nil {
|
if err := open.inventory.RecordComposedPlans(ctx, node, plansKept(declared)); err != nil {
|
||||||
errs = append(errs, fmt.Errorf("%s: its provisioner's plans: %w", node, err))
|
errsOf[node] = append(errsOf[node], fmt.Errorf("%s: its provisioner's plans: %w", node, err))
|
||||||
}
|
}
|
||||||
comps, moved, err := compositionsOf(node, body, fps, manifests, declared.modules)
|
comps, moved, err := compositionsOf(node, body, fps, manifests, declared.modules)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, fmt.Errorf("%s: %w", node, err))
|
errsOf[node] = append(errsOf[node], fmt.Errorf("%s: %w", node, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(moved) > 0 {
|
if len(moved) > 0 {
|
||||||
@@ -116,15 +139,21 @@ func composeModules(ctx context.Context, open *stores, nodes []string) (map[stri
|
|||||||
// And where a data-keeping provision bound each consumer: what the next resolution keeps it at (novox/hq ADR 0232).
|
// And where a data-keeping provision bound each consumer: what the next resolution keeps it at (novox/hq ADR 0232).
|
||||||
if len(declared.Bindings) > 0 {
|
if len(declared.Bindings) > 0 {
|
||||||
if err := open.inventory.RecordBindings(ctx, node, declared.Bindings); err != nil {
|
if err := open.inventory.RecordBindings(ctx, node, declared.Bindings); err != nil {
|
||||||
errs = append(errs, fmt.Errorf("%s: where its consumers are bound to their data: %w", node, err))
|
errsOf[node] = append(errsOf[node], fmt.Errorf("%s: where its consumers are bound to their data: %w", node, err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, fmt.Errorf("%s: %w", node, err))
|
errsOf[node] = append(errsOf[node], fmt.Errorf("%s: %w", node, err))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
return out, errors.Join(errs...)
|
byNode := map[string]error{}
|
||||||
|
for node, errs := range errsOf {
|
||||||
|
if len(errs) > 0 {
|
||||||
|
byNode[node] = errors.Join(errs...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, byNode
|
||||||
}
|
}
|
||||||
|
|
||||||
// composeAndSay is composeModules for an act that has nothing to do with its answer but say it: what each former send
|
// composeAndSay is composeModules for an act that has nothing to do with its answer but say it: what each former send
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"sort"
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
@@ -13,22 +14,30 @@ import (
|
|||||||
// revision mismatch"), retried every 30 s, and the take-in that mattered waited behind them (2026-10-25 at 11:18 and
|
// revision mismatch"), retried every 30 s, and the take-in that mattered waited behind them (2026-10-25 at 11:18 and
|
||||||
// 12:32). A caller waits for the pass that began after it asked, and hears that pass's error.
|
// 12:32). A caller waits for the pass that began after it asked, and hears that pass's error.
|
||||||
type composer struct {
|
type composer struct {
|
||||||
composeNodes func(ctx context.Context, nodes []string) error
|
composeNodes func(ctx context.Context, nodes []string) map[string]error
|
||||||
writeGrants func(ctx context.Context) error
|
writeGrants func(ctx context.Context) error
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
pending map[string]bool
|
pending map[string]bool
|
||||||
grants bool
|
grants bool
|
||||||
waiters []chan error
|
waiters []waiter
|
||||||
wake chan struct{}
|
wake chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newComposer(compose func(context.Context, []string) error, grants func(context.Context) error) *composer {
|
// waiter is a caller waiting for its pass: the nodes it asked for, whether it asked for the grants, and where its answer
|
||||||
|
// goes.
|
||||||
|
type waiter struct {
|
||||||
|
nodes []string
|
||||||
|
grants bool
|
||||||
|
done chan error
|
||||||
|
}
|
||||||
|
|
||||||
|
func newComposer(compose func(context.Context, []string) map[string]error, grants func(context.Context) error) *composer {
|
||||||
return &composer{composeNodes: compose, writeGrants: grants, pending: map[string]bool{}, wake: make(chan struct{}, 1)}
|
return &composer{composeNodes: compose, writeGrants: grants, pending: map[string]bool{}, wake: make(chan struct{}, 1)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// compose asks for the nodes to be composed, and the grants written after when grants is true, and waits for the pass
|
// compose asks for the nodes to be composed, and the grants written after when grants is true, and waits for the pass
|
||||||
// that does it.
|
// that does it, with the errors of its own nodes alone.
|
||||||
func (c *composer) compose(ctx context.Context, nodes []string, grants bool) error {
|
func (c *composer) compose(ctx context.Context, nodes []string, grants bool) error {
|
||||||
done := make(chan error, 1)
|
done := make(chan error, 1)
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
@@ -36,7 +45,7 @@ func (c *composer) compose(ctx context.Context, nodes []string, grants bool) err
|
|||||||
c.pending[n] = true
|
c.pending[n] = true
|
||||||
}
|
}
|
||||||
c.grants = c.grants || grants
|
c.grants = c.grants || grants
|
||||||
c.waiters = append(c.waiters, done)
|
c.waiters = append(c.waiters, waiter{nodes: nodes, grants: grants, done: done})
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
select {
|
select {
|
||||||
case c.wake <- struct{}{}:
|
case c.wake <- struct{}{}:
|
||||||
@@ -70,15 +79,27 @@ func (c *composer) run(ctx context.Context) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
sort.Strings(nodes)
|
sort.Strings(nodes)
|
||||||
var err error
|
var failed map[string]error
|
||||||
if len(nodes) > 0 {
|
if len(nodes) > 0 {
|
||||||
err = c.composeNodes(ctx, nodes)
|
failed = c.composeNodes(ctx, nodes)
|
||||||
}
|
}
|
||||||
if err == nil && grants {
|
// The grants whenever a caller asked for them, whatever a node's composition said: a node that failed fails its
|
||||||
err = c.writeGrants(ctx)
|
// own callers alone (issue 1945).
|
||||||
|
var grantsErr error
|
||||||
|
if grants {
|
||||||
|
grantsErr = c.writeGrants(ctx)
|
||||||
}
|
}
|
||||||
for _, w := range waiters {
|
for _, w := range waiters {
|
||||||
w <- err
|
var errs []error
|
||||||
|
for _, n := range w.nodes {
|
||||||
|
if err := failed[n]; err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if w.grants && grantsErr != nil {
|
||||||
|
errs = append(errs, grantsErr)
|
||||||
|
}
|
||||||
|
w.done <- errors.Join(errs...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func TestABurstComposesEachNodeOncePerPass(t *testing.T) {
|
|||||||
running, maxRunning := 0, 0
|
running, maxRunning := 0, 0
|
||||||
release := make(chan struct{})
|
release := make(chan struct{})
|
||||||
first := true
|
first := true
|
||||||
c := newComposer(func(_ context.Context, nodes []string) error {
|
c := newComposer(func(_ context.Context, nodes []string) map[string]error {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
running++
|
running++
|
||||||
maxRunning = max(maxRunning, running)
|
maxRunning = max(maxRunning, running)
|
||||||
@@ -78,10 +78,10 @@ func TestABurstComposesEachNodeOncePerPass(t *testing.T) {
|
|||||||
// A caller hears its own pass's failure, and only a pass that began after it asked counts as its own.
|
// A caller hears its own pass's failure, and only a pass that began after it asked counts as its own.
|
||||||
func TestACallerHearsThePassThatComposedItsNodes(t *testing.T) {
|
func TestACallerHearsThePassThatComposedItsNodes(t *testing.T) {
|
||||||
calls := 0
|
calls := 0
|
||||||
c := newComposer(func(_ context.Context, nodes []string) error {
|
c := newComposer(func(_ context.Context, nodes []string) map[string]error {
|
||||||
calls++
|
calls++
|
||||||
if calls == 1 {
|
if calls == 1 {
|
||||||
return context.DeadlineExceeded
|
return map[string]error{"ace": context.DeadlineExceeded}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}, func(context.Context) error { return nil })
|
}, func(context.Context) error { return nil })
|
||||||
@@ -95,3 +95,49 @@ func TestACallerHearsThePassThatComposedItsNodes(t *testing.T) {
|
|||||||
t.Fatalf("a good pass said %v", err)
|
t.Fatalf("a good pass said %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// One node that fails its composition fails only the callers that asked for it, and the grants are written whenever a
|
||||||
|
// caller asked for them (the review of issue 1945: one joined error failed every take-in of the pass and stopped their
|
||||||
|
// grants).
|
||||||
|
func TestANodeThatFailsFailsOnlyItsCallers(t *testing.T) {
|
||||||
|
grants := 0
|
||||||
|
release := make(chan struct{})
|
||||||
|
first := true
|
||||||
|
c := newComposer(func(_ context.Context, nodes []string) map[string]error {
|
||||||
|
if first {
|
||||||
|
first = false
|
||||||
|
<-release
|
||||||
|
}
|
||||||
|
out := map[string]error{}
|
||||||
|
for _, n := range nodes {
|
||||||
|
if n == "broken" {
|
||||||
|
out[n] = context.DeadlineExceeded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}, func(context.Context) error { grants++; return nil })
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
go c.run(ctx)
|
||||||
|
go func() { _ = c.compose(ctx, []string{"warm"}, false) }()
|
||||||
|
time.Sleep(20 * time.Millisecond)
|
||||||
|
errs := make(chan error, 2)
|
||||||
|
go func() { errs <- c.compose(ctx, []string{"ace"}, true) }()
|
||||||
|
go func() { errs <- c.compose(ctx, []string{"broken"}, false) }()
|
||||||
|
time.Sleep(20 * time.Millisecond)
|
||||||
|
close(release)
|
||||||
|
var failed, fine int
|
||||||
|
for range 2 {
|
||||||
|
if err := <-errs; err != nil {
|
||||||
|
failed++
|
||||||
|
} else {
|
||||||
|
fine++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if failed != 1 || fine != 1 {
|
||||||
|
t.Fatalf("failed %d, fine %d: only the caller of the broken node fails", failed, fine)
|
||||||
|
}
|
||||||
|
if grants != 1 {
|
||||||
|
t.Fatalf("the grants were written %d times; a caller asked for them", grants)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user