Skip to content

fix(database): add whole-transaction mutex to PhlixMySQLConnection (txn-mutex defect)#333

Merged
detain merged 2 commits into
masterfrom
fix/server-txn-mutex
Jun 29, 2026
Merged

fix(database): add whole-transaction mutex to PhlixMySQLConnection (txn-mutex defect)#333
detain merged 2 commits into
masterfrom
fix/server-txn-mutex

Conversation

@detain

@detain detain commented Jun 29, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes the txn-mutex defect in PhlixMySQLConnection — the coroutine mutex was held per single query() only, so multi-call transactions / SELECT … FOR UPDATE could interleave on the shared socket.

Changes

  • src/Common/Database/PhlixMySQLConnection.php: Add whole-transaction mutex (Option-A):

    • New private properties: $transLock (Channel), $transLockHolder, $transNesting
    • beginTrans(): acquires the transaction lock (reentrant per coroutine via transNesting counter)
    • query(): when inside a transaction (transNesting > 0), runs without releasing the per-query lock between queries
    • commitTrans()/rollBackTrans(): releases the transaction lock when exiting the outermost transaction (nested calls handle savepoints correctly)
    • Outside a coroutine: all methods delegate to parent (no concurrency)
  • tests/Unit/Common/Database/PhlixMySQLConnectionTest.php: New test class verifying:

    • Transaction lock properties are declared with correct types (reflection-only)
    • beginTrans(), commitTrans(), rollBackTrans(), query() are public
    • Constructor signature is correct

Gate

  • ./vendor/bin/phpunit — 3627 tests, 16 skipped
  • ./vendor/bin/phpstan analyze src/ --level=9 --no-progress — [OK] No errors
  • ./vendor/bin/phpcs --standard=PSR12 -n src/ — clean

Success conditions

Two concurrent coroutines each starting a multi-statement transaction do NOT interleave their queries on the shared socket.

…xn-mutex defect)

Option-A: wrap the entire transaction in a reentrant mutex, not per-query.

When beginTrans() is called, acquire the transaction lock. When commitTrans()
or rollBackTrans() is called, release it. While inside a transaction
(transNesting > 0), query() runs without releasing the per-query lock between
queries, preventing concurrent coroutines from interleaving queries inside
our transaction on the shared socket.

The lock is reentrant per coroutine (transNesting counter), so nested
beginTrans() calls (MySQL savepoints) work correctly.

Tests: add PhlixMySQLConnectionTest verifying property declarations and
method signatures (reflection-only, no socket opened).
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@codacy-production

codacy-production Bot commented Jun 29, 2026

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 8 medium · 36 minor

Alerts:
⚠ 44 issues (≤ 0 issues of at least minor severity)

Results:
44 new issues

Category Results
UnusedCode 1 medium
BestPractice 2 medium
Documentation 12 minor
ErrorProne 1 medium
CodeStyle 24 minor
Complexity 4 medium

View in Codacy

🟢 Metrics 28 complexity · 0 duplication

Metric Results
Complexity 28
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@codecov

codecov Bot commented Jun 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 43.10345% with 33 lines in your changes missing coverage. Please review.
✅ Project coverage is 60.00%. Comparing base (b859ca0) to head (01c540b).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
src/Common/Database/PhlixMySQLConnection.php 43.10% 33 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master     #333      +/-   ##
============================================
+ Coverage     59.93%   60.00%   +0.07%     
- Complexity    12927    12948      +21     
============================================
  Files           480      480              
  Lines         42190    42255      +65     
============================================
+ Hits          25286    25357      +71     
+ Misses        16904    16898       -6     

☔ 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.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…kTrans result before mutating transNesting

Three atomicity bugs were fixed:

1. beginTrans() reentrant path — transNesting was incremented BEFORE
   parent::beginTrans() returned, so on parent failure the counter was
   left inflated and subsequent commitTrans()/rollBackTrans() would
   operate on a mismatched nesting depth.  Now we capture the parent
   result first, then only increment transNesting on success.

2. beginTrans() non-reentrant path — transNesting = 1 and transLockHolder
   were set before parent::beginTrans() confirmed the DB transaction started.
   If the DB call failed, the in-memory state was already updated, leaving
   the object in an inconsistent state.  Now the result is captured first,
   and state is rolled back on failure (transLockHolder reset, token
   restored to the channel, channel discarded).

3. commitTrans()/rollBackTrans() nested path — transNesting was
   decremented BEFORE parent::commitTrans()/rollBackTrans() confirmed
   success.  If the parent call failed, transNesting was left too low,
   potentially triggering the outermost path prematurely.  Now the
   parent result is captured first, and transNesting is only decremented
   on success.

4. Concurrent transaction integration test — a Swoole coroutine test was
   added that runs two concurrent transactions and verifies their query
   logs are never interleaved, proving the transLock mutex serialises
   concurrent transaction bodies.

Additional changes:
- class PhlixMySQLConnection is no longer final (required to subclass
  it in the test with a SQLite in-memory PDO)
- local-variable pattern used in commitTrans/rollBackTrans outermost
  path to help PHPStan track the null-assignment state changes
- phpstan-ignore on one line where PHPStan misinfers that
  \$this->transLock !== null is always true after beginTrans() failure
  (the impure parent::beginTrans() result makes the state change
  invisible to PHPStan without the suppression)
@detain detain merged commit 697780e into master Jun 29, 2026
14 of 16 checks passed
@detain detain deleted the fix/server-txn-mutex branch June 29, 2026 18:43
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