Skip to content

fix(checkout): honour is_publishing_stale in publish_pending_site() to unblock stuck pending sites#1267

Merged
superdav42 merged 1 commit into
mainfrom
fix/publish-pending-site-stale-guard
May 23, 2026
Merged

fix(checkout): honour is_publishing_stale in publish_pending_site() to unblock stuck pending sites#1267
superdav42 merged 1 commit into
mainfrom
fix/publish-pending-site-stale-guard

Conversation

@superdav42

@superdav42 superdav42 commented May 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

publish_pending_site() in inc/models/class-membership.php bailed
silently when is_publishing=true, even after the publishing process
that set the flag had died. The result: the "Creating your site"
overlay hung on the customer's screen until the next Action Scheduler
retry — which often never fires, because the duplicate caller never
reschedules.

This change wires Site::is_publishing_stale() (added in 2.5.3 to
detect this exact "flag set but the process that set it is dead"
condition, default 5-minute window) into publish_pending_site() so
a second caller can finish the job the first caller never completed.
The AJAX poller at class-membership-manager.php:214 already consumed
is_publishing_stale(); this commit closes the symmetry on the
publish path.

A fresh in-flight flag still short-circuits, so the duplicate-call
short-circuit semantics are unchanged for the non-stale case.

Trigger

A real-world reproduction observed in production logs: a duplicate
$0-trial WooCommerce order arrives ~2.5 minutes after the first order
fails mid-publish (PHP process killed by max_execution_time / OOM /
fatal error). Order #1 set is_publishing=true and died; order #2's
publish_pending_site() call saw the flag and returned without doing
anything. The site stayed at type=site_template and the customer's
overlay never completed.

The duplicate-order root cause itself lives in the
ultimate-multisite-woocommerce addon; this is the core defence-in-depth
fix so the symptom self-heals after the 5-minute stale window.

Patch

if ($is_publishing) {
    $is_stale = method_exists($pending_site, 'is_publishing_stale')
        ? $pending_site->is_publishing_stale()
        : false;

    if ( ! $is_stale) {
        return true;
    }

    wu_log_add(/* … stale-recovery log line … */);
}

method_exists() guards the call so any pre-2.5.3 serialized Site
objects deserialized from membership meta still behave like the old
code path. The recovery emits a wu_log_add() line on the membership
log so support can correlate stuck-overlay reports.

Regression coverage

tests/WP_Ultimo/Models/Membership_Test.php

  • test_publish_pending_site_bails_when_flag_is_fresh — a fresh flag
    must still short-circuit (guards against accidentally rewriting the
    check into "always publish").
  • test_publish_pending_site_proceeds_when_flag_is_stale — a stale
    flag must fall through and actually publish the blog. This is the
    regression guard for the BUG fix; reverting just the inc/ patch and
    re-running this test fails as expected.

Both tests use wu_create_customer / wu_create_product /
wu_create_membership (BerlinDB-persisting helpers); the class-level
setUp() uses raw Model constructors which can leave the in-memory ID
at 0 in some environments, silently no-op'ing the meta calls these
tests depend on.

Verification

  • PHPCS on changed inc/ file: clean
  • PHPCS on new test block: clean (lines 1764+; pre-existing warnings
    elsewhere in the test file are not touched)
  • PHPStan level 0: [OK] No errors
  • New tests: OK (2 tests, 14 assertions)
  • Full Membership_Test class (118 tests): green
  • Deterministic WP-CLI reproduction script confirmed the bug
    pre-patch and confirmed the fix post-patch on the dev install

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of pending site publishing when a publishing operation appears to be running but has stalled. The system now detects stale publishing states and completes the process.
  • Tests

    • Added test coverage for pending site publishing recovery scenarios.

Review Change Stack

…o unblock stuck pending sites

When two callers race to publish the same pending site (most commonly a
duplicate $0-trial WooCommerce order, but also any caller retried while a
prior PHP process died mid-publish), the second caller observed
is_publishing=true and returned without doing anything, leaving the
"Creating your site" overlay hung on the customer's screen until the next
Action Scheduler retry — which often never fires, because the duplicate
caller never reschedules.

