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 }