mirror of https://github.com/sipwise/rtpengine.git
MT#55283 websocket: prevent HTTP disconnect deadlock by deferring writable callbacks (#2197)
Replace spinning for websocket_service_lock with a short mutex-protected writable queue. All lws_callback_on_writable() calls are now marshalled onto the LWS service thread and only run after lws_service() returns, instead of being issued directly from worker threads while the service thread may be holding the same lock. The callback queue mutex is released before calling into libwebsockets, and the amount of queued work drained per service cycle is bounded to avoid starving other connections under a connection-abort storm. Pending callbacks are dropped on teardown before the LWS session storage is freed. Add an HTTP disconnect-storm regression test (two worker threads hammering /metrics with aborted connections while a liveness /ping request keeps proving the service thread is not stuck) to t/auto-daemon-tests-websocket.py, plus a standalone stress script. The stress test waits for the listening socket to come up before firing, avoiding a FileNotFoundError race on CI. Closes #2206 Change-Id: I81e03c1d7f1a0b5650aa719d00d60be3d3d0213b Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>pull/2157/merge
parent
0780b3c0ac
commit
beb1a204da
@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regression stress test for issue #2197.
|
||||
|
||||
Start rtpengine separately with --listen-http=127.0.0.1:2225 and a
|
||||
small --http-threads value, then run this script. The test deliberately
|
||||
closes concurrent /metrics connections before the server finishes.
|
||||
It also verifies that /ping remains responsive throughout the storm.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import http.client
|
||||
import socket
|
||||
import struct
|
||||
import time
|
||||
|
||||
|
||||
def ping(host, port, timeout):
|
||||
connection = http.client.HTTPConnection(host, port, timeout=timeout)
|
||||
try:
|
||||
connection.request("GET", "/ping", headers={"Connection": "close"})
|
||||
response = connection.getresponse()
|
||||
body = response.read()
|
||||
if response.status != 200 or body != b"pong\n":
|
||||
raise RuntimeError("unexpected /ping response: %s %r" %
|
||||
(response.status, body))
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
|
||||
def abort_metrics(host, port, timeout):
|
||||
with socket.create_connection((host, port), timeout=timeout) as sock:
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER,
|
||||
struct.pack("ii", 1, 0))
|
||||
sock.sendall(b"GET /metrics HTTP/1.1\r\n"
|
||||
b"Host: localhost\r\nConnection: close\r\n\r\n")
|
||||
# Abort without consuming the response.
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--host", default="127.0.0.1")
|
||||
ap.add_argument("--port", type=int, default=2225)
|
||||
ap.add_argument("--workers", type=int, default=32)
|
||||
ap.add_argument("--rounds", type=int, default=50)
|
||||
ap.add_argument("--timeout", type=float, default=2.0)
|
||||
args = ap.parse_args()
|
||||
ping(args.host, args.port, args.timeout)
|
||||
|
||||
failures = 0
|
||||
started = time.monotonic()
|
||||
with ThreadPoolExecutor(max_workers=args.workers) as pool:
|
||||
for round_number in range(args.rounds):
|
||||
futures = [pool.submit(abort_metrics, args.host, args.port,
|
||||
args.timeout)
|
||||
for _ in range(args.workers)]
|
||||
for future in futures:
|
||||
try:
|
||||
future.result(timeout=args.timeout + 2)
|
||||
except OSError:
|
||||
# A reset during the storm is expected and harmless.
|
||||
pass
|
||||
except Exception as exc:
|
||||
failures += 1
|
||||
print("request error: %s" % exc)
|
||||
# The LWS service thread must continue to make progress.
|
||||
ping(args.host, args.port, args.timeout)
|
||||
if (round_number + 1) % 10 == 0:
|
||||
print("completed %d/%d rounds" %
|
||||
(round_number + 1, args.rounds))
|
||||
|
||||
ping(args.host, args.port, args.timeout)
|
||||
if failures:
|
||||
raise SystemExit("%d unexpected worker errors" % failures)
|
||||
print("PASS: /ping remained responsive over %d aborted requests "
|
||||
"(%.1f sec)" % (args.workers * args.rounds,
|
||||
time.monotonic() - started))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in new issue