Skip to content

the register gate's retry cannot save a lost answer: attempt 2 gets a 422 and fails anyway #116

Description

@jhgaylor

a24f420 (#107) gave the register gate a captured body and one retry, to survive kubectl run -i losing a short-lived pod's output. The retry cannot actually save the case it was built for, because registration is not idempotent.

Observed on main at 1fc15f3, 2026-08-07 20:06 UTC (run):

── the account path: register, verify, get a key ──
  (registration pod produced no output — attach flake, retrying once)
curl: (22) The requested URL returned error: 422
pod fountain/e2e-register-2 terminated (Error)
  ✗ e2e: registration did not return a user_id

Attempt 1 registered the account and lost its output. Attempt 2 re-posted the same address, hit the unique-email constraint, and returned 422 — so the gate reported that registration failed, which is precisely the false red the retry exists to prevent.

The existing comment anticipates the mechanism but reads it as acceptable:

Registering the same address twice cannot double-register: the second attempt fails on the unique email, which still fails this gate — the retry only forgives losing the answer, never the act.

That holds when attempt 1 genuinely did not register. But the likelier attach-flake shape is the one seen here — the pod ran, the act happened, only the answer was lost — and there the retry converts a lost answer into a hard failure. The retry helps in the rarer half of the cases and is inert in the common one.

This is the same root flake as #111's red first-admin gate, fixed for the eval-pod recipes in #112. Those two were safe to retry only because promote_admin/1 and Accounts.verify_email/2 are idempotent. Registration is the one that isn't, which is why it needs a different answer rather than the same loop.

A fix that does not weaken the gate

On attempt 2, a 422 / has already been taken for the address attempt 1 used is not a failed registration — it is attempt 1's receipt, arriving by the only route left once its own output was lost. Accepting it costs nothing in assertion strength:

  • user_id is only ever a presence check (grep -q user_id). It is never captured and nothing downstream uses it.
  • The next line runs just verify-email "$email", which hard-fails with no account for … if the account does not exist. A non-existent account still cannot get through the gate.

Sketch:

reg=""; registered=""
for attempt in 1 2; do
  reg="$(kubectl run "e2e-register-$attempt"|| true)"
  if printf '%s' "$reg" | grep -q user_id; then registered=yes; break; fi
  if [ "$attempt" = 2 ] && printf '%s' "$reg" | grep -qE '422|already been taken'; then
    echo "  (attempt 1 registered after all — the 422 on the retry is its receipt)"
    registered=yes; break
  fi
  echo "  (registration pod produced no output — attach flake, retrying once)"
done
[ -n "$registered" ] || { printf '%s\n' "$reg" | tail -5; fail "registration did not return a user_id"; }

Frequency

Twice in one day, both on CI runners: once on #111 (the first-admin gate, a different recipe, same root cause) and once here on main. Both passed on a rerun of identical input, so the current cost is a rerun per occurrence plus the time spent deciding whether the red was real — on #111 it was misread as a possible fountain-pin regression before the evidence ruled that out.

Raised rather than fixed as part of #112, which was scoped to the eval-pod recipes.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions