Skip to content

fix: guard core DB upgrades against concurrent queueing#540

Merged
kilbot merged 6 commits intomainfrom
codex/fix-migration-lock-free
Feb 18, 2026
Merged

fix: guard core DB upgrades against concurrent queueing#540
kilbot merged 6 commits intomainfrom
codex/fix-migration-lock-free

Conversation

@kilbot
Copy link
Copy Markdown
Contributor

@kilbot kilbot commented Feb 18, 2026

Summary

  • add a short-lived core DB upgrade lock (woocommerce_pos_db_upgrade_lock) to avoid repeated migration queueing during concurrent requests
  • update Activator::version_check() to return early when a fresh lock exists
  • keep version bump behavior, and clear the lock in a finally block after deferred woocommerce_init migration execution
  • extend Test_Activator with regression tests for lock creation and lock-based migration skip behavior

Test plan

  • Run composer run lint in /Users/kilbot/Projects/woocommerce-pos
  • Run pnpm run test:unit:php in /Users/kilbot/Projects/woocommerce-pos
  • Verify Activator coverage includes:
    • test_no_migration_queued_when_fresh_upgrade_lock_exists
    • test_upgrade_lock_is_set_when_migration_is_queued
  • On a staging site, update WCPOS from an older DB version and confirm migration queueing happens once, with no repeated upgrade loop under concurrent admin/API requests

Summary by CodeRabbit

  • Bug Fixes

    • Improved database upgrade reliability by adding a short-lived upgrade lock, deferring the upgrade to initialization, and ensuring the lock is released in all paths (including a shutdown fallback).
  • Tests

    • Added tests covering upgrade-lock scenarios: fresh lock prevents migration, expired lock allows migration, lock is set when migration is queued, and shutdown fallback releases the lock.
  • Chores

    • Pinned a build dependency to ensure consistent front-end asset resolution.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ff11e6591a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread includes/Activator.php Outdated
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 18, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉


📝 Walkthrough

Walkthrough

Adds a TTL-based DB upgrade lock and lock-management helpers to Activator, reworks version_check() to acquire/release the lock, defers db_upgrade to the woocommerce_init hook with a shutdown fallback to release the lock, and adds tests covering lock presence, expiry, creation, and shutdown fallback behavior.

Changes

Cohort / File(s) Summary
Activator (upgrade lock + flow)
includes/Activator.php
Adds private constants DB_UPGRADE_LOCK_NAME and DB_UPGRADE_LOCK_TTL; new helpers acquire_db_upgrade_lock() and release_db_upgrade_lock(); reworks version_check() to cast old version, acquire a TTL-based WP Upgrader lock (fast add + expiry/steal path), defer db_upgrade to woocommerce_init, register a shutdown fallback to ensure lock release, and guarantee lock release on all code paths.
Tests (lock scenarios + shutdown fallback)
tests/includes/Test_Activator.php
Adds test constants DB_VERSION_OPTION and DB_UPGRADE_LOCK_OPTION; resets options and hooks in setUp/tearDown; replaces hard-coded option keys; adds tests: test_no_migration_queued_when_fresh_upgrade_lock_exists, test_migration_queued_when_upgrade_lock_has_expired, test_upgrade_lock_is_set_when_migration_is_queued, and test_shutdown_fallback_releases_upgrade_lock_when_migration_does_not_run.
Tooling (package override)
package.json
Adds top-level pnpm.overrides mapping to pin @wordpress/upload-media to 0.24.0 (package resolution override).

Sequence Diagram(s)

sequenceDiagram
  participant Activator
  participant WP_Options as "WP Options (DB)"
  participant WooCommerce as "woocommerce_init"
  participant PHP as "shutdown"

  Activator->>WP_Options: version_check() read old_version
  Activator->>Activator: cast old_version -> string, compare
  Activator->>Activator: acquire_db_upgrade_lock()
  Activator->>WP_Options: add_option(lock_key, now)
  alt add_option succeeded
    Activator->>WP_Options: schedule db_upgrade on woocommerce_init (defer)
    Activator->>PHP: register shutdown fallback to release lock
  else add_option failed (locked)
    Activator->>WP_Options: read lock timestamp
    alt lock fresh (not expired)
      Activator-->>Activator: bail (no migration queued)
    else lock expired
      Activator->>WP_Options: atomic UPDATE to steal stale lock
      alt update succeeded
        Activator->>WP_Options: schedule db_upgrade on woocommerce_init (defer)
        Activator->>PHP: register shutdown fallback to release lock
      else
        Activator-->>Activator: bail (another process won)
      end
    end
  end
  WooCommerce->>Activator: woocommerce_init triggers db_upgrade
  Activator->>Activator: try { db_upgrade(locked_old) } finally { release_db_upgrade_lock() }
  Activator->>WP_Options: delete_option(lock_key)
  PHP->>Activator: on shutdown -> ensure release_db_upgrade_lock()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I hop in with a TTL latch snug and bright,
If fresh it holds — I wait; if stale — I fight,
WooCommerce wakes and I finish the chore,
If it never comes, my shutdown paw unlocks the door,
Hoppity-hop — the DB sleeps safe through the night.

