Documentation
¶
Overview ¶
Package hook says whether an identity is hooked in: the one record, in the bus Redis, of the role an identity hooked in as, which pushes that role needs, and which of them are live and proven now (docs/SPEC-HOOK.md; the machine is tla/Hook.tla). Every tool that sends, reads or takes work as an identity asks Gate first and acts only on a yes; a no carries the breadcrumb, the exact command lines that hook the identity in.
The record's truth lives in Redis alone. A push is live while its holder, the process that receives that push, renews a lease key with a TTL; a dead holder's lease expires on its own and the gate refuses. A push is proven while a proof key with a TTL stands: a session answered the challenge its hook printed, or a daemon holder proved its own round trip. This package is its own module, with the Redis client its one dependency, so every tool that gates (nova-tools and nova-sprint alike) reads the same record.
Index ¶
- Constants
- Variables
- func AgentLabel(name string, r Role) string
- func Breadcrumb(name string, role Role, need []Push, st Status) (string, []string)
- func ChallengeKey(name string) string
- func DeliveryKey(name string) string
- func DeliveryOnce(name string, epoch int64, nonce string) string
- func DownReason(downAfter time.Duration, misses int) string
- func HeldBy(name string, r Role, p Push) string
- func HookCommand(name string, r Role) string
- func LiveKey(name string, p Push) string
- func ProofKey(name string, p Push) string
- func PushOnce(name string, epoch int64, state string, gen int64) string
- func PushState(st Status) string
- func RecordKey(name string) string
- func RestartCommand(goos, label string) string
- func Sum(nonce string) string
- type Beat
- type Holder
- type Judgment
- type Passed
- type Proof
- type Push
- type PushRead
- type PushStatus
- type Pushed
- type Refusal
- type Role
- type Served
- type Status
- type Store
- func (s Store) Answer(ctx context.Context, name, nonce, by string) (int, error)
- func (s Store) AnswerProof(ctx context.Context, name, nonce, by string) (Proof, error)
- func (s Store) Answered(ctx context.Context, j Judgment) (bool, error)
- func (s Store) Arm(ctx context.Context, name string, role Role, by string, fresh bool) (epoch int64, joined bool, err error)
- func (s Store) Baseline(ctx context.Context, states map[string]string) error
- func (s Store) Challenge(ctx context.Context, name string, role Role, epoch int64, pushes []Push, ...) (created bool, err error)
- func (s Store) Close(ctx context.Context, j Judgment) error
- func (s Store) Delivered(ctx context.Context, beat Beat) error
- func (s Store) Gate(ctx context.Context, w Want) error
- func (s Store) HookAs(ctx context.Context, name string, role Role, by string) (epoch int64, joined bool, err error)
- func (s Store) Judgments(ctx context.Context) ([]Judgment, time.Time, error)
- func (s Store) Lost(ctx context.Context, name string, from, until time.Time) error
- func (s Store) Names(ctx context.Context) ([]string, error)
- func (s Store) OpenJudgment(ctx context.Context, j Judgment, within time.Duration) error
- func (s Store) Pass(ctx context.Context, names []string, every, downAfter time.Duration, ...) (Passed, error)
- func (s Store) PlanPass(ctx context.Context, names []string, every, downAfter time.Duration, by string, ...) (Passed, error)
- func (s Store) Prove(ctx context.Context, name string, role Role, p Push, epoch int64, how string) error
- func (s Store) Pushed(ctx context.Context) (map[string]Pushed, bool, error)
- func (s Store) Read(ctx context.Context, names ...string) ([]Status, time.Time, error)
- func (s Store) Renew(ctx context.Context, name string, role Role, p Push, epoch int64, by string) error
- func (s Store) Sent(ctx context.Context, once string) (bool, error)
- func (s Store) Serve(ctx context.Context) (Served, error)
- func (s Store) Told(ctx context.Context, p Passed, audience string) error
- func (s Store) Unhook(ctx context.Context, name string) (was string, err error)
- func (s Store) Unprove(ctx context.Context, name string, role Role, p Push, epoch int64) error
- type Told
- type ToldMark
- type Want
Constants ¶
const ( // LeaseTTL is how long a push stays live after its holder last renewed it. LeaseTTL = 90 * time.Second // RenewEvery is how often a holder renews: three renewals inside one lease. RenewEvery = 30 * time.Second // ChallengeEvery is how often a session's hook asks it again. ChallengeEvery = 30 * time.Minute // AnswerBound is how long a challenge waits for its answer. AnswerBound = 5 * time.Minute // ProofTTL is how long a proof stands: a challenge's cadence and twice its // answer's bound, so a proof answered at the last moment of one challenge's // bound outlives the whole bound of the next (a session answering every // challenge inside its bound is never unproven between them). ProofTTL = ChallengeEvery + 2*AnswerBound // BeatEvery is the server's pass interval, not its challenge cadence // (tla/HookBeat.tla, Pass). BeatEvery = time.Minute // BusyQuietAfter applies to a friend with ready or working sprint cards. BusyQuietAfter = 5 * time.Minute // QuietAfter applies to an idle friend or comms name. QuietAfter = 10 * time.Minute // CoordinatorEvery is the coordinator's quiet window. CoordinatorEvery = 30 * time.Minute // DownAfter is silence required in addition to an outstanding challenge // past AnswerBound before the server unhooks a name (tla/HookBeat.tla). DownAfter = 5 * time.Minute )
The clocks of the record (docs/SPEC-HOOK.md, the clocks; tla/Hook.tla).
const ( // NamesKey is the set of every name that ever hooked in. NamesKey = prefix + "names" // ServeKey is the heartbeat server's own stamp: a hash of at, every, by. ServeKey = prefix + "serve" // EventsKey is the capped stream of the heartbeat's unhooks, one entry each. EventsKey = prefix + "events" // EventsMax caps EventsKey (approximately, MAXLEN ~). EventsMax = 10000 )
The keys (docs/SPEC-HOOK.md, the key layout), all in the bus Redis.
const ( StateNone = "none" // no record: never hooked in StateUnhooked = "unhooked" // unhooked by nova-hook unhook StateExpired = "expired" // hooked, and no push of the role is live StatePartial = "partial" // hooked, some pushes of the role live and some not StateLive = "live" // hooked, every push live, not every one proven StateProven = "proven" // hooked, every push live and proven: the gate's one yes PushDown = "down" // no live lease: its holder is not running PushArmed = "armed" // a live lease and no proof PushProven = "proven" // a live lease and a proof )
The record's states and a push's (docs/SPEC-HOOK.md, the states).
const ( BeatLeft = 0 // not hooked, no bus push, or challenged already this interval BeatChallenged = 1 // a fresh challenge is set: deliver its nonce BeatUnhooked = 2 // a miss with no answer for down-after: unhooked )
What one heartbeat pass did to one name.
const ( ToCoordinator = "coordinator" ToRest = "rest" )
The audiences a pass tells, each marked told on its own (Store.Told): the coordinator, who gets the judgment, and the rest of the live names.
const ( NeverRanReason = "hook server never ran" StaleReason = "hook server stale" NoAnswerReason = "no answer to a heartbeat challenge within the window" )
Why a hooked record reads down although its state says otherwise: up needs evidence, a heartbeat server passing and the name's own answer.
const JudgmentsKey = prefix + "judgments"
JudgmentsKey holds the heartbeat's open judgments: a hash of message id to the judgment (JSON), each open until one of its recipients answers it (the bus's answers record, nova-bus send --re <id>), re-pushed as OVERDUE at most once past its due (tla/HookBeat.tla, Judge).
const RecentMax = 64
RecentMax bounds the challenges a name's record keeps for its proof window (the record's recent): a pass keeps the ones the down-after window can hold at one pass per nine tenths of an interval, never more than this.
const StateDown = "down"
StateDown is a proven record that reads down at this read (Served.Down: no answer within the window, or no server), the state nova-hook friends shows as down; every other state is the record's own.
Variables ¶
var ( ErrNoChallenge = errors.New("no challenge stands for this name: its hook has not asked one, or it lapsed past its bound; read the hook's stream for the next HOOK CHALLENGE") ErrWrongNonce = errors.New("the nonce is not the one the challenge asked, nor one the heartbeat issued to this name in this epoch within its down-after window and not yet answered; copy it from the newest HOOK CHALLENGE line of the hook's stream") ErrExpired = errors.New("the heartbeat challenge is past its interval and its window: it counts as missed; answer the next one, which the heartbeat server sends within the interval") )
The refusals of Answer, by the script's code.
var Audiences = []string{ToCoordinator, ToRest}
Audiences is every audience, in the order the pass reply carries them.
Challenged is the pushes a heartbeat challenge names for each role a session answers in: the bus push. The sprint push is the seat hook's own (nova-sprint run), and a member's or reader's loop has no session: neither is challenged.
var ErrNotHooked = errors.New("the record is not hooked under this epoch: it was unhooked, or hooked again since")
ErrNotHooked is what Renew, Prove and Challenge answer when the record is not hooked under the holder's epoch: unhooked, or hooked again since.
var ErrOtherRole = errors.New("the record is hooked as another role")
ErrOtherRole is Arm's answer for a record hooked as another role: a holder never takes a record over; only a fresh arm (nova-hook hook, asked by the identity itself) does.
var Pushes = []Push{Bus, Sprint, Work}
Pushes is every push, in the order the record's keys list them.
var Roles = []Role{Coordinator, Friend, Comms, Member, Reader}
Roles is every role, in the order help lists them.
Functions ¶
func AgentLabel ¶
AgentLabel is the service that runs a friend's lanes: her daemon (com.nova.friend-<name>).
func Breadcrumb ¶
Breadcrumb is the remedy and the lines that say who holds each push and how to see and end the hook, each runnable as printed (docs/SPEC-HOOK.md, the breadcrumb). With a role, the remedy is the command that hooks name in as it. With no role known the breadcrumb never guesses one: the remedy is to look (nova-hook status), and the lines offer every role that carries need, comms first, then friend, then coordinator.
func ChallengeKey ¶
ChallengeKey is name's outstanding challenge: a hash of epoch, sum, pushes, with a TTL.
func DeliveryKey ¶
DeliveryKey holds the private pending challenge payload until bus delivery is confirmed (tla/HookBeat.tla, delivery obligation).
func DeliveryOnce ¶
DeliveryOnce is the bus once-key of this challenge, stable across retries.
func DownReason ¶
DownReason is the reason a heartbeat unhook records: no answer for downAfter, and the challenges missed in a row by then.
func HeldBy ¶
HeldBy is who holds push p of a role for name: the service or the command that runs it, each line runnable as printed on this machine.
func HookCommand ¶
HookCommand is the line that hooks name in as role.
func PushOnce ¶
PushOnce is the once-key of a name's gen-th push, naming its epoch and the state it pushes (tla/HookBeat.tla, NeverPushedTwice): the count makes a flap back to a state in the same epoch a push of its own.
func PushState ¶
PushState is the state of st a friend-status push names: the record's state, or down for a proven one the heartbeat's evidence does not stand behind.
func RestartCommand ¶
RestartCommand is the line that restarts the service label on goos, runnable as printed: launchctl in the user's gui domain on darwin, systemctl --user elsewhere (pkg/units.Load names the services the same way).
Types ¶
type Beat ¶
type Beat struct {
Name string
Did int // BeatLeft, BeatChallenged or BeatUnhooked
Nonce string // pending payload; the private outbox retains it until delivery
Misses int
Epoch int64
// DownAfter is the pass's down-after: the silence that unhooks.
DownAfter time.Duration
}
Beat is one name's outcome of a heartbeat pass.
type Holder ¶
type Holder struct {
Store Store
Name string
Role Role
Pushes []Push // the pushes this process holds: some or all of the role's
By string // who holds them, as the lease shows it
// contains filtered or unexported fields
}
Holder holds some pushes of one identity for a long-running process (a friend daemon, a member loop): it hooks in at start, renews each push it holds every RenewEvery, proves or unproves each as its owner's own round trip says, and answers the gate, so the process acts only while the record says proven (docs/SPEC-HOOK.md, holders; tla/Hook.tla, Arm, Renew and ProveDaemon).
func (*Holder) Beat ¶
Beat renews each push the holder holds and proves the ones proven names, unproving the rest. When the record moved since, the holder rejoins only a record hooked as its own role again, and re-arms only a record that is gone (none: lost, or flushed, under a running holder: its start would arm it, so its beat does); after an unhook, or a hook as another role, it answers ErrNotHooked and holds nothing until someone hooks the identity in as its role again, so an unhook stands.
func (*Holder) Gate ¶
Gate is the gate for the holder's identity: nil when every push of its role is live and proven, else the refusal or the store's error (fail closed).
func (*Holder) Start ¶
Start hooks the identity in as the holder's role and renews every push it holds once: the process's start is its hook. It joins a record hooked as its role, and arms one only where there is no record at all. A record hooked as another role is ErrOtherRole and an unhooked one ErrNotHooked, each left as it is: a holder never undoes the identity's word, so an unhook stands across the holder's restart until someone hooks the identity in again (nova-hook hook, which arms fresh).
type Judgment ¶
type Judgment struct {
ID string `json:"id"`
To []string `json:"to"`
Subject string `json:"subject"`
Due time.Time `json:"due"`
Overdue bool `json:"overdue"` // re-pushed once already: never again
}
Judgment is one open judgment: the bus message that raised it.
func (Judgment) OverdueMark ¶
OverdueMark is j's re-push's once-key and the mark that records it re-pushed (a field of JudgmentsKey), written with the re-push in one step, so it is re-pushed at most once.
type Passed ¶
type Passed struct {
Taken bool // false: another pass (another server's) was taken inside nine tenths of the interval; nothing was done
Beats []Beat // one per name, in the order asked
Unhooked []string // every name unhooked now, sorted
Coord string // the coordinator now: the first hooked name of that role, "" for none
Live []string // every hooked name of a challenged role: who hears a broadcast
Was map[string]Told
}
Passed is what one heartbeat pass did and found.
func (Passed) Diff ¶
Diff is what an audience has not been told: the names down and back since, and the coordinator it was told.
func (Passed) Telling ¶
Telling is what an audience's message for p carries so that the message and its told mark are one write (tla/HookBeat.tla, NeverToldTwice and EveryTransitionToldAtLeastOnce): the once-key of this telling, the audience's generation, which a later send under it finds and writes nothing; and the marks: what it was told, and the next generation.
type Proof ¶
type Proof struct {
Proven int // the pushes proven
// Back is which challenge the nonce matched: 0 the newest the heartbeat
// issued (or the standing challenge), k the one k challenges before it.
Back int
// Issued is when the heartbeat issued the matched challenge, by the
// store's TIME; zero for a challenge the record's recent does not hold
// (a bootstrap's).
Issued time.Time
}
Proof is what an answer proved and which challenge it matched.
type Push ¶
type Push string
Push is one stream of notifications an identity must be hooked into.
type PushRead ¶
PushRead is one push's keys as one read found them: the lease and the proof values ("" for none) and their remaining TTLs.
type PushStatus ¶
type PushStatus struct {
Push Push
State string // PushDown, PushArmed, PushProven
By string // the lease's holder, "" when down
LiveLeft time.Duration
ProofLeft time.Duration
}
PushStatus is one push of a role, judged.
type Refusal ¶
type Refusal struct {
Status Status
Why string // one line: what is not live
Hook string // the command that hooks in, the remedy
Lines []string // the rest of the breadcrumb, one runnable line each
}
Refusal is the gate's no: the identity is not hooked in for the act. It carries the status it judged and the breadcrumb, the exact command lines that hook the identity in, runnable as printed.
type Role ¶
type Role string
Role is what an identity hooks in as; it names the pushes it needs.
const ( Coordinator Role = "coordinator" // the seat: the bus and the sprint Friend Role = "friend" // a friend working the sprint: the bus and its work Comms Role = "comms" // a friend on the bus that takes no sprint work Member Role = "member" // a machine's member loop: its work Reader Role = "reader" // a machine's reader loop: its reads )
The roles (docs/SPEC-HOOK.md, roles and pushes).
type Served ¶
type Served struct {
At, Now time.Time
Every time.Duration
// DownAfter is the server's down-after (nova-hook serve --down-after):
// the window a name's last answer must fall in to read up. A row from a
// server that writes none reads as two intervals, that server's own rule.
DownAfter time.Duration
OK bool
}
Served is the heartbeat server's stamp as the store holds it: when it last passed and its interval, with the store's now; ok false when no server ever passed.
func (Served) Down ¶
Down is the shared reader rule (tla/HookBeat.tla, ActivityUp): a missing or stale server is down. A challenged role needs an initial nonce answer; afterward a delivered expired challenge with old activity reads down. The independent reader backstop also bounds silence when delivery is lost; quiet eligibility alone never changes reader status. The separate proof lease gate remains mandatory.
type Status ¶
type Status struct {
Name string
Role Role
State string // StateNone .. StateProven
Epoch int64
Armed time.Time // when the record was last hooked, zero when never
Unhooked time.Time // when it was unhooked, zero when it is not
Reason string // why it was unhooked, when the heartbeat did it ("no answer in 5m0s (5 challenges missed)")
BeatAt time.Time // when the heartbeat server last challenged it, zero when never
ActivityAt time.Time // latest explicit model bus send, at store TIME
PendingSent bool // the bus committed this challenge, not merely prepared it
PendingUntil time.Time // outstanding server challenge deadline, even after its key expires
QuietAfter time.Duration // policy selected from the authoritative sprint row at the last pass
AnswerAt time.Time // when the session last answered a challenge, zero when never
// Down is why the record reads down at this read whatever its state says
// (Served.Down: no server, a stale one, or no answer), "" when the evidence
// stands. No reader trusts the last written state then, and the gate refuses.
Down string
Misses int // challenges in a row left unanswered
Pushes []PushStatus
}
Status is one identity's record, judged at the store's now.
func Judge ¶
Judge is the status a record and its pushes' reads say: the state machine, a pure function (tla/Hook.tla, GateOK). rec is the record hash (empty: no record); reads holds one PushRead per push of Pushes, in that order. A lease or proof whose epoch is not the record's counts for nothing.
func (Status) Gate ¶
Gate is nil when st lets w act: the record is hooked, every push of its role is live and proven, its role carries every push w needs, and it is one of w.Roles when w names any. Otherwise it is the Refusal with its breadcrumb (tla/Hook.tla, Gate and GateMeansHooked).
func (Status) LastActivity ¶
LastActivity is actual model activity or the initial arm, never a holder's lease renewal (tla/HookBeat.tla, lastActivity).
func (Status) QuietWindow ¶
QuietWindow is the policy selected by the last pass, or the role's default.
type Store ¶
type Store struct {
C redis.Cmdable
// Timeout, when set, bounds each round trip on its own, inside the
// caller's context: a long-running holder's beat makes several, and one
// slow call never spends the next one's time.
Timeout time.Duration
}
Store is the record over one Redis client: the bus Redis. Every verb is one round trip, a script or a pipeline (docs/SPEC-HOOK.md, the verbs).
func (Store) Answer ¶
Answer answers name's challenge with nonce: every push it named whose lease is live is proven for ProofTTL. It answers how many were proven.
func (Store) AnswerProof ¶
AnswerProof answers name's challenge with nonce (answerLua): the newest challenge or any the heartbeat issued to name in its epoch within the down-after window and not yet spent (tla/HookBeat.tla, AnswerIssued and ProofNamesIssuedInWindow). The answer counts at the time of the proof. A refusal of another nonce names the server's window.
func (Store) Answered ¶
Answered says whether any of j's recipients replied to it: the bus's answers record (bus2:answers:<name>, field the id replied to), in one round trip.
func (Store) Arm ¶
func (s Store) Arm(ctx context.Context, name string, role Role, by string, fresh bool) (epoch int64, joined bool, err error)
Arm hooks name in as role. Not fresh (a holder's start), it joins a record already hooked as that role at its epoch (joined true) and arms only where there is no record at all; a record hooked as another role is ErrOtherRole and an unhooked one ErrNotHooked, each left as it is, so neither a holder's start nor its restart ever undoes the identity's word. Fresh (nova-hook hook, the identity's own word) always starts a new epoch. by is who armed it, as the record shows it.
func (Store) Baseline ¶
Baseline writes the states of the first pass as pushed, with no message: a server row with no pushed mark would else push every name at once. A mark already there stays (HSETNX).
func (Store) Challenge ¶
func (s Store) Challenge(ctx context.Context, name string, role Role, epoch int64, pushes []Push, nonce string) (created bool, err error)
Challenge bootstraps a challenge for pushes under epoch and role (tla/HookBeat.tla, Bootstrap). It returns false without writing if a challenge stands or the heartbeat owns this epoch, including after Answer. Only a created challenge's nonce is delivered; a skipped client waits for the heartbeat's next delivery. A bootstrap lasts AnswerBound.
func (Store) Delivered ¶
Delivered confirms the bus accepted a challenge, without extending its deadline.
func (Store) Gate ¶
Gate reads w.Name's record and says whether w may act: nil, a *Refusal with the breadcrumb, or an error of the store. Fail closed: a caller acts only on nil; a store that does not answer is a refusal of its own, never a pass.
func (Store) HookAs ¶
func (s Store) HookAs(ctx context.Context, name string, role Role, by string) (epoch int64, joined bool, err error)
HookAs is the identity's own word, nova-hook hook: it joins a record hooked as role at its epoch (joined true); otherwise (no record, an unhooked one, one hooked as another role, which it takes over) it starts a new epoch.
func (Store) Lost ¶
Lost is a client's report of an episode it could not reach the server: from and until by its own clock, logged once on hook:events when it reaches the server again.
func (Store) OpenJudgment ¶
OpenJudgment records j (its ID the message's) as open, due within of the store's now.
func (Store) Pass ¶
func (s Store) Pass(ctx context.Context, names []string, every, downAfter time.Duration, nonce func() string, by string, busy ...string) (Passed, error)
Pass applies the silence/proof-renewal rule in one store round trip (tla/HookBeat.tla, Pass). busy names come from the authoritative sprint ready/working rows; omitted names use the idle policy. Beats with a nonce require delivery, including retry of a prepared but undelivered nonce.
func (Store) PlanPass ¶
func (s Store) PlanPass(ctx context.Context, names []string, every, downAfter time.Duration, by string, busy ...string) (Passed, error)
PlanPass is what Pass would do now, from the same script, writing nothing (nova-hook serve --dry-run): the same Passed, with no nonce on any beat, as no challenge is set. A plan of a pass inside another's interval is not taken.
func (Store) Prove ¶
func (s Store) Prove(ctx context.Context, name string, role Role, p Push, epoch int64, how string) error
Prove writes the proof of name's push p under epoch and role for ProofTTL: a daemon holder's own proof of its round trip. ErrNotHooked when the record is not hooked at that epoch as that role; an error naming the lease when p is not live.
func (Store) Pushed ¶
Pushed reads what was last pushed for each name, and whether any push or baseline was ever written (false: the first pass of a fresh row).
func (Store) Read ¶
Read is each name's status at the store's now, in one round trip (a pipeline: the record, each push's lease and proof with their TTLs, the TIME).
func (Store) Renew ¶
func (s Store) Renew(ctx context.Context, name string, role Role, p Push, epoch int64, by string) error
Renew renews the lease of name's push p under epoch and role for LeaseTTL; ErrNotHooked when the record is not hooked at that epoch as that role.
func (Store) Serve ¶
Serve reads the heartbeat server's stamp and the store's TIME, in one round trip.
type Told ¶
Told is what an audience was last told: the unhooked set and the coordinator, and the generation of that telling.
type ToldMark ¶
type ToldMark struct{ Field, Value string }
ToldMark is one field of the server row (ServeKey) an audience's message writes in its own atomic step.
type Want ¶
type Want struct {
Name string
Need []Push
// Roles, when set, are the only roles the act is for: a record hooked as
// another role is refused even when that role carries every push Need
// names (a friend daemon arming the seat's name as friend opens none of the
// coordinator's verbs). Empty: any role that carries Need.
Roles []Role
Role Role
}
Want is what a gate asks of an identity: its name, the pushes the act needs, the roles the act is for, and the role the caller would hook in as when the record names none (the breadcrumb's role; "" offers every role that carries the pushes, and the remedy is to look first).
Directories
¶
| Path | Synopsis |
|---|---|
|
Package hooktest is the test rig of the hook record: a hook.Store over a miniredis reached through net.Pipe, so no TCP connection is made (the cleanup fails a test whose fake took one), on a clock that moves only when a test moves it.
|
Package hooktest is the test rig of the hook record: a hook.Store over a miniredis reached through net.Pipe, so no TCP connection is made (the cleanup fails a test whose fake took one), on a clock that moves only when a test moves it. |