Skip to content

fix(lock): implement Redlock single-instance pattern in LockManagerService - #537

Open
romanetar wants to merge 7 commits into
mainfrom
fix/release-on-failed-acquire-and-add-ownership-tokens
Open

fix(lock): implement Redlock single-instance pattern in LockManagerService#537
romanetar wants to merge 7 commits into
mainfrom
fix/release-on-failed-acquire-and-add-ownership-tokens

Conversation

@romanetar

@romanetar romanetar commented May 4, 2026

Copy link
Copy Markdown
Collaborator

ref https://app.clickup.com/t/86b9f3a22

Recommended actions for a follow-up ticket:

  1. Move the SendAttendeeInvitationEmail::dispatch call outside the lock callback (dispatch after the lock is released).
  2. Consider a tighter explicit lifetime (e.g. 30 s) that matches the realistic worst-case DB write time rather than the 3600 s default.
  3. Fix the missing . in the key: 'ticket_type.' . $type_id . '.promo_code.' . $promo_code_val . '.sell.lock'.

Summary by CodeRabbit

  • New Features
    • Added atomic compare-and-delete support for cache entries.
    • Locks now use ownership tokens, retry with backoff, and prevent unauthorized releases.
  • Bug Fixes
    • Improved cache initialization and expiration handling.
    • Updated promo-code and reservation locking with explicit 30-second timeouts and corrected lock coordination.
  • Tests
    • Added coverage for lock ownership, Redis outages, cache expiration, single-value additions, and token-mismatch protection.

@coderabbitai

coderabbitai Bot commented May 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@smarcet, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 50 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a86bfe38-bf76-477b-b157-277f996ee479

📥 Commits

Reviewing files that changed from the base of the PR and between e1a0952 and 1f5bae7.

📒 Files selected for processing (7)
  • .github/workflows/push.yml
  • app/Services/Model/Imp/SummitOrderService.php
  • app/Services/Utils/LockManagerService.php
  • app/Services/Utils/RedisCacheService.php
  • tests/Integration/RedisCacheServiceAddSingleValueTest.php
  • tests/SummitOrderServiceTest.php
  • tests/Unit/Services/LockManagerServiceOwnershipTest.php

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
  • Review rate limited - (🔄 Check again to try again)
📝 Walkthrough

Walkthrough

The PR adds atomic Redis compare-and-delete support and ownership tokens for distributed locks. It updates lock consumers with explicit lifetimes, adds Redis and lock ownership tests, and enables the integration suite in CI.

Changes

Ownership-Based Distributed Locking

Layer / File(s) Summary
Cache and lock contracts
Libs/Utils/ICacheService.php, app/Services/Utils/ILockManagerService.php
Adds the atomic deleteIfValueMatches() contract and updates lock methods to use ownership tokens and explicit return types.
Redis conditional operations
app/Services/Utils/RedisCacheService.php
Uses conditional SET commands with inline TTL options. Adds Lua-based atomic compare-and-delete.
Token-based lock acquisition and release
app/Services/Utils/LockManagerService.php
Validates lifetimes, generates tokens, retries with exponential microsecond backoff, and releases locks only when the token matches.
Lock ownership validation
tests/Unit/Services/LockManagerServiceOwnershipTest.php
Tests failed acquisition, successful lifecycle pairing, token arguments, guarded release, mismatch metrics, and TTL preservation.
Summit order lock integration
app/Services/Model/Imp/SummitOrderService.php
Adds 30-second lock lifetimes, injects the promo-code repository, uses exclusive promo-code lookup, and corrects the lock key format.
Redis integration validation
tests/Integration/RedisCacheServiceAddSingleValueTest.php, .github/workflows/push.yml
Tests TTL, NX, re-acquisition, and matching or mismatching deletion. Adds the Integration PHPUnit suite to CI.

