mesh/merge-gate head 19e3eac74658 merged into trunk 89cfa9431d8f — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head 19e3eac74658 merged into trunk 89cfa9431d8f — pass: its merge-check.sh passed
mesh/delivery delivered
135 lines
5.8 KiB
Go
135 lines
5.8 KiB
Go
package inventory
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
)
|
|
|
|
// The durations the core's bounds are set from (novox/hq to-be 45 Phase 0): see migration 0066.
|
|
|
|
// The kinds of duration recorded.
|
|
const (
|
|
DurationApply = "apply"
|
|
DurationPlanTier = "plan-tier"
|
|
DurationBuild = "build"
|
|
// DurationWalkPhase is one phase of an ended walk, per class (novox/hq ADR 0282 decision 6): subject
|
|
// "<class>/<phase>", and "<class>/total" for its merge to every machine running it.
|
|
DurationWalkPhase = "walk-phase"
|
|
// DurationBusy is a machine's wait for a busy container (novox/hq ADR 0335): from its first report of a
|
|
// send, which said a container waits because it is busy, to the report that says it replaced — the same
|
|
// send's ref as its `apply` row, whose detail is ApplyBusy. Its own measure, never in the apply's.
|
|
DurationBusy = "busy-wait"
|
|
)
|
|
|
|
// ApplyBusy is the detail of an `apply` duration whose report said a container waits because it is busy: the
|
|
// machine answered, and is not yet running what it was sent (novox/hq ADR 0335).
|
|
const ApplyBusy = "busy"
|
|
|
|
// DurationKinds are every kind, in the order `durations` shows them.
|
|
var DurationKinds = []string{DurationApply, DurationPlanTier, DurationBuild, DurationWalkPhase,
|
|
DurationBusy}
|
|
|
|
// DurationsKeptFor is how long a duration is kept: long enough to set a bound from, and to correct it
|
|
// in Phase 1's first live week.
|
|
const DurationsKeptFor = 30 * 24 * time.Hour
|
|
|
|
// Duration is one measurement.
|
|
type Duration struct {
|
|
Kind string `json:"kind"`
|
|
Subject string `json:"subject"`
|
|
Node string `json:"node,omitempty"`
|
|
Ref string `json:"ref"`
|
|
Started time.Time `json:"started"`
|
|
Took time.Duration `json:"took"`
|
|
Detail string `json:"detail,omitempty"`
|
|
}
|
|
|
|
// RecordDuration keeps one measurement, once: the same thing measured again is not a second row.
|
|
func (i *Inventory) RecordDuration(ctx context.Context, d Duration) error {
|
|
if d.Took < 0 {
|
|
return nil // a clock that went back measures nothing
|
|
}
|
|
_, err := i.store.Pool().Exec(ctx,
|
|
`insert into duration (kind, subject, node, ref, started, took_ms, detail)
|
|
values ($1, $2, $3, $4, $5, $6, $7) on conflict do nothing`,
|
|
d.Kind, d.Subject, d.Node, d.Ref, d.Started, d.Took.Milliseconds(), d.Detail)
|
|
return err
|
|
}
|
|
|
|
// RecordApplyDuration measures a machine's report of the declaration it was last sent: from the send
|
|
// to the first report of it. A report of anything else, or of a send already measured, measures
|
|
// nothing — a machine reconciling reports the same declaration every few minutes.
|
|
func (i *Inventory) RecordApplyDuration(ctx context.Context, node, declared, outcome string) error {
|
|
if declared == "" {
|
|
return nil
|
|
}
|
|
_, err := i.store.Pool().Exec(ctx,
|
|
`insert into duration (kind, subject, node, ref, started, took_ms, detail)
|
|
select $1, n.name, n.name, n.sent || '@' || to_char(n.sent_at at time zone 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US'),
|
|
n.sent_at, (extract(epoch from (now() - n.sent_at)) * 1000)::bigint, $4
|
|
from node n
|
|
where n.name = $2 and n.sent = $3 and n.sent_at is not null
|
|
on conflict do nothing`,
|
|
DurationApply, node, declared, outcome)
|
|
return err
|
|
}
|
|
|
|
// RecordBusyDuration measures a machine's wait for a busy container, at the report that says it replaced
|
|
// (novox/hq ADR 0335): from the end of its `apply` duration of the send it was last sent, which said it waited, to
|
|
// now. Nothing when that send's first report did not say a wait, and once per send.
|
|
func (i *Inventory) RecordBusyDuration(ctx context.Context, node, declared string) error {
|
|
if declared == "" {
|
|
return nil
|
|
}
|
|
_, err := i.store.Pool().Exec(ctx,
|
|
`insert into duration (kind, subject, node, ref, started, took_ms, detail)
|
|
select $1, d.subject, d.node, d.ref, d.started + d.took_ms * interval '1 millisecond',
|
|
greatest(0, (extract(epoch from (now() - (d.started + d.took_ms * interval '1 millisecond'))) * 1000)::bigint), ''
|
|
from duration d join node n on n.name = d.node
|
|
where d.kind = $2 and d.subject = $3 and d.detail = $5 and n.sent = $4 and n.sent_at is not null
|
|
and d.ref = n.sent || '@' || to_char(n.sent_at at time zone 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US')
|
|
on conflict do nothing`,
|
|
DurationBusy, DurationApply, node, declared, ApplyBusy)
|
|
return err
|
|
}
|
|
|
|
// Durations is every measurement of a kind since a moment, oldest first; every kind when kind is empty.
|
|
func (i *Inventory) Durations(ctx context.Context, kind string, since time.Time) ([]Duration, error) {
|
|
rows, err := i.store.Pool().Query(ctx,
|
|
`select kind, subject, node, ref, started, took_ms, detail from duration
|
|
where ($1 = '' or kind = $1) and recorded >= $2 order by recorded`, kind, since)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []Duration
|
|
for rows.Next() {
|
|
var d Duration
|
|
var ms int64
|
|
if err := rows.Scan(&d.Kind, &d.Subject, &d.Node, &d.Ref, &d.Started, &ms, &d.Detail); err != nil {
|
|
return nil, err
|
|
}
|
|
d.Took = time.Duration(ms) * time.Millisecond
|
|
out = append(out, d)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// WalkPhasesKept says whether any phase of a walk is kept as a walk-phase duration, and whether its total is.
|
|
func (i *Inventory) WalkPhasesKept(ctx context.Context, plan string) (anyKept, totalKept bool, err error) {
|
|
err = i.store.Pool().QueryRow(ctx,
|
|
`select count(*) > 0, count(*) filter (where ref = $2) > 0 from duration where kind = $1 and ref like $3`,
|
|
DurationWalkPhase, plan+"/total", plan+"/%").Scan(&anyKept, &totalKept)
|
|
return anyKept, totalKept, err
|
|
}
|
|
|
|
// ForgetOldDurations removes what is older than DurationsKeptFor, and says how many.
|
|
func (i *Inventory) ForgetOldDurations(ctx context.Context) (int64, error) {
|
|
tag, err := i.store.Pool().Exec(ctx, `delete from duration where recorded < $1`,
|
|
time.Now().Add(-DurationsKeptFor))
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return tag.RowsAffected(), nil
|
|
}
|