Never publish the same post to the same channel twice - #10
Open
pallaoro wants to merge 2 commits into
Open
Conversation
Publishing was safe to run once. Three ordinary things run it again:
- the scheduler that fires a post at its scheduled time delivers at least
once, so a delivery whose response is lost comes back;
- the queue view flags a half-delivered post as needing attention, and the
obvious answer is to retry it;
- someone edits a post that has already gone out and saves it.
In each case publishPost() walked every channel on the post and sent again,
so the channels that had already succeeded got a second live post. There is
no undo on a published post, so this could not be cleaned up afterwards.
Delivery is now claimed per channel before anything is sent:
- a channel already marked published is reported with the post id and link
it got the first time, and nothing is sent;
- the rest are claimed with a compare-and-swap on the attempts counter (a
column that was written but never read), so two deliveries racing each
other resolve to exactly one send. A crashed run wedges nothing: the row
is still pending or failed, so the next retry claims it again;
- the post's own status is rolled up by re-reading post_channels, because a
retry only touches part of it and a concurrent delivery may have settled
the rest.
Editing a post no longer deletes and re-inserts its channel rows either. That
row is where the delivery state lives, so re-inserting it dropped the link to
a post that was already live, reset the channel to pending, and re-armed the
duplicate. Channels that stay on the post are updated in place; only the ones
actually removed are deleted.
With that, Retry finally works on a partial post — the status that most needs
it, and the one the button used to skip.
Adds `pnpm test`: the real API against an in-memory SQLite database, in
process, counting the calls made to each platform. All nine of these defects
reproduce on the previous code.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #9 (
per-channel-content), which this builds on.The problem
Publishing was safe to run once. Three ordinary things run it again:
In each case
publishPost()walked every channel on the post and sent again, so channels that had already succeeded got a second live post. There is no undo on a published post, so this could not be cleaned up afterwards.The fix
Delivery is claimed per channel before anything is sent:
publishedis reported with the post id and link it got the first time, and nothing is sent;attemptscounter (a column that was written but never read), so two deliveries racing each other resolve to exactly one send. A crashed run wedges nothing — the row is stillpending/failed, so the next retry claims it again;post_channels, because a retry only touches part of it and a concurrent delivery may have settled the rest.Editing a post no longer deletes and re-inserts its channel rows. That row is where the delivery state lives, so re-inserting it dropped the link to a post that was already live, reset the channel to
pending, and re-armed the duplicate. Channels that stay on the post are updated in place; only the ones actually removed are deleted.With that, Retry finally works on a
partialpost — the status that most needs it, and the one the button used to skip.What this deliberately does not cover: a crash after the platform accepted the post but before the result was written. Only a platform-side idempotency key could, and none of these APIs offers one. It's called out in a comment rather than left implied.
Verifying it
Adds
pnpm test— the real Hono API against an in-memory SQLite database, in process, with no dev server and nothing to deploy. It counts the calls the app actually makes to each platform, so "did this post go out twice" is a fact rather than an inference.Nine of those eighteen fail on the previous code — including a redelivery posting a second tweet and a second LinkedIn post, a retry re-tweeting, and an edit blanking the live post's link.
Needs Node 22.5+ for
node:sqlite.