Hold a node from composing to sending, so a push composed before converge or adopt is never sent after it (hq ADR 0100)
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// HoldNodes serialises composing and sending a declaration per node (novox/hq ADR 0100): while
|
||||
// one caller holds a node, another asking for it waits. Without it a push that composed a node as
|
||||
// adopted could send that declaration after `converge --yes` sent the converged one, and the node
|
||||
// would return to adopted with nobody having asked.
|
||||
//
|
||||
// Session-level advisory locks on one connection, taken in name order so two callers holding
|
||||
// overlapping sets cannot each wait on the other. Release gives every one back, and may be called
|
||||
// more than once; a caller whose context ends while waiting holds nothing.
|
||||
func (i *Inventory) HoldNodes(ctx context.Context, names []string) (func(), error) {
|
||||
sorted := slices.Clone(names)
|
||||
slices.Sort(sorted)
|
||||
sorted = slices.Compact(sorted)
|
||||
conn, err := i.store.Pool().Acquire(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var once sync.Once
|
||||
release := func() {
|
||||
once.Do(func() {
|
||||
// Unlocking all of this session's advisory locks, then handing the connection back: a
|
||||
// connection returned still holding one would hold it for whoever borrows it next.
|
||||
_, err := conn.Exec(context.WithoutCancel(ctx), `select pg_advisory_unlock_all()`)
|
||||
if err != nil {
|
||||
// The session's locks die with the session: close it rather than return it.
|
||||
_ = conn.Conn().Close(context.WithoutCancel(ctx))
|
||||
}
|
||||
conn.Release()
|
||||
})
|
||||
}
|
||||
for _, name := range sorted {
|
||||
if _, err := conn.Exec(ctx,
|
||||
`select pg_advisory_lock(hashtext('mesh-node-declaration:' || $1)::bigint)`,
|
||||
name); err != nil {
|
||||
release()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return release, nil
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Composing and sending one node's declaration is serialised: a second holder waits for the first.
|
||||
func TestHoldingANodeMakesTheNextHolderWait(t *testing.T) {
|
||||
inv := fresh(t)
|
||||
ctx := t.Context()
|
||||
release, err := inv.HoldNodes(ctx, []string{"anchor", "laptop"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := make(chan func(), 1)
|
||||
go func() {
|
||||
second, err := inv.HoldNodes(ctx, []string{"laptop"})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
got <- func() {}
|
||||
return
|
||||
}
|
||||
got <- second
|
||||
}()
|
||||
select {
|
||||
case <-got:
|
||||
t.Fatal("a node held by one caller was held by another at the same time")
|
||||
case <-time.After(300 * time.Millisecond):
|
||||
}
|
||||
// Another node is not held up.
|
||||
other, err := inv.HoldNodes(ctx, []string{"joiner"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
other()
|
||||
release()
|
||||
select {
|
||||
case second := <-got:
|
||||
second()
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("releasing the node did not let the next holder in")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user