Estimated code review effort: 4 (Complex) | ~50 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant LockManagerService
  participant RedisCacheService
  participant Redis
  Caller->>LockManagerService: lock(name, callback, lifetime)
  LockManagerService->>RedisCacheService: addSingleValue(name, token, lifetime)
  RedisCacheService->>Redis: SET name token NX EX lifetime
  Redis-->>RedisCacheService: acquisition result
  RedisCacheService-->>LockManagerService: acquired or retry
  LockManagerService->>Caller: execute callback
  LockManagerService->>RedisCacheService: deleteIfValueMatches(name, token)
  RedisCacheService->>Redis: EVAL compare-and-delete
  Redis-->>RedisCacheService: deletion result
  RedisCacheService-->>LockManagerService: release result
Loading

Possibly related PRs

Suggested reviewers: smarcet

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: implementing the Redlock single-instance pattern in LockManagerService.
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.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch fix/release-on-failed-acquire-and-add-ownership-tokens
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/release-on-failed-acquire-and-add-ownership-tokens

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.

@github-actions

github-actions Bot commented May 4, 2026

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/summit-api/openapi/pr-537/

This page is automatically updated on each push to this PR.

@romanetar
romanetar requested a review from smarcet May 4, 2026 14:42

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
app/Services/Utils/LockManagerService.php (1)

62-70: 💤 Low value

Minor: Unnecessary sleep before throwing on final retry.

When attempt >= MaxRetries - 1, the code still executes usleep before throwing. This adds ~400ms of unnecessary delay on the final failed attempt.

Consider moving the retry check before the sleep:

Suggested reorder
-        $wait_interval = (int)(self::BackOffBaseInterval * (self::BackOffMultiplier ** $attempt));
-        Log::debug(sprintf("LockManagerService::acquireLock name %s retrying in %s µs (attempt %s)", $name, $wait_interval, $attempt));
-        usleep($wait_interval);
         if ($attempt >= (self::MaxRetries - 1)) {
             Log::error(sprintf("LockManagerService::acquireLock name %s lifetime %s ERROR MAX RETRIES attempt %s", $name, $lifetime, $attempt));
             throw new UnacquiredLockException(sprintf("lock name %s", $name));
         }
+        $wait_interval = (int)(self::BackOffBaseInterval * (self::BackOffMultiplier ** $attempt));
+        Log::debug(sprintf("LockManagerService::acquireLock name %s retrying in %s µs (attempt %s)", $name, $wait_interval, $attempt));
+        usleep($wait_interval);
         ++$attempt;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Services/Utils/LockManagerService.php` around lines 62 - 70, The loop in
LockManagerService::acquireLock sleeps (usleep) even when $attempt >=
(self::MaxRetries - 1), causing an unnecessary delay before throwing
UnacquiredLockException; reorder the logic so the check for final retry (if
$attempt >= (self::MaxRetries - 1)) occurs before calling usleep and before
incrementing $attempt, log and throw immediately on final attempt, otherwise
perform the usleep, increment $attempt and continue the loop to preserve backoff
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/Services/Utils/LockManagerService.php`:
- Around line 62-70: The loop in LockManagerService::acquireLock sleeps (usleep)
even when $attempt >= (self::MaxRetries - 1), causing an unnecessary delay
before throwing UnacquiredLockException; reorder the logic so the check for
final retry (if $attempt >= (self::MaxRetries - 1)) occurs before calling usleep
and before incrementing $attempt, log and throw immediately on final attempt,
otherwise perform the usleep, increment $attempt and continue the loop to
preserve backoff behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a4fb76b4-55ed-4ec3-a248-fdf0d070fc95

📥 Commits

Reviewing files that changed from the base of the PR and between c39160a and b3dbd7a.

📒 Files selected for processing (4)
  • Libs/Utils/ICacheService.php
  • app/Services/Utils/LockManagerService.php
  • app/Services/Utils/RedisCacheService.php
  • tests/Unit/Services/LockManagerServiceOwnershipTest.php

@romanetar
romanetar force-pushed the fix/release-on-failed-acquire-and-add-ownership-tokens branch from b3dbd7a to acb8447 Compare May 7, 2026 17:33
@github-actions

