fix(functions): publish a deployment as ready only once it is executable by ChiragAgg5k · Pull Request #13741 · appwrite/appwrite · GitHub
Skip to content

fix(functions): publish a deployment as ready only once it is executable - #13741

Merged
ChiragAgg5k merged 2 commits into
mainfrom
fix/deployment-ready-after-activate
Sep 18, 2026
Merged

ChiragAgg5k merged 2 commits into
mainfrom
fix/deployment-ready-after-activate

Conversation

@ChiragAgg5k

Copy link
Copy Markdown
Member

What

Jobs::finalize() wrote the terminal status onto the deployment before it pointed the resource at that deployment. Between those two writes the API reported a build as ready while the function still had an empty deploymentId, so a client that polled for ready and immediately executed got 404 DEPLOYMENT_NOT_FOUND from POST /v1/functions/:functionId/executions.

The order was: guarded status => ready write → updateLatestDeployment() → activate(), and activate() walks every matching platform rule in dbForPlatform before it writes deploymentId. That rule scan is what makes the window wide enough to lose in practice.

Why now

It is the cause of the flake on FunctionsCustomServerTest::testDeploymentBuildOutputIsServedFromTheBuildsDevice in the s3 group. On this run 19 assertions passed and then the execution came back 404, with the server logging Deployment not found. Create a deployment before trying to execute a function from Executions/Create.php. The whole test ran in 4.0 seconds, so it lost the race by milliseconds. It is not a test-only problem: any SDK user doing create-then-poll-then-execute can hit the same window.

How

Split the terminal write in two so the status flip is the last thing finalize() does. Build metadata and the latestDeployment* pointer land first, then activation, then the status. The invariant becomes: a deployment never reports ready until everything reachable from a ready deployment is already durable.

Details worth reviewing:

  • Both writes keep the Query::notEqual('status', 'canceled') guard, so a concurrent cancel still wins. The second guard matters because the cancel endpoint falls back to an unlocked write under lock contention.
  • latestDeploymentStatus is now refreshed after the flip instead of carrying it. Leading with it would let a client that force-activates on seeing ready race BUILD_NOT_READY from the activation endpoint. updateLatestDeployment() returns the id it pointed at so the follow-up write is sparse and needs no second query.
  • updateLatestDeployment() still runs before activate(), preserving the existing fix for clients observing a stale latestDeploymentId.
  • The legacy Builds.php worker never writes ready, so no change is needed there.

Test

The e2e probe now asserts the invariant in the same iteration that first observes ready, and throws Critical so assertEventually aborts instead of retrying the failure away. That makes it deterministic rather than a second flaky check: on the old ordering it fails whenever the window is open, and on the new ordering it cannot fail.

Verified locally: php -l, Pint, and PHPStan level 4 all clean on the changed files. E2E needs CI.

finalize() wrote the terminal status onto the deployment before it pointed
the resource at that deployment, so between those two writes the API
reported a build as ready while the function still had an empty
deploymentId. A client that polled for ready and immediately executed got
404 DEPLOYMENT_NOT_FOUND from POST /v1/functions/:functionId/executions.

The window is wide enough to hit in practice because activate() walks every
matching platform rule before it writes deploymentId, and is what makes
testDeploymentBuildOutputIsServedFromTheBuildsDevice flaky in the s3 group.

Split the terminal write in two so the status flip is the last thing
finalize() does: build metadata and the latestDeployment pointer land first,
then activation, then the status. Both writes keep the notEqual('canceled')
guard, so a concurrent cancel still wins. latestDeploymentStatus is
refreshed after the flip rather than before it, so a client that
force-activates on seeing ready cannot race BUILD_NOT_READY.

The e2e probe now asserts the invariant in the same iteration that first
observes ready, throwing Critical so assertEventually cannot retry the
failure away.
@ChiragAgg5k
ChiragAgg5k force-pushed the fix/deployment-ready-after-activate branch from 246d3ac to 1012b5a Compare September 17, 2026 12:03
@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

@greptile-apps

greptile-apps Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Comment thread src/Appwrite/Platform/Modules/Functions/Workers/Jobs.php Outdated
…status

The status refresh keyed off an id captured before activate(), so a newer
deployment for the same resource finishing during the activation rule scan
left latestDeploymentId pointing at the newer build while
latestDeploymentStatus carried the older one's outcome.

Re-run updateLatestDeployment() instead, so the id and the status it writes
come from the same query.
@ChiragAgg5k
ChiragAgg5k merged commit 65cffb1 into main Sep 18, 2026
49 checks passed
@ChiragAgg5k
ChiragAgg5k deleted the fix/deployment-ready-after-activate branch September 18, 2026 08:49
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.

2 participants