-
Notifications
You must be signed in to change notification settings - Fork 27
Eagerly cancel in-flight Oz review runs when a PR is closed #481
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+503
−1
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,233 @@ | ||
| """Eagerly cancel in-flight review runs when a pull request closes. | ||
|
|
||
| The webhook routes ``pull_request.closed`` to this fully-synchronous | ||
| path. The helper scans the in-flight KV records for review runs that | ||
| target the closed PR, cancels them via the Oz API, and deletes the KV | ||
| record on success so the cron poller never treats the cancellation as | ||
| a workflow failure. Cancel failures fail open: the record is left in | ||
| place and the cron drains the run with today's semantics. | ||
|
|
||
| Webhook deliveries are not ordered, so a stale ``closed`` event can | ||
| arrive after the PR was already reopened (and a fresh review run | ||
| dispatched). Before cancelling anything, the helper re-checks the | ||
| PR's live state on GitHub and skips cancellation unless the PR is | ||
| still closed. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
| import time | ||
| from typing import Any, Callable, Mapping | ||
|
|
||
| from .routing import WORKFLOW_REVIEW_PR | ||
| from .state import RunState, StateStore, delete_run_state, list_in_flight_runs | ||
| from .workflow_adapters import reconstruct_progress | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| # The cancel endpoint returns 409 while a run is still PENDING; one short | ||
| # retry covers the dispatch-to-queued transition without blocking the | ||
| # webhook for long. | ||
| _PENDING_RETRY_DELAY_SECONDS = 1.0 | ||
| _PENDING_STATUS_CODE = 409 | ||
|
|
||
| CANCELLED_PROGRESS_MESSAGE = ( | ||
| "I cancelled the in-progress review run because this pull request was closed." | ||
| ) | ||
|
|
||
|
|
||
| def _status_code(exc: Exception) -> int: | ||
| try: | ||
| return int(getattr(exc, "status_code", 0) or 0) | ||
| except (TypeError, ValueError): | ||
| return 0 | ||
|
|
||
|
|
||
| def _cancel_run( | ||
| canceller: Callable[[str], Any], | ||
| run_id: str, | ||
| *, | ||
| sleep: Callable[[float], None], | ||
| ) -> bool: | ||
| for attempt in range(2): | ||
| try: | ||
| canceller(run_id) | ||
| return True | ||
| except Exception as exc: | ||
| if _status_code(exc) == _PENDING_STATUS_CODE and attempt == 0: | ||
| sleep(_PENDING_RETRY_DELAY_SECONDS) | ||
| continue | ||
| logger.warning( | ||
| "cancel-review-runs: failed to cancel run %s; leaving its " | ||
| "in-flight record for the cron poller: %s", | ||
| run_id, | ||
| exc, | ||
| ) | ||
| return False | ||
| return False | ||
|
|
||
|
|
||
| def _complete_progress_comment( | ||
| state: RunState, | ||
| *, | ||
| github_client_factory: Callable[[int], Any] | None, | ||
| ) -> None: | ||
| if github_client_factory is None: | ||
| return | ||
| try: | ||
| client = github_client_factory(state.installation_id) | ||
| repo_handle = client.get_repo(state.repo) | ||
| progress = reconstruct_progress( | ||
| repo_handle, | ||
| state=state, | ||
| workflow=WORKFLOW_REVIEW_PR, | ||
| ) | ||
| progress.complete(CANCELLED_PROGRESS_MESSAGE) | ||
| except Exception: | ||
| logger.exception( | ||
| "cancel-review-runs: failed to update progress comment for run %s", | ||
| state.run_id, | ||
| ) | ||
|
|
||
|
|
||
| def _live_pr_state( | ||
| *, | ||
| github_client_factory: Callable[[int], Any], | ||
| installation_id: int, | ||
| repo_full_name: str, | ||
| pr_number: int, | ||
| ) -> str: | ||
| """Return the PR's current state on GitHub, or ``""`` when unknown.""" | ||
| try: | ||
| pr = ( | ||
| github_client_factory(installation_id) | ||
| .get_repo(repo_full_name) | ||
| .get_pull(pr_number) | ||
| ) | ||
| return str(getattr(pr, "state", "") or "").strip().lower() | ||
| except Exception: | ||
| logger.exception( | ||
| "cancel-review-runs: failed to verify state of %s PR #%s", | ||
| repo_full_name, | ||
| pr_number, | ||
| ) | ||
| return "" | ||
|
|
||
|
|
||
| def _matches_pr(state: RunState, *, repo_full_name: str, pr_number: int) -> bool: | ||
| if state.workflow != WORKFLOW_REVIEW_PR: | ||
| return False | ||
| if state.repo.lower() != repo_full_name.lower(): | ||
| return False | ||
| try: | ||
| return int((state.payload_subset or {}).get("pr_number") or 0) == pr_number | ||
| except (TypeError, ValueError): | ||
| return False | ||
|
|
||
|
|
||
| def cancel_in_flight_review_runs( | ||
| *, | ||
| store: StateStore, | ||
| canceller: Callable[[str], Any], | ||
| payload: Mapping[str, Any], | ||
| github_client_factory: Callable[[int], Any] | None = None, | ||
| sleep: Callable[[float], None] = time.sleep, | ||
| ) -> dict[str, Any]: | ||
| """Cancel every in-flight review run targeting the closed PR. | ||
|
|
||
| Returns a structured outcome the webhook surfaces in the 202 | ||
| response body: | ||
|
|
||
| - ``{"action": "skipped", "reason": ...}`` when the payload is | ||
| missing the repository slug or PR number, or when the PR is not | ||
| verifiably still closed on GitHub (stale ``closed`` delivery | ||
| after a reopen, or a failed state lookup). | ||
| - ``{"action": "noop", ...}`` when no in-flight review run targets | ||
| the PR (the common case). | ||
| - ``{"action": "cancelled", "cancelled_run_ids": [...], | ||
| "failed_run_ids": [...]}`` otherwise. Failed cancels keep their | ||
| KV record so the cron poller drains them as before. | ||
| """ | ||
| repo_payload = payload.get("repository") or {} | ||
| full_name = str( | ||
| repo_payload.get("full_name") or "" if isinstance(repo_payload, dict) else "" | ||
| ).strip() | ||
| pr_payload = payload.get("pull_request") or {} | ||
| try: | ||
| pr_number = int( | ||
| (pr_payload.get("number") if isinstance(pr_payload, dict) else 0) or 0 | ||
| ) | ||
| except (TypeError, ValueError): | ||
| pr_number = 0 | ||
| if "/" not in full_name or pr_number <= 0: | ||
| return { | ||
| "action": "skipped", | ||
| "reason": "missing repository.full_name or pull_request.number", | ||
| } | ||
|
|
||
| matches = [ | ||
| state | ||
| for state in list_in_flight_runs(store) | ||
| if _matches_pr(state, repo_full_name=full_name, pr_number=pr_number) | ||
| ] | ||
| if not matches: | ||
| return { | ||
| "action": "noop", | ||
| "reason": "no in-flight review runs", | ||
| "pr_number": pr_number, | ||
| } | ||
|
|
||
| # Deliveries can arrive late or out of order, so the event alone | ||
| # does not prove the PR is still closed. Only cancel when the live | ||
| # GitHub state confirms it; an unknown state also skips so a stale | ||
| # delivery can never kill a run dispatched for a reopened PR. | ||
| if github_client_factory is not None: | ||
| try: | ||
| installation_id = int( | ||
| (payload.get("installation") or {}).get("id") or 0 | ||
| ) | ||
| except (TypeError, ValueError): | ||
| installation_id = 0 | ||
| if installation_id <= 0: | ||
| installation_id = matches[0].installation_id | ||
| pr_state = _live_pr_state( | ||
| github_client_factory=github_client_factory, | ||
| installation_id=installation_id, | ||
| repo_full_name=full_name, | ||
| pr_number=pr_number, | ||
| ) | ||
| if pr_state != "closed": | ||
| return { | ||
| "action": "skipped", | ||
| "reason": ( | ||
| "pull request is no longer closed" | ||
| if pr_state == "open" | ||
| else "could not verify pull request is closed" | ||
| ), | ||
| "pr_number": pr_number, | ||
| } | ||
|
|
||
| cancelled_run_ids: list[str] = [] | ||
| failed_run_ids: list[str] = [] | ||
| for state in matches: | ||
| if not _cancel_run(canceller, state.run_id, sleep=sleep): | ||
| failed_run_ids.append(state.run_id) | ||
| continue | ||
| delete_run_state(store, state.run_id) | ||
| cancelled_run_ids.append(state.run_id) | ||
| _complete_progress_comment( | ||
| state, github_client_factory=github_client_factory | ||
| ) | ||
| return { | ||
| "action": "cancelled", | ||
| "pr_number": pr_number, | ||
| "cancelled_run_ids": cancelled_run_ids, | ||
| "failed_run_ids": failed_run_ids, | ||
| } | ||
|
|
||
|
|
||
| __all__ = [ | ||
| "CANCELLED_PROGRESS_MESSAGE", | ||
| "cancel_in_flight_review_runs", | ||
| ] | ||
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
Oops, something went wrong.
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pull_request.closedwebhook after a quick reopen will match by repo/PR number here and cancel the fresh review run thatreopenedjust dispatched.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is probably very rare and deals with the case that a reopen webhook event's processed before a close event (since webhook events aren't inherently ordered?). Latest commit includes a fix for this race.