github-actions Bot commented May 7, 2026

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/summit-api/openapi/pr-537/

This page is automatically updated on each push to this PR.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
tests/Unit/Services/LockManagerServiceOwnershipTest.php (1)

139-144: 💤 Low value

Consider asserting the token is non-empty for stronger ownership guarantee coverage.

Mockery::type('string') accepts any string including ''. A complementary assertion with Mockery::on(fn($v) => strlen($v) >= 16) (or similar) would confirm the service is actually generating a meaningful random token rather than an empty or trivial value.

♻️ Tighter token constraint
-              ->with('test.lock', Mockery::type('string'), 3600)
+              ->with('test.lock', Mockery::on(fn(string $v) => strlen($v) >= 16), 3600)
🤖 Prompt for 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.

In `@tests/Unit/Services/LockManagerServiceOwnershipTest.php` around lines 139 -
144, Replace the loose Mockery::type('string') expectation in
LockManagerServiceOwnershipTest (the mock of ICacheService used with
addSingleValue) with a stricter constraint that asserts the token is
non-empty/strong (e.g. Mockery::on(fn($v) => is_string($v) && strlen($v) >= 16))
or add an additional expectation using Mockery::on to verify token length,
keeping the same call to addSingleValue and the deleteIfValueMatches
expectation; target the mock for addSingleValue on the ICacheService to ensure
the generated token is meaningful rather than allowing an empty string.
🤖 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.

Nitpick comments:
In `@tests/Unit/Services/LockManagerServiceOwnershipTest.php`:
- Around line 139-144: Replace the loose Mockery::type('string') expectation in
LockManagerServiceOwnershipTest (the mock of ICacheService used with
addSingleValue) with a stricter constraint that asserts the token is
non-empty/strong (e.g. Mockery::on(fn($v) => is_string($v) && strlen($v) >= 16))
or add an additional expectation using Mockery::on to verify token length,
keeping the same call to addSingleValue and the deleteIfValueMatches
expectation; target the mock for addSingleValue on the ICacheService to ensure
the generated token is meaningful rather than allowing an empty string.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 757f3521-79bb-45c3-a48d-09cc2ce97243

📥 Commits

Reviewing files that changed from the base of the PR and between b3dbd7a and acb8447.

📒 Files selected for processing (4)
  • Libs/Utils/ICacheService.php
  • app/Services/Utils/LockManagerService.php
  • app/Services/Utils/RedisCacheService.php
  • tests/Unit/Services/LockManagerServiceOwnershipTest.php
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/Services/Utils/RedisCacheService.php
  • app/Services/Utils/LockManagerService.php
  • Libs/Utils/ICacheService.php

@romanetar
romanetar force-pushed the fix/release-on-failed-acquire-and-add-ownership-tokens branch from acb8447 to c6e6473 Compare May 8, 2026 14:00
@github-actions

github-actions Bot commented May 8, 2026

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/summit-api/openapi/pr-537/

This page is automatically updated on each push to this PR.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
tests/Unit/Services/LockManagerServiceOwnershipTest.php (1)

139-145: ⚡ Quick win

Assert token identity across acquire and release, not just token type.

At Line [142] and Line [144], the test validates string token creation and release call count, but it does not verify deleteIfValueMatches receives the same token captured during addSingleValue. A token-mismatch regression could still pass.

