Files
blackbird/pymsg/tests/test_threadpool.py
T
2026-07-30 20:15:02 +08:00

165 lines
5.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""MsgServer 线程池模式(workers > 0)与默认同步模式的单元测试。
纯标准库实现,直接运行:
cd pymsg
python -m unittest discover -s tests -v
"""
import socket
import threading
import time
import unittest
import unittest.mock
from pymsg import MsgClient, MsgServer, recv, send
def _free_port() -> int:
with socket.socket() as s:
s.bind(("127.0.0.1", 0))
return s.getsockname()[1]
class _ServerFixture:
"""在后台线程运行 MsgServer;handler 签名 handler(server, sock, addr, text)。"""
def __init__(self, handler, workers=0):
self.port = _free_port()
self.server = MsgServer("127.0.0.1", self.port, workers=workers)
self.thread = threading.Thread(
target=self.server.start,
args=(lambda sock, addr, text: handler(self.server, sock, addr, text),),
daemon=True,
)
def __enter__(self):
self.thread.start()
deadline = time.time() + 5
while time.time() < deadline:
try:
with socket.create_connection(("127.0.0.1", self.port), timeout=0.2):
return self
except OSError:
time.sleep(0.02)
raise RuntimeError("server did not start in time")
def __exit__(self, *exc):
self.server.stop()
self.thread.join(timeout=5)
return False
def _parallel_calls(port, messages, timeout=10.0):
"""多线程并发 call,按 messages 顺序返回响应列表。"""
results = [None] * len(messages)
def worker(i, msg):
results[i] = MsgClient("127.0.0.1", port, f"t{i}").call(msg, timeout=timeout)
threads = [threading.Thread(target=worker, args=(i, m)) for i, m in enumerate(messages)]
for t in threads:
t.start()
for t in threads:
t.join(timeout=timeout + 5)
return results
class TestSyncMode(unittest.TestCase):
"""workers=0(默认):行为与旧版一致。"""
def test_rpc_echo(self):
def handler(server, sock, addr, text):
server.reply(sock, f"echo:{text}")
with _ServerFixture(handler, workers=0) as fx:
client = MsgClient("127.0.0.1", fx.port, "t")
self.assertEqual(client.call("hello", timeout=5), "echo:hello")
def test_serial_baseline(self):
"""同步模式处理慢回调是串行的:6 × 0.4s 应超过 2.0s。"""
def handler(server, sock, addr, text):
time.sleep(0.4)
server.reply(sock, "ok")
with _ServerFixture(handler, workers=0) as fx:
t0 = time.monotonic()
results = _parallel_calls(fx.port, ["m"] * 6)
elapsed = time.monotonic() - t0
self.assertEqual(results, ["ok"] * 6)
self.assertGreater(elapsed, 2.0, f"同步模式应为串行,实际 {elapsed:.2f}s")
class TestThreadPoolMode(unittest.TestCase):
"""workers>0:回调并发执行,同一连接保持串行。"""
def test_concurrent_calls(self):
"""6 个并发 call、回调各 sleep 0.4s、4 个 worker:约 2 批 ≈ 0.8s。"""
def handler(server, sock, addr, text):
time.sleep(0.4)
server.reply(sock, f"ok:{text}")
with _ServerFixture(handler, workers=4) as fx:
t0 = time.monotonic()
results = _parallel_calls(fx.port, [f"m{i}" for i in range(6)])
elapsed = time.monotonic() - t0
self.assertEqual(results, [f"ok:m{i}" for i in range(6)])
self.assertLess(elapsed, 1.8, f"线程池模式应并发,实际 {elapsed:.2f}s")
def test_same_connection_serialized(self):
"""同一连接连发两条消息,回调执行区间不得重叠。"""
intervals = []
lock = threading.Lock()
def handler(server, sock, addr, text):
start = time.monotonic()
time.sleep(0.2)
end = time.monotonic()
with lock:
intervals.append((start, end))
server.reply(sock, f"r:{text}")
with _ServerFixture(handler, workers=4) as fx:
with socket.create_connection(("127.0.0.1", fx.port), timeout=5) as s:
send(s, "first")
send(s, "second")
r1 = recv(s, timeout=5)
r2 = recv(s, timeout=5)
self.assertEqual((r1, r2), ("r:first", "r:second"))
self.assertEqual(len(intervals), 2)
(s1, e1), (s2, e2) = sorted(intervals)
self.assertLessEqual(e1, s2, "同一连接的两次回调发生了并发执行")
def test_large_frames_not_interleaved(self):
"""并发回复大载荷(100KB)时帧不得交错损坏。"""
payload = "x" * 100_000
def handler(server, sock, addr, text):
time.sleep(0.01) # 放大交错窗口
server.reply(sock, f"{text}|{payload}")
with _ServerFixture(handler, workers=4) as fx:
messages = [f"client{i}" for i in range(8)]
results = _parallel_calls(fx.port, messages)
self.assertEqual(results, [f"client{i}|{payload}" for i in range(8)])
def test_callback_exception_keeps_server_alive(self):
"""回调抛非连接异常:打印 traceback、移除该连接,服务整体存活。"""
def handler(server, sock, addr, text):
if text == "boom":
raise ValueError("bad message")
server.reply(sock, "alive")
with _ServerFixture(handler, workers=2) as fx:
with unittest.mock.patch("traceback.print_exc"):
with self.assertRaises(ConnectionError):
MsgClient("127.0.0.1", fx.port, "t").call("boom", timeout=5)
# 后续请求不受影响
resp = MsgClient("127.0.0.1", fx.port, "t").call("ping", timeout=5)
self.assertEqual(resp, "alive")
if __name__ == "__main__":
unittest.main()