{{ message }}
Repository navigation
fix(functions): publish a deployment as ready only once it is executable - #13741
Merged
Merged
Conversation
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
force-pushed
the
fix/deployment-ready-after-activate
branch
from
September 17, 2026 12:03
246d3ac to
1012b5a
Compare
Contributor
…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.
HarshMN2345
approved these changes
Sep 18, 2026
2 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

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 asreadywhile the function still had an emptydeploymentId, so a client that polled forreadyand immediately executed got404 DEPLOYMENT_NOT_FOUNDfromPOST /v1/functions/:functionId/executions.The order was: guarded
status => readywrite →updateLatestDeployment()→activate(), andactivate()walks every matching platform rule indbForPlatformbefore it writesdeploymentId. 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::testDeploymentBuildOutputIsServedFromTheBuildsDevicein thes3group. On this run 19 assertions passed and then the execution came back 404, with the server loggingDeployment not found. Create a deployment before trying to execute a functionfromExecutions/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 thelatestDeployment*pointer land first, then activation, then the status. The invariant becomes: a deployment never reportsreadyuntil everything reachable from a ready deployment is already durable.Details worth reviewing:
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.latestDeploymentStatusis now refreshed after the flip instead of carrying it. Leading with it would let a client that force-activates on seeingreadyraceBUILD_NOT_READYfrom 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 beforeactivate(), preserving the existing fix for clients observing a stalelatestDeploymentId.Builds.phpworker never writesready, so no change is needed there.Test
The e2e probe now asserts the invariant in the same iteration that first observes
ready, and throwsCriticalsoassertEventuallyaborts 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.