mesh-mail: decide kept by settleSend's own returned revision, not its error, so a changed revision is actually retried instead of silently accepted as another writer's state; emit mail.send.failed from claimQueuedSend only once its own write lands (review round 2; issue 1198)
This commit is contained in:
@@ -759,11 +759,13 @@ func claimQueuedSend(store SendStore, record RouterRecord, emit Emit, id string,
|
||||
// crash between this and the write below must still leave it findable once refused does land;
|
||||
// tellSend itself tolerates finding it still sending, queued or retrying in the meantime.
|
||||
indexErr := addTellPending(store, req.ID)
|
||||
d, _, err := settleSend(store, req, rev, SendDecision{At: now, Outcome: "refused: " + reason})
|
||||
_ = emit(MailSendFailed, req.ID, OutcomeEvent{Send: req.ID, Account: req.Account, Outcome: d.Outcome})
|
||||
if err != nil {
|
||||
d, newRev, err := settleSend(store, req, rev, SendDecision{At: now, Outcome: "refused: " + reason})
|
||||
if newRev == 0 {
|
||||
// The write did not land (another writer's decision already stands, or it failed outright):
|
||||
// nothing of this call's own making to emit (novox/hq review round 2, issue 1198).
|
||||
return req, d, 0, claimRefused, errOr(indexErr, err)
|
||||
}
|
||||
_ = emit(MailSendFailed, req.ID, OutcomeEvent{Send: req.ID, Account: req.Account, Outcome: d.Outcome})
|
||||
// Only once refused is durably recorded is the sender-pending index's entry dropped.
|
||||
if rmErr := removeSenderPending(store, req.ID); rmErr != nil {
|
||||
indexErr = errOr(indexErr, fmt.Errorf("dropping %s from the sender-pending index: %w", req.ID, rmErr))
|
||||
@@ -775,11 +777,13 @@ func claimQueuedSend(store SendStore, record RouterRecord, emit Emit, id string,
|
||||
// A previous delivery may have handed it to the server already; never sent twice, and never
|
||||
// retried automatically either way (ADR 0436 clause 10). Same ordering as refuse() above.
|
||||
indexErr := addTellPending(store, req.ID)
|
||||
d, _, err := settleSend(store, req, rev, SendDecision{At: now, Outcome: "not proven", Attempt: req.Decided.Attempt})
|
||||
_ = emit(MailSendFailed, req.ID, OutcomeEvent{Send: req.ID, Account: req.Account, Outcome: d.Outcome})
|
||||
if err != nil {
|
||||
d, newRev, err := settleSend(store, req, rev, SendDecision{At: now, Outcome: "not proven", Attempt: req.Decided.Attempt})
|
||||
if newRev == 0 {
|
||||
// The write did not land: nothing of this call's own making to emit (novox/hq review round 2,
|
||||
// issue 1198).
|
||||
return req, d, 0, claimNotProven, errOr(indexErr, err)
|
||||
}
|
||||
_ = emit(MailSendFailed, req.ID, OutcomeEvent{Send: req.ID, Account: req.Account, Outcome: d.Outcome})
|
||||
if rmErr := removeSenderPending(store, req.ID); rmErr != nil {
|
||||
indexErr = errOr(indexErr, fmt.Errorf("dropping %s from the sender-pending index: %w", req.ID, rmErr))
|
||||
}
|
||||
@@ -887,7 +891,12 @@ func recordOutcome(store SendStore, emit Emit, req SendRequest, claimed SendDeci
|
||||
// fresh one) before giving up: kept is false only when the record's own write could not be kept even
|
||||
// after that retry, in which case the caller emits nothing and leaves the message to the bus entirely —
|
||||
// never announcing an outcome (sent, failed, retrying) that was not actually recorded (novox/hq review
|
||||
// round 1 finding 5).
|
||||
// round 1 finding 5). "Kept" is decided from settleSend's own returned revision, never from its error
|
||||
// alone: on a changed revision whose record already carries another writer's decision, settleSend
|
||||
// (through updateDecision) returns that other decision with revision 0 and a nil error — a nil error
|
||||
// is not success here, and without checking the revision the retry never ran at all, so the caller
|
||||
// could emit an outcome (and ack) while the record actually held someone else's state (novox/hq review
|
||||
// round 2, issue 1198).
|
||||
write := func(d SendDecision, final, tellWorthy bool) (result SendDecision, kept bool, outErr error) {
|
||||
var indexErr error
|
||||
if tellWorthy {
|
||||
@@ -897,11 +906,18 @@ func recordOutcome(store SendStore, emit Emit, req SendRequest, claimed SendDeci
|
||||
indexErr = addTellPending(store, req.ID)
|
||||
}
|
||||
curReq, curRev := req, rev
|
||||
var newRev uint64
|
||||
var writeErr error
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
result, _, writeErr = settleSend(store, curReq, curRev, d)
|
||||
if writeErr == nil || !errors.Is(writeErr, stdio.ErrChanged) {
|
||||
break
|
||||
result, newRev, writeErr = settleSend(store, curReq, curRev, d)
|
||||
if newRev != 0 {
|
||||
break // this write landed
|
||||
}
|
||||
if attempt == 1 {
|
||||
break // retried once already; give up
|
||||
}
|
||||
if writeErr != nil && !errors.Is(writeErr, stdio.ErrChanged) {
|
||||
break // a hard failure, not a revision race: retrying blindly would not help
|
||||
}
|
||||
fresh, freshRev, ok, rerr := getSend(store, req.ID)
|
||||
if rerr != nil || !ok {
|
||||
@@ -909,8 +925,12 @@ func recordOutcome(store SendStore, emit Emit, req SendRequest, claimed SendDeci
|
||||
}
|
||||
curReq, curRev = fresh, freshRev
|
||||
}
|
||||
if writeErr != nil {
|
||||
return d, false, errOr(indexErr, fmt.Errorf("the send %s's outcome could not be kept, even retried on a changed revision (%w)", req.ID, writeErr))
|
||||
if newRev == 0 {
|
||||
reportErr := writeErr
|
||||
if reportErr == nil {
|
||||
reportErr = fmt.Errorf("the send %s's outcome could not be kept: another writer's decision already stands, even retried on its fresh revision", req.ID)
|
||||
}
|
||||
return d, false, errOr(indexErr, reportErr)
|
||||
}
|
||||
if final {
|
||||
// Only once the outcome is durably recorded is the sender-pending index's entry dropped: from
|
||||
|
||||
Reference in New Issue
Block a user