Proposed test hardening
     public function testAddSingleValueCalledOnceWithTokenAndLifetime(): void
     {
         $cache = Mockery::mock(ICacheService::class);
+        $token = null;
         $cache->shouldReceive('addSingleValue')
               ->once()
-              ->with('test.lock', Mockery::type('string'), 3600)
+              ->with(
+                  'test.lock',
+                  Mockery::on(function ($value) use (&$token) {
+                      if (!is_string($value) || $value === '') return false;
+                      $token = $value;
+                      return true;
+                  }),
+                  3600
+              )
               ->andReturn(true);
-        $cache->shouldReceive('deleteIfValueMatches')->once()->andReturn(true);
+        $cache->shouldReceive('deleteIfValueMatches')
+              ->once()
+              ->with('test.lock', Mockery::on(fn($value) => $value === $token))
+              ->andReturn(true);
🤖 Prompt for 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.

In `@tests/Unit/Services/LockManagerServiceOwnershipTest.php` around lines 139 -
145, The test currently only asserts the token is a string and that
deleteIfValueMatches is called, but not that it's the same token; update
LockManagerServiceOwnershipTest to capture the token passed to
ICacheService::addSingleValue (use Mockery capture or an on/closure) and then
assert ICacheService::deleteIfValueMatches is invoked with the same captured
token (e.g., expect deleteIfValueMatches('test.lock', <capturedToken>)). Keep
addSingleValue and deleteIfValueMatches expectations tied to the captured
variable so the test fails on token mismatches.
🤖 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.

Nitpick comments:
In `@tests/Unit/Services/LockManagerServiceOwnershipTest.php`:
- Around line 139-145: The test currently only asserts the token is a string and
that deleteIfValueMatches is called, but not that it's the same token; update
LockManagerServiceOwnershipTest to capture the token passed to
ICacheService::addSingleValue (use Mockery capture or an on/closure) and then
assert ICacheService::deleteIfValueMatches is invoked with the same captured
token (e.g., expect deleteIfValueMatches('test.lock', <capturedToken>)). Keep
addSingleValue and deleteIfValueMatches expectations tied to the captured
variable so the test fails on token mismatches.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b5de5ba1-752b-4d1c-9d24-3a6a5e3e917a

📥 Commits

Reviewing files that changed from the base of the PR and between acb8447 and c6e6473.

📒 Files selected for processing (4)
  • Libs/Utils/ICacheService.php
  • app/Services/Utils/LockManagerService.php
  • app/Services/Utils/RedisCacheService.php
  • tests/Unit/Services/LockManagerServiceOwnershipTest.php
🚧 Files skipped from review as they are similar to previous changes (3)
  • Libs/Utils/ICacheService.php
  • app/Services/Utils/LockManagerService.php
  • app/Services/Utils/RedisCacheService.php

Comment thread tests/Unit/Services/LockManagerServiceOwnershipTest.php
Comment thread app/Services/Utils/RedisCacheService.php
Comment thread app/Services/Utils/LockManagerService.php Outdated
Comment thread app/Services/Utils/LockManagerService.php Outdated
Comment thread app/Services/Utils/RedisCacheService.php Outdated
@smarcet

smarcet commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

Malformed lock key — missing dot separator ()

This line cannot be commented inline since SummitOrderService.php is not in the diff, but the issue is worth tracking here.

The lock key is missing a . between $type_id and 'promo_code.':

// Current — produces e.g. "ticket_type.42promo_code.SUMMER25.sell.lock"
$this->lock_service->lock('ticket_type.' . $type_id . 'promo_code.' . $promo_code_val . '.sell.lock', ...)

// Intended — "ticket_type.42.promo_code.SUMMER25.sell.lock"
$this->lock_service->lock('ticket_type.' . $type_id . '.promo_code.' . $promo_code_val . '.sell.lock', ...)

With integer type IDs there is no key collision today, but the key is semantically malformed and will confuse any tooling (monitoring, manual Redis inspection, key-expiry scripts) that parses the key pattern. There is also no test asserting the exact key string passed to ILockManagerService::lock() in this code path — a mock expectation on the key argument would catch this immediately.

The PR description already flags this as a follow-up: "Fix the missing . in the key." Recommend addressing it in a dedicated follow-up ticket before the ownership token changes are deployed, so the key format stabilises.

@smarcet smarcet left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@romanetar please review comments

@github-actions

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/summit-api/openapi/pr-537/

This page is automatically updated on each push to this PR.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
tests/Unit/Services/LockManagerServiceOwnershipTest.php (1)

119-124: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Assert release uses the exact acquired token.

This test accepts any string on acquire and any release token, so it would still pass if releaseLock() used the wrong token. Capture the acquired token and assert deleteIfValueMatches() receives that exact value.

Proposed fix
 $cache = Mockery::mock(ICacheService::class);
+$capturedToken = null;
 $cache->shouldReceive('addSingleValue')
       ->once()
-      ->with('test.lock', Mockery::type('string'), 3600)
+      ->withArgs(function ($name, $token, $lifetime) use (&$capturedToken) {
+          $capturedToken = $token;
+          return $name === 'test.lock'
+              && is_string($token)
+              && preg_match('/\A[0-9a-f]{32}\z/', $token) === 1
+              && $lifetime === 3600;
+      })
       ->andReturn(true);
-$cache->shouldReceive('deleteIfValueMatches')->once()->andReturn(true);
+$cache->shouldReceive('deleteIfValueMatches')
+      ->once()
+      ->withArgs(function ($name, $token) use (&$capturedToken) {
+          return $name === 'test.lock' && $token === $capturedToken;
+      })
+      ->andReturn(true);
🤖 Prompt for 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.

In `@tests/Unit/Services/LockManagerServiceOwnershipTest.php` around lines 119 -
124, The test is not validating that releaseLock() uses the exact same token
that was acquired, since the mocks accept any string values without
verification. Capture the token value that is returned when addSingleValue() is
called on the cache mock, and then modify the deleteIfValueMatches mock
assertion to verify it receives that exact captured token value instead of
accepting any string parameter.
app/Services/Model/Imp/SummitOrderService.php (1)

1539-1554: ⚠️ Potential issue | 🟠 Major

Add $ticket_dto to the closure's use() clause.

The callback accesses $ticket_dto['attendee_company'], $ticket_dto['attendee_first_name'], and $ticket_dto['attendee_last_name'] (lines 1545, 1550, 1554), but $ticket_dto is not captured in the closure. Under PHP closure scoping, this variable is unavailable and falls back to null through the ?? operator, causing submitted attendee data to be ignored in favor of $this->payload or $this->owner defaults.

Proposed fix
 $order = $this->lock_service->lock('ticket_type.' . $type_id . '.promo_code.' . $promo_code_val . '.sell.lock',
-    function () use ($promo_code_val, $type_id) {
+    function () use ($promo_code_val, $type_id, $ticket_dto) {
🤖 Prompt for 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.

In `@app/Services/Model/Imp/SummitOrderService.php` around lines 1539 - 1554, The
closure passed to the lock_service->lock() method is missing the $ticket_dto
variable in its use() clause. The callback function accesses $ticket_dto array
elements (attendee_company, attendee_first_name, attendee_last_name) but without
capturing this variable, it will be unavailable in the closure scope. Add
$ticket_dto to the use() clause alongside $promo_code_val and $type_id so the
submitted attendee data from the ticket_dto parameter is properly accessible
within the callback function.
🤖 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 `@app/Services/Utils/LockManagerService.php`:
- Around line 73-76: The releaseLock method calls deleteIfValueMatches() but
does not capture or check its return value. This means when
deleteIfValueMatches() returns false (indicating the key was not deleted due to
token mismatch or other reasons), the failure is silently dropped and not
logged. Capture the boolean return value from the deleteIfValueMatches() call
and add logging to record when the deletion fails, so that stuck locks, Redis
release failures, or ownership mismatches become observable through logs rather
than remaining hidden.
- Around line 48-54: The acquireLock method does not validate the $lifetime
parameter before passing it to addSingleValue. Add validation at the start of
the acquireLock method to ensure $lifetime is positive (greater than 0), and
reject or throw an exception for non-positive values. This prevents the creation
of locks with no expiration when addSingleValue receives a zero or negative TTL
value.

---

Outside diff comments:
In `@app/Services/Model/Imp/SummitOrderService.php`:
- Around line 1539-1554: The closure passed to the lock_service->lock() method
is missing the $ticket_dto variable in its use() clause. The callback function
accesses $ticket_dto array elements (attendee_company, attendee_first_name,
attendee_last_name) but without capturing this variable, it will be unavailable
in the closure scope. Add $ticket_dto to the use() clause alongside
$promo_code_val and $type_id so the submitted attendee data from the ticket_dto
parameter is properly accessible within the callback function.

In `@tests/Unit/Services/LockManagerServiceOwnershipTest.php`:
- Around line 119-124: The test is not validating that releaseLock() uses the
exact same token that was acquired, since the mocks accept any string values
without verification. Capture the token value that is returned when
addSingleValue() is called on the cache mock, and then modify the
deleteIfValueMatches mock assertion to verify it receives that exact captured
token value instead of accepting any string parameter.
🪄 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: ca02481e-92fd-46d7-b26b-1b9c5ed0f1f9

📥 Commits

Reviewing files that changed from the base of the PR and between c6e6473 and e1a0952.

📒 Files selected for processing (6)
  • app/Services/Model/Imp/SummitOrderService.php
  • app/Services/Utils/ILockManagerService.php
  • app/Services/Utils/LockManagerService.php
  • app/Services/Utils/RedisCacheService.php
  • tests/Integration/RedisCacheServiceAddSingleValueTest.php
  • tests/Unit/Services/LockManagerServiceOwnershipTest.php
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/Services/Utils/RedisCacheService.php

Comment thread app/Services/Utils/LockManagerService.php
Comment thread app/Services/Utils/LockManagerService.php Outdated
@romanetar
romanetar force-pushed the fix/release-on-failed-acquire-and-add-ownership-tokens branch from e1a0952 to 9ef1bf6 Compare June 15, 2026 13:47
@romanetar
romanetar requested a review from smarcet June 15, 2026 13:47
@github-actions

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/summit-api/openapi/pr-537/

This page is automatically updated on each push to this PR.

@romanetar
romanetar force-pushed the fix/release-on-failed-acquire-and-add-ownership-tokens branch from 9ef1bf6 to fbe981c Compare June 15, 2026 14:06
@github-actions

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/summit-api/openapi/pr-537/

This page is automatically updated on each push to this PR.

@smarcet
smarcet requested a review from Copilot July 11, 2026 00:25

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread app/Services/Model/Imp/SummitOrderService.php
Comment thread app/Services/Utils/RedisCacheService.php Outdated
Comment thread tests/Integration/RedisCacheServiceAddSingleValueTest.php
Comment thread app/Services/Model/Imp/SummitOrderService.php

@smarcet smarcet left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@romanetar please re review

@romanetar
romanetar requested a review from smarcet August 3, 2026 13:18

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
app/Services/Model/Imp/SummitOrderService.php (1)

1548-1548: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a regression test for the exact lock key.

Mock ILockManagerService::lock() and assert ticket_type.42.promo_code.SUMMER25.sell.lock. This prevents a valid-but-malformed key from splitting the prepaid-assignment lock namespace again.

🤖 Prompt for 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.

In `@app/Services/Model/Imp/SummitOrderService.php` at line 1548, Add a regression
test covering the lock call in SummitOrderService, mock
ILockManagerService::lock(), and assert it receives the exact key
ticket_type.42.promo_code.SUMMER25.sell.lock. Keep the test focused on
preventing malformed keys from creating a separate prepaid-assignment lock
namespace.
🤖 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.

Nitpick comments:
In `@app/Services/Model/Imp/SummitOrderService.php`:
- Line 1548: Add a regression test covering the lock call in SummitOrderService,
mock ILockManagerService::lock(), and assert it receives the exact key
ticket_type.42.promo_code.SUMMER25.sell.lock. Keep the test focused on
preventing malformed keys from creating a separate prepaid-assignment lock
namespace.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4a2189a5-79d1-4403-86c3-72deb415d4a8

📥 Commits

Reviewing files that changed from the base of the PR and between e1a0952 and 4b0e311.

📒 Files selected for processing (7)
  • .github/workflows/push.yml
  • app/Services/Model/Imp/SummitOrderService.php
  • app/Services/Utils/ILockManagerService.php
  • app/Services/Utils/LockManagerService.php
  • app/Services/Utils/RedisCacheService.php
  • tests/Integration/RedisCacheServiceAddSingleValueTest.php
  • tests/Unit/Services/LockManagerServiceOwnershipTest.php
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/Services/Utils/RedisCacheService.php
  • app/Services/Utils/ILockManagerService.php
  • tests/Unit/Services/LockManagerServiceOwnershipTest.php

…rvice

Signed-off-by: romanetar <roman_ag@hotmail.com>
Signed-off-by: romanetar <roman_ag@hotmail.com>
Signed-off-by: romanetar <roman_ag@hotmail.com>
@romanetar
romanetar force-pushed the fix/release-on-failed-acquire-and-add-ownership-tokens branch from 4b0e311 to 3ce71a3 Compare August 6, 2026 17:51
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/summit-api/openapi/pr-537/

This page is automatically updated on each push to this PR.

Comment thread .github/workflows/push.yml Outdated
Comment thread app/Services/Model/Imp/SummitOrderService.php Outdated
Comment thread app/Services/Utils/RedisCacheService.php Outdated
Comment thread tests/Unit/Services/LockManagerServiceOwnershipTest.php Outdated

@smarcet smarcet left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@romanetar please re review i do still see some pending issues

phpunit --filter matches test/class names via regex, not directory
paths, so --filter tests/Repositories/ and --filter tests/Unit/Services/
matched zero tests while the jobs still exited 0, silently dropping
CI coverage.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/summit-api/openapi/pr-537/

This page is automatically updated on each push to this PR.

The inner lock() closure read $ticket_dto['attendee_company'/'attendee_first_name'/
'attendee_last_name'] but never captured it via use(), so PHP treated it as
undefined and every read silently fell back to the order owner's own profile
instead of the attendee actually being assigned the ticket.

Adds a regression test that reproduces the RED/GREEN pair from the review.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/summit-api/openapi/pr-537/

This page is automatically updated on each push to this PR.

Predis returns null on a SET...NX miss, but PhpRedis's C extension returns
false for the same variadic form; `!== null` treats the PhpRedis miss as a
success. addSingleValue and incCounter both used this check, so under
REDIS_CLIENT=phpredis a second caller racing for an already-held lock would
report success while the first token still owns it, defeating
LockManagerService's mutual exclusion, and incCounter's
lock_manager.release_mismatch counter would stick at 1 instead of
incrementing.

Adds a shared setNxSucceeded() helper that excludes both drivers' failure
sentinels, an incCounter regression test, and runs the Integration suite
under REDIS_CLIENT=phpredis in CI so this can't regress silently again.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/summit-api/openapi/pr-537/

This page is automatically updated on each push to this PR.

…oken

testAddSingleValueCalledOnceWithTokenAndLifetime only checked that
addSingleValue got some string token; deleteIfValueMatches had no
constraint at all, so a future refactor that broke token threading
between acquireLock and releaseLock would pass this suite undetected.

Captures the token from addSingleValue and asserts deleteIfValueMatches
receives that same value, verified by injecting a token-threading
regression locally and confirming the test catches it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/summit-api/openapi/pr-537/

This page is automatically updated on each push to this PR.

@smarcet
smarcet requested review from smarcet and a lite review from Copilot August 10, 2026 18:35

Copilot AI 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.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

app/Services/Utils/LockManagerService.php:81

  • The warning log includes the full lock ownership token, which is high-cardinality and unnecessary for diagnosing most mismatch cases. Consider truncating/redacting it to reduce log volume and avoid leaking the full token into logs.
            Log::warning(sprintf("LockManagerService::releaseLock name %s token %s lock was not held by this token at release time (expired or stolen).", $name, $token));

Comment on lines +45 to +48
final class RedisCacheServiceAddSingleValueTest extends TestCase
{
use CreatesApplication;

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.

3 participants