From 92e8334169756eb4b26d26034754b9cd805d295c Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sat, 18 Apr 2026 06:13:44 +1200 Subject: [PATCH] (fix): purge cached project document before Realtime auth lookup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- app/realtime.php | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/app/realtime.php b/app/realtime.php index 822dd8eba8..a12cf875f2 100644 --- a/app/realtime.php +++ b/app/realtime.php @@ -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);