Return success for a forced stop of a stopped environment

🤖 AI-authored change.

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

  1. Create an environment with no on_stop action and stop it, so its state is stopped.
  2. curl --request POST --header "PRIVATE-TOKEN: <token>" "http://127.0.0.1:3000/api/v4/projects/<id>/environments/<env_id>/stop?force=true"
  3. 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.


🤖 Automated change — mention me to get a response. Feedback welcome. Ask to speak to John directly anytime.

Edited by John Mason

Merge request reports

Loading
Loading