| index e47b0919c..c196c7ed7 100644 |
| --- a/internal/host/movespaging.go |
| +++ b/internal/host/movespaging.go |
| @@ -44,15 +44,46 @@ import ( |
| // two, or a send that fails (logged), pages nothing more, as the console's |
| // move_pages records an attempt whether or not delivery worked (pages are |
| // best effort). A journal that cannot be read or written sends nothing, so a |
| -// rule that fires every pass can never flood. Unwired (nil Page), nothing |
| -// pages and nothing is marked. |
| +// rule that fires every pass can never flood. |
| +// |
| +// Unwired (nil Page, as production is until Plan 5 Task 9b wires it), a rule |
| +// is never silent: the agent's log says so loudly, once per rule per move |
| +// (the journal's unwired:<rule> step remembers it across a restart), and |
| +// nothing is refused (fix round 1, ruling 2). |
| |
| // pagingLogf is the agent's log for the paging code (a variable so tests |
| // can read it). |
| var pagingLogf = log.Printf |
| |
| -// pagedStep is the journal step that records a rule's page. |
| -const pagedStep = "paged:" |
| +// pagedStep is the journal step that records a rule's page; unwiredStep |
| +// records that its warning about an unwired pager was logged. |
| +const ( |
| + pagedStep = "paged:" |
| + unwiredStep = "unwired:" |
| +) |
| + |
| +// pageOutcome is what became of one rule's page. |
| +type pageOutcome int |
| + |
| +const ( |
| + pageSent pageOutcome = iota // handed to the pager without error |
| + pagePagedBefore // the journal says it was sent before |
| + pageUnwired // no pager is wired (logged loudly once) |
| + pageUndelivered // the journal or the pager failed (logged) |
| +) |
| + |
| +// String is the outcome as the agent's log says it. |
| +func (o pageOutcome) String() string { |
| + switch o { |
| + case pageSent: |
| + return "paged" |
| + case pagePagedBefore: |
| + return "paged before" |
| + case pageUnwired: |
| + return "not paged: no pager is wired" |
| + } |
| + return "not paged: the page was not delivered" |
| +} |
| |
| // The rules' names in the journal. |
| const ( |
| @@ -67,34 +98,54 @@ func pageRuleStuck(m *provision.MoveState) string { |
| } |
| |
| // pageOnce sends one page for rule unless the journal of (host, op) under |
| -// dataDir says it was sent: it marks the journal, then pages. sent is true |
| -// only when the page was handed to page without error. |
| -func pageOnce(ctx context.Context, page func(ctx context.Context, level, subject, body string) error, dataDir, host, op, rule string, now time.Time, level, subject, body string) (sent bool) { |
| +// dataDir says it was sent: it marks the journal, then pages. Unwired, it |
| +// warns instead (unwiredOnce). |
| +func pageOnce(ctx context.Context, page func(ctx context.Context, level, subject, body string) error, dataDir, host, op, rule string, now time.Time, level, subject, body string) pageOutcome { |
| if page == nil { |
| - return false |
| + unwiredOnce(dataDir, host, op, rule, now) |
| + return pageUnwired |
| } |
| j, err := moves.ReadJournal(dataDir, host, op) |
| if err != nil { |
| - log.Printf("moves: %s: the %s page of %s is not sent: its journal: %v", host, rule, op, err) |
| - return false |
| + pagingLogf("moves: %s: the %s page of %s is not sent: its journal: %v", host, rule, op, err) |
| + return pageUndelivered |
| } |
| if j.Done(pagedStep + rule) { |
| - return false |
| + return pagePagedBefore |
| } |
| j.Mark(pagedStep+rule, now.UTC().Format(time.RFC3339)) |
| if err := j.Write(dataDir); err != nil { |
| - log.Printf("moves: %s: the %s page of %s is not sent: its journal: %v", host, rule, op, err) |
| - return false |
| + pagingLogf("moves: %s: the %s page of %s is not sent: its journal: %v", host, rule, op, err) |
| + return pageUndelivered |
| } |
| if err := page(ctx, level, subject, body); err != nil { |
| - log.Printf("moves: %s: the %s page of %s: %v", host, rule, op, err) |
| - return false |
| + pagingLogf("moves: %s: the %s page of %s: %v", host, rule, op, err) |
| + return pageUndelivered |
| + } |
| + return pageSent |
| +} |
| + |
| +// unwiredOnce logs, loudly, that rule's page for op is not sent because no |
| +// pager is wired: once per rule per move, which the journal's unwired:<rule> |
| +// step remembers. A journal that cannot be read or written does not quiet |
| +// it: the warning is logged again the next time. |
| +func unwiredOnce(dataDir, host, op, rule string, now time.Time) { |
| + j, err := moves.ReadJournal(dataDir, host, op) |
| + if err == nil && j.Done(unwiredStep+rule) { |
| + return |
| + } |
| + pagingLogf("moves: %s: WARNING: no operator pager is wired on this box, so the %s page of move %s is not sent; the move goes on (Plan 5 Task 9b wires the pager)", host, rule, op) |
| + if err != nil { |
| + return |
| + } |
| + j.Mark(unwiredStep+rule, now.UTC().Format(time.RFC3339)) |
| + if err := j.Write(dataDir); err != nil { |
| + pagingLogf("moves: %s: the warning about the unwired %s page of %s is not recorded, so it is logged again: %v", host, rule, op, err) |
| } |
| - return true |
| } |
| |
| // pageOnce is the worker's pageOnce: its pager, journals and clock. |
| -func (w *movesWorker) pageOnce(ctx context.Context, host, op, rule, level, subject, body string) bool { |
| +func (w *movesWorker) pageOnce(ctx context.Context, host, op, rule, level, subject, body string) pageOutcome { |
| return pageOnce(ctx, w.cfg.Page, w.cfg.DataDir, host, op, rule, w.now(), level, subject, body) |
| } |
| |
| @@ -145,16 +196,17 @@ func (w *movesWorker) pageStray(ctx context.Context, rec provision.Record) { |
| return |
| } |
| subject, body := strayPage(rec.Host, w.cfg.Box) |
| - if !w.pageOnce(ctx, rec.Host, rec.Move.ID, pageRuleStray, alerting.LevelIncident, subject, body) { |
| + if w.pageOnce(ctx, rec.Host, rec.Move.ID, pageRuleStray, alerting.LevelIncident, subject, body) != pageSent { |
| return |
| } |
| pagingLogf("moves: %s: move %s: %s", rec.Host, rec.Move.ID, strayLine(w.cfg.Box, rec.Host)) |
| } |
| |
| // pageStartRefused pages an incident once per op for a refused §5.1 start |
| -// check on this box (§13). The probe owners' lock is held across the |
| -// journal's read and write, as their other journal writes hold it. |
| -func (m *movesProbeOwners) pageStartRefused(ctx context.Context, host, op string) { |
| +// check on this box (§13), and returns what became of the page. The probe |
| +// owners' lock is held across the journal's read and write, as their other |
| +// journal writes hold it. |
| +func (m *movesProbeOwners) pageStartRefused(ctx context.Context, host, op string) pageOutcome { |
| now := time.Now() |
| if m.Now != nil { |
| now = m.Now() |
| @@ -162,7 +214,7 @@ func (m *movesProbeOwners) pageStartRefused(ctx context.Context, host, op string |
| subject, body := startRefusedPage(host, op, m.Box) |
| m.mu.Lock() |
| defer m.mu.Unlock() |
| - pageOnce(ctx, m.Page, m.DataDir, host, op, pageRuleStartCheck, now, alerting.LevelIncident, subject, body) |
| + return pageOnce(ctx, m.Page, m.DataDir, host, op, pageRuleStartCheck, now, alerting.LevelIncident, subject, body) |
| } |
| |
| // rolledBackPage is the rolled_back page: the subject (MV-N385) and the |