b9e963bf Changed failed newsletter pages to discard their pending updates
ref https://linear.app/ghost/issue/BER-3933/make-batched-email-event-writes-transactional-with-exact-per-email
Review found that a page which failed after some emails had committed
kept every pending entry, including the committed ones, and that a
failure before the flush (for example while saving a permanent failure)
left the queued page behind entirely. The storage is shared by every
fetch job, so the next job would flush those entries and report another
page's transitions as its own, and a persistently failing email re-ran
a locking read for every accumulated email on each attempt. The batch
processor now owns the page lifecycle: pending updates are discarded
whether or not the flush ran or succeeded. The caller already keeps its
cursor behind a failed page and replays it, and the guarded updates make
that replay a no-op for rows that did commit.
Lock waits are as likely as deadlocks while batch creation inserts
recipients for the same email, so ER_LOCK_WAIT_TIMEOUT is retried too,
with jitter. The guarded update now asserts it matched the locked set,
so a database without row locks fails loudly instead of reporting a
transition that did not happen. The three batched handlers share one
queueing helper, MySQL detection reuses DatabaseInfo, and the JSDoc
return type now references the instance method rather than the class.
The MySQL plan checks also assert a primary-key range access path for
a multi-type page.