This commit is contained in:
qing committed 2026-07-30 21:57:04 +08:00
1 parent f935448389
commit 3f97dd92ef
26 files changed
+1865 -451

No files matched your search

+43 -44
View File
@@ -37,6 +37,7 @@ from datetime import datetime, date, time as dt_time, timezone, timedelta
from sqlalchemy import (
create_engine, MetaData, Table,
select, insert, update, delete, func, and_,
event,
)
from sqlalchemy import types as sa_types
from sqlalchemy.exc import SQLAlchemyError
@@ -44,12 +45,32 @@ from sqlalchemy.exc import SQLAlchemyError
# pymsg 导入
import sys as _sys
_sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from pymsg import MsgServer # noqa: E402
from pymsg import RpcServer # noqa: E402
# ── 数据库配置 ──────────────────────────────────────────────────────────
DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:///database.db")
# ── SQLite 连接配置(针对 Docker/多线程环境优化)──────────────────────
def _configure_sqlite(dbapi_connection, connection_record):
"""为每个 SQLite 连接设置 PRAGMA,优化并发与持久化性能。
- WAL 模式:读写不互斥,并发性能大幅提升,Docker 卷挂载下更稳定
- NORMAL 同步级别:在断电时可能丢失最近提交的事务(但不会损坏数据库)
配合 WAL checkpoint 足够安全,性能显著提升
- foreign_keys=ON:启用外键约束
- busy_timeout:锁等待超时 5 秒,避免 SQLITE_BUSY 错误
- mmap_size:使用内存映射 I/O 提升读取性能
"""
cursor = dbapi_connection.cursor()
cursor.execute("PRAGMA journal_mode=WAL")
cursor.execute("PRAGMA synchronous=NORMAL")
cursor.execute("PRAGMA foreign_keys=ON")
cursor.execute("PRAGMA busy_timeout=5000")
cursor.execute("PRAGMA mmap_size=268435456") # 256MB
cursor.close()
# ── 日志模块常量 ────────────────────────────────────────────────────────
MAX_MESSAGE_LEN = 8192 # message 上限 8KB
@@ -80,9 +101,20 @@ class DBServer:
def __init__(self, addr="127.0.0.1", port=8005, workers=4):
self.addr = addr
self.port = port
self.engine = create_engine(DATABASE_URL)
self.engine = create_engine(
DATABASE_URL,
connect_args={"check_same_thread": False} if DATABASE_URL.startswith("sqlite") else {},
)
# 为 SQLite 连接应用 PRAGMA 优化
if DATABASE_URL.startswith("sqlite"):
event.listen(self.engine, "connect", _configure_sqlite)
self.metadata = MetaData()
self._server = MsgServer(addr, port, workers=workers)
self._server = RpcServer(
addr, port, workers=workers,
json_default=_json_default,
on_error=lambda route, e: traceback.print_exc(),
)
self._server.add_route("execute", self._handle_execute)
# 日志表缓存(避免重复反射)
self._logs_table_cache = {}
@@ -98,54 +130,18 @@ class DBServer:
# 抛 InvalidRequestError),反射操作需串行化
self._reflect_lock = threading.Lock()
# 路由表
self._routes = {
"execute": self._handle_execute,
}
# ── 启动 / 停止 ──────────────────────────────────────────────────
def start(self) -> None:
"""启动服务(阻塞当前线程)。"""
print(f"[dbv2] 监听 tcp://{self.addr}:{self.port}")
self._ensure_logs_table() # 确保 Logs 表已建好(外部 MsgClient 写日志前)
self._ensure_logs_table() # 确保 Logs 表已建好(外部写日志前)
self._start_cleanup_thread()
self._server.start(self._dispatch)
self._server.start()
def stop(self) -> None:
"""从另一线程停止服务。"""
self._server.stop()
# ── 消息调度 -------------------------------------------------------
def _dispatch(self, sock, addr, text):
"""解析 JSON 中的 _route,分发给对应的 handler,并回复结果。"""
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, sock=sock)
self._reply(sock, True, data=result)
except Exception as e:
traceback.print_exc()
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=_json_default))
# ── 表反射 ---------------------------------------------------------
def _get_table(self, table_name):
# double-check:_tables_cache 只在反射完整完成后写入(dict 读写 GIL
@@ -245,7 +241,7 @@ class DBServer:
# 路由处理 — execute
# ═══════════════════════════════════════════════════════════════════
def _handle_execute(self, data, **kw):
def _handle_execute(self, data):
action = data.get("action", "select")
table_name = data.get("table")
if not table_name:
@@ -460,9 +456,12 @@ class DBServer:
if __name__ == "__main__":
import argparse
default_addr = os.environ.get("LISTEN_ADDR", "0.0.0.0")
default_port = int(os.environ.get("LISTEN_PORT", "8005"))
parser = argparse.ArgumentParser(description="dbv2 — pymsg 数据库服务")
parser.add_argument("--addr", default="127.0.0.1", help="监听地址")
parser.add_argument("--port", type=int, default=8005, help="监听端口")
parser.add_argument("--addr", default=default_addr, help="监听地址")
parser.add_argument("--port", type=int, default=default_port, help="监听端口")
parser.add_argument("--workers", type=int, default=4,
help="回调并发线程数(0=同步串行模式)")
args = parser.parse_args()