Skip to content

Commit 2d2c9fb

Browse files
committed
test: retry Dapr example port bind flake
Signed-off-by: Ghxst <200635707+GHX5T-SOL@users.noreply.github.com>
1 parent d577d20 commit 2d2c9fb

2 files changed

Lines changed: 83 additions & 1 deletion

File tree

tests/examples/conftest.py

Lines changed: 28 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 = 1,
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,19 @@ 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+
continue
75+
return output
76+
77+
return output
78+
79+
def _run_once(self, args: str, *, timeout: int, until: list[str] | None) -> str:
5380
proc = subprocess.Popen(
5481
args=('dapr', 'run', *shlex.split(args)),
5582
cwd=self._cwd,

tests/examples/test_dapr_runner.py

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

0 commit comments

Comments
 (0)