Skip to content

[MINOR BC] [4.x] Fix pending tenant pull race conditions#1463

Open
lukinovec wants to merge 5 commits into
masterfrom
pending-fixes
Open

[MINOR BC] [4.x] Fix pending tenant pull race conditions#1463
lukinovec wants to merge 5 commits into
masterfrom
pending-fixes

Conversation

@lukinovec

@lukinovec lukinovec commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

pullPendingFromPool had a race condition when user A attempted to pull a tenant at the same time as user B. Both could end up grabbing the same tenant, and the result was unexpected, e.g. one of them ending up with no pending tenant pulled at all even though there was a pending tenant in the pool.

instead of selecting a pending tenant and updating the same model, we now run update() conditionally -- it clears pending_since only if the tenant is still pending, and we check the affected row count. Only one process can get a row back, the other gets 0 and retries with the next pending candidate in the pool. The loop always terminates since every lost claim means the pool shrank by one. Eventually it's empty and we create a new tenant (or return null).

The claim and the attribute update happen in a single transaction now, so if updating $attributes fails, the claim rolls back and the tenant stays in the pool.

Added a regression test that simulates a concurrent "steal" synchronously via a PullingPendingTenant listener. Fails with the old code, passes with the HasPending changes.

Very minor BC:

  • Clearing pending_since no longer fires model updating/updated events (since the update goes through query builder). PendingTenantPulled still fires the same as before and is the listener you'd want to use anyway.
  • PullingPendingTenant now fires before the claim (and outside the transaction), so it can fire more than once with concurrent pulls (e.g. when a tenant gets claimed by someone else). PendingTenantPulled is still the one that fires exactly once for the actually pulled tenant.

Summary by CodeRabbit

  • Bug Fixes

    • Safer pending-tenant claiming with retry and atomic claim handling to prevent lost or double claims under concurrency.
    • Ensure failed attribute application is rolled back so tenants remain in the pending pool when a claim fails.
  • Tests

    • Added tests for concurrent claiming behavior and for failure during attribute application to verify retry and rollback logic.

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 465fb294-8786-44a1-8f91-b25e2074ee1e

📥 Commits

Reviewing files that changed from the base of the PR and between 698589e and e5234da.

📒 Files selected for processing (1)
  • tests/PendingTenantsTest.php

📝 Walkthrough

Walkthrough

This PR refactors pullPendingFromPool() into a retrying loop that atomically claims pending tenants with a conditional update, emitting PullingPendingTenant and PendingTenantPulled events, and adds tests for concurrent stealing and transactional rollback on attribute-write failure.

Changes

Concurrency-safe pending tenant pulling

Layer / File(s) Summary
Retry-loop claiming implementation
src/Database/Concerns/HasPending.php
pullPendingFromPool now loops to select fresh pending candidates, emits PullingPendingTenant, performs an atomic transaction that clears pending_since only if the row is still pending, retries on contention, reloads and applies attributes, emits PendingTenantPulled, and returns the claimed tenant.
Concurrent and rollback test cases
tests/PendingTenantsTest.php
Adds DB facade import and two tests: one intercepts PullingPendingTenant to simulate stealing a candidate and verifies the pull skips it and drains the pool; the other alters tenants.slug to a nullable unique constraint, seeds a conflicting tenant, asserts a QueryException on attribute application, and verifies the pulled tenant remains pending after rollback.
sequenceDiagram
  participant TestCaller
  participant HasPending
  participant Database
  participant Tenant

  TestCaller->>HasPending: pullPendingFromPool()
  loop retry until success or empty
    HasPending->>Database: onlyPending()->first()
    alt found
      HasPending->>HasPending: emit PullingPendingTenant(candidate)
      HasPending->>Database: conditional update pending_since -> null (whereKey)
      alt success
        HasPending->>Database: findOrFail(candidate)
        HasPending->>Tenant: update(attributes)
        HasPending->>HasPending: emit PendingTenantPulled(tenant)
        HasPending-->>TestCaller: return tenant
      else lost
        HasPending->>HasPending: loop again
      end
    else none
      HasPending-->>TestCaller: create or return null
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • stancl

Poem

🐰 I hop in loops to find a spare,
I nudge the rows with gentle care,
If another snatches what I sought,
I try again till one is caught,
Transactions keep my web threadbare.

🚥 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 title directly describes the main change: fixing race conditions in pending tenant pulling logic, which is the core purpose of this PR.
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 pending-fixes

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.

@lukinovec lukinovec changed the title [4.x] Fix race conditions while pulling pending tenants [4.x] Fix pending tenant pull race conditions Jun 11, 2026
@codecov

codecov Bot commented Jun 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.03%. Comparing base (652bc98) to head (e5234da).

Additional details and impacted files
@@             Coverage Diff              @@
##             master    #1463      +/-   ##
============================================
+ Coverage     86.00%   86.03%   +0.02%     
- Complexity     1175     1178       +3     
============================================
  Files           185      185              
  Lines          3429     3436       +7     
============================================
+ Hits           2949     2956       +7     
  Misses          480      480              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/PendingTenantsTest.php`:
- Around line 157-160: The test currently calls $process->wait() with no timeout
which can hang; set a reasonable timeout on each Process before waiting (e.g.,
call $process->setTimeout(10) on the $process instances in the loop or where
processes are created) and then call $process->wait(); keep the existing
assertion expect($process->isSuccessful())->toBeTrue($process->getErrorOutput())
so failures surface quickly instead of blocking CI.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6bac5abc-b34a-4fd8-8ae4-6b8c0e2e28c4

📥 Commits

Reviewing files that changed from the base of the PR and between 652bc98 and 0995a07.

📒 Files selected for processing (3)
  • src/Database/Concerns/HasPending.php
  • tests/Etc/pull-worker.php
  • tests/PendingTenantsTest.php

Comment thread tests/PendingTenantsTest.php Outdated

@stancl stancl left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Race conditions aren't really something that can be tested well by brute force due to their inconsistent nature.

Instead, looking at the implementation, it seems that with the current placement of PullingPendingTenant you can easily simulate using synchronous code a case where another process "steals" a $pullCandidate without actually having to use two separate processes? Just make the listener only do this once so the following loop iteration succeeds.

That in itself should also work as a regression test I think? Should be able to write a test that'd fail with the old code but pass with the new implementation. And we wouldn't need all the concurrent tests that have been added here.

Remove the worker solution, simulate the concurrent pull by updating pending_since while PullingPendingTenant
@lukinovec lukinovec changed the title [4.x] Fix pending tenant pull race conditions [MINOR BC] [4.x] Fix pending tenant pull race conditions Jun 12, 2026
@lukinovec lukinovec marked this pull request as ready for review June 12, 2026 10:03
@lukinovec

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

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.

2 participants