Return success for a forced stop of a stopped environment
What does this MR do and why?
POST /api/v4/projects/:id/environments/:environment_id/stop with force=true returned HTTP 500 when the environment was already stopped. Environments::StopService#unsafe_execute! called the bang event stop_complete!, and stop_complete is declared for %i[available stopping] => :stopped only, so the bang form raised StateMachines::InvalidTransition and it escaped the Grape endpoint as a 500.
The non-force branch was already idempotent: Environment#stop_with_actions! returns early unless the environment is available. This makes the forced path match, using the non-bang event so an already-stopped environment falls through to the existing stopped? || stopping? success check.
Seen in production; impact and traffic figures are in the linked confidential issue. The failures are retry-shaped, so a caller that retries a forced stop keeps getting 500s indefinitely.
The EnvironmentStop GraphQL mutation forwards force: into the same branch and inherits the fix. Environments::AutoStopWorker does not reach it: it builds the service without params, so params[:force] is always nil and it always takes the stop_with_actions! branch.
References
Fixes https://gitlab.com/gitlab-org/gitlab/-/issues/629465 (confidential).
How to set up and validate locally
- Create an environment with no
on_stopaction and stop it, so its state isstopped. curl --request POST --header "PRIVATE-TOKEN: <token>" "http://127.0.0.1:3000/api/v4/projects/<id>/environments/<env_id>/stop?force=true"- Before this change the call returns 500; after it returns 200 and the state stays
stopped.
Verification
Local GDK worktree, RED/GREEN against the same specs (the RED arm reverts only app/services/environments/stop_service.rb to master and keeps the new examples):
| arm | spec/services/environments/stop_service_spec.rb |
spec/requests/api/environments_spec.rb |
|---|---|---|
| red (bang event) | 38 examples, 2 failures | 25 examples, 1 failure, 2 pending |
| green (this change) | 38 examples, 0 failures | 25 examples, 0 failures, 2 pending |
The RED request-spec failure is the 500 with StateMachines::InvalidTransition raised from Environments::StopService#unsafe_execute!. No infrastructure failures in those runs. rubocop clean on the three touched files.
What I did not verify: the GraphQL EnvironmentStop mutation path was not exercised locally — it shares the fixed branch but has its own specs, left to CI.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist.