Skip to content

Release: ORM pool self-heal + coroutine-safety + One Way P2#39

Open
syntaxwanderer wants to merge 2 commits into
masterfrom
develop
Open

Release: ORM pool self-heal + coroutine-safety + One Way P2#39
syntaxwanderer wants to merge 2 commits into
masterfrom
develop

Conversation

@syntaxwanderer

@syntaxwanderer syntaxwanderer commented Jun 18, 2026

Copy link
Copy Markdown
Member

Merges the current develop batch (9 commits) into master for release. The headline work is closing out the SingleConnectionPool coroutine-crash class of bug; the rest is the accumulated develop backlog.

Pool coroutine-safety (this session)

  • f0232b0 guard SingleConnectionPool against cross-coroutine double-bindpop() tracks the owning coroutine and mints a dedicated short-lived connection instead of double-binding a hooked PDO socket, converting an uncatchable "Socket already bound to another coroutine" worker fatal into graceful degradation.
  • 2db95cb self-heal cached pool selection in OrmManager — a SingleConnectionPool cached before SWOOLE_HOOK_ALL is applied now upgrades to the coroutine-safe ConnectionPool once hooks are live (ensureCoroutineSafePool()), invalidating the 7 memoized fields that captured the stale pool/adapter; guarded by TransactionManager::isActive().

Together these are defense-in-depth: the tripwire guarantees "never crash", the self-heal restores "real pooling once the runtime is up". Builds on 73c47e4 (gate on Runtime::getHookFlags()), also in this batch.

Also in this batch

  • aac631c CollectionQueryCompiler + whereAnyLike OR-group (One Way P2)
  • 73c47e4 pick coroutine-safe pool under a running Swoole server
  • 9f849b3 / f4779a4 / 77c6680 Track R — ORM event dispatch (ResourceChangedEvent from AggregateWriteEngine + default-dispatcher resolver)
  • 93090fe #[ResourceKey] attribute + ResourceMetadata::getResourceKey()
  • 06d98ad remove obsolete semver release metadata

Verification

  • ai:verify clean on both pool changes (6/6, 5/5).
  • Full packages/semitexa-orm Connection + Adapter suites: 25/25 green, incl. PoolSelfHealTest (4 tests, exercises the upgrade under real Swoole hook toggling) and SingleConnectionPoolTest.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed potential fatal errors when Swoole coroutines attempt to reuse database connections bound to other coroutines.
  • Improvements

    • Connection pool now self-heals by automatically upgrading to coroutine-safe pooling when Swoole hooks are activated at runtime.
  • Tests

    • Added test coverage for coroutine connection ownership and pool self-healing behavior.

Profile 朝 and others added 2 commits June 18, 2026 08:34
A wrong pool-selection guess at boot (SingleConnectionPool cached before
SWOOLE_HOOK_ALL is applied) would hand the same hooked PDO socket to two
coroutines under load, raising the uncatchable C-level "Socket already bound
to another coroutine" fatal that kills the worker.

pop() now tracks the owning coroutine; if the cached connection is still bound
to a different, live coroutine it mints a dedicated short-lived connection
instead of double-binding. The hot CLI path (getCid() < 0) is unchanged and
guarded by class_exists for no-Swoole hosts. push() drops a foreign connection
rather than overwriting the cached one and releases ownership.

Defense-in-depth: the worker degrades to an extra connection instead of crashing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
getPool() cached the pool-selection decision for the worker's life, so a
SingleConnectionPool chosen before the coroutine runtime was live (master-side
warmup before fork) was frozen in — degrading every request on that worker with
no true pooling.

Extract the gate into shouldUseCoroutinePool() and add ensureCoroutineSafePool(),
called from getPool()/getAdapter(): once hooks are live, a stale SingleConnectionPool
is closed and rebuilt as the coroutine-safe ConnectionPool, and the 7 memoized
fields that captured the old pool/adapter (adapter, schemaComparator, syncEngine,
transactionManager, seedRunner, resourceModelRelationLoader, aggregateWriteEngine)
are invalidated so they rebuild over it. Guarded by TransactionManager::isActive()
so the pool is never yanked out from under an open transaction.

PoolSelfHealTest exercises the upgrade under real Swoole hook toggling.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 88194064-5a07-4c2c-96c9-80239e96a3d6

📥 Commits

Reviewing files that changed from the base of the PR and between 87b665d and 2db95cb.

📒 Files selected for processing (4)
  • src/Adapter/SingleConnectionPool.php
  • src/OrmManager.php
  • tests/Unit/Adapter/SingleConnectionPoolTest.php
  • tests/Unit/Connection/PoolSelfHealTest.php

📝 Walkthrough

Walkthrough

SingleConnectionPool gains a coroutine ownership field (ownerCid) so that pop() returns a fresh factory-created PDO to foreign coroutines instead of re-binding the owned socket. OrmManager adds shouldUseCoroutinePool() and ensureCoroutineSafePool(), which upgrade a stale SingleConnectionPool to a ConnectionPool on subsequent getPool()/getAdapter() calls once Swoole hooks activate, while skipping the upgrade during active transactions.

Changes

Swoole Coroutine-Safe Connection Pool

Layer / File(s) Summary
SingleConnectionPool coroutine ownership tracking
src/Adapter/SingleConnectionPool.php, tests/Unit/Adapter/SingleConnectionPoolTest.php
Adds ownerCid field; pop() checks the current coroutine ID via currentCid(), returns a fresh PDO from the factory if the cached connection is still bound to another live coroutine, and otherwise claims ownership. push() recaches only the owned connection and clears ownerCid; close() clears both. reuseOrCreate() and currentCid() are extracted as private helpers. A new test verifies that pushing a foreign PDO does not overwrite the owned cache.
OrmManager self-healing pool upgrade
src/OrmManager.php
Extracts Swoole runtime checks into shouldUseCoroutinePool(). Adds ensureCoroutineSafePool() which replaces a memoized SingleConnectionPool with a coroutine-safe ConnectionPool when hooks activate, clears all dependent cached services (adapter, schemaComparator, syncEngine, transactionManager, seedRunner, resourceModelRelationLoader, aggregateWriteEngine), and skips the upgrade when a transaction is active. getAdapter() and getPool() both invoke this upgrade check on the already-initialized path.
PoolSelfHealTest suite
tests/Unit/Connection/PoolSelfHealTest.php
New test class that skips without Swoole, captures/restores hook flags in setUp/tearDown, and verifies: pool upgrades from SingleConnectionPool to ConnectionPool when hooks switch ON, adapter is rebuilt after upgrade, pool instance is stable under repeated calls with hooks OFF, and upgrade is deferred when an active transaction is injected.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 Hop hop, coroutines race through the night,
Each socket claims its own cozy burrow right,
A stale pool heals when Swoole hooks ignite,
Transactions pause—the upgrade waits polite,
No fatal crash, no socket shared in fright!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title relates to the changeset but is overly broad and vague. It mentions 'pool self-heal + coroutine-safety + One Way P2' without clearly conveying the main technical changes. Terms like 'Release:' and the abbreviated 'One Way P2' reduce clarity for someone scanning PR history. Consider a more specific title like 'Make SingleConnectionPool coroutine-safe with pool self-healing upgrade' that highlights the primary change without release metadata or feature abbreviations.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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 develop

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.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2db95cb6bd

ℹ️ 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 src/OrmManager.php
$this->adapter = null;
$this->schemaComparator = null;
$this->syncEngine = null;
$this->transactionManager = null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Re-check before returning cached transaction managers

This invalidation only happens after ensureCoroutineSafePool() is entered via getPool() or getAdapter(), but getTransactionManager() returns an already memoized TransactionManager without performing that check. In a worker that warms ConnectionRegistry::transactionManagerFor() before Swoole hooks are enabled, a later request that enters through the same transaction path will keep using the old SingleConnectionPool and old adapter, so the self-heal never runs for transactions under coroutine load. Add the same pre-return recheck there, while preserving the active-transaction guard.

Useful? React with 👍 / 👎.

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