#172: three more owner-notice conditions: no-browser-route, sidecar-down, adapter-broken
FaultsFrom judges three more faults at the shared twelve-hour OwnerWindow: a no-browser-route Site's refusing run (browser-backed Sites excluded), a sidecar no Lane has reached (one site-wide fault), and a Site where more than half of Series hold an old no-chapter failure row. recordPass fills the inputs from three new store reads (RefusingSince, SidecarOK, NoChapterShare), each failing independently and never failing a pass, and the clear loop forgets the empty-Site row too so a lifted sidecar-down fires again on return.
This commit is contained in:
@@ -568,18 +568,50 @@ func (p *Poller) recordPass(ctx context.Context, rec passRecord, fig passFigures
|
||||
|
||||
// ownerNotices judges the owner-notice conditions for the pass just recorded
|
||||
// and fires (issue #171). It sits in recordPass because that deferred call is
|
||||
// the one place every return path passes through: two of the four conditions
|
||||
// occur on early returns and the success path can never see them. Per fault:
|
||||
// NoticeSent → send → MarkNoticeSent, so a fault lasting a month sends one
|
||||
// message, not one per pass; a condition absent from this pass's fault list
|
||||
// forgets its episode, so the next occurrence sends again. Everything here is
|
||||
// best-effort: a failed send, a failed store read and a failed notice write
|
||||
// are all logged and never change the pass's outcome counts or its return
|
||||
// value. The clear runs even when Notify is nil, so a deployment that turns
|
||||
// the webhook off does not leave stale rows that suppress the first real
|
||||
// notice after it is turned back on.
|
||||
// the one place every return path passes through: three of the four
|
||||
// conditions occur on early returns and the success path can never see them.
|
||||
// The judgement reads the whole pass log plus three derived reads — the
|
||||
// other Lanes' latest passes, each Site's refusing-run start, its last
|
||||
// sidecar-reaching pass, and its no-chapter share — so one pass judges every
|
||||
// condition (issue #172). Per fault: NoticeSent → send → MarkNoticeSent, so
|
||||
// a fault lasting a month sends one message, not one per pass; a condition
|
||||
// absent from this pass's fault list forgets its episode, so the next
|
||||
// occurrence sends again. Everything here is best-effort: a failed send, a
|
||||
// failed store read and a failed notice write are all logged and never
|
||||
// change the pass's outcome counts or its return value. The clear runs even
|
||||
// when Notify is nil, so a deployment that turns the webhook off does not
|
||||
// leave stale rows that suppress the first real notice after it is turned
|
||||
// back on.
|
||||
func (p *Poller) ownerNotices(ctx context.Context, row store.LanePass) {
|
||||
faults := FaultsFrom(FaultInput{Passes: []store.LanePass{row}}, p.Now())
|
||||
now := p.Now()
|
||||
in := FaultInput{Passes: []store.LanePass{row}}
|
||||
// Each read fails independently: a failure logs and contributes no fault,
|
||||
// never a false one.
|
||||
if passes, err := p.Store.LatestLanePasses(); err != nil {
|
||||
log.Printf("latest poll %s: latest lane passes: %v", row.Site, err)
|
||||
} else {
|
||||
in.Passes = passes
|
||||
}
|
||||
if since, err := p.Store.RefusingSince(now.UnixMilli()); err != nil {
|
||||
log.Printf("latest poll %s: refusing since: %v", row.Site, err)
|
||||
} else {
|
||||
in.RefusingSince = since
|
||||
}
|
||||
if ok, err := p.Store.SidecarOK(browserBackedSites()); err != nil {
|
||||
log.Printf("latest poll %s: sidecar ok: %v", row.Site, err)
|
||||
} else {
|
||||
in.SidecarOK = ok
|
||||
}
|
||||
if share, err := p.Store.NoChapterShare(now.Add(-OwnerWindow).UnixMilli()); err != nil {
|
||||
log.Printf("latest poll %s: no-chapter share: %v", row.Site, err)
|
||||
} else {
|
||||
in.NoChapterShare = share
|
||||
}
|
||||
faults := FaultsFrom(in, now)
|
||||
bySite := make(map[string]store.LanePass, len(in.Passes))
|
||||
for _, pass := range in.Passes {
|
||||
bySite[pass.Site] = pass
|
||||
}
|
||||
for _, f := range faults {
|
||||
if p.Notify == nil {
|
||||
continue
|
||||
@@ -592,14 +624,20 @@ func (p *Poller) ownerNotices(ctx context.Context, row store.LanePass) {
|
||||
if sent {
|
||||
continue
|
||||
}
|
||||
sentence, href := noticeFor(f, row, p.Now())
|
||||
// The fault's own Site's pass renders its sentence — a stall judged
|
||||
// from another Lane's pass must not quote this pass's figures.
|
||||
pass, ok := bySite[f.Site]
|
||||
if !ok {
|
||||
pass = row
|
||||
}
|
||||
sentence, href := noticeFor(f, pass, now)
|
||||
if err := p.Notify.Notify(ctx, f, sentence, href); err != nil {
|
||||
// The stamp stays unset: no queue, no backoff — the condition is
|
||||
// durable, so the next pass tries again while it holds.
|
||||
log.Printf("latest poll %s: owner notice %s: %v", row.Site, f.Condition, err)
|
||||
continue
|
||||
}
|
||||
if err := p.Store.MarkNoticeSent(f.Condition, f.Site, p.Now().UnixMilli()); err != nil {
|
||||
if err := p.Store.MarkNoticeSent(f.Condition, f.Site, now.UnixMilli()); err != nil {
|
||||
log.Printf("latest poll %s: mark notice sent: %v", row.Site, err)
|
||||
}
|
||||
}
|
||||
@@ -609,6 +647,13 @@ func (p *Poller) ownerNotices(ctx context.Context, row store.LanePass) {
|
||||
log.Printf("latest poll %s: clear owner notice: %v", row.Site, err)
|
||||
}
|
||||
}
|
||||
// The site-wide conditions suppress under the empty Site; clear that
|
||||
// row too, so a lifted sidecar-down fires again when it returns.
|
||||
if !hasFault(faults, cond, "") {
|
||||
if err := p.Store.ClearNotice(cond, ""); err != nil {
|
||||
log.Printf("latest poll %s: clear owner notice: %v", row.Site, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user