Give the controller lease back at the stop signal and say a takeover once, so a controller delivery raises no urgent alarm (issue 906, ADR 0558)

The 2026-10-26 replacements showed the lease was given back, but last: the
shutdown took 10 s, the gate had closed at half the key's age, and the release
recorded the epoch released only through the gate, so the next holder found
it expired and S12 said it, urgent, for an hour.

Now the stop signal gives the key back first and records the epoch released
whenever the delete succeeded; a candidate reads the key's delete marker on
the bus to tell a handover from an expiry; and a real takeover (lost or
expired) is raised and cleared once by the controller that took it, instead
of an hour of S12 observation.

Research 072 piece moved: the lease's takeover read from its own key on the
bus and said as an event (03-domains, mesh-controller row; 04-findings 2,
the watchdogs' age reading).
Quick fix weighed: drop the gate check from the release record only; it
leaves the release last in the shutdown and the hour-long alarm for a real
takeover.
This commit is contained in:
2026-10-27 00:49:13 +01:00
parent 2dafbe60e9
commit fbaa463a38
8 changed files with 262 additions and 36 deletions
+13 -3
View File
@@ -67,14 +67,24 @@ func (i *Inventory) HighestEpoch(ctx context.Context) (uint64, error) {
}
// TookEpoch records an epoch taken, and ends every earlier one still open as found expired: the lease
// was free to take, so whoever held it last neither holds it nor said it let go. Answers the epochs it
// ended that way, newest first.
func (i *Inventory) TookEpoch(ctx context.Context, e Epoch) ([]Epoch, error) {
// was free to take, so whoever held it last neither holds it nor said it let go. givenBack is the bus's word
// that the key was given back (the lease's own delete marker, novox/hq issue 906): then the newest earlier
// epoch still open is ended as released — its holder gave the key back and had not yet recorded it — and only
// the ones before it as expired. Answers the epochs it ended as expired, newest first.
func (i *Inventory) TookEpoch(ctx context.Context, e Epoch, givenBack bool) ([]Epoch, error) {
tx, err := i.store.Pool().Begin(ctx)
if err != nil {
return nil, err
}
defer func() { _ = tx.Rollback(ctx) }()
if givenBack {
if _, err := tx.Exec(ctx,
`update controller_epoch set ended = now(), how = $2
where epoch = (select max(epoch) from controller_epoch where ended is null and epoch < $1)`,
int64(e.Epoch), EpochReleased); err != nil {
return nil, err
}
}
rows, err := tx.Query(ctx,
`update controller_epoch set ended = now(), how = $2
where ended is null and epoch < $1