Skip to content

Commit 9a44bbc

Browse files
fix(client): align delete_stream poll window with 300s server drain
librtmp2-server now waits up to 300s (DELETE_DRAIN_TIMEOUT) before finalizing an async stream delete. The panel client still defaulted to 35s, so operators deleting live streams saw a false failure while the server was still draining normally. Raise DELETE_STREAM_DRAIN_WAIT_SECONDS to 305 and bump the Docker Gunicorn --timeout to 330 so the synchronous delete route can wait out the full server drain window. Co-authored-by: Alexander Wagner <info@alexanderwagnerdev.com>
1 parent a7429f5 commit 9a44bbc

5 files changed

Lines changed: 55 additions & 11 deletions

File tree

.cursor/bug-scan-progress.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,39 @@
11
# Bug scan progress
22

3-
Last scanned: app.py — 2026-07-15
3+
Last scanned: lrtmp2_client.py — 2026-08-12
44

55
## Module checklist
66

77
- [x] `app.py` — Flask routes, auth, session handling, stream CRUD
8-
- [ ] `lrtmp2_client.py` — librtmp2-server REST API client
8+
- [x] `lrtmp2_client.py` — librtmp2-server REST API client
99
- [ ] `config.py` — startup validation and environment configuration
1010
- [ ] `templates/` — Jinja2 templates (XSS, CSRF forms)
1111
- [ ] `static/js/` — frontend JavaScript (DOM injection, fetch logic)
1212

1313
(`templates/`/`static/js/` were actually scanned 2026-07-05/06, see findings
1414
below — checkboxes just hadn't been ticked.)
1515

16+
## Findings (2026-08-12 lrtmp2_client.py pass)
17+
18+
- **Bug (fixed):** `delete_stream()` default `wait_timeout=35` was sized for an
19+
obsolete 30s librtmp2-server RTMP drain window. Current server
20+
`DELETE_DRAIN_TIMEOUT` is **300s** (`librtmp2-server` `src/http.rs`): during
21+
drain the stream stays in `GET /api/v1/streams` with `enabled=false` until
22+
finalize. Scenario: operator deletes a live stream with long-lived RTMP
23+
sessions; server returns HTTP 202 and keeps draining; after 35s the panel
24+
raises `Lrtmp2ApiError` ("stream is still present…") even though the delete
25+
is still progressing normally on the server for up to five more minutes.
26+
Impact: false failure during incident response — operator believes the revoke
27+
failed and may stop monitoring while publish/play keys remain valid until
28+
drain completes. Fixed by defaulting `wait_timeout` to 305s
29+
(`DELETE_STREAM_DRAIN_WAIT_SECONDS`) and raising the Docker Gunicorn
30+
`--timeout` to 330s so the synchronous delete route can outlast the server
31+
drain.
32+
- Reviewed but not a bug: network/JSON errors wrapped as `Lrtmp2ApiError`;
33+
path segments URL-encoded; Bearer token only in Authorization header;
34+
`delete_stream` 202 polling until stream disappears; `cluster_remove_node`
35+
surfaces 404; per-call timeouts; no shared mutable request state.
36+
1637
## Findings (2026-07-15 app.py pass)
1738

1839
- **Bug (fixed):** `delete_stream()` moved deletes into a daemon background

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,4 @@ HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
3737
CMD wget -qO- http://localhost:8000/login || exit 1
3838

3939
ENTRYPOINT ["entrypoint.sh"]
40-
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--worker-class", "gthread", "--threads", "4", "--timeout", "60", "app:app"]
40+
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--worker-class", "gthread", "--threads", "4", "--timeout", "330", "app:app"]

lrtmp2_client.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ class Lrtmp2ApiError(Exception):
88
pass
99

1010

11+
# librtmp2-server's production DELETE_DRAIN_TIMEOUT is 300s (src/http.rs).
12+
# The panel must outlast that window when polling list_streams after HTTP 202.
13+
DELETE_STREAM_DRAIN_WAIT_SECONDS = 305
14+
15+
1116
def _api_error(resp, operation):
1217
"""Return a user-safe error; log details only in the exception message prefix."""
1318
msg = f"{operation} failed (HTTP {resp.status_code})"
@@ -110,16 +115,22 @@ def create_stream(
110115
json=payload,
111116
)
112117

113-
def delete_stream(self, stream_id, wait_timeout=35, poll_interval=0.5):
118+
def delete_stream(
119+
self,
120+
stream_id,
121+
wait_timeout=DELETE_STREAM_DRAIN_WAIT_SECONDS,
122+
poll_interval=0.5,
123+
):
114124
"""Delete a stream. librtmp2-server may accept the request with `202`
115125
and finish the delete asynchronously (draining active RTMP sessions
116126
first) — poll until the stream actually disappears from the list so
117127
callers can rely on the stream being gone once this returns, rather
118-
than racing the background delete. librtmp2-server waits up to 30s for
119-
active RTMP sessions to drain before giving up, so the default
120-
wait_timeout is 35s. If the stream is still listed after that window,
121-
raises Lrtmp2ApiError so the panel can surface the incomplete delete
122-
instead of silently redirecting while the stream remains.
128+
than racing the background delete. librtmp2-server waits up to 300s for
129+
active RTMP sessions to drain before abandoning local roles and
130+
finalizing, so the default wait_timeout is 305s. If the stream is
131+
still listed after that window, raises Lrtmp2ApiError so the panel can
132+
surface the incomplete delete instead of silently redirecting while the
133+
stream remains.
123134
"""
124135
resp = self._request(
125136
requests.delete,

tests/test_delete_runtime.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def test_container_uses_threaded_gunicorn_for_long_running_deletes():
1919
assert command[0] == "gunicorn"
2020
assert command[command.index("--worker-class") + 1] == "gthread"
2121
assert command[command.index("--threads") + 1] == "4"
22-
assert command[command.index("--timeout") + 1] == "60"
22+
assert command[command.index("--timeout") + 1] == "330"
2323

2424

2525
def test_delete_stream_logs_api_failure(monkeypatch):

tests/test_lrtmp2_client.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
import pytest
44
import requests
55

6-
from lrtmp2_client import Lrtmp2ApiError, Lrtmp2Client
6+
from lrtmp2_client import (
7+
DELETE_STREAM_DRAIN_WAIT_SECONDS,
8+
Lrtmp2ApiError,
9+
Lrtmp2Client,
10+
)
711

812

913
def test_health_sends_bearer_token():
@@ -43,6 +47,14 @@ def test_create_player_posts_optional_fields():
4347
assert payload["play_key"] == "play_key_with_sufficient_length_here01"
4448

4549

50+
def test_delete_stream_default_wait_matches_server_drain_window():
51+
import inspect
52+
53+
sig = inspect.signature(Lrtmp2Client.delete_stream)
54+
assert sig.parameters["wait_timeout"].default == DELETE_STREAM_DRAIN_WAIT_SECONDS
55+
assert DELETE_STREAM_DRAIN_WAIT_SECONDS == 305
56+
57+
4658
def test_delete_stream_treats_404_as_success():
4759
client = Lrtmp2Client("http://example.test", "tok")
4860
with patch("lrtmp2_client.requests.delete") as mock_delete:

0 commit comments

Comments
 (0)