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 }