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>
(cherry picked from commit beb1a204da)
mr26.2
parent
0ca305fb32
commit
921bda20bc
@ -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