Site::is_publishing_stale() was added in 2.5.3 to detect this exact
"flag set but the process that set it is dead" condition (default 5
minute window) and the AJAX poller already consumes it; this change
wires it into publish_pending_site() too, so the second caller can
finish the job the first caller never completed.

A fresh in-flight flag still short-circuits as before. method_exists()
guards the call so any pre-2.5.3 serialized Site objects in stored meta
still behave like the old code path. The recovery emits a wu_log_add()
line on the membership log so support can correlate the symptom.

Regression coverage in tests/WP_Ultimo/Models/Membership_Test.php:

- test_publish_pending_site_bails_when_flag_is_fresh: a fresh flag must
  still short-circuit (guards against accidentally rewriting the check
  into "always publish").
- test_publish_pending_site_proceeds_when_flag_is_stale: a stale flag
  must fall through and actually publish the blog (this is the
  regression guard for the BUG 2 fix).

Both tests use wu_create_customer/wu_create_product/wu_create_membership
to build a fully-persisted membership; the class-level setUp() uses raw
Model constructors which can leave the in-memory ID at 0 in some
environments, silently no-op'ing the meta calls the test depends on.

The duplicate-order trigger in the WooCommerce gateway addon is filed
separately; this commit fixes the core bail-out only.
@coderabbitai

coderabbitai Bot commented May 23, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 22a07ebd-09c4-4f04-aa8f-e965ce3cf84e

📥 Commits

Reviewing files that changed from the base of the PR and between 2a6c033 and 6d3feb4.

📒 Files selected for processing (2)
  • inc/models/class-membership.php
  • tests/WP_Ultimo/Models/Membership_Test.php

📝 Walkthrough

Walkthrough

Membership::publish_pending_site() adds detection for stale is_publishing flags, recovering stuck publishing states by logging a message and proceeding when the flag is detected as stale. Comprehensive test coverage verifies both the fresh-flag short-circuit path and the stale-flag recovery path, including blog record creation validation.

Changes

Stale Publishing Flag Recovery

Layer / File(s) Summary
Stale publishing flag detection logic
inc/models/class-membership.php
publish_pending_site() checks if the pending site's is_publishing flag is stale via Site::is_publishing_stale() when available. If stale, it logs a "stale flag detected" message and falls through to complete publishing; otherwise it returns early as before.
Test fixtures and coverage
tests/WP_Ultimo/Models/Membership_Test.php
Two helpers (make_membership_with_pending_meta(), attach_pending_site_with_publishing()) create fully persisted test fixtures with controllable publishing timestamps. Two tests verify that a fresh is_publishing flag causes publish_pending_site() to short-circuit (no blog row created), and a stale flag allows recovery with blog row creation.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • Ultimate-Multisite/ultimate-multisite#797: Prevents check_pending_site_created() from enqueueing a redundant async retry when the is_publishing flag is stale, complementing this PR's early-return bypass in publish_pending_site().
  • Ultimate-Multisite/ultimate-multisite#792: Also uses Site::is_publishing_stale() to override short-circuiting behavior in the AJAX pending-site handler, addressing the same stuck-publishing scenario from a different entry point.

Suggested labels

bug, origin:worker

Poem

A flag grown stale need not delay,
The pending site now finds its way—
Through careful check and logged confession,
Lost publishes find redemption,
Tests ensure the path runs true. 🐰

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title directly describes the main change: honouring the is_publishing_stale flag to unblock stuck pending sites.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/publish-pending-site-stale-guard

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown

🔨 Build Complete - Ready for Testing!

📦 Download Build Artifact (Recommended)

Download the zip build, upload to WordPress and test:

🌐 Test in WordPress Playground (Very Experimental)

Click the link below to instantly test this PR in your browser - no installation needed!
Playground support for multisite is very limitied, hopefully it will get better in the future.

🚀 Launch in Playground

Login credentials: admin / password

@github-actions

Copy link
Copy Markdown

Performance Test Results

Performance test results for b8bdbdd are in 🛎️!

Note: the numbers in parentheses show the difference to the previous (baseline) test run. Differences below 2% or 0.5 in absolute values are not shown.

URL: /

