Research 031 counted the repairs people made by hand: a push to unstick a plan waiting on a report, a controller restarted to make an object again, a plan closed, a consumer re-made from now. Each was the ordinary path taken again by someone who noticed. The healer registry makes each a registered response to one condition kind, with a budget, a settle and its event: - H1 sent-not-reported: ask the machine's node-engine to report again (mesh.node.<n>.ask.report); if it does not report what it was sent, send it again, never moving a build a policy or a plan holds back - H2 stalled: close a plan whose wait is superseded or finished - H3 holder-silent / consumer-lost: the send's own assertion of the bus's objects (issue 208's note) - H4 consumer-behind: consumer-reset, only for a consumer the stream table marks resettable (the controller's own events consumer) - H5 is the identity provider's own repair (ADR 0224 §5), registered only Success is the observation clearing the condition, never the healer; a spent budget hands the condition to the operator, urgent, with what was tried, and no healer touches it again. Every act is begun in the store before it is made (migration 0070), kept in the condition's tried as "healer Hn" and said as the seat event healer-acted; a heal is never a hand act. More than twelve acts in an hour stop every healer until an hour after the last, said urgently. Only the lease holder heals. S15 is live: a cause repaired by hand twice in a fortnight raises healer-wanted, naming the healer that was not enough where one exists. D6's far-behind finding has its own kind, consumer-behind. Nodes are granted the question; the controller's grant gains healer-acted (genesis lock in mesh-host). `healers` lists the registry, the acts and the brake; status counts the week's heals.
127 lines
4.3 KiB
Go
127 lines
4.3 KiB
Go
package inventory
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// The healers' acts (novox/hq to-be 45 §7, Phase 3): see migration 0070. The controller is their
|
|
// only writer, under its lease; `healers` and `conditions` read them.
|
|
|
|
// The outcomes of a heal.
|
|
const (
|
|
// HealActing is an act begun and not yet finished: counted against the budget and the brake from
|
|
// the moment it starts, so a controller that dies mid-act has still spent it.
|
|
HealActing = "acting"
|
|
// HealActed is an act that did what it does. Whether it repaired anything is the observation's to
|
|
// say, never the healer's: the condition clears, or it does not.
|
|
HealActed = "acted"
|
|
// HealFailed is an act that could not be done.
|
|
HealFailed = "failed"
|
|
// HealEscalated is a budget spent, said: the condition was handed to the operator.
|
|
HealEscalated = "escalated"
|
|
)
|
|
|
|
// HealsKeptFor is how long a heal is kept: as long as a condition's history.
|
|
const HealsKeptFor = 90 * 24 * time.Hour
|
|
|
|
// Heal is one act of a healer.
|
|
type Heal struct {
|
|
ID int64 `json:"id"`
|
|
Healer string `json:"healer"`
|
|
ConditionKey string `json:"condition"`
|
|
Kind string `json:"kind"`
|
|
BudgetKey string `json:"budget-key"`
|
|
Act string `json:"act"`
|
|
Outcome string `json:"outcome"`
|
|
Said string `json:"said,omitempty"`
|
|
At time.Time `json:"at"`
|
|
Finished *time.Time `json:"finished,omitempty"`
|
|
Epoch uint64 `json:"epoch,omitempty"`
|
|
}
|
|
|
|
// BeginHeal writes an act about to be taken, under the lease: the act is counted from here. Refused,
|
|
// and nothing written, by a process that may not act.
|
|
func (i *Inventory) BeginHeal(ctx context.Context, h Heal) (Heal, error) {
|
|
epoch, err := i.actingEpoch(ctx)
|
|
if err != nil {
|
|
return h, fmt.Errorf("the heal %s of %s is not begun: %w", h.Healer, h.ConditionKey, err)
|
|
}
|
|
if strings.TrimSpace(h.Healer) == "" || strings.TrimSpace(h.ConditionKey) == "" || strings.TrimSpace(h.Act) == "" {
|
|
return h, errors.New("a heal names its healer, its condition and its act")
|
|
}
|
|
if h.BudgetKey == "" {
|
|
h.BudgetKey = h.ConditionKey
|
|
}
|
|
if h.Outcome == "" {
|
|
h.Outcome = HealActing
|
|
}
|
|
// At the runner's moment when it gives one, so its budgets and the brake are counted on one clock.
|
|
var at *time.Time
|
|
if !h.At.IsZero() {
|
|
at = &h.At
|
|
}
|
|
err = i.store.Pool().QueryRow(ctx,
|
|
`insert into heal (healer, condition_key, kind, budget_key, act, outcome, said, epoch, at)
|
|
values ($1, $2, $3, $4, $5, $6, $7, $8, coalesce($9, now())) returning id, at`,
|
|
h.Healer, h.ConditionKey, h.Kind, h.BudgetKey, h.Act, h.Outcome, h.Said, epoch, at).Scan(&h.ID, &h.At)
|
|
if err != nil {
|
|
return h, err
|
|
}
|
|
if epoch != nil {
|
|
h.Epoch = uint64(*epoch)
|
|
}
|
|
return h, nil
|
|
}
|
|
|
|
// FinishHeal says what came of an act begun. Written whatever the lease says by now: what was done was
|
|
// done, and its record must not depend on the doer still holding the lease a moment later.
|
|
func (i *Inventory) FinishHeal(ctx context.Context, id int64, outcome, said string) error {
|
|
tag, err := i.store.Pool().Exec(ctx,
|
|
`update heal set outcome = $2, said = $3, finished = now() where id = $1`, id, outcome, said)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if tag.RowsAffected() != 1 {
|
|
return fmt.Errorf("no heal %d to finish", id)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// HealsSince is every heal from a moment, oldest first.
|
|
func (i *Inventory) HealsSince(ctx context.Context, since time.Time) ([]Heal, error) {
|
|
rows, err := i.store.Pool().Query(ctx,
|
|
`select id, healer, condition_key, kind, budget_key, act, outcome, said, at, finished, epoch
|
|
from heal where at >= $1 order by at, id`, since)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []Heal
|
|
for rows.Next() {
|
|
var h Heal
|
|
var epoch *int64
|
|
if err := rows.Scan(&h.ID, &h.Healer, &h.ConditionKey, &h.Kind, &h.BudgetKey, &h.Act, &h.Outcome, &h.Said,
|
|
&h.At, &h.Finished, &epoch); err != nil {
|
|
return nil, err
|
|
}
|
|
if epoch != nil {
|
|
h.Epoch = uint64(*epoch)
|
|
}
|
|
out = append(out, h)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// ForgetOldHeals removes what is older than HealsKeptFor, and says how many.
|
|
func (i *Inventory) ForgetOldHeals(ctx context.Context) (int64, error) {
|
|
tag, err := i.store.Pool().Exec(ctx, `delete from heal where at < $1`, time.Now().Add(-HealsKeptFor))
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return tag.RowsAffected(), nil
|
|
}
|