chore: adopt utopia-php/http concurrency fixes#12169
Conversation
Adopts the breaking changes from utopia-php/http#251 (concurrency races on shared Http/Route singletons): - Replace `Http::getRoute()` / `setRoute()` with `getResource('route')` and context container writes. - Replace `Route::getMatchedPath()` with `getResource('matchedPath')`. - Use `Adapter::getContext()` for the per-request container in `app/http.php` (`getContainer()` now always returns the global singleton). - Read request params from `$request->getParams()` in the api shutdown hook instead of `Route::getParamsValues()`, which is no longer populated. - Update Swoole promise context key to `__utopia_http_context`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Greptile SummaryThis PR migrates the Appwrite codebase from per-request singleton mutations (
Confidence Score: 3/5Not safe to merge — multiple P1 null-safety and lock-leak defects on core request paths, plus the dev-branch dependency pin. Three distinct P1 findings (lock leak in GraphQL resolver, TypeError in shutdown hook, partially null-safe chain in Span annotation) plus the temporary dev-branch pin of utopia-php/http lower the score below the P1 ceiling of 4. src/Appwrite/GraphQL/Resolvers.php (lock leak), app/controllers/shared/api.php (shutdown hook TypeError), app/http.php (partial null-safe chain), app/controllers/mock.php (unconditional route dereference) Important Files Changed
Reviews (7): Last reviewed commit: "chore: use public Http::setContext() in ..." | Re-trigger Greptile |
| "utopia-php/dns": "1.6.*", | ||
| "utopia-php/dsn": "0.2.1", | ||
| "utopia-php/http": "0.34.*", | ||
| "utopia-php/http": "dev-fix/concurrency-shared-state as 0.34.99", |
There was a problem hiding this comment.
Dev-branch dependency pinned to
1.9.x
utopia-php/http is locked to dev-fix/concurrency-shared-state as 0.34.99, an unmerged feature branch. The PR description notes this must be reverted before merging, but having it land on the release branch — even temporarily — means any build from 1.9.x pulls a mutable, non-stable reference that could change from under deployments. Until the upstream PR ships and a tagged release exists, merging this to 1.9.x poses a real stability risk.
This comment has been minimized.
This comment has been minimized.
✨ Benchmark resultsComparing 1.9.x (before) to fix/coroutines (after). Before
After
Delta
Top API waits
|
…solver
- Use ->inject('route') and ->inject('matchedPath') in actions instead of
reading via \$utopia->getResource() — this was the cause of the e2e 500s,
the bus resolver was hitting the global container after the upstream
Adapter::getContainer() semantics changed.
- Switch the bus resolver in app/http.php to use \$swooleAdapter->getContext()
so per-request resources (locale, platform, dbForProject) resolve from the
per-coroutine context container.
- Drop the dead ?->label('router', true) calls in general.php — the label was
never read.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
🔄 PHP-Retry SummaryFlaky tests detected across commits: Commit
|
| Test | Retries | Total Time | Details |
|---|---|---|---|
UsageTest::testFunctionsStats |
1 | 10.19s | Logs |
UsageTest::testPrepareSitesStats |
1 | 7ms | Logs |
UsageTest::testEmbeddingsTextUsageDoesNotBreakProjectUsage |
1 | 5ms | Logs |
LegacyConsoleClientTest::testNotBetween |
1 | 240.47s | Logs |
VectorsDBConsoleClientTest::testGetCollectionLogs |
1 | 4ms | Logs |
DatabasesConsoleClientTest::testGetCollectionLogs |
1 | 6ms | Logs |
Commit 2cc1f6a - 5 flaky tests
| Test | Retries | Total Time | Details |
|---|---|---|---|
UsageTest::testFunctionsStats |
1 | 10.17s | Logs |
UsageTest::testPrepareSitesStats |
1 | 7ms | Logs |
UsageTest::testEmbeddingsTextUsageDoesNotBreakProjectUsage |
1 | 6ms | Logs |
VectorsDBConsoleClientTest::testGetCollectionLogs |
1 | 5ms | Logs |
DatabasesConsoleClientTest::testGetCollectionLogs |
1 | 7ms | Logs |
Commit 98f6ca3 - 4 flaky tests
| Test | Retries | Total Time | Details |
|---|---|---|---|
UsageTest::testFunctionsStats |
1 | 10.28s | Logs |
UsageTest::testPrepareSitesStats |
1 | 8ms | Logs |
UsageTest::testEmbeddingsTextUsageDoesNotBreakProjectUsage |
1 | 6ms | Logs |
RealtimeConsoleClientTest::testCreateDeployment |
1 | 2.10s | Logs |
Commit d47cf5a - 4 flaky tests
| Test | Retries | Total Time | Details |
|---|---|---|---|
UsageTest::testFunctionsStats |
1 | 10.24s | Logs |
UsageTest::testPrepareSitesStats |
1 | 7ms | Logs |
UsageTest::testEmbeddingsTextUsageDoesNotBreakProjectUsage |
1 | 5ms | Logs |
AvatarsConsoleClientTest::testGetScreenshot |
1 | 82.20s | Logs |
Commit 79d2cfc - 5 flaky tests
| Test | Retries | Total Time | Details |
|---|---|---|---|
SitesCustomServerTest::testDomainForFailedDeployment |
1 | 100.45s | Logs |
SitesCustomServerTest::testErrorPages |
1 | 120.39s | Logs |
UsageTest::testFunctionsStats |
1 | 10.20s | Logs |
UsageTest::testPrepareSitesStats |
1 | 8ms | Logs |
UsageTest::testEmbeddingsTextUsageDoesNotBreakProjectUsage |
1 | 9ms | Logs |
Note: Flaky test results are tracked for the last 5 commits
The api shutdown hook substitutes route labels like
\`cache.resource = 'file/{request.fileId}'\` against \$requestParams. We
were reading those from \$request->getParams(), which only returns body
or query params — path params (\`fileId\`, \`bucketId\`) were missing,
so cache documents got written with literal \`file/{request.fileId}\`
strings and subsequent cache hits 404'd because the placeholder was
treated as a real fileId.
Merge \$route->getPathValues(\$request) ahead of \$request->getParams() so
path params are available for substitution. Replicates the previous
behaviour of \$route->getParamsValues(), which was populated by the
upstream Hook param writeback that has since been removed.
Also rename the per-request container variable to \$context and the
loader callable to \$registerContext in app/http.php to match the new
upstream terminology.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps utopia-php/http to the latest fix/concurrency-shared-state, which
collapses the separate route / matchedPath / arguments context keys into
a single immutable RouteMatch under the 'match' key.
- Replace ->inject('route') / ->inject('matchedPath') with
->inject('match'), reading \$match->route / \$match->path.
- Replace the manual array_merge(\$route->getPathValues(), \$request->
getParams()) workaround in api.php's shutdown hook with the framework-
provided \$match->arguments — same data the action saw, no path-value
reconstruction needed.
- Update GraphQL resolver to snapshot/restore the 'match' value instead
of 'route'.
- Update top-level call sites in app/http.php that read from getResource
('route') to read getResource('match')->route.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rce resolvers Replaces \$utopia->match(\$request) calls in the project, team, and auth resolvers with the per-request RouteMatch injected directly via DI: - app/init/resources/request.php — project / team resource closures now declare 'match' as a dependency and read \$match?->route. - app/controllers/shared/api/auth.php — auth init hook injects 'match' instead of resolving via \$utopia. - app/controllers/shared/api.php — drop the redundant re-match in the storage cache init hook; \$route from the action's \$match inject is already in scope. Only Resolvers::resolve still calls \$utopia->match(...) — that path genuinely matches a synthesized sub-request URL. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Upstream now returns null instead of throwing when the request hasn't been matched yet, so the defensive try/catch wrappers in app/http.php and Resolvers.php are no longer needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Upstream removed Adapter::getContainer() in favor of a single public reader Adapter::getContext(): Container that returns the container for the current execution context (per-request inside a request, global outside one). Migration is a one-line rename. The only call site was the installer's bootstrap, which runs at boot and therefore receives the global container — same semantics as before. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
setContext() is now public on Http (utopia-php/http#251). The graphql resolver no longer needs to reach into the per-request container directly to swap the active match for nested resolution — it can just ask Http to do it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| $original = $utopia->getResource('match'); | ||
| try { |
There was a problem hiding this comment.
Lock leaked when
getResource('match') throws
$lock->acquire() fires on line 360, but $utopia->getResource('match') on line 362 is outside the try block. In the old code getRoute() always returned a value (or null) and never threw; getResource() will throw if the 'match' key is absent from the context container (e.g., on a request that errored before routing completed, or under the new upstream API when the key isn't yet populated). When that happens the finally block — which calls $lock->release() — is never entered, permanently blocking any subsequent GraphQL call that maps to the same $utopia instance.
Move the getResource call inside the try block:
$lock->acquire();
$original = null;
try {
$original = $utopia->getResource('match');
$request = clone $request;
...
} catch (\Throwable $e) {
$reject($e);
return;
} finally {
if ($original !== null) {
$utopia->setContext('match', static fn () => $original);
}
$lock->release();
unset(self::$locks[\spl_object_hash($utopia)]);
}
Summary
Bumps
utopia-php/httpto thefix/concurrency-shared-statebranch (utopia-php/http#251), which removes per-request mutations on shared singletons (Http::$route,Route::$matchedPath,Http::$wildcardRoute,Hookparam writeback). This PR migrates the appwrite codebase to the new public surface.Changes
composer.json: pinutopia-php/httptodev-fix/concurrency-shared-state as 0.34.99(revert before merge once the upstream PR ships).app/http.php:$swooleAdapter->getContext()instead ofgetContainer()for the per-request container (getContainer()now always returns the global singleton).$app->getResource('route')instead ofgetRoute().app/controllers/general.php,app/controllers/mock.php,app/controllers/shared/api.php: replaceHttp::getRoute()calls withgetResource('route'). ReplaceRoute::getMatchedPath()withgetResource('matchedPath'). Calls toRequest::getRoute()/setRoute()(Appwrite's own subclass) are left alone.app/controllers/shared/api.php: read request params via$request->getParams()in the shutdown hook —Route::getParamsValues()is no longer populated by request handling.src/Appwrite/GraphQL/Resolvers.php: snapshot/restore the parent route via the per-request context container instead ofHttp::setRoute().src/Appwrite/Promises/Swoole.php: update the coroutine context key constant from__utopia_http_request_containerto__utopia_http_contextto match the upstream rename.Verification
composer analyze(PHPStan) — clean.composer format(Pint) — clean.Test plan
getResource('route')).🤖 Generated with Claude Code