🚥 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 'fix: guard core DB upgrades against concurrent queueing' is specific, concise, and directly reflects the main purpose of the changeset: preventing repeated migration queueing during concurrent requests through a DB upgrade lock mechanism.
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 unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/fix-migration-lock-free

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

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

🧹 Nitpick comments (1)
tests/includes/Test_Activator.php (1)

184-198: Consider adding a test for expired lock scenario.

This test verifies that a fresh lock blocks migration, but there's no complementary test ensuring an expired lock allows migration to proceed. Given the TTL-based locking mechanism, testing the boundary condition would improve confidence.

💡 Suggested additional test
/**
 * Test: migration is queued when upgrade lock has expired.
 *
 * `@covers` ::version_check
 */
public function test_migration_queued_when_upgrade_lock_has_expired(): void {
    update_option( self::DB_VERSION_OPTION, '1.8.6' );
    // Set an expired lock (e.g., 60 seconds in the past, assuming TTL < 60s).
    update_option( self::DB_UPGRADE_LOCK_OPTION, time() - 60, false );

    $activator     = new Activator();
    $reflection    = new ReflectionClass( $activator );
    $version_check = $reflection->getMethod( 'version_check' );
    $version_check->setAccessible( true );
    $version_check->invoke( $activator );

    $this->assertTrue(
        has_action( 'woocommerce_init' ) !== false,
        'Migration should be queued when upgrade lock has expired'
    );
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/includes/Test_Activator.php` around lines 184 - 198, Add a
complementary test that asserts migration is queued when the TTL-based upgrade
lock has expired: in tests/includes/Test_Activator.php create a new test method
(e.g., test_migration_queued_when_upgrade_lock_has_expired) that sets
DB_VERSION_OPTION to '1.8.6', sets DB_UPGRADE_LOCK_OPTION to a timestamp
sufficiently in the past (e.g., time() - 60), instantiates Activator, uses
ReflectionClass to access and invoke the protected/private version_check method,
then assert that has_action('woocommerce_init') indicates the migration was
queued; reference the existing
test_no_migration_queued_when_fresh_upgrade_lock_exists for structure and reuse
the same reflection approach to call version_check.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@includes/Activator.php`:
- Around line 212-237: Replace the current non-atomic get_option/update_option
lock logic in Activator with WordPress's atomic lock APIs: require_once the
class-wp_upgrader.php and call WP_Upgrader::create_lock('wc_db_upgrade',
self::DB_UPGRADE_LOCK_TTL) to attempt to acquire the lock, return early if it
fails, then run Services\Settings::bump_versions() (and defer the rest of
woocommerce_init work) inside a try block and ensure you call
WP_Upgrader::release_lock('wc_db_upgrade') in a finally block so the lock is
always released.

---

Nitpick comments:
In `@tests/includes/Test_Activator.php`:
- Around line 184-198: Add a complementary test that asserts migration is queued
when the TTL-based upgrade lock has expired: in
tests/includes/Test_Activator.php create a new test method (e.g.,
test_migration_queued_when_upgrade_lock_has_expired) that sets DB_VERSION_OPTION
to '1.8.6', sets DB_UPGRADE_LOCK_OPTION to a timestamp sufficiently in the past
(e.g., time() - 60), instantiates Activator, uses ReflectionClass to access and
invoke the protected/private version_check method, then assert that
has_action('woocommerce_init') indicates the migration was queued; reference the
existing test_no_migration_queued_when_fresh_upgrade_lock_exists for structure
and reuse the same reflection approach to call version_check.

Comment thread includes/Activator.php
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 18, 2026

PHPUnit Test Results

    8 files    360 suites   9m 59s ⏱️
  755 tests   747 ✅  8 💤 0 ❌
6 040 runs  5 976 ✅ 64 💤 0 ❌

Results for commit 08d7ade.

♻️ This comment has been updated with latest results.

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.

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

Duplicate comments:
In `@includes/Activator.php`:
- Around line 249-280: The add_option() fast-path in acquire_db_upgrade_lock()
is TOCTOU-vulnerable; replace that branch with a call to
WP_Upgrader::create_lock() (using the same option name
self::DB_UPGRADE_LOCK_OPTION and value (string) $now) so the insertion uses
INSERT IGNORE/atomic semantics instead of get_option()+insert, and return true
when create_lock() indicates the lock was created; keep the existing stale-lock
UPDATE/steal logic unchanged.

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 `@includes/Activator.php`:
- Around line 232-241: The current flow sets a DB upgrade lock (via $locked_old)
and defers release to the closure attached with add_action('woocommerce_init',
...) which may never run; update the logic so that after bump_versions()
completes you either call release_db_upgrade_lock() immediately (if db_upgrade()
does not need the lock) or also register a shutdown handler
(register_shutdown_function) that calls release_db_upgrade_lock() as a safety
net; ensure the change references the existing bump_versions(), db_upgrade(...,
VERSION) call and release_db_upgrade_lock() so the lock is always released even
if the woocommerce_init hook never fires.

Comment thread includes/Activator.php
@kilbot kilbot merged commit dd60980 into main Feb 18, 2026
16 checks passed
@kilbot kilbot deleted the codex/fix-migration-lock-free branch February 18, 2026 22:52
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