DBX Agent Protocol v2 详解:多会话运行时的会话生命周期、并发模型与结构化错误恢复机制
【免费下载链接】dbx15MB,轻量级跨平台数据库客户端、数据库管理工具。支持 MySQL、PostgreSQL、SQLite、Redis、MongoDB、DuckDB、ClickHouse、SQL Server 等。15MB, lightweight, cross-platform database client. Supports MySQL, PostgreSQL, SQLite, Redis, MongoDB, DuckDB, ClickHouse, SQL Server and more.项目地址: https://gitcode.com/t8y2/dbx
DBX 的 Agent 协议 v2 让单个 Agent 进程从"一个池一个进程"升级为"一个进程服务多个相互隔离的数据库会话",并配套引入了multi_session、structured_error_v1两项能力以及统一的会话级错误恢复语义。本文基于 agents/docs/agent-protocol-v2.md 展开,结合 agents/common 下MultiSessionJsonRpcServer、AgentProtocol、AgentRpcError等源码实现,完整梳理 v2 协议的会话生命周期、手动事务、并发规则、运行时复用、资源限制与结构化错误契约,并给出 Java 与原生 Agent 的落地指南。读完你将能够理解 v2 协议下 Agent 进程内部如何工作,以及如何把现有 Agent 迁移到多会话运行时。
从 v1 到 v2:一个进程为何要服务多个会话
协议 v1 的生命周期是"一个进程对应一个连接池"(one-process-per-pool):每个数据库连接都独占一个 Agent 进程,DBX 需要按池启动、回收进程。协议 v2 允许一个 Agent 进程同时承载多个相互隔离的逻辑数据库会话(logical database session),从而显著降低进程数量与启动开销。
判断一个 Agent 是否运行在 v2 多会话路径上,取决于握手(handshake)时声明的能力:
- 使用共享 JDBC 基础(common 结构化错误生产者)的池化 JDBC Agent 会在握手时同时广告
protocolVersion: 2、multi_session、structured_error_v1三个字段; - 通用/自定义 v2 handler(如非 JDBC 的原生 Agent)可以只广告
multi_session; - 当
multi_session能力缺失时,DBX 自动回退到 v1 的 one-process-per-pool 生命周期,保证旧 Agent 二进制与 JAR 的兼容。
在源码侧,AgentProtocol.java 中定义了两组能力集合:MULTI_SESSION_CAPABILITIES在 v1 基础能力(connect、test_connection、metadata、query、paged_query、transaction、ddl)之上追加multi_session,MULTI_SESSION_JDBC_CAPABILITIES再追加structured_error_v1,并通过multiSessionJdbcHandshakeResult()返回给 DBX。协议方法的完整清单(含handshake、会话管理方法与查询/元数据方法)同时维护在 agent-protocol-v2.json 中,其中明确规定:
sessionField为agentSessionId(逻辑连接标识);cursorSessionField为sessionId(游标分页标识,二者不可混用)。
会话生命周期:从 open 到 shutdown 的五步闭环
v2 协议通过五个核心 RPC 方法管理会话,完整定义在 agent-protocol-v2.json 的commonMethods中:
| 方法 | 作用 |
|---|---|
open_session | 创建一个逻辑数据库会话。参数包含常规连接字段,外加agentSessionId与可选的sessionRole |
validate_session | 校验该会话,并在支持的情况下仅对该会话重连 |
cancel_session | 仅取消该会话正在执行的活动语句与游标抓取,同一运行时内的其他会话不受影响 |
close_session | 关闭该会话的会话资源、查询游标与表读取游标,不影响其他会话 |
shutdown | 关闭全部会话并终止整个运行时进程 |
此外,每一个连接作用域(connection-scoped)的 RPC 请求都必须携带agentSessionId,服务端用它把请求路由到对应的逻辑会话。
在 MultiSessionJsonRpcServer.java 的实现中,会话被存放在ConcurrentHashMap<String, Session>中:
openSession(sessionId, params)会先检查sessions.size() >= MAX_SESSIONS(常量MAX_SESSIONS = 256),超限时抛出资源类错误;随后为会话创建DatabaseAgent实例(JDBC 模式下还会在满足条件时把连接池注册表挂到 agent 上),连接失败时立即从 map 中移除并隔离关闭,防止失败会话残留;closeSession(sessionId)从 map 中移除会话并调度清理;若清理触发"隔离配额上限",会返回resource类错误并请求替换整个运行时;run()启动时先向 stdout 输出{"ready":true},随后逐行读取 stdin 上的 JSON-RPC 请求,遇到shutdown方法即停止循环并close()关闭全部会话、请求线程池与清理线程池。
agentSessionId 与 sessionId 的职责分离
v2 协议明确区分两类标识符:
agentSessionId标识一条逻辑数据库连接,是会话路由的唯一依据;- 既有
sessionId字段仍然是分页游标标识符(pagination cursor identifier),禁止被当作逻辑连接标识使用。
这一点在 agent-protocol-v2.json 中通过sessionField/cursorSessionField两个字段固化下来,避免新实现把游标 ID 误用于会话路由。
sessionRole:workload 与 metadata 的分工
sessionRole用于标识会话的用途:
- 默认值为
workload,对应编辑器执行等真实负载; - DBX 在发起对象树、补全等只读元数据任务时发送
metadata; - 新运行时应该用该角色为元数据会话保留"检出容量"(metadata checkout capacity);旧运行时可以忽略该字段,因此它是向后兼容的可选字段。
由于 DBX 会为独立的元数据任务使用短生命周期的唯一逻辑会话(避免它们排在编辑器执行之后排队),sessionRole帮助运行时识别这类短暂会话并合理调度。
手动(交互式)事务:一条会话内的三阶段 RPC
当运行时广告了transaction能力并且支持会话粘性(sticky sessions)时,DBX 可能为手动事务打开一个专用工作负载会话,然后依次调用:
begin_manual_transaction{ schema? }— 固定一条物理连接并开启一个打开状态的事务;execute_query(及相关查询方法)— 在打开的事务上执行,直到 commit/rollback;commit_manual_transaction/rollback_manual_transaction— 结束交互式事务。
这与一次性execute_transaction是两种不同的语义:execute_transaction在单个 RPC 内部完成 begin、执行语句列表、commit/rollback 的完整闭环,而手动事务跨多个 RPC 且依赖会话粘性。
协议还有一个重要的运行时约束:如果运行时通过validate_session重连了会话,必须清除该会话上任何打开的手动事务,否则会出现"重连后仍残留未提交事务"的状态泄漏。
并发模型:跨会话并行、同会话串行
v2 的并发规则非常明确:
- 不同会话的请求可以并发执行;
- 同一会话的请求被串行化——因为连接状态、事务、schema 变更与驱动连接通常不能安全地并发使用;
- JSON-RPC 响应允许乱序返回,通过请求
id关联,客户端不能依赖响应顺序。
在 MultiSessionJsonRpcServer.java 中可以找到对应的实现证据:
- 每个
Session内部持有一个ReentrantLock,handle()与connect()都在lock.lock()保护下执行,从而保证同一会话内的请求串行; - 请求执行使用
ThreadPoolExecutor(MAX_REQUEST_THREADS = 64,SynchronousQueue+AbortPolicy)作为有界执行器,请求被提交到该线程池异步处理,因此不同会话可以并行、响应可以乱序; - 当线程池拒绝请求(容量耗尽)时,通过
AgentRpcError.backpressure("request", ...)返回背压错误(category=resource、retryable=true、sessionDisposition=keep),让 DBX 稍后重试而不是丢弃会话; - 清理操作使用独立的
MAX_CLEANUP_THREADS = 16有界执行器,保证"返回/驱逐/物理关闭连接"不会与"检出/创建连接"互相死锁。
共享连接池基础:不可变连接身份与有状态连接驱逐
所有 Java JDBC 运行时通过AbstractJdbcAgent以**不可变连接身份(immutable connection identity)**共享 HikariCP 连接池。核心机制(见 AbstractJdbcAgent.java):
- 无状态请求:借出连接、使用完归还(borrow/return),不占用会话状态;
- 有状态请求:分页游标、显式会话状态 SQL 会把物理连接**固定(pin)**到逻辑会话上;
- 有状态连接驱逐:当该会话关闭时,被固定的连接会被驱逐,保证连接状态不会跨会话泄漏;
- 自定义 URL 构造、传输回退、连接初始化与原生驱动访问仍然保留在共享生命周期钩子(如
beforePooledConnectionReturn)之后,Agent 作者只需关注数据库差异,无需重写池化逻辑。
此外,池化连接还带有一套"毒化"(poison)机制:一旦某次借用/请求的超时边界无法确认物理连接状态,该连接身份即被标记为 poisoned,后续借用必须驱逐并关闭它,而不是把它发布给新的请求。
运行时兼容与复用:什么属于会话、什么属于运行时
运行时(runtime)复用键(reuse key)由以下要素构成:
- Agent 驱动键(driver key);
- 可执行文件或 JAR 路径;
- 启动参数(launch arguments);
- 工作目录;
- JRE 选择、JVM 选项、影响 classpath 的选项;
- 原生可执行文件的版本边界(native executable version boundary)。
而host、account、schema、credentials 属于会话数据,不参与运行时键——这正是"一个运行时服务多个不同数据库会话"的前提。
v2 协议在兼容性上采取渐进策略:
- ZooKeeper Agent不广告
multi_session能力,因此保留传统单会话路径; - etcd Agents(v3 与 v2)与 SQL Agents 一样运行在共享多会话路径上;
- 旧版 Agent 二进制与 JAR继续走传统(v1)生命周期路径。
资源限制与恢复:256 会话、30 秒宽限期与绝对截止时间
会话数量与进程退出
- 单个运行时最多接受256个逻辑会话(源码常量
MAX_SESSIONS = 256,超限抛IllegalStateException("Agent session limit reached: 256"),转换为背压错误); - 关闭最后一个会话后,进程进入30 秒宽限期(grace period)再退出,避免用户快速开关标签页时反复冷启动运行时;
- 进程 EOF 会失败所有挂起请求,该运行时从复用池中移除,并在下次需要时重新创建;
- 连接校验与重连(
validate_session)只作用于单个逻辑会话。
结构化错误契约(structured_error_v1)
v2 的 JSON-RPC 失败响应可以携带结构化恢复数据,完整字段如下:
{ "contractVersion": 1, "category": "timeout|canceled|connection|protocol|resource|sql", "retryable": false, "sessionDisposition": "keep|quarantine|replace_runtime", "agentSessionId": "optional-session-id", "stage": "request|checkout|connect|validate|execute|fetch|cancel|close", "operationOutcome": "not_started|unknown", "sqlState": "optional-jdbc-sql-state", "vendorCode": 0, "exceptionClass": "optional-java-exception-class" }字段语义与约束:
contractVersion: 1只在握手广告structured_error_v1时才被保证;- 允许出现未知的附加字段,但以下情况属于契约违规:未知的枚举值、缺少必填字段、类型非法,或
agentSessionId与当前请求不匹配; operationOutcome描述用户操作是否可能已经到达数据库:not_started(request/checkout/connect/validate 阶段)或unknown(execute/fetch/cancel/close 阶段);retryable仅是内部提示,绝不授权自动重放 SQL——防止对可能已执行的写操作做无脑重试。
在 AgentRpcError.java 中可以看到该契约的生成逻辑:classify()根据异常类型与 RPC 方法推导出category、stage、disposition、retryable,其中SQLTimeoutException归类为timeout,SQLRecoverableException/SQLTransientConnectionException/ SQLState 以08开头等连接类错误归类为connection,其余 SQLException 归类为sql,CancellationException/InterruptedException归类为canceled,兜底为protocol。
sessionDisposition 三态语义
| 取值 | 含义 |
|---|---|
keep | 保留逻辑会话,可继续服务请求 |
quarantine | 仅将该会话从路由中移除(隔离) |
replace_runtime | DBX 需在终止该运行时之前,原子地移除所有共享该运行时的连接池 |
关键约束:Agent 代码只负责报告 disposition,绝不能自行终止共享运行时——因为它并不拥有 DBX 的路由状态。典型的临时工作负载检出背压使用category=resource、retryable=true、sessionDisposition=keep;只有不可恢复的运行时级或清理饱和问题才请求replace_runtime。
有界执行器与绝对截止时间
完整的 JDBC 连接池检出(checkout)过程运行在有界运行时执行器内,覆盖 HikariCP 空闲连接校验、物理连接创建与驱动初始化:
- 工作负载准入、运行时级物理连接预算、物理创建、检出共享同一个绝对截止时间(absolute deadline),而不是在每个阶段各自重启超时——避免"每个阶段都允许几分钟"导致总体超时失控;
- 连接归还、驱逐与物理关闭使用独立的有界执行器,从而不会反过来阻塞检出或创建流程;
- 如果驱动调用超出了它的时间边界,或清理流程无法确认物理连接状态,该连接身份即被毒化,并在当前或下一次检出时返回
category=resource+sessionDisposition=replace_runtime; - 迟到的连接必须被驱逐并关闭而不是发布给请求方;DBX 也不得自动重放超时的用户操作。
驱动作者指南:Java Agent 与原生 Agent 的落地方式
Java SQL Agent:一行代码接入
对于 Java SQL Agent,推荐直接使用MultiSessionJsonRpcServer(YourAgent::new):每个逻辑会话都会获得一个全新的DatabaseAgent实例与相互隔离的连接状态,物理 JDBC 连接池归共享运行时所有。
模板工程 TemplateAgent.java 展示了最简形态:
public final class TemplateAgent extends ConfiguredJdbcAgent { public static final JdbcAgentProfile TEMPLATE_PROFILE = new JdbcAgentProfile( "com.example.jdbc.TemplateDriver", "jdbc:template://{host}:{port}/{database}", 1234 ); public TemplateAgent() { super(TEMPLATE_PROFILE); } @Override public String setSchemaSQL(String schema) { return "SET SCHEMA " + JdbcIdentifiers.INSTANCE.doubleQuote(schema); } public static void main(String[] args) { new MultiSessionJsonRpcServer(TemplateAgent::new).run(); } }仓库中 30 余个 JDBC 驱动模块(如 Db2Agent.java、H2Agent.java、FirebirdAgent.java 等)均以该方式启动,可直接作为参考实现。
作者必须遵守的硬性约束:
- 不要在静态可变字段中保存连接、语句、游标、事务或 schema 状态——这会让隔离在多会话下失效;
- 分页查询资源必须挂在会话执行上下文(session execution context)上,随会话生命周期释放;
- 非 JDBC 的通用 v2 服务端可通过
MultiSessionJsonRpcServer.forSessionHandlers(...)接入SessionRpcHandler(见 MultiSessionJsonRpcServer.java 的forSessionHandlers工厂方法),并在run()主循环中处理handshake/open_session等会话级方法。
原生 Agent:等效的逐会话状态与同步输出
非 Java 的原生 Agent 必须提供等效能力:
- 逐会话状态隔离(per-session state):每个逻辑会话拥有独立状态,不能共享可变全局量;
- 同步的 stdout 写入(synchronized stdout writes):多会话并发响应输出时,必须保证每条 JSON-RPC 响应的写入是原子的,防止交错破坏 JSON 帧。
案例:Xugu 原生 Agent 的取消策略
Xugu 原生 Agent 采用"每逻辑会话一条数据库连接 + 每个数据库端点一条共享控制连接"的结构。由于go-xugu-driver无法通过context.Context中断网络读取,取消操作的实现方式是:
- 记录服务端会话 ID;
- 通过共享控制连接调用
DBMS_DBA.KILL_SESSION_TRANS杀掉目标事务。
这个模式说明:当驱动缺少可中断 I/O 能力时,Agent 需要借助服务端原语(如 kill session)配合共享控制连接来兑现cancel_session的语义。
测试与验证
v2 多会话行为在仓库中有对应的测试覆盖:
- JdbcConnectionPoolingTest.java 验证 JDBC 连接池化与共享连接身份下的借还、固定与驱逐行为;
- CommonJavaCompatibilityTest.java 校验能力集合与协议常量的一致性;
- 各驱动模块(如 H2AgentProcessTest.java、MongoAgentTest.java)以及 Go 驱动的
main_test.go覆盖握手与执行路径的进程级回归。
驱动作者在交付前应执行python3 scripts/validate_agents.py与./gradlew test shadowJar --continue,具体检查清单参见 agent-authoring.md 与 release-checklist.md。
小结
Agent Protocol v2 用"一个进程、多逻辑会话、共享连接池、结构化错误恢复"重构了 DBX 的 Agent 运行时模型:agentSessionId承担逻辑连接路由、sessionId仅作游标标识,sessionRole区分 workload 与 metadata 负载;同会话串行、跨会话并行的并发规则配合有界线程池与绝对截止时间保证了进程内资源可控;structured_error_v1的sessionDisposition三态(keep/quarantine/replace_runtime)把"会话该留、该隔离、还是整个运行时该换"的决策权明确划归 DBX,Agent 只负责如实上报。对驱动作者而言,Java Agent 用MultiSessionJsonRpcServer(YourAgent::new)即可获得全部隔离与池化能力,原生 Agent 则需自行保证逐会话状态与同步输出——理解这套契约,是让新数据库平滑接入 DBX 多会话架构的第一步。
【免费下载链接】dbx15MB,轻量级跨平台数据库客户端、数据库管理工具。支持 MySQL、PostgreSQL、SQLite、Redis、MongoDB、DuckDB、ClickHouse、SQL Server 等。15MB, lightweight, cross-platform database client. Supports MySQL, PostgreSQL, SQLite, Redis, MongoDB, DuckDB, ClickHouse, SQL Server and more.项目地址: https://gitcode.com/t8y2/dbx
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考