update
This commit is contained in:
1 parent
f935448389
commit
3f97dd92ef
26 files changed
+1865
-451
No files matched your search
+69
-49
@@ -179,61 +179,85 @@ server = MsgServer("0.0.0.0", 8005, workers=4)
|
||||
- `MsgClient.stop()` 可从另一线程调用以中断 `listen()`。
|
||||
- `send()` 和 `recv()` 内部不做同步 —— 如果多线程共享同一 socket,调用方需自行串行化。
|
||||
|
||||
## JSON RPC 约定层(v0.3.0)
|
||||
|
||||
项目内所有 pymsg 通信都遵循同一 JSON RPC 约定(与 C++/Go 客户端字节级兼容):
|
||||
|
||||
```
|
||||
请求: {"_route": "<路由名>", ...业务字段}
|
||||
响应: {"success": true, "data": ...} | {"success": false, "error": "..."}
|
||||
```
|
||||
|
||||
`RpcServer` / `RpcClient` 把这套约定的编解码、路由分发、错误包装收敛为开箱即用的类(`pymsg/rpc.py`),各服务无需再手写 `_dispatch` / `_reply`。
|
||||
|
||||
### `RpcServer` — JSON RPC 服务端
|
||||
|
||||
```python
|
||||
from pymsg import RpcServer
|
||||
|
||||
rpc = RpcServer("127.0.0.1", 8005, workers=4)
|
||||
|
||||
@rpc.route("execute")
|
||||
def _execute(req): # handler(req: dict) -> data
|
||||
return {"rows": []} # 返回值自动包成 {"success": true, "data": ...}
|
||||
# 抛异常自动包成 {"success": false, "error": str(e)}
|
||||
rpc.start() # 阻塞当前线程
|
||||
```
|
||||
|
||||
| 构造参数 | 含义 |
|
||||
|----------|------|
|
||||
| `addr` / `port` | 监听地址与端口 |
|
||||
| `workers` | 回调并发线程数(透传 `MsgServer`,默认 0) |
|
||||
| `json_default` | 响应 JSON 序列化兜底函数(如日期转 ISO 8601) |
|
||||
| `on_error` | 可选钩子 `on_error(route, exc)`,handler 异常时调用(记日志用) |
|
||||
|
||||
### `RpcClient` — JSON RPC 客户端
|
||||
|
||||
```python
|
||||
from pymsg import RpcClient, RpcError
|
||||
|
||||
rpc = RpcClient("127.0.0.1", 8005, "dnfs", timeout=10.0)
|
||||
try:
|
||||
data = rpc.call("execute", {"action": "select", "table": "Users"})
|
||||
except RpcError as e:
|
||||
... # 服务端业务错误(success=false)
|
||||
```
|
||||
|
||||
`call(route, payload=None, *, timeout=None)`:自动注入 `_route`、JSON 编解码、检查 `success`,返回 `data` 字段;业务错误抛 `RpcError`,网络故障仍抛 `ConnectionRefusedError` / `TimeoutError`(来自 `MsgClient`)。
|
||||
|
||||
## 实际用例
|
||||
|
||||
### 服务端 — 数据库服务(dbv2)
|
||||
|
||||
db 服务通过 pymsg 暴露通用 CRUD 接口。每条请求是带 `_route` 字段的 JSON 对象,服务端按路由分发到对应处理函数,通过同一 socket 回复结果。
|
||||
db 服务基于 `RpcServer` 暴露通用 CRUD 接口(路由 `execute`):
|
||||
|
||||
```python
|
||||
import json
|
||||
from pymsg import MsgServer
|
||||
from pymsg import RpcServer
|
||||
|
||||
class DBServer:
|
||||
def __init__(self):
|
||||
self._server = MsgServer("127.0.0.1", 8005)
|
||||
self._routes = {"execute": self._handle_execute}
|
||||
def __init__(self, addr="127.0.0.1", port=8005, workers=4):
|
||||
self._server = RpcServer(addr, port, workers=workers,
|
||||
json_default=_json_default)
|
||||
self._server.add_route("execute", self._handle_execute)
|
||||
|
||||
def start(self):
|
||||
self._server.start(self._dispatch)
|
||||
|
||||
def _dispatch(self, sock, addr, text):
|
||||
req = json.loads(text)
|
||||
route = req.pop("_route", "")
|
||||
handler = self._routes.get(route)
|
||||
if handler is None:
|
||||
self._server.reply(sock,
|
||||
json.dumps({"success": False, "error": "未知路由"}))
|
||||
return
|
||||
try:
|
||||
result = handler(req)
|
||||
self._server.reply(sock,
|
||||
json.dumps({"success": True, "data": result}))
|
||||
except Exception as e:
|
||||
self._server.reply(sock,
|
||||
json.dumps({"success": False, "error": str(e)}))
|
||||
self._server.start()
|
||||
|
||||
def _handle_execute(self, data):
|
||||
# ... 通过 SQLAlchemy 执行 SQL,返回 dict 列表
|
||||
# ... 通过 SQLAlchemy 执行 SQL,返回 dict 列表;异常自动包装为错误响应
|
||||
pass
|
||||
```
|
||||
|
||||
### 客户端 — RPC 调用(dnfs → db)
|
||||
|
||||
dnfs 笔记服务自身没有数据库,所有 CRUD 操作均通过 pymsg 调用 db。
|
||||
dnfs 笔记服务自身没有数据库,所有 CRUD 操作均通过 `RpcClient` 调用 db:
|
||||
|
||||
```python
|
||||
import json
|
||||
from pymsg import MsgClient
|
||||
from pymsg import RpcClient
|
||||
|
||||
def _call_dbv2(payload):
|
||||
client = MsgClient("127.0.0.1", 8005, "dnfs")
|
||||
resp = client.call(json.dumps(payload), timeout=10.0)
|
||||
return json.loads(resp)
|
||||
_rpc = RpcClient("127.0.0.1", 8005, "dnfs")
|
||||
|
||||
# 创建笔记
|
||||
result = _call_dbv2({
|
||||
"_route": "execute",
|
||||
data = _rpc.call("execute", {
|
||||
"action": "insert",
|
||||
"table": "Notes",
|
||||
"values": {"title": "会议记录", "content": "..."}
|
||||
@@ -242,24 +266,19 @@ result = _call_dbv2({
|
||||
|
||||
### 客户端 — 异步日志推送(log_client)
|
||||
|
||||
各服务通过 pymsg 将日志异步推送到 db。使用 `MsgClient.call()` 逐批插入。
|
||||
各服务通过统一 `log_client.py` 将日志异步推送到 db,底层为 `RpcClient`:
|
||||
|
||||
```python
|
||||
import json
|
||||
from pymsg import MsgClient
|
||||
from pymsg import RpcClient
|
||||
|
||||
def _send(batch):
|
||||
client = MsgClient("127.0.0.1", 8005, "gateway-log")
|
||||
for entry in batch:
|
||||
client.call(json.dumps({
|
||||
"_route": "execute",
|
||||
"action": "insert",
|
||||
"table": "Logs",
|
||||
"values": {
|
||||
"ts": now, "service": entry["service"],
|
||||
"level": entry["level"], "message": entry["message"],
|
||||
}
|
||||
}), timeout=5)
|
||||
client = RpcClient("127.0.0.1", 8005, "log-client")
|
||||
for entry in batch:
|
||||
client.call("execute", {
|
||||
"action": "insert",
|
||||
"table": "Logs",
|
||||
"values": {"ts": now, "service": entry["service"],
|
||||
"level": entry["level"], "message": entry["message"]},
|
||||
}, timeout=5)
|
||||
```
|
||||
|
||||
## 跨语言互操作
|
||||
@@ -289,6 +308,7 @@ pymsg 的线格式有意设计得极其简单,使其他语言的服务也能
|
||||
| `ConnectionError`(内置) | 完整消息接收前 socket 断开 |
|
||||
| `ConnectionRefusedError`(内置) | `MsgClient.call()` 或 `send()` 无法连接目标 |
|
||||
| `RuntimeError` | `MsgClient.listen()` 在 `connect()` 之前调用 |
|
||||
| `pymsg.RpcError` | `RpcClient.call()` 收到 `success=false` 业务错误或非法 RPC 响应 |
|
||||
|
||||
`MsgServer` 内部会吞掉客户端断连错误(移除该客户端,继续服务)。`broadcast()` 静默跳过已断开的 socket。
|
||||
|
||||
|
||||
+19
-3
@@ -1,7 +1,7 @@
|
||||
# pymsg 模块技术手册
|
||||
|
||||
> 文档版本:2026-07-30
|
||||
> 代码版本:0.2.0
|
||||
> 代码版本:0.3.0
|
||||
> 语言:Python 3.9+(核心)、C++(gateway 集成)、Go(waf 集成)
|
||||
> 定位:跨语言 TCP 消息帧协议库
|
||||
|
||||
@@ -144,9 +144,11 @@ log_client.push(entry)
|
||||
```
|
||||
pymsg/
|
||||
├── pymsg/
|
||||
│ └── __init__.py # 核心实现(~460 行)
|
||||
│ ├── __init__.py # 核心协议与 MsgClient/MsgServer(~470 行)
|
||||
│ └── rpc.py # JSON RPC 约定层 RpcClient/RpcServer(~170 行,v0.3.0)
|
||||
├── tests/
|
||||
│ └── test_threadpool.py # 线程池/同步模式单元测试(纯 stdlib)
|
||||
│ ├── test_threadpool.py # 线程池/同步模式单元测试(纯 stdlib)
|
||||
│ └── test_rpc.py # RPC 约定层单元测试(纯 stdlib)
|
||||
├── README.md # 用户文档
|
||||
├── pyproject.toml # 项目配置
|
||||
└── .gitignore
|
||||
@@ -189,6 +191,19 @@ pymsg/
|
||||
| `MsgServer._run_task(...)` | L413 | 工作线程:执行回调、失败打标记 |
|
||||
| `MsgServer._reap_failed(socketlist)` | L434 | 主循环统一移除失败连接 |
|
||||
|
||||
#### RPC 约定层(rpc.py,v0.3.0)
|
||||
|
||||
| 函数/类 | 行号 | 功能 |
|
||||
|---------|------|------|
|
||||
| `RpcError` | L28 | 业务错误异常(success=false / 非法响应) |
|
||||
| `RpcClient` | L32 | JSON RPC 客户端 |
|
||||
| `RpcClient.call(route, payload, *, timeout)` | L46 | 注入 _route、编解码、检查 success,返回 data |
|
||||
| `RpcServer` | L80 | JSON RPC 服务端(基于 MsgServer) |
|
||||
| `RpcServer.route(name)` / `add_route` | L119/L126 | 注册路由 handler(`fn(req) -> data`) |
|
||||
| `RpcServer.start()` / `stop()` | L131/L135 | 启动(阻塞)/ 停止 |
|
||||
| `RpcServer._dispatch` | L140 | 路由分发与异常包装 |
|
||||
| `RpcServer._reply` | L164 | 响应包装(支持 json_default) |
|
||||
|
||||
#### C++ 实现(Msg.hpp)
|
||||
|
||||
| 函数/类 | 行号 | 功能 |
|
||||
@@ -736,6 +751,7 @@ python test_runner.py
|
||||
|------|------|---------|
|
||||
| 0.1.0 | — | 初始版本:核心协议、MsgClient、MsgServer、跨语言实现 |
|
||||
| 0.2.0 | 2026-07-30 | `MsgServer` 新增 `workers` 参数:回调可提交线程池并发执行;同连接串行(busy 标记)、每连接发送锁防帧交错、回调异常故障隔离;`tests/test_threadpool.py` 单元测试 |
|
||||
| 0.3.0 | 2026-07-30 | 新增 `rpc.py` JSON RPC 约定层:`RpcClient`(_route 注入 + 编解码 + success 检查 + `RpcError`)、`RpcServer`(路由分发 + 响应包装 + `json_default`/`on_error` 钩子);db/smtp/captcha 服务端与 dnfs/log_client 客户端完成迁移;`tests/test_rpc.py` 单元测试 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -456,3 +456,9 @@ class MsgServer:
|
||||
pass
|
||||
self._clients.clear()
|
||||
self._send_locks.clear()
|
||||
|
||||
|
||||
# ── JSON RPC 约定层(依赖上面的 MsgClient / MsgServer,故放在末尾导入)──
|
||||
from .rpc import RpcClient, RpcError, RpcServer # noqa: E402
|
||||
|
||||
__all__ += ["RpcClient", "RpcServer", "RpcError"]
|
||||
@@ -0,0 +1,171 @@
|
||||
"""pymsg.rpc — pymsg 线协议之上的 JSON RPC 约定层。
|
||||
|
||||
约定(与 C++/Go 客户端字节级兼容)::
|
||||
|
||||
请求: {"_route": "<路由名>", ...业务字段}
|
||||
响应: {"success": true, "data": ...}
|
||||
{"success": false, "error": "..."}
|
||||
|
||||
把各服务(db / smtp / captcha)重复的 ``_dispatch`` / ``_reply`` 服务端
|
||||
分发逻辑,以及各调用方(dnfs / log_client)重复的 ``_route`` 注入 +
|
||||
JSON 编解码客户端逻辑,收敛为 ``RpcServer`` / ``RpcClient`` 两个类。
|
||||
|
||||
异常分层::
|
||||
|
||||
RpcError — 远端返回的业务错误(success=false)或非法响应
|
||||
pymsg.TimeoutError — 网络层超时(来自 MsgClient)
|
||||
ConnectionRefusedError — 目标不可达(来自 MsgClient)
|
||||
ConnectionError — 连接中断(来自 MsgClient)
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from . import MsgClient, MsgServer
|
||||
|
||||
__all__ = ["RpcClient", "RpcServer", "RpcError"]
|
||||
|
||||
|
||||
class RpcError(Exception):
|
||||
"""远端返回 ``success=false`` 的业务错误,或响应不符合 RPC 约定。"""
|
||||
|
||||
|
||||
class RpcClient:
|
||||
"""JSON RPC 客户端(短连接,每次调用独立 TCP 连接)。
|
||||
|
||||
Usage::
|
||||
|
||||
rpc = RpcClient("127.0.0.1", 8005, "dnfs")
|
||||
data = rpc.call("execute", {"action": "select", "table": "Users"})
|
||||
"""
|
||||
|
||||
def __init__(self, addr: str, port: int, name: str = "rpc",
|
||||
timeout: float = 10.0) -> None:
|
||||
self._client = MsgClient(addr, port, name)
|
||||
self._timeout = timeout
|
||||
|
||||
def call(self, route: str, payload: "dict | None" = None, *,
|
||||
timeout: "float | None" = None):
|
||||
"""调用指定路由,返回响应中的 ``data`` 字段。
|
||||
|
||||
Args:
|
||||
route: 路由名(注入为 ``_route`` 字段)。
|
||||
payload: 业务字段 dict(``_route`` 键会被覆盖)。
|
||||
timeout: 本次调用的超时秒数,缺省用构造时的值。
|
||||
|
||||
Returns:
|
||||
服务端返回的 ``data``(可能为 None)。
|
||||
|
||||
Raises:
|
||||
RpcError: 服务端返回 success=false,或响应非法。
|
||||
pymsg.TimeoutError: 等待响应超时。
|
||||
ConnectionRefusedError: 目标服务不可达。
|
||||
"""
|
||||
req = dict(payload or {})
|
||||
req["_route"] = route
|
||||
resp = self._client.call(
|
||||
json.dumps(req, ensure_ascii=False),
|
||||
timeout=self._timeout if timeout is None else timeout,
|
||||
)
|
||||
try:
|
||||
obj = json.loads(resp)
|
||||
except json.JSONDecodeError as e:
|
||||
raise RpcError(f"响应不是合法 JSON: {e}") from None
|
||||
if not isinstance(obj, dict) or "success" not in obj:
|
||||
raise RpcError(f"非法 RPC 响应: {resp[:200]!r}")
|
||||
if not obj["success"]:
|
||||
raise RpcError(obj.get("error", "未知错误"))
|
||||
return obj.get("data")
|
||||
|
||||
|
||||
class RpcServer:
|
||||
"""JSON RPC 服务端:基于 MsgServer,自带路由分发与响应包装。
|
||||
|
||||
handler 签名为 ``handler(req: dict) -> data``:
|
||||
|
||||
- 返回值放入 ``{"success": true, "data": ...}`` 回复;
|
||||
- 抛出的异常自动包装为 ``{"success": false, "error": str(e)}``,
|
||||
服务不会因此崩溃。
|
||||
|
||||
Usage::
|
||||
|
||||
rpc = RpcServer("127.0.0.1", 8005, workers=4)
|
||||
|
||||
@rpc.route("execute")
|
||||
def _execute(req):
|
||||
return {"rows": []}
|
||||
|
||||
rpc.start() # 阻塞当前线程
|
||||
"""
|
||||
|
||||
def __init__(self, addr: str, port: int, *, workers: int = 0,
|
||||
json_default: "callable | None" = None,
|
||||
on_error: "callable | None" = None) -> None:
|
||||
"""
|
||||
Args:
|
||||
addr: 监听地址。
|
||||
port: 监听端口。
|
||||
workers: 回调并发线程数(透传 MsgServer,0=同步串行)。
|
||||
json_default: 响应 JSON 序列化的兜底函数(如日期转 ISO),
|
||||
透传 ``json.dumps(default=...)``。
|
||||
on_error: 可选钩子 ``on_error(route, exc)``,handler 抛异常
|
||||
时调用(用于记日志);None 表示静默。
|
||||
"""
|
||||
self._server = MsgServer(addr, port, workers=workers)
|
||||
self._routes: "dict" = {}
|
||||
self._json_default = json_default
|
||||
self._on_error = on_error
|
||||
|
||||
# ── 路由注册 -------------------------------------------------------
|
||||
def route(self, name: str):
|
||||
"""装饰器:注册路由处理函数。"""
|
||||
def deco(fn):
|
||||
self._routes[name] = fn
|
||||
return fn
|
||||
return deco
|
||||
|
||||
def add_route(self, name: str, fn: "callable") -> None:
|
||||
"""直接注册路由处理函数(等价于 route 装饰器)。"""
|
||||
self._routes[name] = fn
|
||||
|
||||
# ── 启动 / 停止 ----------------------------------------------------
|
||||
def start(self) -> None:
|
||||
"""启动服务(阻塞当前线程)。"""
|
||||
self._server.start(self._dispatch)
|
||||
|
||||
def stop(self) -> None:
|
||||
"""从另一线程停止服务。"""
|
||||
self._server.stop()
|
||||
|
||||
# ── 分发与响应包装 --------------------------------------------------
|
||||
def _dispatch(self, sock, addr, text):
|
||||
try:
|
||||
req = json.loads(text)
|
||||
except json.JSONDecodeError as e:
|
||||
self._reply(sock, False, error=f"JSON 解析失败: {e}")
|
||||
return
|
||||
|
||||
route = req.pop("_route", "")
|
||||
handler = self._routes.get(route)
|
||||
if handler is None:
|
||||
self._reply(sock, False, error=f"未知路由: {route}")
|
||||
return
|
||||
|
||||
try:
|
||||
result = handler(req)
|
||||
self._reply(sock, True, data=result)
|
||||
except Exception as e:
|
||||
if self._on_error is not None:
|
||||
try:
|
||||
self._on_error(route, e)
|
||||
except Exception:
|
||||
pass
|
||||
self._reply(sock, False, error=str(e))
|
||||
|
||||
def _reply(self, sock, success, data=None, error=None):
|
||||
resp = {"success": success}
|
||||
if data is not None:
|
||||
resp["data"] = data
|
||||
if error is not None:
|
||||
resp["error"] = error
|
||||
self._server.reply(sock, json.dumps(
|
||||
resp, ensure_ascii=False, default=self._json_default))
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pymsg"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
description = "Length-prefixed variable-length socket message framing"
|
||||
requires-python = ">=3.9"
|
||||
license = "MIT"
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
"""RpcClient / RpcServer(JSON RPC 约定层)单元测试。
|
||||
|
||||
纯标准库,直接运行:
|
||||
|
||||
cd pymsg
|
||||
python -m unittest discover -s tests -v
|
||||
"""
|
||||
|
||||
import json
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
import unittest
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from pymsg import RpcClient, RpcError, RpcServer, send
|
||||
|
||||
|
||||
def _free_port() -> int:
|
||||
with socket.socket() as s:
|
||||
s.bind(("127.0.0.1", 0))
|
||||
return s.getsockname()[1]
|
||||
|
||||
|
||||
class _RpcFixture:
|
||||
"""后台线程运行 RpcServer;routes: {name: fn}。"""
|
||||
|
||||
def __init__(self, routes, workers=0, json_default=None):
|
||||
self.port = _free_port()
|
||||
self.server = RpcServer("127.0.0.1", self.port, workers=workers,
|
||||
json_default=json_default)
|
||||
for name, fn in routes.items():
|
||||
self.server.add_route(name, fn)
|
||||
self.thread = threading.Thread(target=self.server.start, 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
|
||||
|
||||
|
||||
class TestRpc(unittest.TestCase):
|
||||
|
||||
def test_call_returns_data(self):
|
||||
with _RpcFixture({"echo": lambda req: {"got": req["msg"]}}) as fx:
|
||||
rpc = RpcClient("127.0.0.1", fx.port, "t")
|
||||
self.assertEqual(rpc.call("echo", {"msg": "hi"}), {"got": "hi"})
|
||||
|
||||
def test_route_field_is_injected(self):
|
||||
seen = {}
|
||||
|
||||
def handler(req):
|
||||
seen.update(req)
|
||||
return None
|
||||
|
||||
with _RpcFixture({"r": handler}) as fx:
|
||||
RpcClient("127.0.0.1", fx.port).call("r", {"a": 1})
|
||||
# _route 已被服务端 pop,handler 只看到业务字段
|
||||
self.assertEqual(seen, {"a": 1})
|
||||
|
||||
def test_business_error_raises_rpcerror(self):
|
||||
def handler(req):
|
||||
raise ValueError("bad input")
|
||||
|
||||
with _RpcFixture({"x": handler}) as fx:
|
||||
with self.assertRaises(RpcError) as cm:
|
||||
RpcClient("127.0.0.1", fx.port).call("x")
|
||||
self.assertIn("bad input", str(cm.exception))
|
||||
|
||||
def test_unknown_route_raises_rpcerror(self):
|
||||
with _RpcFixture({}) as fx:
|
||||
with self.assertRaises(RpcError) as cm:
|
||||
RpcClient("127.0.0.1", fx.port).call("nope")
|
||||
self.assertIn("未知路由", str(cm.exception))
|
||||
|
||||
def test_malformed_json_gets_error_response(self):
|
||||
"""服务端收到非法 JSON:返回 success=false 且不崩溃,后续请求正常。"""
|
||||
with _RpcFixture({"ok": lambda req: "fine"}) as fx:
|
||||
with socket.create_connection(("127.0.0.1", fx.port), timeout=5) as s:
|
||||
send(s, "not-json{{{")
|
||||
from pymsg import recv
|
||||
resp = json.loads(recv(s, timeout=5))
|
||||
self.assertFalse(resp["success"])
|
||||
self.assertIn("JSON 解析失败", resp["error"])
|
||||
# 服务仍然存活
|
||||
self.assertEqual(RpcClient("127.0.0.1", fx.port).call("ok"), "fine")
|
||||
|
||||
def test_data_none_when_absent(self):
|
||||
with _RpcFixture({"n": lambda req: None}) as fx:
|
||||
self.assertIsNone(RpcClient("127.0.0.1", fx.port).call("n"))
|
||||
|
||||
def test_json_default_serialization(self):
|
||||
def default(o):
|
||||
if isinstance(o, datetime):
|
||||
return o.isoformat()
|
||||
return str(o)
|
||||
|
||||
now = datetime(2026, 7, 30, 12, 0, tzinfo=timezone.utc)
|
||||
with _RpcFixture({"t": lambda req: {"ts": now}}, json_default=default) as fx:
|
||||
data = RpcClient("127.0.0.1", fx.port).call("t")
|
||||
self.assertEqual(data["ts"], now.isoformat())
|
||||
|
||||
def test_concurrent_rpc_with_workers(self):
|
||||
def handler(req):
|
||||
time.sleep(0.3)
|
||||
return req["i"] * 2
|
||||
|
||||
with _RpcFixture({"dbl": handler}, workers=4) as fx:
|
||||
results = [None] * 6
|
||||
|
||||
def w(i):
|
||||
results[i] = RpcClient("127.0.0.1", fx.port).call("dbl", {"i": i})
|
||||
|
||||
t0 = time.monotonic()
|
||||
ts = [threading.Thread(target=w, args=(i,)) for i in range(6)]
|
||||
for t in ts:
|
||||
t.start()
|
||||
for t in ts:
|
||||
t.join(10)
|
||||
elapsed = time.monotonic() - t0
|
||||
|
||||
self.assertEqual(results, [i * 2 for i in range(6)])
|
||||
self.assertLess(elapsed, 1.2, f"workers=4 应并发,实际 {elapsed:.2f}s")
|
||||
|
||||
def test_per_call_timeout_override(self):
|
||||
with _RpcFixture({"slow": lambda req: time.sleep(2)}) as fx:
|
||||
rpc = RpcClient("127.0.0.1", fx.port, timeout=10.0)
|
||||
from pymsg import TimeoutError
|
||||
with self.assertRaises(TimeoutError):
|
||||
rpc.call("slow", timeout=0.3)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user