Repository navigation
fix: [NACT-2932] retry GitLab MR creation and stop blocking on traceback rendering - #650
Conversation
8d59890 to
39ee978
Compare
…ack rendering foxops asks GitLab to open a merge request immediately after pushing the branch. GitLab answers from a branch cache that can lag the push, rejects the request with `Source branch "<x>" does not exist`, and foxops did not retry. The resulting traceback then took ~52s to log, because structlog's ConsoleRenderer falls back to rich's traceback formatter whenever rich is installed, and that formatter renders every frame's local variables. It ran twice per failed request: once in catch_all, once more after Starlette re-raises and uvicorn logs it. Rendering is synchronous on the event loop and happens before the error response is sent, so replies missed the 60s reverse-proxy timeout and callers saw a misleading 504. - retry the merge request POST for up to 10s while GitLab reports the pushed source branch as missing - raise MergeRequestCreationFailedError with GitLab's own message instead of httpx's "Client error '400 Bad Request' for url ..." - render plain tracebacks, without frame locals
39ee978 to
f6c581e
Compare
ermirry
left a comment
There was a problem hiding this comment.
Found one timing issue with the retry deadline. All six added tests pass locally.
| @retry( | ||
| retry=retry_if_exception_type(_SourceBranchNotYetVisible), | ||
| stop=stop_after_delay(MERGE_REQUEST_SOURCE_BRANCH_RETRY_TIMEOUT.total_seconds()), | ||
| wait=wait_fixed(MERGE_REQUEST_SOURCE_BRANCH_RETRY_WAIT.total_seconds()), |
There was a problem hiding this comment.
[P2] Enforce the retry deadline during HTTP requests
stop_after_delay(10) checks elapsed time after a failed attempt; it does not interrupt an in-flight POST. Each retry inherits the client's 120-second HTTP timeout, so a quick missing-branch response followed by a slow retry can exceed both the stated 10-second budget and the proxy's 60-second timeout, producing another 504. Apply a deadline covering retry waits and HTTP requests.
Reproduced with a 0.1-second budget: a slow second attempt kept the call running for 0.62 seconds.
There was a problem hiding this comment.
Thanks for catching this! Fixed
I give the POST request itself a timeout, same value as the 10s retry budget. So even GitLab answer very slow, this one request cannot run more than 10s (before it use client's default 120s).
Also I simplify the retry logic: now reuse httpx.HTTPStatusError (from raise_for_status()) directly as the retry signal, not need custom exception class anymore. One small function decide: if error is "branch not visible yet" then retry it, if other error then fail immediately, not retry.
…gaps between attempts stop_after_delay only checks elapsed time between attempts; it doesn't interrupt an in-flight POST, which inherits the client's 120s timeout. Wrap each attempt in asyncio.wait_for scoped to what's left of the retry budget, floored at the retry wait so a fast call near the deadline still gets a chance to complete.
…mr-creation-race-and-timeout-handling
…f a tracked deadline asyncio.wait_for + a hand-tracked remaining-budget deadline was more machinery than this needed. Passing the retry timeout straight through as the request's own timeout gets the same practical effect for the case this guards against (one slow attempt), with far less code. Test now asserts the timeout is wired onto the request instead of simulating a slow GitLab response, since httpx.MockTransport never enforces timeouts itself.
…tusError _SourceBranchNotYetVisible plus the manual is_client_error check and a body-truncation helper were more machinery than needed. raise_for_status() already gives an httpx.HTTPStatusError for GitLab's 4xx; a single predicate classifies it as retryable or not, reused by both the tenacity retry condition and the inner/outer error handling. Also applied the team's Python style checklist across the whole PR diff (not just this round's changes): no comments/docstrings, test functions annotated -> None, assertions expected-value-first.
Two spots already know this happened but said nothing: the retry giving up in _create_merge_request, and update_incomplete_change() when /fix finds a commit and branch but no MR. Log both with the repository and branch name so this is searchable/alertable instead of only surfacing as a generic 500 or a 422 on manual repair.
Keeps the non-obvious bit (ConsoleRenderer silently picks rich's exception formatter, which repr()s every frame's locals on the event loop) and drops the incident narrative, which belongs in the MR.
|
Tested this change on a real environment, not only unit tests. What I did:
Result:
Limitation:
|
The pattern only matched 'Source branch "x" does not exist', which comes from
MergeRequests::BuildService behind the web UI. Creating a merge request over
the REST API reaches a model validation instead, which reports the rejection
per attribute as {"source_branch": ["does not exist"]}, so the retry never
fired for the case it was written for. Verified against Gitlab 19.3.7-ee.
The unit tests passed because the mocked body was written from the same
assumption as the pattern, so they now cover both wordings.
The sentence form comes from MergeRequests::BuildService, which only the web UI goes through: lib/api/merge_requests.rb calls MergeRequests::CreateService directly, and neither it nor CreateService mentions BuildService, on 17.2.8 or on 19.3.6. foxops only ever calls the API, so matching that wording covered a response it cannot receive. The pattern now states the shape it expects, which leaves nothing for a comment to add.
WHY?
Nightly
nactl updateruns fail intermittently. foxops pushes the branch andasks GitLab to open an MR in the same breath; GitLab answers from a branch cache
that can lag the push and rejects it with
Source branch "<x>" does not exist.foxops did not retry. Logging the resulting error then took ~52s, which pushed
the reply past the 60s reverse-proxy timeout, so the caller saw a misleading 504
instead of the real GitLab error.
WHAT?
pushed source branch as missing. Safe to retry: GitLab only sends this message
when it created nothing.
MergeRequestCreationFailedErrorcarrying GitLab's own message, insteadof httpx's
Client error '400 Bad Request' for url ....ConsoleRenderersilently switches torich's formatter when
richis installed, and that formatter reprs everyframe's local variables.
ADDITIONAL NOTES
The traceback was rendered twice per failed request — once in
catch_all, oncemore after Starlette re-raises and uvicorn logs it. Rendering is synchronous on
the event loop, so it stalled every other in-flight request too, and the first
render happens before the error response is sent.
Not touched: the mergeability-check loop in
merge_request()can still spend upto 25s of the same 60s budget. Separate concern.