Skip to content

Commit 3f80ed6

Browse files
authored
Merge branch 'main' into async-compat
2 parents a4d23de + 1ae3c25 commit 3f80ed6

2 files changed

Lines changed: 100 additions & 1 deletion

File tree

tests/examples/conftest.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212

1313
REPO_ROOT = Path(__file__).resolve().parent.parent.parent
1414
EXAMPLES_DIR = REPO_ROOT / 'examples'
15+
DAPR_PORT_BIND_FAILURE_MARKERS = (
16+
'bind: address already in use',
17+
'failed to start internal gRPC server: could not listen on any endpoint',
18+
)
1519

1620

1721
def pytest_configure(config: pytest.Config) -> None:
@@ -38,7 +42,18 @@ def _terminate(proc: subprocess.Popen[str]) -> None:
3842
terminate_process_group(proc, force=True)
3943
proc.wait()
4044

41-
def run(self, args: str, *, timeout: int = 30, until: list[str] | None = None) -> str:
45+
@staticmethod
46+
def _is_dapr_port_bind_failure(output: str) -> bool:
47+
return all(marker in output for marker in DAPR_PORT_BIND_FAILURE_MARKERS)
48+
49+
def run(
50+
self,
51+
args: str,
52+
*,
53+
timeout: int = 30,
54+
until: list[str] | None = None,
55+
port_bind_retries: int = 3,
56+
) -> str:
4257
"""Run a foreground command, block until it finishes, and return output.
4358
4459
Use this for short-lived processes (e.g. a publisher that exits on its
@@ -49,7 +64,24 @@ def run(self, args: str, *, timeout: int = 30, until: list[str] | None = None) -
4964
timeout: Maximum seconds to wait before killing the process.
5065
until: If provided, the process is terminated as soon as every
5166
string in this list has appeared in the accumulated output.
67+
port_bind_retries: Retry count for Dapr sidecar startup failures
68+
caused by a transient random-port collision.
5269
"""
70+
attempts = max(1, port_bind_retries + 1)
71+
for attempt in range(attempts):
72+
output = self._run_once(args, timeout=timeout, until=until)
73+
if attempt < attempts - 1 and self._is_dapr_port_bind_failure(output):
74+
print(
75+
'Dapr sidecar failed to bind a random port; '
76+
f'retrying startup after {2**attempt}s '
77+
f'(attempt {attempt + 1}/{attempts})',
78+
flush=True,
79+
)
80+
time.sleep(2**attempt)
81+
continue
82+
return output
83+
84+
def _run_once(self, args: str, *, timeout: int, until: list[str] | None) -> str:
5385
proc = subprocess.Popen(
5486
args=('dapr', 'run', *shlex.split(args)),
5587
cwd=self._cwd,

tests/examples/test_dapr_runner.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import subprocess
2+
import time
3+
from pathlib import Path
4+
5+
import pytest
6+
7+
from tests.examples.conftest import DaprRunner
8+
9+
10+
class FakeProcess:
11+
def __init__(self, output: str, returncode: int = 1) -> None:
12+
self.stdout = iter(output.splitlines(keepends=True))
13+
self.returncode = returncode
14+
15+
def poll(self) -> int:
16+
return self.returncode
17+
18+
def wait(self, timeout: int | None = None) -> int:
19+
return self.returncode
20+
21+
22+
def test_run_retries_transient_dapr_port_bind_failure(
23+
monkeypatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
24+
) -> None:
25+
outputs = [
26+
(
27+
'level=error msg="Failed to listen for gRPC server on TCP address :33223 '
28+
'with error: listen tcp :33223: bind: address already in use"\n'
29+
'level=fatal msg="Fatal error from runtime: failed to start internal gRPC '
30+
'server: could not listen on any endpoint"\n'
31+
),
32+
"{'secretKey': 'secretValue'}\n",
33+
]
34+
popen_calls = []
35+
36+
def fake_popen(*args, **kwargs) -> FakeProcess:
37+
popen_calls.append((args, kwargs))
38+
return FakeProcess(outputs.pop(0))
39+
40+
monkeypatch.setattr(subprocess, 'Popen', fake_popen)
41+
sleeps: list[int] = []
42+
monkeypatch.setattr(time, 'sleep', sleeps.append)
43+
44+
output = DaprRunner(tmp_path).run('--app-id=secretsapp -- python3 example.py', timeout=1)
45+
46+
assert output == "{'secretKey': 'secretValue'}\n"
47+
assert len(popen_calls) == 2
48+
assert sleeps == [1]
49+
assert (
50+
'Dapr sidecar failed to bind a random port; retrying startup after 1s'
51+
in capsys.readouterr().out
52+
)
53+
54+
55+
def test_run_does_not_retry_non_port_bind_failure(monkeypatch, tmp_path: Path) -> None:
56+
popen_calls = []
57+
58+
def fake_popen(*args, **kwargs) -> FakeProcess:
59+
popen_calls.append((args, kwargs))
60+
return FakeProcess('application failed before printing expected output\n')
61+
62+
monkeypatch.setattr(subprocess, 'Popen', fake_popen)
63+
64+
output = DaprRunner(tmp_path).run('--app-id=secretsapp -- python3 example.py', timeout=1)
65+
66+
assert output == 'application failed before printing expected output\n'
67+
assert len(popen_calls) == 1

0 commit comments

Comments
 (0)