docs(skill): record why a retried merge-request PUT cannot double-apply (#616 follow-up) - #617
Merged
Merged
Conversation
…ly (#616 follow-up) The reviewer on #616 flagged that RETRY_SAFE_METHODS treats every PUT as retry-safe, while client/merge_requests.py uses PUT for four action-style transitions (/request-review, /approve, /request-changes, /merge). The worry was that a 5xx raised after the transition committed would be retried and fire it -- and its notifications -- twice. Verified against the keboola/connection source: it cannot. The MR lifecycle is a Symfony Workflow state_machine, so three of the four transitions are refused structurally on a second call (enabled only from their declared `from` place); /approve is the one self-loop and carries AddApprovalGuard instead. Notifications ride workflow.merge_request_lifecycle.completed from inside apply(), inside MergeRequestService's transactional() -- no transition, no notification. No code change. Two things the audit did surface are recorded with it: - A retried PUT reports attempt 2's error, so an operation that succeeded and merely lost its response surfaces as 422/409. That applies to every retried PUT/DELETE, not just merge requests. - bi_rMergeRequestsApprovals has no unique constraint on (mergeRequestId, idAdmin) and hasEnoughApprovals() counts rows rather than distinct admins. Server-side and narrow; filed as keboola/connection#8209. Deliberately not worked around here -- the blanket method rule stays, with no per-call-site retry opt-out.
… upstream issue The gotcha stated the mechanism conditionally but never said the duplicate row was not reproduced and the isolation level was not checked -- a caveat the upstream issue does carry, so the two documents disagreed on how firm the finding is. It also had no pointer to keboola/connection#8209, leaving a future reader no handle to re-check it or notice it was fixed.
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.
Follow-up to #616 (issue #599). Documentation only — no code change, and none is needed.
The question
The reviewer on #616 flagged, non-blocking and outside that diff, that
RETRY_SAFE_METHODStreats everyPUTas retry-safe whileclient/merge_requests.pyusesPUTfor four action-style transitions:/request-review,/approve,/request-changes,/merge.PUTis formallyidempotent, so the rule follows RFC 9110 — but these endpoints read as commands,
not replacements. A 500 raised after the transition committed would be
retried, potentially firing the transition and its notifications twice.
Worth noting this was never a regression from #616: before it, every method was
retried on a 5xx, so these were already exposed. #616 simply did not narrow them.
The answer: the server refuses the second call, structurally
Verified against the
keboola/connectionsource. The MR lifecycle is a SymfonyWorkflow
state_machine(MergeRequestLifecycleStateMachine), and such atransition is enabled only from its declared
fromplaces:/request-reviewin_review(orapprovedviaskip_review)development→ 422/request-changesdevelopmentin_review/approved→ 422/mergein_mergeMergeProcessorcheckscan(MERGE)(only fromapproved) behind a per-project MySQL table lock → 409storage.mergeRequests.notReadyToMerge/approvein_review(self-loop) orapprovedAddApprovalGuard: "This reviewer has already approved this request." → 422approveis the only self-loop (in_review -> in_review), which is exactly whyit carries a dedicated guard rather than relying on the place structure.
Notifications cannot double-fire either: emails, audit-log entries and storage
events are emitted by
TransitionListeneronworkflow.merge_request_lifecycle.completed— from insideapply(), insideMergeRequestService'stransactional(). The side effects are welded to thetransition.
Two things the audit did surface
Both are recorded in the same gotcha:
A retried
PUTreports attempt 2's error. The retry loop surfaces thelast response, so an operation that succeeded on attempt 1 and merely lost
its response (500 after commit, read timeout) is reported as
422/409.That is a reporting hazard, not a state one, and it applies to every retried
PUT/DELETEin kbagent — not only to merge requests. The guidance is toread a 4xx state conflict as "check the MR, this may already be done".
A server-side race in
approve.bi_rMergeRequestsApprovalshas nounique constraint on
(mergeRequestId, idAdmin), andhasEnoughApprovals()counts rows rather than distinct admins — so twooverlapping approvals from one admin could satisfy a two-approval
requirement. Filed as keboola/connection#8209 with the schema and source
evidence; the duplicate row was not reproduced, and that caveat is stated in
the issue.
What was deliberately not done
No per-call-site
retry_safe=True/Falseoverride. That was considered andrejected when #616 was designed — one auditable method rule beats an opt-out
anyone can flip on the wrong endpoint — and nothing here changes that
trade-off. A client-side workaround would not close the UI double-submit path
for (2) anyway.
Verification
Source-based, against
keboola/connectionviagh api. A live E2E was notpossible and would not have been conclusive: no reachable project carries both
protected-default-branchandbranches-merge-requests,approverequires asecond admin (
AddApprovalGuardforbids self-approval), and a 500 raised afterthe transaction commits cannot be induced from the client side.
ruff checkandruff format --checkclean. The change touches one Markdownfile, so no test surface is affected.