Skip to content

fix: auto-reset stale is_publishing flag to prevent infinite Creating spinner#792

Merged
superdav42 merged 2 commits intomainfrom
bugfix/stuck-publishing-timeout
Apr 11, 2026
Merged

fix: auto-reset stale is_publishing flag to prevent infinite Creating spinner#792
superdav42 merged 2 commits intomainfrom
bugfix/stuck-publishing-timeout

Conversation

@superdav42
Copy link
Copy Markdown
Collaborator

@superdav42 superdav42 commented Apr 11, 2026

Summary

Fixes the infinite "Creating" spinner on the thank-you page when the site creation process is killed mid-execution.

Root cause: publish_pending_site() sets is_publishing=true on the pending site object and saves it to membership meta. If the PHP process dies (OOM, timeout, server restart) after the flag is set but before the site creation completes or error handlers run, the flag stays true permanently. The AJAX poll handler (wu_check_pending_site_created) returns running forever, and the Action Scheduler retry bails out because it sees the flag and assumes another process is already handling it.

Fix:

  • Add publishing_started_at timestamp to the Site model, automatically recorded when set_publishing(true) is called
  • Add is_publishing_stale($timeout) method — returns true when publishing has been running longer than the timeout (default 5 minutes)
  • check_pending_site_created() now detects stale state, resets the flag, re-enqueues the async action, and returns stopped so the JS can retry
  • Pre-2.5.3 serialized pending site objects (which lack the timestamp field) are treated as stale when is_publishing is true, since the process that set it is clearly gone

Files changed:

  • inc/models/class-site.php — new property, getter, and staleness check method
  • inc/managers/class-membership-manager.php — staleness detection in AJAX handler
  • tests/WP_Ultimo/Models/Site_Test.php — 7 new test cases for the staleness logic

Testing instructions:

  1. Install the build from this branch
  2. Go through a WooCommerce checkout flow that creates a site
  3. Verify the thank-you page transitions from "Creating" to the completed state
  4. To test the stuck state recovery: if a site is currently stuck in "Creating", this fix should auto-recover within 5 minutes of installing

Summary by CodeRabbit

Release Notes

  • New Features

    • Implemented automatic detection and recovery for site publishing processes that become stalled or unresponsive
    • Publishing tasks now automatically resume when a stalled state is detected beyond the timeout threshold
  • Bug Fixes

    • Resolved issues where sites could remain indefinitely stuck in an incomplete publishing state
  • Tests

    • Added comprehensive unit tests validating publishing state detection, timeout handling, and automatic recovery mechanisms

…g' spinner

When publish_pending_site() sets is_publishing=true but the PHP process is
killed mid-creation (OOM, timeout, server restart), the flag stays true
permanently. The AJAX polling handler returns 'running' forever, and the
Action Scheduler retry bails out because it sees is_publishing=true.

Fix:
- Add publishing_started_at timestamp to Site model, recorded automatically
  when set_publishing(true) is called
- Add is_publishing_stale() method with configurable timeout (default 5 min)
- check_pending_site_created() now detects stale state, resets the flag,
  re-enqueues the async action, and returns 'stopped' so the JS retry
  flow can pick it up
- Pre-2.5.3 serialized objects (without timestamp) are treated as stale
  when is_publishing is true, since the process that set it is clearly gone

Reported by Michael Carter (maatos.app) — site accessible immediately but
thank-you page stuck on 'Creating' indefinitely.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 11, 2026

📝 Walkthrough

Walkthrough

The changes implement stale publishing state detection for pending sites. A new publishing_started_at timestamp property tracks when site publishing begins. Enhanced logic detects when publishing is stuck beyond a timeout threshold, resets the publishing flag, persists changes, logs the event, and re-enqueues the publish task.

Changes

Cohort / File(s) Summary
Publishing State Tracking
inc/models/class-site.php
Added publishing_started_at property to track publishing start timestamp. Updated set_publishing() to record time() when publishing starts and reset to 0 when stopped. Introduced get_publishing_started_at() getter and is_publishing_stale() method to detect publishing states stuck beyond configurable timeout (300s default), with legacy support for pre-2.5.3 objects.
Stale State Detection & Recovery
inc/managers/class-membership-manager.php
Enhanced check_pending_site_created() to detect stale publishing states via is_publishing_stale(). When stale, sets is_publishing to false, persists the update, logs a membership-specific event, immediately re-enqueues wu_async_publish_pending_site, and returns publish_status: stopped.
Publishing Timestamp & Staleness Tests
tests/WP_Ultimo/Models/Site_Test.php
Added unit tests verifying set_publishing() records and clears timestamps correctly, and is_publishing_stale() behaves properly: returns false when not publishing, false immediately after start, true` after timeout, supports custom timeouts, and treats zero timestamps (legacy objects) as stale.

Sequence Diagram

sequenceDiagram
    participant MembMgr as Membership Manager
    participant PendSite as Pending Site
    participant Site as Site Model
    participant DB as Database
    participant Logger as Logger
    participant Queue as Async Queue
    
    MembMgr->>PendSite: check_pending_site_created()
    PendSite->>Site: is_publishing_stale()
    Site-->>PendSite: true (timeout exceeded)
    PendSite->>Site: set_publishing(false)
    Site->>DB: record timestamp reset
    PendSite->>MembMgr: update_pending_site()
    MembMgr->>DB: persist updated state
    MembMgr->>Logger: log stale publishing detected
    Logger-->>MembMgr: event logged
    MembMgr->>Queue: re-enqueue wu_async_publish_pending_site
    Queue-->>MembMgr: task queued
    MembMgr-->>MembMgr: return publish_status: stopped
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A timestamp to track when the publishing did start,
When it lingers too long, we detect the abort,
Reset the flags and re-queue the task,
Stale states are caught—problem solved at last! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main fix: detecting and resetting a stale is_publishing flag to resolve the infinite 'Creating' spinner issue.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ 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 bugfix/stuck-publishing-timeout

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@inc/managers/class-membership-manager.php`:
- Around line 221-227: The code currently enqueues
wu_enqueue_async_action('wu_async_publish_pending_site', ...) and then returns
wp_send_json(['publish_status' => 'stopped']), creating two competing retry
sources; remove the Action Scheduler enqueue here so only the frontend retry
(publish_status => 'stopped') is relied on, or alternatively gate the enqueue
behind a check that ensures Membership::publish_pending_site() won't be retried
by both sources (e.g., consult the serialized is_publishing flag or a new marker
before calling wu_enqueue_async_action). Update the block containing
wu_enqueue_async_action and wp_send_json to either drop the enqueue call
entirely or add the guard so only a single retry path is used.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a5ea38a7-c5cc-4f3a-867e-f8dd2603c664

📥 Commits

Reviewing files that changed from the base of the PR and between a4bb4df and 3c07e3b.

📒 Files selected for processing (3)
  • inc/managers/class-membership-manager.php
  • inc/models/class-site.php
  • tests/WP_Ultimo/Models/Site_Test.php

Comment on lines +221 to +227
/*
* Re-enqueue the async action so Action Scheduler retries
* the site creation without waiting for the next cron tick.
*/
wu_enqueue_async_action('wu_async_publish_pending_site', ['membership_id' => $membership->get_id()], 'membership');

wp_send_json(['publish_status' => 'stopped']);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid starting two retries from the stale branch.

This path already reports publish_status => 'stopped', and the PR contract says the frontend retries from that state. Enqueuing wu_async_publish_pending_site here means stale recovery can race a frontend retry against an Action Scheduler retry, while Membership::publish_pending_site() still only has the serialized is_publishing flag as a coarse guard. Pick one retry source here.

🔧 Suggested change
-			/*
-			 * Re-enqueue the async action so Action Scheduler retries
-			 * the site creation without waiting for the next cron tick.
-			 */
-			wu_enqueue_async_action('wu_async_publish_pending_site', ['membership_id' => $membership->get_id()], 'membership');
-
 			wp_send_json(['publish_status' => 'stopped']);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/*
* Re-enqueue the async action so Action Scheduler retries
* the site creation without waiting for the next cron tick.
*/
wu_enqueue_async_action('wu_async_publish_pending_site', ['membership_id' => $membership->get_id()], 'membership');
wp_send_json(['publish_status' => 'stopped']);
wp_send_json(['publish_status' => 'stopped']);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@inc/managers/class-membership-manager.php` around lines 221 - 227, The code
currently enqueues wu_enqueue_async_action('wu_async_publish_pending_site', ...)
and then returns wp_send_json(['publish_status' => 'stopped']), creating two
competing retry sources; remove the Action Scheduler enqueue here so only the
frontend retry (publish_status => 'stopped') is relied on, or alternatively gate
the enqueue behind a check that ensures Membership::publish_pending_site() won't
be retried by both sources (e.g., consult the serialized is_publishing flag or a
new marker before calling wu_enqueue_async_action). Update the block containing
wu_enqueue_async_action and wp_send_json to either drop the enqueue call
entirely or add the guard so only a single retry path is used.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 11, 2026

Performance Test Results

Performance test results for 9af2e77 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 40 (-1 / -3% ) 37.73 MB 813.00 ms (-41.00 ms / -5% ) 161.50 ms 1021.00 ms (-36.50 ms / -4% ) 1896.00 ms (-138.00 ms / -7% ) 1810.05 ms (-136.65 ms / -8% ) 88.40 ms (-3.65 ms / -4% )
1 56 49.02 MB 904.00 ms (+19.00 ms / +2% ) 143.00 ms 1047.00 ms 2008.00 ms 1921.95 ms 80.75 ms

@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

@superdav42 superdav42 merged commit 8c0e873 into main Apr 11, 2026
11 checks passed
@superdav42
Copy link
Copy Markdown
Collaborator Author

Summary

Fixes the infinite "Creating" spinner on the thank-you page when the site creation process is killed mid-execution.
Root cause: publish_pending_site() sets is_publishing=true on the pending site object and saves it to membership meta. If the PHP process dies (OOM, timeout, server restart) after the flag is set but before the site creation completes or error handlers run, the flag stays true permanently. The AJAX poll handler (wu_check_pending_site_created) returns running forever, and the Action Scheduler retry bails out because it sees the flag and assumes another process is already handling it.
Fix:

  • Add publishing_started_at timestamp to the Site model, automatically recorded when set_publishing(true) is called
  • Add is_publishing_stale($timeout) method — returns true when publishing has been running longer than the timeout (default 5 minutes)
  • check_pending_site_created() now detects stale state, resets the flag, re-enqueues the async action, and returns stopped so the JS can retry
  • Pre-2.5.3 serialized pending site objects (which lack the timestamp field) are treated as stale when is_publishing is true, since the process that set it is clearly gone
    Files changed:
  • inc/models/class-site.php — new property, getter, and staleness check method
  • inc/managers/class-membership-manager.php — staleness detection in AJAX handler
  • tests/WP_Ultimo/Models/Site_Test.php — 7 new test cases for the staleness logic
    Testing instructions:
  1. Install the build from this branch
  2. Go through a WooCommerce checkout flow that creates a site
  3. Verify the thank-you page transitions from "Creating" to the completed state
  4. To test the stuck state recovery: if a site is currently stuck in "Creating", this fix should auto-recover within 5 minutes of installing

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


aidevops.sh v3.6.235 spent 7m 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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant