Files
mesh-controller/internal/inventory/hold.go
T

48 lines
1.6 KiB
Go

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
}