fix: expire stale Instant Sharing queue entries instead of flooding accounts - #1104
Conversation
Fixes four defects in the publish-now queue reported in #1102: - WP_Query ignores numberposts, so the drain batch silently fell back to the posts_per_page option; use posts_per_page => 300 - queue entries had no staleness cutoff, so a stalled cron blasted months-old posts on recovery; entries older than a filterable rop_publish_now_expiration (default 1 day) are now marked expired instead of shared - routine edits of already-published posts re-queued shares from leftover meta; wp_after_insert_post now only queues on new publishes unless the classic metabox explicitly submits publish_now - entries with no accounts left rop_publish_now_status stuck at queued forever; the status is now cleared when they are skipped Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PHPUnit (tests/test-publish-now.php, new `publish-now` suite): 8 of the 15 tests fail on unfixed development, one per defect — stale entries sharing, a backlog delaying the fresh post, the batch capped at posts_per_page, orphan entries stuck at queued, an edit of a published post re-queueing, and the metabox pre-checking itself on archive content. The other 7 are guards for the paths that must keep sharing: publishing a draft, a scheduled post going live, an explicit Classic Editor submit, and the Block Editor re-share action. The edit test clears `rop_maybe_publish_now_<id>` first. Publishing sets that transient for a minute, so without clearing it the test passes on unfixed code for the wrong reason instead of exercising the defect. E2E (publish-now-backlog.spec.js): seeds three months-old queue entries via a new `/queued-post` endpoint, publishes a post through the editor and asserts the mocked X API only ever receives the fresh one. On unfixed code all four posts are shared — the customer's flood. A second test shares an entry queued seconds ago so the expiry cannot regress into dropping legitimate shares. Also clears leftover `rop_publish_now_history` in `/reset`, which otherwise leaks between runs, and adds `/publish-now-state` so tests can assert on queue meta rather than on editor UI a cron run can erase. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Fixes stale Instant Sharing queues so expired posts are skipped instead of shared after cron recovery.
Changes:
- Adds expiration and orphan handling for queued shares.
- Prevents routine edits from re-queuing published posts.
- Adds PHPUnit and E2E regression coverage.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
includes/class-rop.php |
Registers the new post-save wrapper. |
includes/admin/class-rop-admin.php |
Adjusts post-save and metabox behavior. |
includes/admin/models/class-rop-queue-model.php |
Expires stale queue entries. |
includes/admin/models/class-rop-posts-selector-model.php |
Corrects the query batch parameter. |
phpunit.xml |
Registers the publish-now test suite. |
tests/test-publish-now.php |
Adds queue regression tests. |
tests/e2e/fixtures/index.js |
Exposes new E2E helpers. |
tests/e2e/mu-plugins/rop-e2e-bootstrap.php |
Adds backlog fixtures and state endpoints. |
tests/e2e/specs/dashboard/publish-now-backlog.spec.js |
Tests stale-backlog behavior end to end. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ), | ||
| ), | ||
| 'numberposts' => 300, | ||
| 'posts_per_page' => 300, // NOTE: WP_Query ignores `numberposts`; without this the batch silently fell back to the site's `posts_per_page` option. |
There was a problem hiding this comment.
Fixed in 79677b0. Confirmed the gap: the drain runs on a single event and rop_cron_job_publish_now never scheduled a follow-up, so with more than one batch of queued rows the oldest 300 were retired and everything after them — possibly including the just-published post — waited for an unrelated share event.
build_queue_publish_now() now calls manage_cron( array( 'action' => 'publish-now' ) ) when the selector returned a full batch. This terminates: get_publish_now_posts() sets rop_publish_now = 'no' on every row it returns, so each pass removes its own batch from the query window and the window strictly shrinks.
The batch size moved to Rop_Posts_Selector_Model::PUBLISH_NOW_BATCH_SIZE behind a new rop_publish_now_batch_size filter, which also makes it testable — test_full_batch_schedules_another_pass drops it to 2, queues 3 entries and asserts a follow-up event exists; test_partial_batch_does_not_reschedule guards the other direction so a drained queue does not keep rescheduling itself.
| if ( ! $accounts || ! is_array( $accounts ) ) { | ||
| // NOTE: clear the status, otherwise the entry lingers as "queued" forever. | ||
| delete_post_meta( $post_id, 'rop_publish_now_status' ); | ||
| continue; | ||
| } |
There was a problem hiding this comment.
Good catch, fixed in 79677b0. Verified against src/instant/PostUpdate.js — isQueued = history.some( item => 'queued' === item.status ) at :134, and :155 renders the spinner on 'queued' === status || isQueued, so a leftover queued history row does keep the sidebar polling every 5s indefinitely even with the top-level status gone.
The orphan branch now retires the entry through expire_publish_now() instead of just deleting the status, so the accounts meta, the status and the history rows are all cleaned up together. expire_publish_now() took an optional $reason so the log line distinguishes the two cases — "has no accounts left to share to" vs "expired before it could be shared".
Covered by test_orphan_entry_retires_history, which asserts no queued row survives; it fails on the previous commit.
| if ( $post_before instanceof WP_Post && 'publish' === $post_before->post_status && empty( $_POST['publish_now'] ) ) { | ||
| return; |
There was a problem hiding this comment.
You're right, and this was the sharpest of the three — fixed in 79677b0.
The interaction is exactly as described: publish_now_attributes() pre-checks the box precisely when rop_publish_now is yes, i.e. when a share is still pending, so every ordinary Classic Editor save of such a post submits publish_now, passes the guard, and update_publish_now_history() merges into the existing queued row and overwrites timestamp with time(). A stalled entry's expiry clock reset on every edit, which defeats the cutoff this PR adds.
I went with "leave an already-queued request alone" rather than preserving the timestamp inside update_publish_now_history(), because the deliberate re-share path (REST share/{id} → rop_publish_now_instant_share → maybe_publish_now( $id, true )) merges into that same row and does want a fresh timestamp — preserving it unconditionally would make an explicit re-share expire instantly. Since a post that is already queued has nothing to add, maybe_publish_now_after_insert() now returns early on published posts when rop_publish_now_status is queued, which keeps the original timestamp intact as a side effect. Classic re-shares of a published post that is not already queued still work.
test_saving_a_pending_share_does_not_refresh_its_timestamp covers it: a post queued 5 days ago, edited with publish_now submitted, must keep its original timestamp and must still expire instead of sharing. On the previous commit it fails with the timestamp exactly 432000s newer.
- reschedule another drain pass when a full batch is consumed, so a fresh post behind a large stale backlog is not stranded until an unrelated share event (batch size now filterable via rop_publish_now_batch_size) - retire orphaned entries (no accounts left) through the expiration path so their queued history rows are cleared and the editor stops polling forever - skip re-queueing on ordinary saves of a post with a pending share: the Classic metabox keeps publish_now checked, and refreshing the history timestamp let stalled entries evade the expiration cutoff Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.
Suppressed comments (1)
includes/admin/models/class-rop-queue-model.php:452
- Deleting this registered meta does not leave a neutral value:
rop_publish_now_statushas the REST/meta defaultpending(includes/admin/class-rop-admin.php:2141-2148). For an expired legacy entry or orphan with no history, the editor then skips its only empty-history guard (which requiresdoneatsrc/instant/PostUpdate.js:164) and renders the successful-share UI with an empty history. Retire the request with the established terminal status instead.
delete_post_meta( $post_id, 'rop_publish_now_status' );
Summary
On a site whose cron stalled, Instant Sharing entries piled up for months and flooded every connected account when cron recovered (#1102). Queue entries older than a filterable cutoff (default one day) now expire instead of sharing.
Queue drain —
build_queue_publish_now()marks an entry older than therop_publish_now_expirationcutoff asexpiredand logs it. An entry without history predates the tracking meta and counts as stale.Batch size —
WP_Queryignoresnumberposts, so the real batch fell back to theposts_per_pagesite option (about 10). The query now passesposts_per_page, filterable throughrop_publish_now_batch_size(default 300).Drain rescheduling — a pass that consumes a full batch schedules another pass. A fresh post behind a large backlog no longer waits for an unrelated share event.
Save hook —
wp_after_insert_postnow routes throughmaybe_publish_now_after_insert(). An edit of an already-published post queues a share only on an explicit Classic Editor metabox submit. A save while a request isqueuedchanges nothing, so a stalled entry cannot reset its expiry clock.Metabox default — on an already-published post, the share checkbox is pre-checked only while a share is still pending. Before, Enable Instant Sharing By Default pre-checked it on every archive post, which is how the backlog grew for the reporting customer.
Orphaned entries — an entry with no accounts left retires through the same expiration path. Its
queuedhistory rows are cleared, so the editor stops polling "Posting to social media…".Note
The deliberate re-share paths do not change: the Block Editor re-share button (REST
shareendpoint →maybe_publish_now( $id, true )) and a scheduled post going live (transition_post_status()). The recurring-queue fixes in #1101 / #1103 touchrop_cron_jobandbuild_query_args; the two changes do not overlap in code.Instant Sharing drain flow
flowchart LR T["Share or publish event"] --> B["Changed: fetch up to 300<br/>queued entries"]:::changed B --> A{"Accounts<br/>left?"} A -- "No" --> X["New: mark expired,<br/>clear queue meta"]:::added A -- "Yes" --> O{"New: queued over<br/>one day ago?"}:::added O -- "Yes" --> X O -- "No" --> S["Share to accounts"] S --> F{"New: full batch<br/>consumed?"}:::added F -- "Yes" --> R["New: schedule<br/>another pass"]:::added F -- "No" --> D["Drain done"] classDef added fill:#1a7f37,color:#fff,stroke:#116329,stroke-width:3px classDef changed fill:#9a6700,color:#fff,stroke:#5c3d00,stroke-width:3px,stroke-dasharray:6 3Stored meta on expiry
Before, a stale entry kept
rop_publish_now_status = queuedforever. After, expiry clears the queue meta and the history row showsexpired.rop_publish_now_statusrop_publish_now_accountsrop_publish_now_history[].statusqueued→expiredWill affect the visual aspect of the product
NO. A stalled instant share stops showing the endless "Posting to social media…" spinner and reports
Expiredin the Sharing History table.Test instructions
Go to
WP Admin → Revive Social → Dashboardand connect one social account.Expect: the account shows as active on the dashboard.
Open the General Settings tab. Turn on Enable Instant Sharing Feature (Post on Publish) and Enable Instant Sharing By Default, then save.
Expect: both settings stay on after a page reload.
Add
define( 'DISABLE_WP_CRON', true );towp-config.phpand make sure no system cron runs. Publish three posts.Expect:
wp post meta get <ID> rop_publish_now_statusprintsqueuedfor each post, and nothing is shared.Backdate the three queue entries. Run this after step 3, while cron stays disabled. Replace
101, 102, 103with the post IDs:Expect:
wp post meta get 101 rop_publish_now_historyshows a timestamp about 90 days in the past.Remove
DISABLE_WP_CRONand publish one new post.Expect: only the new post reaches the connected account. For each old post,
rop_publish_now_statusis empty and the history status isexpired.Open one of the expired posts in the editor and click Update without touching the Revive Social panel.
Expect: no new share reaches the account, and
rop_publish_now_statusstays empty.Check before Pull Request is ready: