fix: keep the library usable on a PHP without ext-swoole - #962
Conversation
ext-swoole is not in composer.json's require block, so a consumer may resolve
this package onto a PHP that does not have it. Two call sites referenced Swoole
classes unguarded, and a missing class there is a fatal Error rather than a
catchable exception, so the failure surfaced as a crash at runtime instead of at
resolution time.
getEventContext() backs silent() and areEventsSilenced(). Database::create() is
already inside a silent() call, so the first operation any swoole-less consumer
performed crashed, and deleteDocuments() crashed for the same reason further in.
Connection::hasError() reached Swoole's lost-connection detector on the error
path of the reconnecting PDO wrapper, which turned any query exception into a
fatal. Both now check extension_loaded('swoole') first, matching the guard
already used for the coroutine sleep in withRetries(); without the extension the
event context is the same -1 sentinel used outside a coroutine, and lost
connections fall back to the local message list.
Extensions cannot be unloaded from a running interpreter, so the regression test
drives the create/silent/deleteDocuments path in a subprocess started with -n,
which loads no shared extension. Swoole is shared both in the test image and in
any pecl install, so the subprocess genuinely lacks it; the test skips loudly if
a build has it compiled in statically. Reverting either guard turns it red.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 🗂️ Base branches to auto review (2)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
|
Guarding the DetectsLostConnections call on extension_loaded('swoole') left two
holes, both raised in review on the previous commit.
The class comes from Swoole's PHP-land library rather than the extension core,
and swoole.enable_library=Off switches that library off while the extension stays
loaded. extension_loaded() therefore does not establish that the class can be
called, and the guard still fataled on every query-error path under that setting.
class_exists() is the condition that actually holds.
The local fallback list also carried a single needle, 'Max connect timeout
reached', which is the one signature Swoole does not supply. Consumers without
the extension kept 1 of 25 lost-connection signatures, so 'server has gone away'
and 'Lost connection' were rethrown instead of reconnecting, silently dropping
the retry contract for exactly the consumers this change exists to serve. The
remaining 24 needles Swoole matches are now held here. Swoole's check is kept
ahead of them so upstream additions still apply wherever the library is present.
The fixture asserted only that an unrelated exception was not classified as a
lost connection, which held either way. It now drives five real signatures plus
one unrelated message, and a second test runs it under
swoole.enable_library=Off, which the -n subprocess cannot reach.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Both P1s were real and are fixed in a69c106. I reproduced each before and after rather than taking them on description. Issue 1 — helper availability unchecked. Valid. Under that setting the previous guard still fataled on every query-error path. Now gated on Issue 2 — lost connections stop reconnecting. Valid, and the more serious of the two. I enumerated what Swoole actually matches by feeding candidate messages through Issue 3 — test mirrors runtime configuration. Partly taken. I dropped the You were also right that the fixture proved nothing here — a Both fixes verified red first:
Local: unit suite 1595 tests / 6169 assertions OK, PHPStan clean, Pint passed. |
Problem
ext-swooleis not in this package'scomposer.jsonrequireblock, so a consumer can resolveutopia-php/databaseonto a PHP that does not have it. Two call sites referenced Swoole classes unguarded. A missing class is a fatalError, not a catchable exception, so the requirement was invisible until it crashed at runtime.1.
Database::getEventContext()— backssilent()andareEventsSilenced().This is worse than the originally reported
deleteDocuments()path.Database::create()is itself wrapped in asilent()call, so the first operation any swoole-less consumer performs crashes:2.
Connection::hasError()— found while confirming the fix actually made the library usable. It reachesSwoole\Database\DetectsLostConnectionson the error path of the reconnectingUtopia\Database\PDOwrapper, so without the extension any query exception becomes a fatal. Same defect class. Flagging it explicitly since it is beyond the literal scope of the report — happy to split it out if you would rather it landed separately.Fix
Option 1 from the report: guard the coroutine id lookup with
\extension_loaded('swoole'), matching the guard already used for the coroutine sleep inwithRetries(). Without the extension the event context is the same-1sentinel already used outside a coroutine.Connection::hasError()needed more than a guard, per review:extension_loaded()is not sufficient there.DetectsLostConnectionscomes from Swoole's PHP-land library, not the extension core, andswoole.enable_library=Offswitches that library off while leaving the extension loaded —extension_loaded()returnstruewhileclass_exists()returnsfalse. It is now gated onclass_exists(). The same is not true ofSwoole\Coroutine, which is extension-core and still resolves with the library off, soextension_loaded()remains correct inDatabase.php.Max connect timeout reached, the single signature Swoole does not supply. Guarding alone would have left swoole-less consumers with 1 of 25 signatures, rethrowingserver has gone awayandLost connectioninstead of reconnecting, silently dropping the retry contract for exactly the consumers this change exists to serve. Those 24 needles now live here, with Swoole's check kept ahead of them so upstream additions still apply where the library is present.Option 2 (declaring
ext-swooleinrequire) was not taken — it would turn an optional dependency into a hard one for every consumer.ext-swooleis instead documented undersuggest, so the optionality is visible rather than implied.Test
tests/unit/SwooleAbsentTest.phpcovers both environments an optional extension produces:-n, which skipsphp.iniand everyconf.dfile and so loads no shared extension. An extension cannot be unloaded from a running interpreter, so a subprocess is the only honest way to reach this branch. Swoole is shared both in this repo's test image (php:8.5-cli-alpine+docker-php-ext-enable) and in any pecl install, so the subprocess genuinely lacks it. The test skips with a loud message if some build has it compiled in statically, rather than passing vacuously.-d swoole.enable_library=Off, which the-nsubprocess cannot reach.The fixture asserts outcomes, not the absence of a throw:
deleted=3/remaining=0for the delete path, and five real lost-connection signatures detected plus one unrelated message not detected, so it cannot pass by matching everything.Verified red, each guard independently:
Database.php:1463Databaseguard onlyDatabase.php:1463Connectionguard onlyConnection.php:28lostDetected=1, expected 5class_exists→extension_loaded, library offConnection.php:28Checks
vendor/bin/phpunit --testsuite unit— OK, 1595 tests, 6169 assertions. The run exercisesConnection::hasErrorwith swoole present ("Lost connection detected. Re-preparing statement…"), confirming detection is not regressed.vendor/bin/phpstan analyse— no errorsvendor/bin/pint --test— passedNot verified
End-to-end on
utopia-php/monorepo— reproducingtest (audit)needs Docker plus MariaDB. The reproduction here matches the reported stack exactly (Class "Swoole\Coroutine" not found→getEventContext()→silent()→deleteDocuments()), but the monorepo lane has not been run green against this branch.Origin
Surfaced on
utopia-php/monorepoPR #206: dropping the seemingly unusedext-swooledev requirement frompackages/audit/composer.jsonfailedtest (audit)with 17Class "Swoole\Coroutine" not founderrors viacleanup()→deleteDocuments().🤖 Generated with Claude Code