(fix): purge cached project document before Realtime auth lookup

Cross-process negative-cache race in Realtime's onMessage handler: when
the WebSocket opens against a just-created project, an earlier router
probe or subscription lookup may have cached projects:\$projectId as
empty before the project actually existed. Although the HTTP worker
purges on createDocument, a narrow window lets Realtime re-populate the
stale empty entry in its own process cache, after which getProjectDB()
sees an empty Document and falls back to getConsoleDB(), which looks
for users in the console namespace and returns nothing — sessionVerify
then fails with 'Session is not valid.'

Surfaces on cloud#3214 as a recurring Realtime (dedicated) E2E failure.
Diagnosed via [realtime-project-diag] + [realtime-auth-diag]
instrumentation — the logs show consoleDb=appwrite consoleNs=console15x
(correct), project lookup empty, and subsequent user lookup landing in
the console namespace.

Purge is bounded: one cache DEL per WS message (not per read), runs
only when a projectId is present, and only forces the single
projects:\$projectId key to be re-read from adapter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jake Barnby
2026-04-18 06:13:44 +12:00
co-authored by Claude Opus 4.7
parent 8716497d1f
commit 92e8334169
+8 -11
View File
@@ -845,18 +845,15 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
$database->setAuthorization($authorization);
if (!empty($projectId) && $projectId !== 'console') {
$project = $authorization->skip(fn () => $database->getDocument('projects', $projectId));
// Negative-cache race: if any prior code path queried projects:$projectId
// before this project existed (e.g. a router probe during connection
// setup), the Database's shared cache may hold an empty result. HTTP
// worker creates the project via dbForPlatform which purges on write,
// but there's a window during which Realtime can re-populate a stale
// cache entry. Purge before reading to force a fresh adapter hit.
$database->purgeCachedDocument('projects', $projectId);
if ($project->isEmpty()) {
Console::warning(sprintf(
'[realtime-project-diag] projectId=%s consoleDb=%s consoleNs=%s consoleTenant=%s consoleShared=%s — project lookup returned empty, will fall back to console',
$projectId,
$database->getDatabase(),
$database->getNamespace(),
(string) ($database->getTenant() ?? 'null'),
$database->getSharedTables() ? 'yes' : 'no'
));
}
$project = $authorization->skip(fn () => $database->getDocument('projects', $projectId));
$database = getProjectDB($project);
$database->setAuthorization($authorization);