This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch v3-4-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-4-test by this push:
new fc50b5186d3 [v3-4-test] Retry GitHub calls that drop the connection in
the Spot interruption notices (#75829) (#76051)
fc50b5186d3 is described below
commit fc50b5186d3d0aa2e6680bdc31ab3e2ddf1e2097
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Fri Oct 30 16:47:30 2026 +0000
[v3-4-test] Retry GitHub calls that drop the connection in the Spot
interruption notices (#75829) (#76051)
* Retry GitHub calls that drop the connection in the Spot interruption
notices
Only server errors counted as transient, so a connection GitHub dropped
mid-request (an HTTP/2 stream reset) was neither retried nor skipped like
one, and failed the scheduled run although the next run would do the same
work again.
Generated-by: Claude Opus 5
* Test each transient error pattern of the Spot notices' GitHub calls
(cherry picked from commit d2e60aefea255717df3aed0ccde4b4cb240d9c19)
Generated-by: Claude Opus 5
Co-authored-by: Jarek Potiuk <[email protected]>
---
scripts/ci/spot_interruptions.py | 5 +++-
scripts/tests/ci/test_spot_interruptions.py | 36 ++++++++++++++++++++++++++++-
2 files changed, 39 insertions(+), 2 deletions(-)
diff --git a/scripts/ci/spot_interruptions.py b/scripts/ci/spot_interruptions.py
index 08bb4d6da36..e78344fe074 100755
--- a/scripts/ci/spot_interruptions.py
+++ b/scripts/ci/spot_interruptions.py
@@ -80,7 +80,10 @@ COMMENT_AUTHOR = "github-actions[bot]"
# A job cancelled while its run was not, e.g. by its timeout, is a failure too.
FAILED_CONCLUSIONS = ("failure", "cancelled", "timed_out")
# GitHub answers with a server error while overloaded, and the same request
often succeeds later.
-TRANSIENT_ERROR = re.compile(r"\(HTTP 5\d\d\)")
+# Server errors, and connections GitHub dropped mid-request (an HTTP/2 stream
reset, for one).
+TRANSIENT_ERROR = re.compile(
+ r"\(HTTP 5\d\d\)|stream error|connection reset by peer|unexpected EOF|TLS
handshake timeout|i/o timeout"
+)
GH_API_ATTEMPTS = 3
GH_API_RETRY_DELAY_SECONDS = 5
diff --git a/scripts/tests/ci/test_spot_interruptions.py
b/scripts/tests/ci/test_spot_interruptions.py
index 29a6fc3fad3..d516890e658 100644
--- a/scripts/tests/ci/test_spot_interruptions.py
+++ b/scripts/tests/ci/test_spot_interruptions.py
@@ -297,9 +297,14 @@ def test_find_comment_ignores_comments_of_others(spot):
SERVER_ERROR = subprocess.CalledProcessError(1, ["gh"], stderr="gh: Server
Error (HTTP 502)\n")
NOT_FOUND_ERROR = subprocess.CalledProcessError(1, ["gh"], stderr="gh: Not
Found (HTTP 404)\n")
+STREAM_ERROR = subprocess.CalledProcessError(
+ 1, ["gh"], stderr="stream error: stream ID 3; CANCEL; received from peer\n"
+)
[email protected](("error", "expected_ok"), [(SERVER_ERROR, True),
(NOT_FOUND_ERROR, False)])
[email protected](
+ ("error", "expected_ok"), [(SERVER_ERROR, True), (STREAM_ERROR, True),
(NOT_FOUND_ERROR, False)]
+)
def test_run_pr_comments_carries_on_past_a_failing_run(spot, capsys, error,
expected_ok):
runs = [_run(branch="a"), _run(branch="b")]
@@ -329,6 +334,7 @@ def test_run_pr_comments_when_listing_the_runs_fails(spot,
error, expected_ok):
[
pytest.param(("repos/x",), [SERVER_ERROR, SERVER_ERROR], 3, None,
id="retries-server-errors"),
pytest.param(("repos/x",), [SERVER_ERROR] * 3, 3, SERVER_ERROR,
id="gives-up"),
+ pytest.param(("repos/x",), [STREAM_ERROR], 2, None,
id="retries-dropped-connections"),
pytest.param(("repos/x",), [NOT_FOUND_ERROR], 1, NOT_FOUND_ERROR,
id="no-retry-on-client-error"),
pytest.param(("-X", "POST", "repos/x"), [SERVER_ERROR], 1,
SERVER_ERROR, id="no-retry-on-post"),
pytest.param(("-X", "PATCH", "repos/x"), [SERVER_ERROR], 2, None,
id="retries-patch"),
@@ -350,6 +356,34 @@ def test_gh_api_retries_server_errors(
assert mock_run.call_count == expected_calls
[email protected](
+ ("stderr", "expected"),
+ [
+ pytest.param("gh: Server Error (HTTP 502)\n", True, id="server-error"),
+ pytest.param("stream error: stream ID 3; CANCEL; received from
peer\n", True, id="stream-reset"),
+ pytest.param(
+ 'Get "https://api.github.com/repos/x": read tcp
10.1.0.4:52144->140.82.112.6:443: '
+ "read: connection reset by peer\n",
+ True,
+ id="connection-reset",
+ ),
+ pytest.param('Get "https://api.github.com/repos/x": unexpected EOF\n',
True, id="unexpected-eof"),
+ pytest.param(
+ 'Get "https://api.github.com/repos/x": net/http: TLS handshake
timeout\n', True, id="tls-timeout"
+ ),
+ pytest.param(
+ 'Get "https://api.github.com/repos/x": dial tcp 140.82.112.6:443:
i/o timeout\n',
+ True,
+ id="dial-timeout",
+ ),
+ pytest.param("gh: Not Found (HTTP 404)\n", False, id="client-error"),
+ pytest.param(None, False, id="no-stderr"),
+ ],
+)
+def test_is_transient(spot, stderr, expected):
+ assert spot.is_transient(subprocess.CalledProcessError(1, ["gh"],
stderr=stderr)) is expected
+
+
def test_run_slack_writes_outputs(spot, monkeypatch, tmp_path):
output = tmp_path / "output"
monkeypatch.setenv("GITHUB_OUTPUT", str(output))