Run DB Queries Memory Before Template Template WP Total LCP TTFB LCP - TTFB
0 46 (+5 / +10% ) 37.93 MB 866.00 ms (-68.00 ms / -8% ) 150.50 ms (-16.50 ms / -11% ) 1038.50 ms (-73.50 ms / -7% ) 2056.00 ms 1973.80 ms (-42.75 ms / -2% ) 82.60 ms (+5.05 ms / +6% )
1 56 49.13 MB 883.50 ms (-66.50 ms / -8% ) 143.50 ms (-3.50 ms / -2% ) 1029.00 ms (-68.50 ms / -7% ) 2022.00 ms (-78.00 ms / -4% ) 1948.35 ms (-77.75 ms / -4% ) 74.60 ms

@superdav42 superdav42 merged commit f8fc41b into main May 23, 2026
11 checks passed
@superdav42

Copy link
Copy Markdown
Collaborator Author

Summary

publish_pending_site() in inc/models/class-membership.php bailed
silently when is_publishing=true, even after the publishing process
that set the flag had died. The result: the "Creating your site"
overlay hung on the customer's screen until the next Action Scheduler
retry — which often never fires, because the duplicate caller never
reschedules.
This change wires Site::is_publishing_stale() (added in 2.5.3 to
detect this exact "flag set but the process that set it is dead"
condition, default 5-minute window) into publish_pending_site() so
a second caller can finish the job the first caller never completed.
The AJAX poller at class-membership-manager.php:214 already consumed
is_publishing_stale(); this commit closes the symmetry on the
publish path.
A fresh in-flight flag still short-circuits, so the duplicate-call
short-circuit semantics are unchanged for the non-stale case.

Trigger

A real-world reproduction observed in production logs: a duplicate
$0-trial WooCommerce order arrives ~2.5 minutes after the first order
fails mid-publish (PHP process killed by max_execution_time / OOM /
fatal error). Order #1 set is_publishing=true and died; order #2's
publish_pending_site() call saw the flag and returned without doing
anything. The site stayed at type=site_template and the customer's
overlay never completed.
The duplicate-order root cause itself lives in the
ultimate-multisite-woocommerce addon; this is the core defence-in-depth
fix so the symptom self-heals after the 5-minute stale window.

Patch

if ($is_publishing) {
    $is_stale = method_exists($pending_site, 'is_publishing_stale')
        ? $pending_site->is_publishing_stale()
        : false;
    if ( ! $is_stale) {
        return true;
    }
    wu_log_add(/* … stale-recovery log line … */);
}

method_exists() guards the call so any pre-2.5.3 serialized Site
objects deserialized from membership meta still behave like the old
code path. The recovery emits a wu_log_add() line on the membership
log so support can correlate stuck-overlay reports.

Regression coverage

tests/WP_Ultimo/Models/Membership_Test.php

  • test_publish_pending_site_bails_when_flag_is_fresh — a fresh flag
    must still short-circuit (guards against accidentally rewriting the
    check into "always publish").
  • test_publish_pending_site_proceeds_when_flag_is_stale — a stale
    flag must fall through and actually publish the blog. This is the
    regression guard for the BUG fix; reverting just the inc/ patch and
    re-running this test fails as expected.
    Both tests use wu_create_customer / wu_create_product /
    wu_create_membership (BerlinDB-persisting helpers); the class-level
    setUp() uses raw Model constructors which can leave the in-memory ID
    at 0 in some environments, silently no-op'ing the meta calls these
    tests depend on.

Verification

  • PHPCS on changed inc/ file: clean
  • PHPCS on new test block: clean (lines 1764+; pre-existing warnings
    elsewhere in the test file are not touched)
  • PHPStan level 0: [OK] No errors
  • New tests: OK (2 tests, 14 assertions)
  • Full Membership_Test class (118 tests): green
  • Deterministic WP-CLI reproduction script confirmed the bug
    pre-patch and confirmed the fix post-patch on the dev install

Merged via PR #1267 to main.
Merged by deterministic merge pass (pulse-wrapper.sh).


aidevops.sh v3.17.28 spent 32s on this as a headless bash routine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review-feedback-scanned Merged PR already scanned for quality feedback

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant