Open
[开源实习]: Proxool 连接 openGauss B 兼容模式测试代码及自测报告 #126
[开源实习]: Proxool 连接 openGauss B 兼容模式测试代码及自测报告 #126
Open
wsyAdd on Sep 23
Total: 19 files changed+2410-0
@@ -0,0 +1,9 @@
1+target/
2+.idea/
3+*.iml
4+.mvn/
5+mvnw
6+mvnw.cmd
7+ 
8+# 允许提交 lib 目录下的依赖 JAR
9+!lib/*.jar
@@ -0,0 +1,256 @@
1+# Proxool 连接 openGauss B 兼容模式自测报告
2+ 
3+## 1. 测试环境
4+ 
5+| 项目 | 配置 |
6+|---|---|
7+| 操作系统 | openEuler 20.03 LTS x86_64 |
8+| openGauss | 7.0.0-RC3(标准版,含 Dolphin 插件) |
9+| Dolphin | MySQL 协议地址 127.0.0.1:3308 |
10+| JDK | Oracle OpenJDK 1.8.0_202 |
11+| Proxool | 0.9.1 + proxool-cglib 0.9.1 |
12+| MySQL Connector/J | 8.0.28 |
13+| 数据库 / schema | test_b_db / public |
14+ 
15+测试使用 openGauss 7.0.0-RC3 标准版自带的 Dolphin 插件,无需单独构建或安装。
16+ 
17+### 1.1 账号与权限初始化说明
18+ 
19+| 账号 | 用途 | 初始化方式 |
20+|---|---|---|
21+| `dbcp_test` | 业务连接池账号,测试用例统一使用该账号 | 通过 Dolphin 的 `set_native_password` 设置 native 密码后方可用 MySQL 协议登录;已授予 `test_b_db` 下 public schema 的建表权限(`GRANT ALL ON SCHEMA public TO dbcp_test`) |
22+| `dcl_admin` | 仅 DCL 测试类使用的管理员账号,具备 SYSADMIN 权限 | 使用 `CREATE USER dcl_admin WITH PASSWORD '...'` 创建(openGauss 必须显式指定密码),再通过 `ALTER USER dcl_admin SYSADMIN` 授予管理员权限;同样需执行 `set_native_password` 才能在 3308 端口通过 MySQL 协议登录 |
23+ 
24+`set_native_password` 必须在 B 兼容库上下文中执行:
25+ 
26+```sql
27+\c test_b_db
28+SELECT set_native_password('dbcp_test', 'Test@1234');
29+SELECT set_native_password('dcl_admin', 'Test@1234');
30+```
31+ 
32+### 1.2 连接池注册策略
33+ 
34+为避免多个测试类重复注册管理员连接池,连接池的注册拆分为两部分:
35+ 
36+| 连接池别名 | 创建位置 | 关闭位置 |
37+|---|---|---|
38+| `openGaussPool` | 所有测试类的 `@BeforeClass` 通过 `proxool.properties` 加载注册 | 各测试类 `@AfterClass` 中 `removeConnectionPool("openGaussPool")` |
39+| `dclAdminPool` | **仅** `ProxoolSqlCategoryTest#init()` 中以代码方式单独注册 | 仅 `ProxoolSqlCategoryTest#cleanup()` 中 `removeConnectionPool("dclAdminPool")` |
40+ 
41+`proxool.properties` 中只保留 `jdbc-1.*`(openGaussPool)一组配置,不再包含 `jdbc-2.*` 的管理员池配置,因此其余 7 个测试类初始化时不会再注册同名池,彻底消除 `Attempt to register duplicate pool called 'dclAdminPool'` 异常。
42+ 
43+## 2. 执行命令
44+ 
45+在项目根目录执行单次完整回归:
46+ 
47+```bash
48+mvn clean test
49+```
50+ 
51+## 3. 测试结果
52+ 
53+```text
54+Tests run: 38, Failures: 0, Errors: 0, Skipped: 0
55+```
56+ 
L
Lliyangx2 days ago

[P2] 自测结果要跟这次提交一起更新

新增 test06 后,当前是 39 个测试,ProxoolPoolParamTest 有 6 个;报告、README 和两张截图还停留在 38 个。两张截图文件与上一版相同,不能说明这次改完后的运行结果。

报告 4.2 还在引用旧的 test04 方法名和已经删掉的轮询步骤,4.8 也没同步隔离池、池外管理连接的改法。

请修好后完整跑一次 mvn clean test,再把结果、截图和场景说明一起更新。有失败就把实际结果和原因写出来。图片路径和 README 的报告链接这次已经改对了。

likedislike
57+| 测试类 | 用例数 | 结果 | 验证内容 |
58+|---|---:|---|---|
59+| ProxoolJUnitTest | 12 | PASS | 连接初始化、CRUD、事务提交回滚、批量插入、连接池监控与关闭 |
60+| ProxoolPoolParamTest | 5 | PASS | 最大/最小/prototype 连接数配置核对、满池借出、归还复用、最小连接数维护、并发获取 |
61+| ProxoolTransactionTest | 4 | PASS | 隔离级别读取、commit 跨连接可见、rollback 跨连接不可见、池化事务 |
62+| ProxoolSqlCategoryTest | 6 | PASS | DDL、DML、DQL(排序分页)、DCL(授权与撤销 + 普通用户负向用例)、TCL(含保存点) |
63+| ProxoolDataTypeTest | 3 | PASS | 中文、emoji、常用类型与 NULL |
64+| ProxoolStabilityTest | 4 | PASS | 重复借出归还 100 次、多线程并发事务(完整 CRUD)、长连接保活、池状态快照 |
65+| ProxoolConnectionValidationTest | 3 | PASS | 借出连接校验、失效连接淘汰重建(pg_terminate_backend 真实制造失效)、池状态健康 |
66+| ProxoolPoolExhaustTest | 1 | PASS | 满池等待与恢复 |
67+| **合计** | **38** | **PASS** | Failures、Errors、Skipped 均为 0 |
68+ 
69+![测试总览](figures/01_overview.png)
70+![测试总览](figures/02_overview.png)
71+ 
72+ 
73+## 4. 关键场景
74+ 
75+### 4.1 Proxool 配置装配
76+ 
77+`proxool.properties` 配置被 `PropertyConfigurator.configure()` 加载后,注册到 `ProxoolFacade`,通过别名 `openGaussPool` 获取连接。测试断言以下链路:
78+ 
79+```text
80+proxool.properties
81+ -> PropertyConfigurator.configure()
82+ -> ProxoolFacade.registerConnectionPool()
83+ -> DriverManager.getConnection("proxool.openGaussPool")
84+ -> ProxoolDriver.connect()
85+ -> ConnectionPool.getConnection()
86+ -> Proxool 代理连接
87+```
88+ 
89+`test01_连接池初始化` 断言 `ProxoolFacade.getAliases()` 中包含 `openGaussPool`,并通过 Snapshot 验证 `maximum-connection-count=10` 已生效。
90+ 
91+### 4.2 连接池参数核对与维护
92+ 
93+`ProxoolPoolParamTest#test04_最小连接数配置核对与维护` 通过反射访问 `ProxoolFacade.getConnectionPoolDefinition()`,从**实际运行实例**中逐项读取配置并与 `proxool.properties` 中的值断言比对:
94+ 
95+| 参数 | 配置文件值 | 实例读取值 | 断言结果 |
96+|---|---:|---:|---|
97+| `maximum-connection-count` | 10 | 10 | PASS |
98+| `minimum-connection-count` | 2 | 2 | PASS |
99+| `prototype-count` | 4 | 4 | PASS |
100+ 
101+随后进行以下验证:
102+ 
103+1. **借出上限验证**:循环借出 `maximum` 条连接,**任何一次借出失败都直接 `fail()` 终止用例**(不再捕获异常后仅打印继续执行),防止“借出 0 条也判通过”的假阳性。
104+2. **持有期间状态断言**:池满时断言 `active == maximum`、`available >= 0`。
105+3. **维护线程补足验证**:全部归还后,以**有界轮询**方式(最多 10 次、每次 1 秒)读取快照,一旦 `available >= minimum` 立即退出并断言通过,超时则断言失败。
106+ 
107+> 说明:`minimum-connection-count` 是最小保留连接总数,`prototype-count` 是额外保留的可用连接数,两者含义不同,本用例分别断言,不混用。
108+ 
109+### 4.3 满池等待与恢复
110+ 
111+`ProxoolPoolExhaustTest#test01_满池等待超时` 设置 `maximum-connection-count=10`:
112+ 
113+1. 同时借出全部 10 条连接,池满。
114+2. 第 11 条请求进入等待,被 Proxool 拒绝(`Couldn't get connection because we are at maximum connection count`)。
115+3. 释放连接后,新请求可以重新借到连接并执行 `SELECT 1`。
116+4. 测试结束时 `active=0`。
117+ 
118+### 4.4 并发业务
119+ 
120+`ProxoolStabilityTest#test02_多线程并发事务` 使用 8 个工作线程,通过 `held` 和 `release` 双屏障确保所有线程**同时持有**连接并执行完整的事务 CRUD:
121+ 
122+1. 8 个线程各自借到连接后,在 `heldLatch` 处等待,**此时不执行业务**。
123+2. 主线程通过快照确认池中活跃连接数达到 8,再 `releaseLatch.countDown()` 放行。
124+3. 线程执行 INSERT → SELECT → UPDATE → SELECT → DELETE → COMMIT,每一步都用 `assertEquals` 断言 SQL 结果。
125+4. 线程结束后逐项断言:
126+ - 成功数 = 8,失败数 = 0
127+ - 表内记录数 = 0(每个线程自己清理数据)
128+ - Proxool 活跃连接数 = 0(无连接泄漏)
129+5. `endLatch.await(30, TimeUnit.SECONDS)` 阻塞主线程,超时则直接断言失败,保证所有后台线程在连接池关闭前确定性地结束,不会拖入下一个测试类。
130+ 
131+### 4.5 事务
132+ 
133+| 场景 | 断言 |
134+|---|---|
135+| 事务隔离级别读取 | `getTransactionIsolation()` 返回合法值 |
136+| 提交 | `commit()` 后,其他连接可以查询到已提交数据 |
137+| 回滚 | `rollback()` 后,其他连接查询不到回滚数据 |
138+| 池化连接事务 | 通过 Proxool 包装连接执行事务,归还后连接状态恢复正常 |
139+ 
140+### 4.6 DDL、DML、DQL、DCL、TCL
141+ 
142+| 分类 | 操作 | 断言 |
143+|---|---|---|
144+| DDL | CREATE TABLE、ALTER TABLE、DROP TABLE | 对象创建、结构变更、删除成功 |
145+| DML | INSERT、UPDATE、DELETE | 影响行数和最终数据正确 |
146+| DQL | 条件查询、ORDER BY、LIMIT OFFSET | 结果顺序、行数正确 |
147+| DCL | GRANT、REVOKE(正向);普通用户 GRANT(负向) | 授权后权限存在、撤销后权限消失;普通用户 GRANT 被拒绝且错误信息与权限相关 |
148+| TCL | COMMIT、ROLLBACK、SAVEPOINT | 提交数据可见,回滚到保存点后数据正确 |
149+ 
150+**DCL 正向用例** `test04_DCL_授权与撤销` 使用具备 SYSADMIN 权限的独立管理员连接池 `dclAdminPool`(`dcl_admin` 账号):
151+ 
152+1. **前置清理**:通过 `cleanRoleAndTable()` 统一执行 `REASSIGN OWNED BY`、`DROP OWNED BY`、`DROP SCHEMA ... CASCADE`、`DROP TABLE IF EXISTS`、`DROP USER IF EXISTS`,确保角色与测试对象处于干净状态。
153+2. 创建独立的临时角色 `dcl_test_role` 和测试表 `dcl_test_table`。
154+3. 执行 `GRANT SELECT ON ... TO dcl_test_role`,查询 `information_schema.role_table_grants`,断言权限记录数 > 0。
155+4. 执行 `REVOKE SELECT ON ... FROM dcl_test_role`,再次查询 `information_schema.role_table_grants`,断言权限记录数 = 0。
156+5. **后置清理**:`finally` 中再次调用同一个 `cleanRoleAndTable()`,与前置清理保持完全一致,保证无残留。
157+ 
158+> 说明:权限记录查询使用 `lower(grantee) = lower('role')`,避免 openGauss 对角色名大小写规范化导致断言误判。
159+ 
160+**DCL 负向用例** `test04b_DCL_普通用户权限不足` 使用普通用户 `dbcp_test` 执行 GRANT:
161+ 
162+1. **由管理员先准备真实环境**:彻底清理旧角色(含 `REASSIGN OWNED` / `DROP OWNED` / `DROP SCHEMA CASCADE`),再创建确定存在的目标角色 `dcl_neg_test_role` 和测试表,排除“角色不存在”造成的假通过。
163+2. 普通用户尝试 GRANT 操作,`fail()` 兜底若成功;捕获异常后**校验错误信息必须与权限相关**(包含 `permission` / `privilege` / `denied` / `must be owner`),并打印 `SQLState` 以便追溯。openGauss 经 Dolphin 翻译后 SQLState 可能为 `HY000`,故以错误信息为准。
164+3. `finally` 中通过管理员连接执行 `REASSIGN OWNED`、`DROP OWNED`、`DROP USER`、`DROP SCHEMA CASCADE` 和删表,确保环境彻底干净。
165+ 
166+### 4.7 数据类型与字符集
167+ 
168+| 场景 | 断言 |
169+|---|---|
170+| 中文 | 张三丰,存取一致 |
171+| emoji | Hello😀🎉🚀,存取一致 |
172+| 常用类型 | INT / DECIMAL / DATETIME / TEXT 正确读写 |
173+| NULL | `setNull` 后读取为 null |
174+ 
175+### 4.8 连接校验与失效重建
176+ 
177+`ProxoolConnectionValidationTest#test02_失效连接被淘汰重建` 通过真实断连验证 Proxool 的失效连接淘汰与重建机制:
178+ 
179+1. 借出一个连接,通过 `SELECT pg_backend_pid()` 获取其在数据库端的会话 PID。
180+2. 通过另一个**独立借出**的连接执行 `SELECT pg_terminate_backend(pid)`,并**断言该函数返回 true**(确保会话真实被杀掉)。
181+3. 尝试使用原连接执行 `SELECT 1`,**断言必定抛出 SQLException**(证明旧连接已彻底失效),并在 `finally` 中关闭原连接。
182+4. 再次借出新的连接,执行 `SELECT pg_backend_pid()` 获取新会话 PID,**断言新 PID 与旧 PID 不同**,以此作为“旧物理连接已被淘汰、新物理连接已建立”的直接证据。
183+ 
184+> 说明:不以 `servedCount`(借出次数计数器)作为重建依据,该计数与物理连接是否重建无关;后端 PID 变化才是物理连接重建的确定性证据。
185+ 
186+### 4.9 稳定性
187+ 
188+| 用例 | 验证内容 | 断言 |
189+|---|---|---|
190+| `test01_重复借出归还100次` | 连续 100 次借出归还并执行 `SELECT 1` | 每次查询结果正确;结束后 `active=0` |
191+| `test02_多线程并发事务` | 8 线程双屏障同步 + 完整 CRUD | 成功 8、失败 0、表内 0 条、`active=0` |
192+| `test03_长连接与housekeeping` | 借出连接保持 5 秒并每秒执行一次查询 | 短时间内连接持续可用(不覆盖 housekeeping 行为) |
193+| `test04_池状态快照一致性` | 借出 2 个连接前后读取快照 | 借出后 `active` 增加 2,归还后回落 |
194+ 
195+## 5. 兼容性观察
196+ 
197+| 现象 | 说明 |
198+|---|---|
199+| SHOW DATABASES 不显示用户库 | 只显示系统库,但 test_b_db.public.test_proxool 可正常访问 |
200+| USE test_b_db 报 Unknown database | Dolphin 协议层未实现 USE 命令 |
201+| SHOW TABLES FROM db.schema 报 Unknown Schema | Dolphin 不识别复合元数据语法 |
202+| schema 与 database 是两层结构 | 必须使用 database.schema.table 三段式全限定名 |
203+| public schema 默认对普通用户只读 | 需显式授权才能建表 |
204+| 普通用户无 DCL 权限 | GRANT/REVOKE 报 no privileges were granted,与 MySQL 行为一致 |
205+| CREATE USER 必须指定密码 | openGauss 不支持无密码创建用户 |
206+ 
207+## 6. 版本兼容性列表
208+ 
209+| 组件 | 版本 | 兼容性 | 备注 |
210+|---|---|---|---|
211+| Proxool | 0.9.1 | 兼容 | 必须搭配 JDK 8 |
212+| proxool-cglib | 0.9.1 | 必需 | 与 proxool 一同引入,否则报 NoClassDefFoundError |
213+| JDK | 1.8.0_202 | 兼容 | 推荐版本 |
214+| JDK | 21 | 不兼容 | 报 InaccessibleObjectException |
215+| MySQL Connector/J | 8.0.28 | 兼容 | 官方支持上限 |
216+| openGauss | 7.0.0-RC3 | 兼容 | 需开启 dolphin 插件 |
217+| openEuler | 20.03 LTS | 兼容 | 任务指定版本 |
218+ 
219+## 7. 问题与规避措施
220+ 
221+| 问题 | 现象 | 原因 | 解决方案 |
222+|---|---|---|---|
223+| dolphin.so 加载失败 | undefined symbol: xmlXPathNewContext | openEuler 24.03 libxml2 版本过高 | 改用 openEuler 20.03 LTS |
224+| gaussdb 启动失败 | 缺 libopenblas.so.0 | 未装 openblas | dnf install openblas-devel |
225+| dblink.so 加载失败 | 缺 libodbc.so.2 | 未装 unixODBC | dnf install unixODBC unixODBC-devel |
226+| set_native_password 函数不存在 | 函数报错 | 未在 B 兼容库下执行 | 先 `\c test_b_db` 再执行 |
227+| Java 连不上 3308 | Communications link failure | 监听地址或 IP 变更 | listen_addresses='*',核对 JDBC URL 中的 IP |
228+| schema "test_b_db" does not exist | 找不到表 | openGauss database/schema 两层结构 | 用三段式全限定名 |
229+| permission denied for schema public | 无法建表 | public schema 默认只读 | GRANT ALL ON SCHEMA public TO dbcp_test |
230+| InaccessibleObjectException | Proxool 初始化失败 | Proxool 0.9.1 与 Java 21 不兼容 | 换 JDK 8 |
231+| NoClassDefFoundError: cglib/proxy/Callback | 缺少 cglib 类 | proxool-cglib 未引入 | 补充 proxool-cglib-0.9.1.jar |
232+| pg_backend_pid 返回值溢出 | outside of valid range for type java.lang.Integer | backend PID 是 long 类型 | 用 `rs.getLong(1)` |
233+| ConnectionPoolDefinition 反射访问失败 | IllegalAccessException | 类非 public | 对 Method 调用 `setAccessible(true)` |
234+| CREATE USER 报密码为 NULL | The password could not be NULL | openGauss 要求必须指定密码 | `CREATE USER xxx WITH PASSWORD '...'` |
235+| 满池后第 11 个请求被拒绝 | Couldn't get connection because we are at maximum connection count | 符合连接池设计 | 正常行为,已验证归还后恢复 |
236+| 多测试类重复注册管理员池 | `Attempt to register duplicate pool called 'dclAdminPool'`,后续测试类初始化失败 | 管理员池原先写在共享 `proxool.properties` 中,每个测试类 `@BeforeClass` 都会注册,而 `@AfterClass` 只删除了 `openGaussPool` | 管理员池从 properties 中移除,改为仅由 `ProxoolSqlCategoryTest` 用代码单独注册,并在该类 `@AfterClass` 中显式关闭 |
237+| 角色删除失败导致脏数据遗留 | `DROP USER` 报对象依赖错误,脏角色残留影响下次运行 | 角色仍拥有 schema 或其他对象 | 前置与后置清理统一调用 `cleanRoleAndTable()`,按 `REASSIGN OWNED → DROP OWNED → DROP SCHEMA CASCADE → DROP TABLE → DROP USER` 顺序清理 |
238+ 
239+## 8. 结论
240+ 
241+在本报告所列环境中,Proxool 0.9.1 可以通过 Connector/J 8.0.28 和 Dolphin MySQL 协议连接 openGauss B 兼容库。
242+ 
243+单次 `mvn clean test` 完整回归结果:**Tests run: 38, Failures: 0, Errors: 0, Skipped: 0**。
244+ 
245+已验证范围包括:
246+ 
247+- 连接池配置装配与 `maximum-connection-count` / `minimum-connection-count` / `prototype-count` 三项参数的实例级核对及最小连接数维护行为;
248+- CRUD、事务提交/回滚/保存点、满池等待与恢复;
249+- 8 线程并发完整事务 CRUD,双屏障保证同时持有连接,结束后无数据残留、无连接泄漏;
250+- 借出连接校验、通过 `pg_terminate_backend` 真实断连验证失效连接淘汰与重建(以后端 PID 变化作为证据);
251+- DDL、DML、DQL、DCL(含真实权限状态断言和普通用户负向用例)、TCL 各类 SQL 操作;
252+- 中文、emoji、常用数据类型与 NULL 的读写一致性。
253+ 
254+全部用例以实际断言结果为通过依据,无“仅打印日志”的判定路径。
255+ 
256+**结论覆盖范围的边界说明**:`ProxoolStabilityTest#test03_长连接与housekeeping` 仅持有连接 5 秒,小于当前配置的维护周期(`house-keeping-sleep-time=30000`,30 秒),因此该用例只证明**短时间内连接持续可查询**,不构成对后台保活、housekeeping 不误杀或长期稳定性的验证。上述结论中未包含任何 housekeeping 行为相关的断言。如需验证 housekeeping,需要使用隔离测试池、设置较短且明确的维护周期、并以可观察的维护执行记录跨多个维护周期进行验证,此项不在本轮 38 项用例覆盖范围内。
Binary files do not support preview
Binary files do not support preview
@@ -0,0 +1,59 @@
1+<?xml version="1.0" encoding="UTF-8"?>
2+<project xmlns="http://maven.apache.org/POM/4.0.0"
3+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+ <modelVersion>4.0.0</modelVersion>
6+ 
7+ <groupId>org.example</groupId>
8+ <artifactId>ProxoolOpenGaussTest</artifactId>
9+ <version>1.0-SNAPSHOT</version>
10+ 
11+ <properties>
12+ <maven.compiler.source>1.8</maven.compiler.source>
13+ <maven.compiler.target>1.8</maven.compiler.target>
14+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
15+ </properties>
16+ 
17+ <dependencies>
18+ <!-- Proxool 核心 -->
19+ <dependency>
20+ <groupId>proxool</groupId>
21+ <artifactId>proxool</artifactId>
22+ <version>0.9.1</version>
23+ <scope>system</scope>
24+ <systemPath>${project.basedir}/lib/proxool-0.9.1.jar</systemPath>
25+ </dependency>
26+ 
27+ <!-- Proxool CGLib 依赖 (必须添加) -->
28+ <dependency>
29+ <groupId>proxool</groupId>
30+ <artifactId>proxool-cglib</artifactId>
31+ <version>0.9.1</version>
32+ <scope>system</scope>
33+ <systemPath>${project.basedir}/lib/proxool-cglib-0.9.1.jar</systemPath>
34+ </dependency>
35+ 
36+ <!-- commons-logging -->
37+ <dependency>
38+ <groupId>commons-logging</groupId>
39+ <artifactId>commons-logging</artifactId>
40+ <version>1.2</version>
41+ </dependency>
42+ 
43+ <!-- MySQL JDBC 驱动 -->
44+ <dependency>
45+ <groupId>mysql</groupId>
46+ <artifactId>mysql-connector-java</artifactId>
47+ <version>8.0.28</version>
48+ </dependency>
49+ 
50+ <!-- JUnit 4 测试框架 -->
51+ <dependency>
52+ <groupId>junit</groupId>
53+ <artifactId>junit</artifactId>
54+ <version>4.13.2</version>
55+ <scope>test</scope>
56+ </dependency>
57+ 
58+ </dependencies>
59+</project>
@@ -0,0 +1,43 @@
1+# Proxool 连接 openGauss B 兼容模式示例
2+ 
3+## 项目简介
4+本项目演示如何使用 Proxool 连接池配合 MySQL JDBC 驱动连接 openGauss B 兼容模式数据库。
5+ 
6+## 环境要求
7+- openEuler 20.03 LTS
8+- openGauss 7.0.0-RC3(开启 dolphin 插件)
9+- JDK 1.8
10+- MySQL Connector/J 8.0.28
11+- Proxool 0.9.1 + proxool-cglib 0.9.1
12+ 
13+## 前置条件
14+1. openGauss 开启 MySQL 协议兼容:`enable_dolphin_proto = on`、`dolphin_server_port = 3308`
15+2. 创建 B 兼容数据库 `test_b_db`
16+3. 创建用户 `dbcp_test` 并授权 public schema
17+ 
18+## 运行步骤
19+1. 修改 `src/main/resources/proxool.properties` 中的数据库连接信息
20+2. 执行 `mvn clean test` 运行 JUnit 测试用例
21+ 
22+## 测试覆盖
23+ 
24+本项目包含 8 个 JUnit 测试类,共 38 个测试用例:
25+ 
26+- `ProxoolJUnitTest`(12 项):连接初始化、CRUD、事务提交回滚、批量插入、连接池监控与关闭
27+- `ProxoolPoolParamTest`(5 项):最大连接数、满池借出、归还复用、最小空闲、并发获取
28+- `ProxoolTransactionTest`(4 项):隔离级别、commit/rollback 跨连接可见性、池化事务
29+- `ProxoolSqlCategoryTest`(6 项):DDL/DML/DQL/DCL/TCL
30+- `ProxoolDataTypeTest`(3 项):中文、emoji、常用类型与 NULL
31+- `ProxoolStabilityTest`(4 项):100 次借出归还、8 线程并发事务、长连接保活、池状态快照
32+- `ProxoolConnectionValidationTest`(3 项):借出校验、失效连接重建、池状态健康
33+- `ProxoolPoolExhaustTest`(1 项):满池等待与恢复
34+ 
35+## 产出文档
36+ 
37+- `docs/Proxool连接openGaussB兼容模式自测报告.md` —— 完整自测报告
38+- `docs/figures/` —— 测试截图
39+ 
40+## 测试思维导图
41+ 
42+见项目根目录的 `测试思维导图.png`## 关联 issue
43+- openGauss/examples#50
@@ -0,0 +1,135 @@
1+import org.logicalcobwebs.proxool.ProxoolFacade;
L
LliyangxSep 29

测试方向不是很全面,可以参考下其它连接池的测试issue同学的pr

likedislike
wsy
wsy
Oct 1 Comments:
wsy
wsy
Oct 2 Comments:
2+import org.logicalcobwebs.proxool.configuration.PropertyConfigurator;
3+ 
4+import java.io.InputStream;
5+import java.sql.*;
6+import java.util.Properties;
7+import org.logicalcobwebs.proxool.admin.SnapshotIF;
8+ 
9+public class ProxoolTest {
10+ 
11+ /** 初始化 Proxool 连接池 */
12+ public static void init() throws Exception {
13+ InputStream in = ProxoolTest.class
14+ .getClassLoader()
15+ .getResourceAsStream("proxool.properties");
16+ Properties props = new Properties();
17+ props.load(in);
18+ PropertyConfigurator.configure(props);
19+ // 加载 Proxool 驱动
20+ Class.forName("org.logicalcobwebs.proxool.ProxoolDriver");
21+ System.out.println(" 0. Proxool 连接池初始化成功");
22+ }
23+ 
24+ /** 从连接池获取连接 */
25+ public static Connection getConnection() throws Exception {
26+ return DriverManager.getConnection("proxool.openGaussPool");
27+ }
28+ 
29+ public static void main(String[] args) {
30+ try {
31+ init();
32+ 
33+ try (Connection conn = getConnection();
34+ Statement stmt = conn.createStatement()) {
35+ 
36+ System.out.println(" 1. 数据库连接成功");
37+ 
38+ // 2. 建表
39+ stmt.execute("CREATE TABLE IF NOT EXISTS test_b_db.public.test_proxool " +
40+ "(id INT PRIMARY KEY, name VARCHAR(50))");
41+ System.out.println(" 2. 建表成功");
42+ 
43+ // 3. 插入数据
44+ stmt.execute("DELETE FROM test_b_db.public.test_proxool WHERE id IN (1,2)");
45+ stmt.execute("INSERT INTO test_b_db.public.test_proxool VALUES (1, 'Alice'), (2, 'Bob')");
46+ System.out.println(" 3. 插入数据成功");
47+ ResultSet rs = stmt.executeQuery(
48+ "SELECT * FROM test_b_db.public.test_proxool WHERE id IN (1,2) ORDER BY id");
49+ while (rs.next()) {
50+ System.out.println(" 插入后查询: id=" + rs.getInt("id")
51+ + ", name=" + rs.getString("name"));
52+ }
53+ 
54+ // 4. 查询数据
55+ rs = stmt.executeQuery(
56+ "SELECT * FROM test_b_db.public.test_proxool ORDER BY id");
57+ System.out.println(" 4. 查询数据成功,当前表内所有记录:");
58+ while (rs.next()) {
59+ System.out.println(" 查询: id=" + rs.getInt("id")
60+ + ", name=" + rs.getString("name"));
61+ }
62+ 
63+ // 5. 更新数据
64+ rs = stmt.executeQuery(
65+ "SELECT name FROM test_b_db.public.test_proxool WHERE id=1");
66+ String beforeName = rs.next() ? rs.getString("name") : "N/A";
67+ int updated = stmt.executeUpdate(
68+ "UPDATE test_b_db.public.test_proxool SET name='Alice2' WHERE id=1");
69+ rs = stmt.executeQuery(
70+ "SELECT name FROM test_b_db.public.test_proxool WHERE id=1");
71+ String afterName = rs.next() ? rs.getString("name") : "N/A";
72+ System.out.println(" 5. 更新数据: id=1 的 name 从 " + beforeName
73+ + " 改为 " + afterName + ",影响行数=" + updated);
74+ 
75+ // 6. 事务回滚测试
76+ conn.setAutoCommit(false);
77+ stmt.executeUpdate(
78+ "UPDATE test_b_db.public.test_proxool SET name='ROLLBACK_TEST' WHERE id=2");
79+ conn.rollback();
80+ conn.setAutoCommit(true);
81+ rs = stmt.executeQuery(
82+ "SELECT name FROM test_b_db.public.test_proxool WHERE id=2");
83+ String rollbackValue = rs.next() ? rs.getString("name") : "N/A";
84+ System.out.println(" 6. 事务回滚成功,事务内将 id=2 改为 ROLLBACK_TEST,"
85+ + "回滚后查询 id=2 的 name 仍为: " + rollbackValue);
86+ 
87+ // 7. 批量插入测试(先清理,保证可重复运行)
88+ stmt.execute("DELETE FROM test_b_db.public.test_proxool WHERE id BETWEEN 10 AND 19");
89+ conn.setAutoCommit(false);
90+ try (PreparedStatement ps = conn.prepareStatement(
91+ "INSERT INTO test_b_db.public.test_proxool VALUES (?, ?)")) {
92+ for (int i = 10; i < 20; i++) {
93+ ps.setInt(1, i);
94+ ps.setString(2, "User" + i);
95+ ps.addBatch();
96+ }
97+ ps.executeBatch();
98+ conn.commit();
99+ } finally {
100+ conn.setAutoCommit(true);
101+ }
102+ System.out.println(" 7. 批量插入成功,已插入 id=10~19 共 10 条记录");
103+ 
104+ // 8. 验证数据(保留不删)
105+ rs = stmt.executeQuery(
106+ "SELECT COUNT(*) AS total FROM test_b_db.public.test_proxool");
107+ if (rs.next()) {
108+ System.out.println(" 8. 当前表内记录总数: " + rs.getInt("total")
109+ + "(id=1,2 + id=10~19 共 12 条)");
110+ }
111+ 
112+ }
113+ 
114+ // 9. Proxool 连接池监控
115+ System.out.println(" 9. 连接池监控:");
116+ SnapshotIF snapshot = ProxoolFacade.getSnapshot("openGaussPool", true);
117+ System.out.println(" 当前活跃连接数: " + snapshot.getActiveConnectionCount());
118+ System.out.println(" 当前可用连接数: " + snapshot.getAvailableConnectionCount());
119+ System.out.println(" 最大连接数: " + snapshot.getMaximumConnectionCount());
120+ System.out.println(" 已建立连接总数: " + snapshot.getServedCount());
121+ 
122+ } catch (Exception e) {
123+ System.err.println("❌ 测试失败");
124+ e.printStackTrace();
125+ } finally {
126+ // 10. 无论是否异常,都关闭连接池
127+ try {
128+ ProxoolFacade.removeConnectionPool("openGaussPool");
129+ System.out.println(" 10. 连接池已关闭");
130+ } catch (Exception e) {
131+ System.err.println("关闭连接池失败: " + e.getMessage());
132+ }
133+ }
134+ }
135+}
@@ -0,0 +1,32 @@
1+# ============================================
2+# Proxool 连接 openGauss B 兼容模式配置
3+# ============================================
4+ 
5+# 连接池别名
6+jdbc-1.proxool.alias=openGaussPool
7+ 
8+# MySQL JDBC 驱动类
9+jdbc-1.proxool.driver-class=com.mysql.cj.jdbc.Driver
10+ 
11+# JDBC URL:指向 openGauss 的 MySQL 协议端口 3308
12+jdbc-1.proxool.driver-url=jdbc:mysql://192.168.198.135:3308/test_b_db?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
13+ 
14+# 数据库用户名和密码
15+jdbc-1.user=dbcp_test
16+jdbc-1.password=Test@1234
17+ 
18+# 连接池参数
19+jdbc-1.proxool.maximum-connection-count=10
20+jdbc-1.proxool.minimum-connection-count=2
21+jdbc-1.proxool.prototype-count=4
22+jdbc-1.proxool.maximum-active-time=60000
23+jdbc-1.proxool.maximum-connection-lifetime=18000000
24+jdbc-1.proxool.house-keeping-sleep-time=30000
25+jdbc-1.proxool.house-keeping-test-sql=SELECT 1
26+jdbc-1.proxool.test-before-use=true
27+jdbc-1.proxool.test-after-use=false
28+jdbc-1.proxool.verbose=true
29+jdbc-1.proxool.statistics=10s,1m,1d
30+jdbc-1.proxool.statistics-log-level=error
31+ 
32+ 
@@ -0,0 +1,224 @@
1+import org.junit.*;
2+import org.logicalcobwebs.proxool.ProxoolFacade;
3+import org.logicalcobwebs.proxool.admin.SnapshotIF;
4+import org.logicalcobwebs.proxool.configuration.PropertyConfigurator;
5+ 
6+import java.io.InputStream;
7+import java.sql.*;
8+import java.util.Properties;
9+ 
10+import static org.junit.Assert.*;
11+ 
12+public class ProxoolConnectionValidationTest {
13+ 
14+ /** 容量为 1 的隔离验证池(仅 test02 使用,用后即删) */
15+ private static final String VALIDATION_POOL = "validationPool";
16+ 
17+ private static final String DIRECT_URL =
L
Lliyangx2 days ago

[P2] 连接信息统一从配置里取

README 让读者只改 proxool.properties,但这里的 DIRECT_URL,以及 test06 里的地址、账号、密码都写死在源码里。换个环境按 README 操作,这两个用例还是会去连 192.168.198.135,完整测试就跑不起来。

请让新增的隔离池和池外连接也读取同一份连接配置,只单独设置池名、容量和维护参数。需要额外权限的话,在运行说明里写清楚。

likedislike
18+ "jdbc:mysql://192.168.198.135:3308/test_b_db?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC&characterEncoding=utf-8";
19+ 
20+ @BeforeClass
21+ public static void init() throws Exception {
22+ InputStream in = ProxoolConnectionValidationTest.class.getClassLoader()
23+ .getResourceAsStream("proxool.properties");
24+ assertNotNull("proxool.properties 配置文件未找到", in);
25+ Properties props = new Properties();
26+ props.load(in);
27+ PropertyConfigurator.configure(props);
28+ Class.forName("org.logicalcobwebs.proxool.ProxoolDriver");
29+ System.out.println("[INFO] Proxool 连接池初始化成功");
30+ }
31+ 
32+ private Connection getConnection() throws SQLException {
33+ return DriverManager.getConnection("proxool.openGaussPool");
34+ }
35+ 
36+ @Test
37+ public void test01_借出连接时校验() throws Exception {
38+ System.out.println("========== test01_借出连接时校验 开始 ==========");
39+ 
40+ // 连续借出归还多次,test-before-use 在借出时执行 SELECT 1 校验
41+ for (int i = 0; i < 20; i++) {
42+ try (Connection conn = getConnection();
43+ Statement stmt = conn.createStatement();
44+ ResultSet rs = stmt.executeQuery("SELECT 1")) {
45+ assertTrue(rs.next());
46+ assertEquals(1, rs.getInt(1));
47+ }
48+ }
49+ System.out.println("[验证] 连续 20 次借出归还均成功,连接校验生效");
50+ 
51+ System.out.println("========== test01_借出连接时校验 通过 ==========");
52+ }
53+ 
54+ /**
55+ * 失效连接淘汰与重建验证(容量受控隔离池方案)。
56+ *
57+ * 老师复核指出:在多连接池中"前后两次借出 PID 不同"不能证明物理连接被重建——
58+ * 池里可能本来就存在第二条闲置物理连接,归还后再借到它 PID 必然不同,
59+ * 但期间物理销毁计数为 0。
60+ *
61+ * 本用例通过排除所有其他可能性,使"PID 变化 = 必然新建"成为确定性推理:
62+ * 1. 注册隔离池 validationPool:maximum-connection-count=2、minimum=0、prototype=0、
63+ * test-before-use=true,测试开始时池内没有任何闲置连接(available=0);
64+ * 2. 借出 conn1(池内第一条、也是唯一一条物理连接),记录其会话 PID1;
65+ * 3. 用【池外】管理连接(不经 Proxool,直接 DriverManager)执行
66+ * pg_terminate_backend(pid1) 并断言返回 true;
67+ * 4. 断言 conn1 执行 SQL 抛出连接类异常(区分三类失败原因,
68+ * 认证/环境错误不允许进入该断言);
69+ * 5. 归还 conn1 后再次借出。第二次借出的连接只可能来自两种途径,
70+ * 两种都不可能是被终止的旧物理连接:
71+ * a) close 成功、死连接回到 available → test-before-use 检活失败,
72+ * Proxool 剔除它并新建物理连接;
73+ * b) close 失败、死连接卡在 active(Proxool 对底层已死亡连接的已知行为)
74+ * → available 为空,池在剩余容量内新建物理连接。
75+ * 无论哪种情况,借到的 PID2 都只能来自【新建】的物理连接,断言 PID2 != PID1。
76+ */
77+ @Test
78+ public void test02_失效连接被淘汰重建() throws Exception {
79+ System.out.println("========== test02_失效连接被淘汰重建 开始 ==========");
80+ 
81+ // 1. 注册容量为 2 的隔离验证池(1 个槽位留给可能卡死的旧连接,1 个用于重建)
82+ Properties vp = new Properties();
83+ vp.setProperty("jdbc-4.proxool.alias", VALIDATION_POOL);
84+ vp.setProperty("jdbc-4.proxool.driver-class", "com.mysql.cj.jdbc.Driver");
85+ vp.setProperty("jdbc-4.proxool.driver-url", DIRECT_URL);
86+ vp.setProperty("jdbc-4.user", "dbcp_test");
87+ vp.setProperty("jdbc-4.password", "Test@1234");
88+ vp.setProperty("jdbc-4.proxool.maximum-connection-count", "2");
89+ vp.setProperty("jdbc-4.proxool.minimum-connection-count", "0");
90+ vp.setProperty("jdbc-4.proxool.prototype-count", "0");
91+ vp.setProperty("jdbc-4.proxool.test-before-use", "true");
92+ vp.setProperty("jdbc-4.proxool.house-keeping-test-sql", "SELECT 1");
93+ PropertyConfigurator.configure(vp);
94+ System.out.println("[INFO] 隔离验证池 " + VALIDATION_POOL
95+ + " 已注册 (maximum=2, minimum=0, prototype=0, test-before-use=true)");
96+ 
97+ try {
98+ // 2. 借出池内第一条物理连接(此时池内没有其他任何物理连接),记录会话 PID
99+ Connection conn1 = DriverManager.getConnection("proxool." + VALIDATION_POOL);
100+ long pid1;
101+ try (Statement st = conn1.createStatement();
102+ ResultSet rs = st.executeQuery("SELECT pg_backend_pid()")) {
103+ assertTrue(rs.next());
104+ pid1 = rs.getLong(1);
105+ }
106+ SnapshotIF before = ProxoolFacade.getSnapshot(VALIDATION_POOL, true);
107+ System.out.println("[INFO] 已借出第一条物理连接,PID1=" + pid1
108+ + ",池状态: active=" + before.getActiveConnectionCount()
109+ + ", available=" + before.getAvailableConnectionCount());
110+ assertEquals("隔离池容量应为 2", 2, before.getMaximumConnectionCount());
111+ assertEquals("借出前池内不应有其他物理连接", 0,
112+ before.getAvailableConnectionCount());
113+ 
114+ // 3. 池外管理连接执行终止(不经 Proxool,排除池内其他连接的干扰)
115+ long killed;
116+ try (Connection mgmt = DriverManager.getConnection(DIRECT_URL, "dbcp_test", "Test@1234");
117+ Statement mgmtStmt = mgmt.createStatement();
118+ ResultSet rs = mgmtStmt.executeQuery(
119+ "SELECT pg_terminate_backend(" + pid1 + ")")) {
120+ assertTrue(rs.next());
121+ killed = rs.getBoolean(1) ? 1 : 0;
122+ }
123+ assertEquals("终止会话应返回 true", 1, killed);
124+ System.out.println("[验证] 池外管理连接已终止会话 PID1=" + pid1);
125+ 
126+ // 4. 断言旧连接执行 SQL 抛出连接类异常,并区分三类失败原因:
127+ // 预期:连接类错误(SQLState 08xxx / message 含连接失效描述)
128+ // 不接受:认证类(28000 Access denied for user)、其他无关错误——出现则用例失败
129+ boolean failed = false;
130+ try {
131+ try (Statement stOld = conn1.createStatement();
132+ ResultSet rsOld = stOld.executeQuery("SELECT 1")) {
133+ rsOld.next();
134+ }
135+ } catch (SQLException e) {
136+ String sqlState = e.getSQLState() == null ? "" : e.getSQLState();
137+ String msg = e.getMessage() == null ? "" : e.getMessage();
138+ boolean isAuthError = "28000".equals(sqlState)
139+ && msg.toLowerCase().contains("access denied for user");
140+ assertFalse("捕获到认证类错误而非连接失效错误,用例失败: " + msg, isAuthError);
141+ 
142+ boolean isConnectionError = sqlState.startsWith("08")
143+ || msg.toLowerCase().contains("communications link failure")
144+ || msg.toLowerCase().contains("connection")
145+ || msg.toLowerCase().contains("closed");
146+ assertTrue("旧连接抛出的异常不是预期的连接类错误: " + msg
147+ + " (SQLState=" + sqlState + ")", isConnectionError);
148+ 
149+ failed = true;
150+ System.out.println("[验证] 旧连接执行 SQL 抛出连接类异常,已失效: " + msg
151+ + " (SQLState=" + sqlState + ")");
152+ }
153+ 
154+ // 5. 归旧连接:Proxool 对底层已死亡的连接,close() 可能抛异常且不归还槽位,
155+ // 两种情况都重试关闭一次,保证后续借出不受干扰
156+ assertTrue("旧连接并未失效", failed);
157+ try {
158+ conn1.close();
159+ } catch (SQLException first) {
160+ try { conn1.close(); } catch (SQLException ignored) {}
161+ }
162+ 
163+ // 6. 再次借出:见方法注释,无论 close 是否成功归还,
164+ // 本次借到的都只能是新建的物理连接
165+ long pid2;
166+ try (Connection conn2 = DriverManager.getConnection("proxool." + VALIDATION_POOL);
167+ Statement st = conn2.createStatement()) {
168+ try (ResultSet rs = st.executeQuery("SELECT pg_backend_pid()")) {
169+ assertTrue(rs.next());
170+ pid2 = rs.getLong(1);
171+ }
172+ try (ResultSet rs = st.executeQuery("SELECT 1")) {
173+ assertTrue("新连接应可正常执行 SQL", rs.next());
174+ }
175+ }
176+ System.out.println("[验证] 再次借出 PID2=" + pid2);
177+ assertNotEquals("旧物理连接已被终止且不可能被再次借出,"
178+ + "借到相同 PID 意味着池未重建", pid1, pid2);
179+ System.out.println("[验证] PID1=" + pid1 + " != PID2=" + pid2
180+ + ":旧连接已死亡且无法被再次借出(test-before-use 检活剔除,"
181+ + "或 close 失败卡在 active 槽位),本次借到的连接只能是新建物理连接,重建成立");
182+ 
183+ } finally {
184+ // 7. 删除隔离池,不留残留
185+ try {
186+ ProxoolFacade.removeConnectionPool(VALIDATION_POOL);
187+ System.out.println("[INFO] 隔离验证池已关闭");
188+ } catch (Exception e) {
189+ System.err.println("[WARN] 关闭隔离验证池失败: " + e.getMessage());
190+ }
191+ }
192+ 
193+ System.out.println("========== test02_失效连接被淘汰重建 通过 ==========");
194+ }
195+ 
196+ @Test
197+ public void test03_池状态快照反映连接健康() throws Exception {
198+ System.out.println("========== test03_池状态快照反映连接健康 开始 ==========");
199+ 
200+ SnapshotIF snapshot = ProxoolFacade.getSnapshot("openGaussPool", true);
201+ assertNotNull(snapshot);
202+ 
203+ int active = snapshot.getActiveConnectionCount();
204+ int available = snapshot.getAvailableConnectionCount();
205+ int max = snapshot.getMaximumConnectionCount();
206+ 
207+ System.out.println("[验证] active=" + active + ", available=" + available + ", max=" + max);
208+ assertTrue("max 应 > 0", max > 0);
209+ assertTrue("active 应 >= 0", active >= 0);
210+ assertTrue("available 应 >= 0", available >= 0);
211+ 
212+ System.out.println("========== test03_池状态快照反映连接健康 通过 ==========");
213+ }
214+ 
215+ @AfterClass
216+ public static void cleanup() {
217+ try {
218+ ProxoolFacade.removeConnectionPool("openGaussPool");
219+ System.out.println("[INFO] 连接池已关闭");
220+ } catch (Exception e) {
221+ System.err.println("[WARN] 关闭连接池失败: " + e.getMessage());
222+ }
223+ }
224+}
@@ -0,0 +1,146 @@
1+import org.junit.*;
2+import org.logicalcobwebs.proxool.ProxoolFacade;
3+import org.logicalcobwebs.proxool.configuration.PropertyConfigurator;
4+ 
5+import java.io.InputStream;
6+import java.math.BigDecimal;
7+import java.sql.*;
8+import java.util.Properties;
9+ 
10+import static org.junit.Assert.*;
11+ 
12+public class ProxoolDataTypeTest {
13+ 
14+ private static final String TABLE = "test_b_db.public.test_data_type";
15+ 
16+ @BeforeClass
17+ public static void init() throws Exception {
18+ InputStream in = ProxoolDataTypeTest.class.getClassLoader()
19+ .getResourceAsStream("proxool.properties");
20+ assertNotNull("proxool.properties 配置文件未找到", in);
21+ Properties props = new Properties();
22+ props.load(in);
23+ PropertyConfigurator.configure(props);
24+ Class.forName("org.logicalcobwebs.proxool.ProxoolDriver");
25+ System.out.println("[INFO] Proxool 连接池初始化成功");
26+ }
27+ 
28+ private Connection getConnection() throws SQLException {
29+ return DriverManager.getConnection("proxool.openGaussPool");
30+ }
31+ 
32+ @Before
33+ public void prepareTable() throws Exception {
34+ try (Connection conn = getConnection();
35+ Statement stmt = conn.createStatement()) {
36+ stmt.execute("DROP TABLE IF EXISTS " + TABLE);
37+ stmt.execute("CREATE TABLE " + TABLE + " (" +
38+ "id INT PRIMARY KEY, " +
39+ "name VARCHAR(100), " +
40+ "age INT, " +
41+ "score DECIMAL(10,2), " +
42+ "created_at DATETIME, " +
43+ "remark TEXT)");
44+ }
45+ }
46+ 
47+ @Test
48+ public void test01_中文存储与查询() throws Exception {
49+ System.out.println("========== test01_中文存储与查询 开始 ==========");
50+ 
51+ String chineseName = "张三丰";
52+ try (Connection conn = getConnection();
53+ PreparedStatement ps = conn.prepareStatement(
54+ "INSERT INTO " + TABLE + " (id, name) VALUES (?, ?)")) {
55+ ps.setInt(1, 1);
56+ ps.setString(2, chineseName);
57+ ps.executeUpdate();
58+ System.out.println("[INFO] 已插入中文: " + chineseName);
59+ }
60+ 
61+ try (Connection conn = getConnection();
62+ Statement stmt = conn.createStatement();
63+ ResultSet rs = stmt.executeQuery(
64+ "SELECT name FROM " + TABLE + " WHERE id=1")) {
65+ assertTrue(rs.next());
66+ String result = rs.getString("name");
67+ assertEquals("中文应正确存取", chineseName, result);
68+ System.out.println("[验证] 查询结果: " + result);
69+ }
70+ 
71+ System.out.println("========== test01_中文存储与查询 通过 ==========");
72+ }
73+ 
74+ @Test
75+ public void test02_emoji存储与查询() throws Exception {
76+ System.out.println("========== test02_emoji存储与查询 开始 ==========");
77+ 
78+ String emojiName = "Hello😀🎉🚀";
79+ try (Connection conn = getConnection();
80+ PreparedStatement ps = conn.prepareStatement(
81+ "INSERT INTO " + TABLE + " (id, name) VALUES (?, ?)")) {
82+ ps.setInt(1, 2);
83+ ps.setString(2, emojiName);
84+ ps.executeUpdate();
85+ System.out.println("[INFO] 已插入 emoji: " + emojiName);
86+ }
87+ 
88+ try (Connection conn = getConnection();
89+ Statement stmt = conn.createStatement();
90+ ResultSet rs = stmt.executeQuery(
91+ "SELECT name FROM " + TABLE + " WHERE id=2")) {
92+ assertTrue(rs.next());
93+ String result = rs.getString("name");
94+ assertEquals("emoji 应正确存取", emojiName, result);
95+ System.out.println("[验证] 查询结果: " + result);
96+ }
97+ 
98+ System.out.println("========== test02_emoji存储与查询 通过 ==========");
99+ }
100+ 
101+ @Test
102+ public void test03_常用类型与NULL() throws Exception {
103+ System.out.println("========== test03_常用类型与NULL 开始 ==========");
104+ 
105+ try (Connection conn = getConnection();
106+ PreparedStatement ps = conn.prepareStatement(
107+ "INSERT INTO " + TABLE + " (id, name, age, score, created_at, remark) " +
108+ "VALUES (?, ?, ?, ?, ?, ?)")) {
109+ ps.setInt(1, 3);
110+ ps.setString(2, "Normal");
111+ ps.setInt(3, 25);
112+ ps.setBigDecimal(4, new BigDecimal("95.50"));
113+ ps.setTimestamp(5, Timestamp.valueOf("2026-10-01 12:00:00"));
114+ ps.setNull(6, Types.VARCHAR);
115+ ps.executeUpdate();
116+ System.out.println("[INFO] 已插入含 NULL 的完整行");
117+ }
118+ 
119+ try (Connection conn = getConnection();
120+ Statement stmt = conn.createStatement();
121+ ResultSet rs = stmt.executeQuery(
122+ "SELECT age, score, created_at, remark FROM " + TABLE + " WHERE id=3")) {
123+ assertTrue(rs.next());
124+ assertEquals(25, rs.getInt("age"));
125+ assertEquals(new BigDecimal("95.50"), rs.getBigDecimal("score"));
126+ assertNotNull(rs.getTimestamp("created_at"));
127+ assertNull(rs.getString("remark"));
128+ System.out.println("[验证] age=" + rs.getInt("age")
129+ + ", score=" + rs.getBigDecimal("score")
130+ + ", created_at=" + rs.getTimestamp("created_at")
131+ + ", remark=" + rs.getString("remark"));
132+ }
133+ 
134+ System.out.println("========== test03_常用类型与NULL 通过 ==========");
135+ }
136+ 
137+ @AfterClass
138+ public static void cleanup() {
139+ try {
140+ ProxoolFacade.removeConnectionPool("openGaussPool");
141+ System.out.println("[INFO] 连接池已关闭");
142+ } catch (Exception e) {
143+ System.err.println("[WARN] 关闭连接池失败: " + e.getMessage());
144+ }
145+ }
146+}
@@ -0,0 +1,347 @@
1+import org.junit.*;
2+import org.junit.runners.MethodSorters;
3+import org.logicalcobwebs.proxool.ProxoolFacade;
4+import org.logicalcobwebs.proxool.admin.SnapshotIF;
5+import org.logicalcobwebs.proxool.configuration.PropertyConfigurator;
6+ 
7+import java.io.InputStream;
8+import java.sql.*;
9+import java.util.Arrays;
10+import java.util.Properties;
11+ 
12+import static org.junit.Assert.*;
13+ 
14+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
15+public class ProxoolJUnitTest {
16+ 
17+ private static final String TABLE = "test_b_db.public.test_proxool";
18+ 
19+ /** 初始化 Proxool 连接池 */
20+ @BeforeClass
21+ public static void init() throws Exception {
22+ InputStream in = ProxoolJUnitTest.class.getClassLoader()
23+ .getResourceAsStream("proxool.properties");
24+ assertNotNull("proxool.properties 配置文件未找到", in);
25+ 
26+ Properties props = new Properties();
27+ props.load(in);
28+ PropertyConfigurator.configure(props);
29+ Class.forName("org.logicalcobwebs.proxool.ProxoolDriver");
30+ 
31+ System.out.println("[INFO] Proxool 连接池初始化成功");
32+ }
33+ 
34+ /** 从连接池获取连接 */
35+ private Connection getConnection() throws SQLException {
36+ return DriverManager.getConnection("proxool.openGaussPool");
37+ }
38+ 
39+ @Test
40+ public void test01_连接池初始化() {
41+ System.out.println("========== test01_连接池初始化 开始 ==========");
42+ 
43+ String[] aliases = ProxoolFacade.getAliases();
44+ System.out.println("[INFO] 连接池别名列表: " + Arrays.toString(aliases));
45+ 
46+ assertTrue("连接池 openGaussPool 未注册",
47+ Arrays.asList(aliases).contains("openGaussPool"));
48+ 
49+ System.out.println("========== test01_连接池初始化 通过 ==========");
50+ }
51+ 
52+ @Test
53+ public void test02_数据库连接() throws Exception {
54+ System.out.println("========== test02_数据库连接 开始 ==========");
55+ 
56+ try (Connection conn = getConnection()) {
57+ assertNotNull("获取的连接为 null", conn);
58+ assertFalse("连接已关闭", conn.isClosed());
59+ System.out.println("[INFO] 连接对象: " + conn);
60+ System.out.println("[INFO] 连接是否关闭: " + conn.isClosed());
61+ }
62+ 
63+ System.out.println("========== test02_数据库连接 通过 ==========");
64+ }
65+ 
66+ @Test
67+ public void test03_建表() throws Exception {
68+ System.out.println("========== test03_建表 开始 ==========");
69+ 
70+ try (Connection conn = getConnection();
71+ Statement stmt = conn.createStatement()) {
72+ 
73+ stmt.execute("DROP TABLE IF EXISTS " + TABLE);
74+ stmt.execute("CREATE TABLE " + TABLE +
75+ " (id INT PRIMARY KEY, name VARCHAR(50))");
76+ System.out.println("[INFO] 已执行 CREATE TABLE " + TABLE);
77+ 
78+ ResultSet rs = stmt.executeQuery(
79+ "SELECT table_name FROM information_schema.tables " +
80+ "WHERE table_schema='public' AND table_name='test_proxool'");
81+ assertTrue("表 test_proxool 未创建成功", rs.next());
82+ String tableName = rs.getString("table_name");
83+ assertEquals("test_proxool", tableName);
84+ System.out.println("[验证] 数据库端查询到表名: " + tableName);
85+ }
86+ 
87+ System.out.println("========== test03_建表 通过 ==========");
88+ }
89+ 
90+ @Test
91+ public void test04_插入数据() throws Exception {
92+ System.out.println("========== test04_插入数据 开始 ==========");
93+ 
94+ try (Connection conn = getConnection();
95+ Statement stmt = conn.createStatement()) {
96+ 
97+ int affected = stmt.executeUpdate(
98+ "INSERT INTO " + TABLE + " VALUES (1, 'Alice'), (2, 'Bob')");
99+ assertEquals("插入行数不符", 2, affected);
100+ System.out.println("[INFO] 插入影响行数: " + affected);
101+ 
102+ ResultSet rs = stmt.executeQuery(
103+ "SELECT * FROM " + TABLE + " WHERE id IN (1,2) ORDER BY id");
104+ while (rs.next()) {
105+ System.out.println("[验证] id=" + rs.getInt("id") +
106+ ", name=" + rs.getString("name"));
107+ }
108+ }
109+ 
110+ System.out.println("========== test04_插入数据 通过 ==========");
111+ }
112+ 
113+ @Test
114+ public void test05_查询数据() throws Exception {
115+ System.out.println("========== test05_查询数据 开始 ==========");
116+ 
117+ try (Connection conn = getConnection();
118+ Statement stmt = conn.createStatement()) {
119+ 
120+ ResultSet rs = stmt.executeQuery(
121+ "SELECT * FROM " + TABLE + " ORDER BY id");
122+ 
123+ assertTrue("未查到第一条数据", rs.next());
124+ int id1 = rs.getInt("id");
125+ String name1 = rs.getString("name");
126+ assertEquals(1, id1);
127+ assertEquals("Alice", name1);
128+ System.out.println("[验证] 第1条: id=" + id1 + ", name=" + name1);
129+ 
130+ assertTrue("未查到第二条数据", rs.next());
131+ int id2 = rs.getInt("id");
132+ String name2 = rs.getString("name");
133+ assertEquals(2, id2);
134+ assertEquals("Bob", name2);
135+ System.out.println("[验证] 第2条: id=" + id2 + ", name=" + name2);
136+ 
137+ assertFalse("记录数超出预期", rs.next());
138+ }
139+ 
140+ System.out.println("========== test05_查询数据 通过 ==========");
141+ }
142+ 
143+ @Test
144+ public void test06_更新数据() throws Exception {
145+ System.out.println("========== test06_更新数据 开始 ==========");
146+ 
147+ try (Connection conn = getConnection();
148+ Statement stmt = conn.createStatement()) {
149+ 
150+ int affected = stmt.executeUpdate(
151+ "UPDATE " + TABLE + " SET name='Alice2' WHERE id=1");
152+ assertEquals("更新行数不符", 1, affected);
153+ System.out.println("[INFO] 更新影响行数: " + affected);
154+ 
155+ ResultSet rs = stmt.executeQuery(
156+ "SELECT name FROM " + TABLE + " WHERE id=1");
157+ assertTrue(rs.next());
158+ String name = rs.getString("name");
159+ assertEquals("Alice2", name);
160+ System.out.println("[验证] id=1 的 name 已更新为: " + name);
161+ }
162+ 
163+ System.out.println("========== test06_更新数据 通过 ==========");
164+ }
165+ 
166+ @Test
167+ public void test07_事务回滚() throws Exception {
168+ System.out.println("========== test07_事务回滚 开始 ==========");
169+ 
170+ try (Connection conn = getConnection()) {
171+ conn.setAutoCommit(false);
172+ try (Statement stmt = conn.createStatement()) {
173+ stmt.executeUpdate(
174+ "UPDATE " + TABLE + " SET name='ROLLBACK_TEST' WHERE id=2");
175+ System.out.println("[INFO] 事务内将 id=2 改为 ROLLBACK_TEST");
176+ conn.rollback();
177+ System.out.println("[INFO] 已执行 rollback");
178+ } finally {
179+ conn.setAutoCommit(true);
180+ }
181+ 
182+ try (Statement stmt = conn.createStatement();
183+ ResultSet rs = stmt.executeQuery(
184+ "SELECT name FROM " + TABLE + " WHERE id=2")) {
185+ assertTrue(rs.next());
186+ String name = rs.getString("name");
187+ assertEquals("事务回滚失败", "Bob", name);
188+ System.out.println("[验证] 回滚后 id=2 的 name 仍为: " + name);
189+ }
190+ }
191+ 
192+ System.out.println("========== test07_事务回滚 通过 ==========");
193+ }
194+ 
195+ @Test
196+ public void test08_事务提交() throws Exception {
197+ System.out.println("========== test08_事务提交 开始 ==========");
198+ 
199+ try (Connection conn = getConnection()) {
200+ conn.setAutoCommit(false);
201+ try (Statement stmt = conn.createStatement()) {
202+ stmt.executeUpdate(
203+ "UPDATE " + TABLE + " SET name='Bob2' WHERE id=2");
204+ System.out.println("[INFO] 事务内将 id=2 改为 Bob2");
205+ conn.commit();
206+ System.out.println("[INFO] 已执行 commit");
207+ } finally {
208+ conn.setAutoCommit(true);
209+ }
210+ 
211+ try (Statement stmt = conn.createStatement();
212+ ResultSet rs = stmt.executeQuery(
213+ "SELECT name FROM " + TABLE + " WHERE id=2")) {
214+ assertTrue(rs.next());
215+ String name = rs.getString("name");
216+ assertEquals("事务提交失败", "Bob2", name);
217+ System.out.println("[验证] 提交后 id=2 的 name 已变为: " + name);
218+ }
219+ 
220+ try (Statement stmt = conn.createStatement()) {
221+ stmt.executeUpdate(
222+ "UPDATE " + TABLE + " SET name='Bob' WHERE id=2");
223+ System.out.println("[INFO] 已恢复 id=2 的 name 为 Bob");
224+ }
225+ }
226+ 
227+ System.out.println("========== test08_事务提交 通过 ==========");
228+ }
229+ 
230+ @Test
231+ public void test09_批量插入() throws Exception {
232+ System.out.println("========== test09_批量插入 开始 ==========");
233+ 
234+ try (Connection conn = getConnection()) {
235+ conn.setAutoCommit(false);
236+ try (PreparedStatement ps = conn.prepareStatement(
237+ "INSERT INTO " + TABLE + " VALUES (?, ?)")) {
238+ for (int i = 10; i < 20; i++) {
239+ ps.setInt(1, i);
240+ ps.setString(2, "User" + i);
241+ ps.addBatch();
242+ }
243+ int[] result = ps.executeBatch();
244+ assertEquals("批量插入行数不符", 10, result.length);
245+ System.out.println("[INFO] 批量插入行数: " + result.length);
246+ conn.commit();
247+ } finally {
248+ conn.setAutoCommit(true);
249+ }
250+ 
251+ try (Statement stmt = conn.createStatement();
252+ ResultSet rs = stmt.executeQuery(
253+ "SELECT COUNT(*) FROM " + TABLE +
254+ " WHERE id BETWEEN 10 AND 19")) {
255+ assertTrue(rs.next());
256+ int count = rs.getInt(1);
257+ assertEquals(10, count);
258+ System.out.println("[验证] 数据库端 id 10~19 记录数: " + count);
259+ }
260+ }
261+ 
262+ System.out.println("========== test09_批量插入 通过 ==========");
263+ }
264+ 
265+ @Test
266+ public void test10_记录总数() throws Exception {
267+ System.out.println("========== test10_记录总数 开始 ==========");
268+ 
269+ try (Connection conn = getConnection();
270+ Statement stmt = conn.createStatement();
271+ ResultSet rs = stmt.executeQuery(
272+ "SELECT COUNT(*) FROM " + TABLE)) {
273+ assertTrue(rs.next());
274+ int total = rs.getInt(1);
275+ assertEquals("总记录数不符", 12, total);
276+ System.out.println("[验证] 数据库端记录总数: " + total);
277+ }
278+ 
279+ System.out.println("========== test10_记录总数 通过 ==========");
280+ }
281+ 
282+ @Test
283+ public void test11_连接池监控() throws Exception {
284+ System.out.println("========== test11_连接池监控 开始 ==========");
285+ 
286+ try (Connection conn = getConnection()) {
287+ SnapshotIF snapshot = ProxoolFacade.getSnapshot("openGaussPool", true);
288+ assertNotNull("连接池快照为 null", snapshot);
289+ 
290+ int active = snapshot.getActiveConnectionCount();
291+ int available = snapshot.getAvailableConnectionCount();
292+ int max = snapshot.getMaximumConnectionCount();
293+ 
294+ System.out.println("[验证] 当前活跃连接数: " + active);
295+ System.out.println("[验证] 当前可用连接数: " + available);
296+ System.out.println("[验证] 最大连接数: " + max);
297+ 
298+ assertTrue("最大连接数应大于 0", max > 0);
299+ }
300+ 
301+ System.out.println("========== test11_连接池监控 通过 ==========");
302+ }
303+ 
304+ @Test
305+ public void test12_连接池关闭() throws Exception {
306+ System.out.println("========== test12_连接池关闭 开始 ==========");
307+ 
308+ boolean existsBefore = Arrays.asList(ProxoolFacade.getAliases())
309+ .contains("openGaussPool");
310+ assertTrue("关闭前连接池不存在", existsBefore);
311+ System.out.println("[验证] 关闭前,别名列表包含 openGaussPool: " + existsBefore);
312+ 
313+ ProxoolFacade.removeConnectionPool("openGaussPool");
314+ System.out.println("[INFO] 已调用 ProxoolFacade.removeConnectionPool(\"openGaussPool\")");
315+ 
316+ boolean existsAfter = Arrays.asList(ProxoolFacade.getAliases())
317+ .contains("openGaussPool");
318+ assertFalse("关闭后连接池仍存在于别名列表", existsAfter);
319+ System.out.println("[验证] 关闭后,别名列表包含 openGaussPool: " + existsAfter);
320+
321+ boolean failed = false;
322+ try {
323+ DriverManager.getConnection("proxool.openGaussPool");
324+ } catch (SQLException e) {
325+ failed = true;
326+ System.out.println("[验证] 关闭后获取连接抛出异常: " + e.getMessage());
327+ }
328+ assertTrue("连接池关闭后仍能获取连接", failed);
329+ 
330+ System.out.println("========== test12_连接池关闭 通过 ==========");
331+ }
332+ 
333+ /** 清理:确保连接池最终关闭(test12 已关则跳过) */
334+ @AfterClass
335+ public static void cleanup() {
336+ try {
337+ if (Arrays.asList(ProxoolFacade.getAliases()).contains("openGaussPool")) {
338+ ProxoolFacade.removeConnectionPool("openGaussPool");
339+ System.out.println("[INFO] cleanup: 连接池已关闭");
340+ } else {
341+ System.out.println("[INFO] cleanup: 连接池此前已关闭,无需重复操作");
342+ }
343+ } catch (Exception e) {
344+ System.err.println("[WARN] cleanup 关闭连接池失败: " + e.getMessage());
345+ }
346+ }
347+}
@@ -0,0 +1,86 @@
1+import org.junit.*;
2+import org.logicalcobwebs.proxool.ProxoolFacade;
3+import org.logicalcobwebs.proxool.admin.SnapshotIF;
4+import org.logicalcobwebs.proxool.configuration.PropertyConfigurator;
5+ 
6+import java.io.InputStream;
7+import java.sql.*;
8+import java.util.Properties;
9+ 
10+import static org.junit.Assert.*;
11+ 
12+public class ProxoolPoolExhaustTest {
13+ 
14+ @BeforeClass
15+ public static void init() throws Exception {
16+ InputStream in = ProxoolPoolExhaustTest.class.getClassLoader()
17+ .getResourceAsStream("proxool.properties");
18+ assertNotNull("proxool.properties 配置文件未找到", in);
19+ Properties props = new Properties();
20+ props.load(in);
21+ PropertyConfigurator.configure(props);
22+ Class.forName("org.logicalcobwebs.proxool.ProxoolDriver");
23+ System.out.println("[INFO] Proxool 连接池初始化成功");
24+ }
25+ 
26+ private Connection getConnection() throws SQLException {
27+ return DriverManager.getConnection("proxool.openGaussPool");
28+ }
29+ 
30+ @Test
31+ public void test01_满池等待超时() throws Exception {
32+ System.out.println("========== test01_满池等待超时 开始 ==========");
33+ 
34+ // 借出全部 10 个连接
35+ Connection[] conns = new Connection[10];
36+ try {
37+ for (int i = 0; i < 10; i++) {
38+ conns[i] = getConnection();
39+ }
40+ System.out.println("[INFO] 已借出全部 10 个连接,池满");
41+ 
42+ SnapshotIF snapshot = ProxoolFacade.getSnapshot("openGaussPool", true);
43+ int active = snapshot.getActiveConnectionCount();
44+ System.out.println("[验证] 池满时活跃连接数: " + active);
45+ assertTrue("池满时活跃连接数应 >= 10", active >= 10);
46+ 
47+ // 第 11 个请求应被拒绝或超时
48+ long start = System.currentTimeMillis();
49+ boolean gotExtra = false;
50+ try {
51+ Connection extra = getConnection();
52+ gotExtra = true;
53+ extra.close();
54+ } catch (SQLException e) {
55+ System.out.println("[验证] 第 11 个请求被拒绝: " + e.getMessage());
56+ }
57+ long cost = System.currentTimeMillis() - start;
58+ System.out.println("[验证] 第 11 个请求耗时: " + cost + " ms");
59+ assertFalse("池满后第 11 个请求不应成功", gotExtra);
60+ } finally {
61+ for (Connection c : conns) {
62+ if (c != null) c.close();
63+ }
64+ }
65+ 
66+ // 归还后应恢复正常
67+ try (Connection conn = getConnection();
68+ Statement stmt = conn.createStatement();
69+ ResultSet rs = stmt.executeQuery("SELECT 1")) {
70+ assertTrue(rs.next());
71+ System.out.println("[验证] 归还后再次获取连接成功");
72+ }
73+ 
74+ System.out.println("========== test01_满池等待超时 通过 ==========");
75+ }
76+ 
77+ @AfterClass
78+ public static void cleanup() {
79+ try {
80+ ProxoolFacade.removeConnectionPool("openGaussPool");
81+ System.out.println("[INFO] 连接池已关闭");
82+ } catch (Exception e) {
83+ System.err.println("[WARN] 关闭连接池失败: " + e.getMessage());
84+ }
85+ }
86+}
@@ -0,0 +1,301 @@
1+import org.junit.*;
2+import org.logicalcobwebs.proxool.ProxoolFacade;
3+import org.logicalcobwebs.proxool.admin.SnapshotIF;
4+import org.logicalcobwebs.proxool.configuration.PropertyConfigurator;
5+ 
6+import java.io.InputStream;
7+import java.lang.reflect.Method;
8+import java.sql.*;
9+import java.util.Properties;
10+import java.util.concurrent.CountDownLatch;
11+import java.util.concurrent.TimeUnit;
12+import java.util.concurrent.atomic.AtomicInteger;
13+ 
14+import static org.junit.Assert.*;
15+ 
16+public class ProxoolPoolParamTest {
17+ 
18+ private static final String TABLE = "test_b_db.public.test_proxool";
19+ 
20+ /** 独立维护验证池别名(仅 test06 使用,用后即删) */
21+ private static final String HOUSEKEEPING_POOL = "housekeepingPool";
22+ 
23+ @BeforeClass
24+ public static void init() throws Exception {
25+ InputStream in = ProxoolPoolParamTest.class.getClassLoader()
26+ .getResourceAsStream("proxool.properties");
27+ assertNotNull("proxool.properties 配置文件未找到", in);
28+ Properties props = new Properties();
29+ props.load(in);
30+ PropertyConfigurator.configure(props);
31+ Class.forName("org.logicalcobwebs.proxool.ProxoolDriver");
32+ System.out.println("[INFO] Proxool 连接池初始化成功");
33+ }
34+ 
35+ private Connection getConnection() throws SQLException {
36+ return DriverManager.getConnection("proxool.openGaussPool");
37+ }
38+ 
39+ @Test
40+ public void test01_最大连接数配置验证() throws Exception {
41+ System.out.println("========== test01_最大连接数配置验证 开始 ==========");
42+ 
43+ SnapshotIF snapshot = ProxoolFacade.getSnapshot("openGaussPool", true);
44+ int max = snapshot.getMaximumConnectionCount();
45+ assertEquals("最大连接数应为 10", 10, max);
46+ System.out.println("[验证] 最大连接数: " + max);
47+ 
48+ System.out.println("========== test01_最大连接数配置验证 通过 ==========");
49+ }
50+ 
51+ @Test
52+ public void test02_借出连接达到上限() throws Exception {
53+ System.out.println("========== test02_借出连接达到上限 开始 ==========");
54+ 
55+ Connection[] conns = new Connection[10];
56+ try {
57+ for (int i = 0; i < 10; i++) {
58+ conns[i] = getConnection();
59+ }
60+ SnapshotIF snapshot = ProxoolFacade.getSnapshot("openGaussPool", true);
61+ int active = snapshot.getActiveConnectionCount();
62+ System.out.println("[验证] 借出 10 个后活跃连接数: " + active);
63+ assertTrue("活跃连接数应 >= 10", active >= 10);
64+ } finally {
65+ for (Connection c : conns) {
66+ if (c != null) c.close();
67+ }
68+ }
69+ 
70+ System.out.println("========== test02_借出连接达到上限 通过 ==========");
71+ }
72+ 
73+ @Test
74+ public void test03_归还后连接可复用() throws Exception {
75+ System.out.println("========== test03_归还后连接可复用 开始 ==========");
76+ 
77+ Connection c1 = getConnection();
78+ c1.close();
79+ 
80+ SnapshotIF snapshot = ProxoolFacade.getSnapshot("openGaussPool", true);
81+ int available = snapshot.getAvailableConnectionCount();
82+ System.out.println("[验证] 归还后可用连接数: " + available);
83+ assertTrue("可用连接数应 > 0", available > 0);
84+ 
85+ // 立即再借一次,不应超时
86+ long start = System.currentTimeMillis();
87+ Connection c2 = getConnection();
88+ long cost = System.currentTimeMillis() - start;
89+ System.out.println("[验证] 再次获取连接耗时: " + cost + " ms");
90+ assertTrue("再次获取连接耗时应 < 2000ms", cost < 2000);
91+ c2.close();
92+ 
93+ System.out.println("========== test03_归还后连接可复用 通过 ==========");
94+ }
95+ 
96+ /**
97+ * 配置映射核对 + 借出归还验证。
98+ * 注意:本用例【不】声称验证了维护线程的补足行为——
99+ * 维护补足行为由 test06 用独立池真实制造低于目标数状态来验证。
100+ */
101+ @Test
102+ public void test04_连接数配置核对与借出归还() throws Exception {
103+ System.out.println("========== test04_连接数配置核对与借出归还 开始 ==========");
104+ 
105+ // 1. 反射读取配置,逐项核对配置文件到运行实例的映射
106+ Method getDefMethod = ProxoolFacade.class.getMethod("getConnectionPoolDefinition", String.class);
107+ getDefMethod.setAccessible(true);
108+ Object def = getDefMethod.invoke(null, "openGaussPool");
109+ Class<?> defClass = def.getClass();
110+ 
111+ Method getMaxMethod = defClass.getMethod("getMaximumConnectionCount");
112+ getMaxMethod.setAccessible(true);
113+ int maximum = (int) getMaxMethod.invoke(def);
114+ 
115+ Method getMinMethod = defClass.getMethod("getMinimumConnectionCount");
116+ getMinMethod.setAccessible(true);
117+ int minimum = (int) getMinMethod.invoke(def);
118+ 
119+ Method getPrototypeMethod = defClass.getMethod("getPrototypeCount");
120+ getPrototypeMethod.setAccessible(true);
121+ int prototype = (int) getPrototypeMethod.invoke(def);
122+ 
123+ System.out.println("[验证] maximum=" + maximum + ", minimum=" + minimum
124+ + ", prototype=" + prototype);
125+ assertEquals(10, maximum);
126+ assertEquals(2, minimum);
127+ assertEquals(4, prototype);
128+ 
129+ // 2. 借出全部连接,任何一次借出失败都直接 fail
130+ Connection[] conns = new Connection[maximum];
131+ int borrowed = 0;
132+ try {
133+ for (int i = 0; i < maximum; i++) {
134+ try {
135+ conns[i] = getConnection();
136+ borrowed++;
137+ } catch (SQLException e) {
138+ fail("借出第 " + (i + 1) + " 个连接失败: " + e.getMessage());
139+ }
140+ }
141+ assertEquals("实际借出连接数应等于最大值", maximum, borrowed);
142+ 
143+ // 3. 持有期间状态检查
144+ SnapshotIF snapshot = ProxoolFacade.getSnapshot("openGaussPool", true);
145+ assertEquals("活跃连接数不符", maximum, snapshot.getActiveConnectionCount());
146+ assertTrue("可用连接数不应为负", snapshot.getAvailableConnectionCount() >= 0);
147+ 
148+ } finally {
149+ for (Connection c : conns) { if (c != null) c.close(); }
150+ }
151+ 
152+ // 4. 归还后池状态回归检查(仅作为借还完整性检查,不作为维护行为证据)
153+ SnapshotIF after = ProxoolFacade.getSnapshot("openGaussPool", true);
154+ System.out.println("[验证] 归还后 active=" + after.getActiveConnectionCount()
155+ + ", available=" + after.getAvailableConnectionCount());
156+ assertEquals("归还后活跃连接数应回到 0", 0, after.getActiveConnectionCount());
157+ assertTrue("归还后应有可用连接", after.getAvailableConnectionCount() > 0);
158+ 
159+ System.out.println("========== test04_连接数配置核对与借出归还 通过 ==========");
160+ }
161+ 
162+ @Test
163+ public void test05_并发获取连接() throws Exception {
164+ System.out.println("========== test05_并发获取连接 开始 ==========");
165+ 
166+ int threadCount = 8;
167+ CountDownLatch latch = new CountDownLatch(threadCount);
168+ AtomicInteger successCount = new AtomicInteger(0);
169+ AtomicInteger failCount = new AtomicInteger(0);
170+ 
171+ for (int i = 0; i < threadCount; i++) {
172+ new Thread(() -> {
173+ try (Connection conn = getConnection();
174+ Statement stmt = conn.createStatement();
175+ ResultSet rs = stmt.executeQuery("SELECT 1")) {
176+ if (rs.next()) {
177+ successCount.incrementAndGet();
178+ }
179+ } catch (Exception e) {
180+ failCount.incrementAndGet();
181+ } finally {
182+ latch.countDown();
183+ }
184+ }).start();
185+ }
186+ 
187+ boolean done = latch.await(30, TimeUnit.SECONDS);
188+ assertTrue("并发执行超时", done);
189+ System.out.println("[验证] 成功数: " + successCount.get() + ", 失败数: " + failCount.get());
190+ assertEquals("应有 8 个线程成功", 8, successCount.get());
191+ 
192+ System.out.println("========== test05_并发获取连接 通过 ==========");
193+ }
194+ 
195+ /**
196+ * 维护线程补足验证(真实验证,替代原先"轮询 available>=minimum"的无效断言)。
197+ *
198+ * 原理:
199+ * 1. 注册一个独立的小池 housekeepingPool(minimum=3, maximum=5, prototype=0,
200+ * house-keeping-sleep-time=2000,维护周期远短于主池),不影响主池参数;
201+ * 2. 有界轮询等待池稳定(available >= minimum=3,此时 Prototyper 已建满目标数);
202+ * 3. 借出一条"杀手"连接,用它对数据库执行 pg_terminate_backend,
203+ * 杀掉除自己以外的全部 dbcp_test 后端会话——池中空闲物理连接全部真实死亡,
204+ * 此时池仍以为这些连接可用(available 表面 >= 3,但底层已断);
205+ * 4. 归还杀手连接后有界轮询:维护线程按 house-keeping-test-sql 周期检活,
206+ * 发现死连接并剔除,Prototyper 重新建满,available 恢复到 >= minimum。
207+ * 只有维护线程真实工作,这个恢复才会发生。
208+ */
209+ @Test
210+ public void test06_维护线程补足验证() throws Exception {
211+ System.out.println("========== test06_维护线程补足验证 开始 ==========");
212+ 
213+ // 1. 注册独立验证池(维护周期 2 秒,加快验证速度)
214+ Properties hp = new Properties();
215+ hp.setProperty("jdbc-3.proxool.alias", HOUSEKEEPING_POOL);
216+ hp.setProperty("jdbc-3.proxool.driver-class", "com.mysql.cj.jdbc.Driver");
217+ hp.setProperty("jdbc-3.proxool.driver-url",
218+ "jdbc:mysql://192.168.198.135:3308/test_b_db?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC&characterEncoding=utf-8");
219+ hp.setProperty("jdbc-3.user", "dbcp_test");
220+ hp.setProperty("jdbc-3.password", "Test@1234");
221+ hp.setProperty("jdbc-3.proxool.maximum-connection-count", "5");
222+ hp.setProperty("jdbc-3.proxool.minimum-connection-count", "3");
223+ hp.setProperty("jdbc-3.proxool.prototype-count", "0");
224+ hp.setProperty("jdbc-3.proxool.house-keeping-sleep-time", "2000");
225+ hp.setProperty("jdbc-3.proxool.house-keeping-test-sql", "SELECT 1");
226+ hp.setProperty("jdbc-3.proxool.test-before-use", "true");
227+ PropertyConfigurator.configure(hp);
228+ System.out.println("[INFO] 独立验证池 " + HOUSEKEEPING_POOL
229+ + " 已注册 (minimum=3, prototype=0, 维护周期=2s)");
230+ 
231+ try {
232+ // 2. 有界轮询等待池稳定:available >= minimum(3)
233+ int stable = -1;
234+ for (int i = 0; i < 15; i++) {
235+ Thread.sleep(1000);
236+ SnapshotIF s = ProxoolFacade.getSnapshot(HOUSEKEEPING_POOL, true);
237+ stable = s.getAvailableConnectionCount();
238+ if (stable >= 3) break;
239+ }
240+ System.out.println("[验证] 池稳定后 available=" + stable);
241+ assertTrue("独立池未在 15 秒内建满最小连接数(3)", stable >= 3);
242+ 
243+ // 3. 借出杀手连接,杀掉其余全部 dbcp_test 后端会话
244+ long killed = 0;
245+ try (Connection killer = DriverManager.getConnection("proxool." + HOUSEKEEPING_POOL);
246+ Statement st = killer.createStatement();
247+ ResultSet rs = st.executeQuery(
248+ "SELECT pg_terminate_backend(pid) FROM pg_stat_activity " +
L
Lliyangx2 days ago

[P1] 这里杀连接的范围太大了

WHERE 只按用户名筛选,会把这个账号的其他会话也杀掉,包括主连接池和人工客户端的连接。新建一个 housekeepingPool,并不能限制这条 SQL 的作用范围。我在本地替身环境里同时建了两个池,跑完这个用例后,另一个池里持有的连接也断了。

请先记下本次隔离池中要终止的连接 PID,再由池外管理连接逐个终止这些 PID。顺便加个检查,确认其他池的连接还能正常查询。

likedislike
249+ "WHERE usename = 'dbcp_test' AND pid <> pg_backend_pid()")) {
250+ while (rs.next()) {
251+ if (rs.getBoolean(1)) killed++;
252+ }
253+ }
254+ System.out.println("[验证] 已真实终止 " + killed + " 个池内空闲物理连接");
255+ 
256+ SnapshotIF afterKill = ProxoolFacade.getSnapshot(HOUSEKEEPING_POOL, true);
257+ System.out.println("[INFO] 杀连接后池表面状态: available="
258+ + afterKill.getAvailableConnectionCount()
259+ + "(此时底层连接已死,池尚未感知)");
260+ assertTrue("应至少成功终止 1 个空闲物理连接", killed >= 1);
261+ 
262+ // 4. 有界轮询等待维护线程检活 + Prototyper 补足:available 恢复 >= 3
263+ // 杀掉的连接只有在 HouseKeeper 周期检活后才会被剔除并重建,
264+ // 因此 available 恢复到 3 以上即为维护线程真实工作的直接证据。
265+ long start = System.currentTimeMillis();
266+ int recovered = -1;
267+ for (int i = 0; i < 30; i++) {
268+ Thread.sleep(1000);
269+ SnapshotIF s = ProxoolFacade.getSnapshot(HOUSEKEEPING_POOL, true);
270+ recovered = s.getAvailableConnectionCount();
271+ if (recovered >= 3) break;
272+ }
273+ long cost = System.currentTimeMillis() - start;
274+ System.out.println("[验证] 维护线程补足后 available=" + recovered
275+ + ",恢复耗时 " + cost + " ms");
276+ assertTrue("维护线程未在 30 秒内将连接补足到 minimum(3),补足行为未生效",
277+ recovered >= 3);
278+ 
279+ } finally {
280+ // 5. 无论成败,删除独立验证池,不留残留
281+ try {
282+ ProxoolFacade.removeConnectionPool(HOUSEKEEPING_POOL);
283+ System.out.println("[INFO] 独立验证池已关闭");
284+ } catch (Exception e) {
285+ System.err.println("[WARN] 关闭独立验证池失败: " + e.getMessage());
286+ }
287+ }
288+ 
289+ System.out.println("========== test06_维护线程补足验证 通过 ==========");
290+ }
291+ 
292+ @AfterClass
293+ public static void cleanup() {
294+ try {
295+ ProxoolFacade.removeConnectionPool("openGaussPool");
296+ System.out.println("[INFO] 连接池已关闭");
297+ } catch (Exception e) {
298+ System.err.println("[WARN] 关闭连接池失败: " + e.getMessage());
299+ }
300+ }
301+}
@@ -0,0 +1,381 @@
1+import org.junit.*;
2+import org.logicalcobwebs.proxool.ProxoolFacade;
3+import org.logicalcobwebs.proxool.admin.SnapshotIF;
4+import org.logicalcobwebs.proxool.configuration.PropertyConfigurator;
5+ 
6+import java.io.InputStream;
7+import java.sql.*;
8+import java.util.Properties;
9+ 
10+import static org.junit.Assert.*;
11+ 
12+public class ProxoolSqlCategoryTest {
13+ 
14+ private static final String TABLE = "test_b_db.public.test_sql_category";
15+ 
16+ @BeforeClass
17+ public static void init() throws Exception {
18+ InputStream in = ProxoolSqlCategoryTest.class.getClassLoader()
19+ .getResourceAsStream("proxool.properties");
20+ assertNotNull("proxool.properties 配置文件未找到", in);
21+ Properties props = new Properties();
22+ props.load(in);
23+ PropertyConfigurator.configure(props);
24+ Class.forName("org.logicalcobwebs.proxool.ProxoolDriver");
25+ 
26+ // 单独注册 DCL 管理员连接池(只在 DCL 测试类里注册,不写入共享 proxool.properties)
27+ Properties adminProps = new Properties();
28+ adminProps.setProperty("jdbc-2.proxool.alias", "dclAdminPool");
29+ adminProps.setProperty("jdbc-2.proxool.driver-class", "com.mysql.cj.jdbc.Driver");
30+ adminProps.setProperty("jdbc-2.proxool.driver-url",
31+ "jdbc:mysql://192.168.198.135:3308/test_b_db?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC&characterEncoding=utf-8");
32+ adminProps.setProperty("jdbc-2.user", "dcl_admin");
33+ adminProps.setProperty("jdbc-2.password", "Test@1234");
34+ adminProps.setProperty("jdbc-2.proxool.maximum-connection-count", "5");
35+ adminProps.setProperty("jdbc-2.proxool.minimum-connection-count", "1");
36+ adminProps.setProperty("jdbc-2.proxool.prototype-count", "1");
37+ adminProps.setProperty("jdbc-2.proxool.house-keeping-test-sql", "SELECT 1");
38+ PropertyConfigurator.configure(adminProps);
39+ 
40+ System.out.println("[INFO] Proxool 连接池初始化成功");
41+ }
42+ 
43+ private Connection getConnection() throws SQLException {
44+ return DriverManager.getConnection("proxool.openGaussPool");
45+ }
46+ 
47+ private Connection getAdminConnection() throws SQLException {
48+ return DriverManager.getConnection("proxool.dclAdminPool");
49+ }
50+ 
51+ /**
52+ * 统一清理:角色、其拥有的对象、同名 schema、测试表。
53+ * 每一步单独 try,任何一步失败都不影响后续清理。
54+ */
55+ private static void cleanRoleAndTable(Statement adminStmt,
56+ String role, String table) {
57+ try { adminStmt.execute("REASSIGN OWNED BY " + role + " TO dcl_admin"); }
58+ catch (SQLException ignored) {}
59+ try { adminStmt.execute("DROP OWNED BY " + role); }
60+ catch (SQLException ignored) {}
61+ try { adminStmt.execute("DROP SCHEMA IF EXISTS " + role + " CASCADE"); }
62+ catch (SQLException ignored) {}
63+ try { adminStmt.execute("DROP TABLE IF EXISTS test_b_db.public." + table); }
64+ catch (SQLException ignored) {}
65+ try { adminStmt.execute("DROP USER IF EXISTS " + role); }
66+ catch (SQLException ignored) {}
67+ }
68+ 
69+ @Test
70+ public void test01_DDL_建表与改表与删表() throws Exception {
71+ System.out.println("========== test01_DDL_建表与改表与删表 开始 ==========");
72+ 
73+ try (Connection conn = getConnection();
74+ Statement stmt = conn.createStatement()) {
75+ 
76+ // DROP + CREATE
77+ stmt.execute("DROP TABLE IF EXISTS " + TABLE);
78+ stmt.execute("CREATE TABLE " + TABLE + " (id INT PRIMARY KEY, name VARCHAR(50))");
79+ System.out.println("[INFO] CREATE TABLE 成功");
80+ 
81+ // ALTER
82+ stmt.execute("ALTER TABLE " + TABLE + " ADD COLUMN age INT");
83+ System.out.println("[INFO] ALTER TABLE ADD COLUMN 成功");
84+ 
85+ // 验证列存在
86+ ResultSet rs = stmt.executeQuery(
87+ "SELECT column_name FROM information_schema.columns " +
88+ "WHERE table_name='test_sql_category' AND column_name='age'");
89+ assertTrue("age 列未添加成功", rs.next());
90+ System.out.println("[验证] age 列已存在");
91+ 
92+ // DROP
93+ stmt.execute("DROP TABLE " + TABLE);
94+ System.out.println("[INFO] DROP TABLE 成功");
95+ }
96+ 
97+ System.out.println("========== test01_DDL_建表与改表与删表 通过 ==========");
98+ }
99+ 
100+ @Test
101+ public void test02_DML_插入更新删除() throws Exception {
102+ System.out.println("========== test02_DML_插入更新删除 开始 ==========");
103+ 
104+ try (Connection conn = getConnection();
105+ Statement stmt = conn.createStatement()) {
106+ 
107+ stmt.execute("CREATE TABLE IF NOT EXISTS " + TABLE +
108+ " (id INT PRIMARY KEY, name VARCHAR(50))");
109+ stmt.execute("DELETE FROM " + TABLE);
110+ 
111+ // INSERT
112+ int insert = stmt.executeUpdate(
113+ "INSERT INTO " + TABLE + " VALUES (1,'Alice'),(2,'Bob')");
114+ assertEquals("INSERT 影响行数应为 2", 2, insert);
115+ System.out.println("[验证] INSERT 影响行数: " + insert);
116+ 
117+ // UPDATE
118+ int update = stmt.executeUpdate(
119+ "UPDATE " + TABLE + " SET name='Alice2' WHERE id=1");
120+ assertEquals("UPDATE 影响行数应为 1", 1, update);
121+ System.out.println("[验证] UPDATE 影响行数: " + update);
122+ 
123+ // DELETE
124+ int delete = stmt.executeUpdate("DELETE FROM " + TABLE + " WHERE id=2");
125+ assertEquals("DELETE 影响行数应为 1", 1, delete);
126+ System.out.println("[验证] DELETE 影响行数: " + delete);
127+ }
128+ 
129+ System.out.println("========== test02_DML_插入更新删除 通过 ==========");
130+ }
131+ 
132+ @Test
133+ public void test03_DQL_查询与排序与分页() throws Exception {
134+ System.out.println("========== test03_DQL_查询与排序与分页 开始 ==========");
135+ 
136+ try (Connection conn = getConnection();
137+ Statement stmt = conn.createStatement()) {
138+ 
139+ stmt.execute("CREATE TABLE IF NOT EXISTS " + TABLE +
140+ " (id INT PRIMARY KEY, name VARCHAR(50))");
141+ stmt.execute("DELETE FROM " + TABLE);
142+ stmt.execute("INSERT INTO " + TABLE + " VALUES (1,'A'),(2,'B'),(3,'C'),(4,'D')");
143+ 
144+ // 查询 + 排序
145+ ResultSet rs = stmt.executeQuery(
146+ "SELECT id, name FROM " + TABLE + " ORDER BY id DESC");
147+ assertTrue(rs.next());
148+ assertEquals(4, rs.getInt("id"));
149+ System.out.println("[验证] ORDER BY DESC 第一条 id=4");
150+ 
151+ // LIMIT 分页
152+ rs = stmt.executeQuery(
153+ "SELECT id FROM " + TABLE + " ORDER BY id LIMIT 2 OFFSET 1");
154+ assertTrue(rs.next());
155+ assertEquals(2, rs.getInt("id"));
156+ assertTrue(rs.next());
157+ assertEquals(3, rs.getInt("id"));
158+ System.out.println("[验证] LIMIT 2 OFFSET 1 返回 id=2,3");
159+ }
160+ 
161+ System.out.println("========== test03_DQL_查询与排序与分页 通过 ==========");
162+ }
163+ 
164+ @Test
165+ public void test04_DCL_授权与撤销() throws Exception {
166+ System.out.println("========== test04_DCL_授权与撤销 开始 ==========");
167+ 
168+ String testRole = "dcl_test_role";
169+ String testTable = "dcl_test_table";
170+ 
171+ try (Connection adminConn = getAdminConnection();
172+ Statement adminStmt = adminConn.createStatement()) {
173+ 
174+ // 0. 前置清理:确保角色和测试表的干净状态
175+ cleanRoleAndTable(adminStmt, testRole, testTable);
176+ System.out.println("[INFO] 前置清理完成");
177+ 
178+ try {
179+ // 1. 创建临时角色
180+ adminStmt.execute("CREATE USER " + testRole + " WITH PASSWORD 'TempDcl@123'");
181+ System.out.println("[INFO] 已创建临时角色: " + testRole);
182+ 
183+ // 2. 创建测试对象
184+ adminStmt.execute("CREATE TABLE test_b_db.public." + testTable
185+ + " (id INT PRIMARY KEY, name VARCHAR(50))");
186+ System.out.println("[INFO] 已创建测试表: " + testTable);
187+ 
188+ // 3. GRANT:授予 SELECT 权限
189+ adminStmt.execute("GRANT SELECT ON test_b_db.public." + testTable
190+ + " TO " + testRole);
191+ System.out.println("[INFO] 已 GRANT SELECT");
192+ 
193+ // 4. 断言:授权后权限存在
194+ ResultSet rs = adminStmt.executeQuery(
195+ "SELECT COUNT(*) FROM information_schema.role_table_grants "
196+ + "WHERE lower(grantee) = lower('" + testRole + "') "
197+ + "AND table_name='" + testTable + "' "
198+ + "AND privilege_type='SELECT'");
199+ assertTrue(rs.next());
200+ int grantCount = rs.getInt(1);
201+ System.out.println("[验证] 授权后匹配的权限记录数: " + grantCount);
202+ assertTrue("授权后应有 SELECT 权限记录", grantCount > 0);
203+ rs.close();
204+ 
205+ // 5. REVOKE:撤销 SELECT 权限
206+ adminStmt.execute("REVOKE SELECT ON test_b_db.public." + testTable
207+ + " FROM " + testRole);
208+ System.out.println("[INFO] 已 REVOKE SELECT");
209+ 
210+ // 6. 断言:撤销后权限消失
211+ rs = adminStmt.executeQuery(
212+ "SELECT COUNT(*) FROM information_schema.role_table_grants "
213+ + "WHERE lower(grantee) = lower('" + testRole + "') "
214+ + "AND table_name='" + testTable + "' "
215+ + "AND privilege_type='SELECT'");
216+ assertTrue(rs.next());
217+ int revokeCount = rs.getInt(1);
218+ System.out.println("[验证] 撤销后匹配的权限记录数: " + revokeCount);
219+ assertEquals("撤销后不应有 SELECT 权限记录", 0, revokeCount);
220+ rs.close();
221+ 
222+ } finally {
223+ // 7. 后置清理:与前置同一套逻辑,保证无残留
224+ cleanRoleAndTable(adminStmt, testRole, testTable);
225+ System.out.println("[INFO] 后置清理完成");
226+ }
227+ }
228+ 
229+ System.out.println("========== test04_DCL_授权与撤销 通过 ==========");
230+ }
231+ 
232+ @Test
233+ public void test04b_DCL_普通用户权限不足() throws Exception {
234+ System.out.println("========== test04b_DCL_普通用户权限不足 开始 ==========");
235+ String testRole = "dcl_neg_test_role";
236+ String testTable = "dcl_neg_test_proxool";
237+ String grantSql = "GRANT SELECT ON test_b_db.public." + testTable + " TO " + testRole;
238+ 
239+ // 1. 管理员先"洗地",准备真实存在的角色和表
240+ try (Connection adminConn = getAdminConnection();
241+ Statement adminStmt = adminConn.createStatement()) {
242+ 
243+ try {
244+ adminStmt.execute("REASSIGN OWNED BY " + testRole + " TO dcl_admin");
245+ adminStmt.execute("DROP OWNED BY " + testRole);
246+ } catch (SQLException ignored) {}
247+ try {
248+ adminStmt.execute("DROP USER IF EXISTS " + testRole);
249+ } catch (SQLException ignored) {}
250+ try {
251+ adminStmt.execute("DROP SCHEMA IF EXISTS " + testRole + " CASCADE");
252+ } catch (SQLException ignored) {}
253+ 
254+ adminStmt.execute("CREATE USER " + testRole + " WITH PASSWORD 'TempDcl@123'");
255+ adminStmt.execute("DROP TABLE IF EXISTS test_b_db.public." + testTable);
256+ adminStmt.execute("CREATE TABLE test_b_db.public." + testTable
257+ + " (id INT PRIMARY KEY)");
258+ System.out.println("[INFO] 管理员已准备好角色与测试表");
259+ }
260+ 
261+ // 2. 连通性预检:普通用户连接必须成功且能执行 SELECT 1。
262+ // 这一步的任何失败(认证错误、连接错误、环境错误)都直接导致用例失败,
263+ // 绝不进入后面的权限异常断言。
264+ Connection negConn;
265+ try {
266+ negConn = getConnection();
267+ } catch (SQLException e) {
268+ fail("普通用户连接失败(环境/认证错误,不属于本用例覆盖范围): "
269+ + e.getMessage() + ", SQLState=" + e.getSQLState());
270+ return; // fail() 不会返回,此处仅为编译器满足类型检查
271+ }
272+ try {
273+ try (Statement checkStmt = negConn.createStatement();
274+ ResultSet checkRs = checkStmt.executeQuery("SELECT 1")) {
275+ assertTrue("连通性预检失败:SELECT 1 未返回结果", checkRs.next());
276+ }
277+ System.out.println("[INFO] 连通性预检通过:普通用户连接可用");
278+ 
279+ // 3. 只对 GRANT 语句捕获预期的授权权限错误。
280+ // 认证错误(SQLState=28000 Access denied for user)、
281+ // 连接类错误(SQLState=08xxx Communications link failure)
282+ // 都必须重新抛出导致用例失败。
283+ try (Statement grantStmt = negConn.createStatement()) {
284+ try {
285+ grantStmt.executeUpdate(grantSql);
286+ fail("普通用户竟然 GRANT 成功了!");
287+ } catch (SQLException e) {
288+ String sqlState = e.getSQLState() == null ? "" : e.getSQLState();
289+ String msg = e.getMessage() == null ? "" : e.getMessage();
290+ 
291+ // 排除连接类错误
292+ if (sqlState.startsWith("08")) {
293+ throw new AssertionError(
294+ "连接类错误被误判为权限不足,用例失败: " + msg, e);
295+ }
296+ // 排除登录认证错误(不是对象授权错误)
297+ if ("28000".equals(sqlState)
298+ && msg.toLowerCase().contains("access denied for user")) {
299+ throw new AssertionError(
300+ "登录认证错误被误判为权限不足,用例失败: " + msg, e);
301+ }
302+ 
303+ // 到这里才认定是预期的对象授权权限错误
304+ boolean isPermissionError =
305+ msg.toLowerCase().contains("permission")
306+ || msg.toLowerCase().contains("privilege")
307+ || msg.toLowerCase().contains("denied")
308+ || msg.toLowerCase().contains("must be owner")
309+ || msg.toLowerCase().contains("no privileges");
310+ assertTrue("GRANT 的错误信息不是对象授权权限错误: " + msg
311+ + " (SQLState=" + sqlState + ")", isPermissionError);
312+ System.out.println("[验证] 普通用户 GRANT 被拒绝: " + msg);
313+ System.out.println("[验证] SQLState: " + sqlState);
314+ }
315+ }
316+ } finally {
317+ try { negConn.close(); } catch (Exception ignored) {}
318+ 
319+ // 4. 严格清理(与前置同一套逻辑)
320+ try (Connection adminConn = getAdminConnection();
321+ Statement adminStmt = adminConn.createStatement()) {
322+ cleanRoleAndTable(adminStmt, testRole, testTable);
323+ System.out.println("[INFO] 负向测试角色与测试表已彻底清理");
324+ }
325+ }
326+ System.out.println("========== test04b_DCL_普通用户权限不足 通过 ==========");
327+ }
328+ 
329+ @Test
330+ public void test05_TCL_提交与回滚与保存点() throws Exception {
331+ System.out.println("========== test05_TCL_提交与回滚与保存点 开始 ==========");
332+ 
333+ try (Connection conn = getConnection()) {
334+ conn.setAutoCommit(false);
335+ try (Statement stmt = conn.createStatement()) {
336+ stmt.execute("CREATE TABLE IF NOT EXISTS " + TABLE +
337+ " (id INT PRIMARY KEY, name VARCHAR(50))");
338+ stmt.execute("DELETE FROM " + TABLE);
339+ conn.commit();
340+ System.out.println("[INFO] 清空表并 commit");
341+ 
342+ // 保存点
343+ stmt.executeUpdate("INSERT INTO " + TABLE + " VALUES (1,'A')");
344+ Savepoint sp = conn.setSavepoint("sp1");
345+ System.out.println("[INFO] 已设置保存点 sp1");
346+ 
347+ stmt.executeUpdate("INSERT INTO " + TABLE + " VALUES (2,'B')");
348+ conn.rollback(sp);
349+ System.out.println("[INFO] 已回滚到保存点 sp1");
350+ 
351+ conn.commit();
352+ 
353+ // 验证:id=1 在,id=2 不在
354+ ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM " + TABLE);
355+ assertTrue(rs.next());
356+ assertEquals("保存点回滚后应只剩 1 条", 1, rs.getInt(1));
357+ System.out.println("[验证] 保存点回滚后记录数: " + rs.getInt(1));
358+ } finally {
359+ conn.setAutoCommit(true);
360+ }
361+ }
362+ 
363+ System.out.println("========== test05_TCL_提交与回滚与保存点 通过 ==========");
364+ }
365+ 
366+ @AfterClass
367+ public static void cleanup() {
368+ try {
369+ ProxoolFacade.removeConnectionPool("openGaussPool");
370+ System.out.println("[INFO] openGaussPool 已关闭");
371+ } catch (Exception e) {
372+ System.err.println("[WARN] 关闭 openGaussPool 失败: " + e.getMessage());
373+ }
374+ try {
375+ ProxoolFacade.removeConnectionPool("dclAdminPool");
376+ System.out.println("[INFO] dclAdminPool 已关闭");
377+ } catch (Exception e) {
378+ // dclAdminPool 可能已被其他类关闭,忽略
379+ }
380+ }
381+}
@@ -0,0 +1,245 @@
1+import org.junit.*;
2+import org.logicalcobwebs.proxool.ProxoolFacade;
3+import org.logicalcobwebs.proxool.admin.SnapshotIF;
4+import org.logicalcobwebs.proxool.configuration.PropertyConfigurator;
5+ 
6+import java.io.InputStream;
7+import java.sql.*;
8+import java.util.Properties;
9+import java.util.concurrent.CountDownLatch;
10+import java.util.concurrent.TimeUnit;
11+import java.util.concurrent.atomic.AtomicInteger;
12+ 
13+import static org.junit.Assert.*;
14+ 
15+public class ProxoolStabilityTest {
16+ 
17+ private static final String TABLE = "test_b_db.public.test_stability";
18+ 
19+ @BeforeClass
20+ public static void init() throws Exception {
21+ InputStream in = ProxoolStabilityTest.class.getClassLoader()
22+ .getResourceAsStream("proxool.properties");
23+ assertNotNull("proxool.properties 配置文件未找到", in);
24+ Properties props = new Properties();
25+ props.load(in);
26+ PropertyConfigurator.configure(props);
27+ Class.forName("org.logicalcobwebs.proxool.ProxoolDriver");
28+ System.out.println("[INFO] Proxool 连接池初始化成功");
29+ }
30+ 
31+ private Connection getConnection() throws SQLException {
32+ return DriverManager.getConnection("proxool.openGaussPool");
33+ }
34+ 
35+ @Test
36+ public void test01_重复借出归还100次() throws Exception {
37+ System.out.println("========== test01_重复借出归还100次 开始 ==========");
38+ 
39+ for (int i = 0; i < 100; i++) {
40+ try (Connection conn = getConnection();
41+ Statement stmt = conn.createStatement();
42+ ResultSet rs = stmt.executeQuery("SELECT 1")) {
43+ assertTrue(rs.next());
44+ assertEquals(1, rs.getInt(1));
45+ }
46+ }
47+ 
48+ SnapshotIF snapshot = ProxoolFacade.getSnapshot("openGaussPool", true);
49+ int active = snapshot.getActiveConnectionCount();
50+ System.out.println("[验证] 100 次借出归还后活跃连接数: " + active);
51+ assertEquals("借出归还后活跃连接数应回到 0", 0, active);
52+ 
53+ System.out.println("========== test01_重复借出归还100次 通过 ==========");
54+ }
55+ 
56+ @Test
57+ public void test02_多线程并发事务() throws Exception {
58+ System.out.println("========== test02_多线程并发事务 开始 ==========");
59+ 
60+ try (Connection conn = getConnection();
61+ Statement stmt = conn.createStatement()) {
62+ stmt.execute("CREATE TABLE IF NOT EXISTS " + TABLE +
63+ " (id INT PRIMARY KEY, name VARCHAR(50))");
64+ stmt.execute("DELETE FROM " + TABLE);
65+ }
66+ 
67+ int threadCount = 8;
68+ 
69+ // 三阶段屏障(与自测报告 4.4 节描述一致):
70+ // heldLatch : 每个线程借到连接并开启事务后 countDown,表示"已持有连接"
71+ // releaseLatch : 主线程确认池中活跃连接数达到 8 后放行,线程才开始执行业务
72+ // endLatch : 等待所有线程执行完毕(有超时),保证线程在关池前确定性结束
73+ CountDownLatch heldLatch = new CountDownLatch(threadCount);
74+ CountDownLatch releaseLatch = new CountDownLatch(1);
75+ CountDownLatch endLatch = new CountDownLatch(threadCount);
76+ AtomicInteger successCount = new AtomicInteger(0);
77+ AtomicInteger failCount = new AtomicInteger(0);
78+ 
79+ for (int i = 0; i < threadCount; i++) {
80+ final int threadId = i;
81+ new Thread(() -> {
82+ try {
83+ // 阶段 1:借出连接并开启事务,但先不执行业务
84+ Connection conn = getConnection();
85+ conn.setAutoCommit(false);
86+ heldLatch.countDown(); // 通知主线程:我已持有连接
87+ 
88+ // 阶段 2:等待主线程确认 8 条连接全部借出后放行
89+ releaseLatch.await(10, TimeUnit.SECONDS);
90+ 
91+ // 阶段 3:执行完整事务 CRUD,每步断言
92+ try {
93+ // 1. INSERT
94+ try (PreparedStatement ps = conn.prepareStatement(
95+ "INSERT INTO " + TABLE + " VALUES (?, ?)")) {
96+ ps.setInt(1, threadId);
97+ ps.setString(2, "Thread-" + threadId);
98+ assertEquals(1, ps.executeUpdate());
99+ }
100+ 
101+ // 2. SELECT
102+ try (PreparedStatement ps = conn.prepareStatement(
103+ "SELECT name FROM " + TABLE + " WHERE id=?")) {
104+ ps.setInt(1, threadId);
105+ ResultSet rs = ps.executeQuery();
106+ assertTrue("SELECT 应返回记录", rs.next());
107+ assertEquals("Thread-" + threadId, rs.getString("name"));
108+ }
109+ 
110+ // 3. UPDATE
111+ try (PreparedStatement ps = conn.prepareStatement(
112+ "UPDATE " + TABLE + " SET name=? WHERE id=?")) {
113+ ps.setString(1, "Updated-" + threadId);
114+ ps.setInt(2, threadId);
115+ assertEquals(1, ps.executeUpdate());
116+ }
117+ 
118+ // 4. SELECT 验证更新
119+ try (PreparedStatement ps = conn.prepareStatement(
120+ "SELECT name FROM " + TABLE + " WHERE id=?")) {
121+ ps.setInt(1, threadId);
122+ ResultSet rs = ps.executeQuery();
123+ assertTrue(rs.next());
124+ assertEquals("Updated-" + threadId, rs.getString("name"));
125+ }
126+ 
127+ // 5. DELETE
128+ try (PreparedStatement ps = conn.prepareStatement(
129+ "DELETE FROM " + TABLE + " WHERE id=?")) {
130+ ps.setInt(1, threadId);
131+ assertEquals(1, ps.executeUpdate());
132+ }
133+ 
134+ conn.commit();
135+ successCount.incrementAndGet();
136+ } finally {
137+ try { conn.close(); } catch (Exception ignored) {}
138+ }
139+ } catch (Exception e) {
140+ failCount.incrementAndGet();
141+ System.err.println("[FAIL] thread-" + threadId + ": " + e.getMessage());
142+ } finally {
143+ endLatch.countDown();
144+ }
145+ }).start();
146+ }
147+ 
148+ // 主线程:等待所有线程持有连接(有界等待,防止线程启动失败导致永久阻塞)
149+ assertTrue("线程未能在 10 秒内全部持有连接",
150+ heldLatch.await(10, TimeUnit.SECONDS));
151+ 
152+ // 确认池中活跃连接数达到 8(全部借出、尚未执行业务)
153+ SnapshotIF heldSnapshot = ProxoolFacade.getSnapshot("openGaussPool", true);
154+ int activeHeld = heldSnapshot.getActiveConnectionCount();
155+ System.out.println("[验证] 8 个线程同时持有连接时 active=" + activeHeld);
156+ assertTrue("应至少有 8 条连接被同时持有,实际 active=" + activeHeld,
157+ activeHeld >= threadCount);
158+ 
159+ // 放行:所有线程同时开始执行业务
160+ releaseLatch.countDown();
161+ 
162+ // 等待所有线程完成(有超时,超时直接断言失败)
163+ boolean done = endLatch.await(30, TimeUnit.SECONDS);
164+ assertTrue("并发执行超时", done);
165+ 
166+ System.out.println("[验证] 成功数: " + successCount.get() + ", 失败数: " + failCount.get());
167+ assertEquals("所有线程应成功", threadCount, successCount.get());
168+ assertEquals("不应有失败线程", 0, failCount.get());
169+ 
170+ // 验证数据库状态:所有线程执行完 DELETE 后应无数据
171+ try (Connection conn = getConnection();
172+ Statement stmt = conn.createStatement();
173+ ResultSet rs = stmt.executeQuery(
174+ "SELECT COUNT(*) FROM " + TABLE)) {
175+ assertTrue(rs.next());
176+ assertEquals("线程结束后表内应无记录", 0, rs.getInt(1));
177+ System.out.println("[验证] 线程结束后表内记录数: " + rs.getInt(1));
178+ }
179+ 
180+ // 验证无连接泄漏:等待 2 秒后检查 active=0
181+ Thread.sleep(2000);
182+ SnapshotIF snapshot = ProxoolFacade.getSnapshot("openGaussPool", true);
183+ int active = snapshot.getActiveConnectionCount();
184+ System.out.println("[验证] 线程结束后 active=" + active);
185+ assertEquals("所有线程结束后不应有活动连接", 0, active);
186+ 
187+ System.out.println("========== test02_多线程并发事务 通过 ==========");
188+ }
189+ 
190+ @Test
191+ public void test03_长连接与housekeeping() throws Exception {
192+ System.out.println("========== test03_长连接与housekeeping 开始 ==========");
193+ 
194+ // 持有连接 5 秒并每秒查询一次。
195+ // 说明:维护周期为 30 秒(house-keeping-sleep-time=30000),
196+ // 本用例仅验证短时间内连接持续可用,不构成对 housekeeping 行为的验证。
197+ try (Connection conn = getConnection();
198+ Statement stmt = conn.createStatement()) {
199+ long start = System.currentTimeMillis();
200+ for (int i = 0; i < 5; i++) {
201+ Thread.sleep(1000);
L
Lliyangx25 days ago

[P2] 此用例没有验证 housekeeping 行为,报告结论超出了覆盖范围

当前配置的维护周期为 30 秒,但此用例仅持有连接 5 秒并主动执行 5 次 SELECT 1,没有确认任何维护周期或维护校验实际执行。使用项目随附库和本地替身连接,不配置 house-keeping-test-sql,调用该原测试方法仍会通过。因此目前只能证明短时间内连接查询可用,不能证明后台保活、housekeeping 不误杀或长期稳定性。请收缩自测报告中的结论,或用隔离测试池、较短且明确的维护周期、可观察的维护执行记录验证跨多个维护周期的行为,并提供相应结果。

likedislike
202+ ResultSet rs = stmt.executeQuery("SELECT 1");
203+ assertTrue(rs.next());
204+ }
205+ long cost = System.currentTimeMillis() - start;
206+ System.out.println("[验证] 5 秒内连接保持可用,耗时: " + cost + " ms");
207+ }
208+ 
209+ System.out.println("========== test03_长连接与housekeeping 通过 ==========");
210+ }
211+ 
212+ @Test
213+ public void test04_池状态快照一致性() throws Exception {
214+ System.out.println("========== test04_池状态快照一致性 开始 ==========");
215+ 
216+ SnapshotIF before = ProxoolFacade.getSnapshot("openGaussPool", true);
217+ int activeBefore = before.getActiveConnectionCount();
218+ System.out.println("[验证] 借出前活跃连接数: " + activeBefore);
219+ 
220+ try (Connection c1 = getConnection();
221+ Connection c2 = getConnection()) {
222+ SnapshotIF during = ProxoolFacade.getSnapshot("openGaussPool", true);
223+ int activeDuring = during.getActiveConnectionCount();
224+ System.out.println("[验证] 借出 2 个后活跃连接数: " + activeDuring);
225+ assertTrue("借出后活跃连接数应增加", activeDuring >= activeBefore + 2);
226+ }
227+ 
228+ SnapshotIF after = ProxoolFacade.getSnapshot("openGaussPool", true);
229+ int activeAfter = after.getActiveConnectionCount();
230+ System.out.println("[验证] 归还后活跃连接数: " + activeAfter);
231+ assertTrue("归还后活跃连接数应回落", activeAfter <= activeBefore + 1);
232+ 
233+ System.out.println("========== test04_池状态快照一致性 通过 ==========");
234+ }
235+ 
236+ @AfterClass
237+ public static void cleanup() {
238+ try {
239+ ProxoolFacade.removeConnectionPool("openGaussPool");
240+ System.out.println("[INFO] 连接池已关闭");
241+ } catch (Exception e) {
242+ System.err.println("[WARN] 关闭连接池失败: " + e.getMessage());
243+ }
244+ }
245+}
@@ -0,0 +1,146 @@
1+import org.junit.*;
2+import org.logicalcobwebs.proxool.ProxoolFacade;
3+import org.logicalcobwebs.proxool.configuration.PropertyConfigurator;
4+ 
5+import java.io.InputStream;
6+import java.sql.*;
7+import java.util.Properties;
8+ 
9+import static org.junit.Assert.*;
10+ 
11+public class ProxoolTransactionTest {
12+ 
13+ private static final String TABLE = "test_b_db.public.test_proxool";
14+ 
15+ @BeforeClass
16+ public static void init() throws Exception {
17+ InputStream in = ProxoolTransactionTest.class.getClassLoader()
18+ .getResourceAsStream("proxool.properties");
19+ assertNotNull("proxool.properties 配置文件未找到", in);
20+ Properties props = new Properties();
21+ props.load(in);
22+ PropertyConfigurator.configure(props);
23+ Class.forName("org.logicalcobwebs.proxool.ProxoolDriver");
24+ System.out.println("[INFO] Proxool 连接池初始化成功");
25+ }
26+ 
27+ private Connection getConnection() throws SQLException {
28+ return DriverManager.getConnection("proxool.openGaussPool");
29+ }
30+ 
31+ @Before
32+ public void prepareTable() throws Exception {
33+ try (Connection conn = getConnection();
34+ Statement stmt = conn.createStatement()) {
35+ stmt.execute("CREATE TABLE IF NOT EXISTS " + TABLE +
36+ " (id INT PRIMARY KEY, name VARCHAR(50))");
37+ stmt.execute("DELETE FROM " + TABLE);
38+ stmt.execute("INSERT INTO " + TABLE + " VALUES (1, 'Alice'), (2, 'Bob')");
39+ }
40+ }
41+ 
42+ @Test
43+ public void test01_事务隔离级别读取() throws Exception {
44+ System.out.println("========== test01_事务隔离级别读取 开始 ==========");
45+ 
46+ try (Connection conn = getConnection()) {
47+ int level = conn.getTransactionIsolation();
48+ System.out.println("[验证] 当前事务隔离级别: " + level);
49+ assertTrue("隔离级别应为合法值", level >= Connection.TRANSACTION_NONE
50+ && level <= Connection.TRANSACTION_SERIALIZABLE);
51+ }
52+ 
53+ System.out.println("========== test01_事务隔离级别读取 通过 ==========");
54+ }
55+ 
56+ @Test
57+ public void test02_commit后跨连接可见() throws Exception {
58+ System.out.println("========== test02_commit后跨连接可见 开始 ==========");
59+ 
60+ // 连接1:开启事务,更新,提交
61+ try (Connection c1 = getConnection()) {
62+ c1.setAutoCommit(false);
63+ try (Statement stmt = c1.createStatement()) {
64+ stmt.executeUpdate("UPDATE " + TABLE + " SET name='ALICE_UPDATE' WHERE id=1");
65+ c1.commit();
66+ System.out.println("[INFO] 连接1 已提交更新");
67+ }
68+ }
69+ 
70+ // 连接2:查询,应看到新值
71+ try (Connection c2 = getConnection();
72+ Statement stmt = c2.createStatement();
73+ ResultSet rs = stmt.executeQuery("SELECT name FROM " + TABLE + " WHERE id=1")) {
74+ assertTrue(rs.next());
75+ String name = rs.getString("name");
76+ assertEquals("commit 后跨连接应可见", "ALICE_UPDATE", name);
77+ System.out.println("[验证] 连接2 查询到: " + name);
78+ }
79+ 
80+ System.out.println("========== test02_commit后跨连接可见 通过 ==========");
81+ }
82+ 
83+ @Test
84+ public void test03_rollback后跨连接不可见() throws Exception {
85+ System.out.println("========== test03_rollback后跨连接不可见 开始 ==========");
86+ 
87+ // 连接1:开启事务,更新,回滚
88+ try (Connection c1 = getConnection()) {
89+ c1.setAutoCommit(false);
90+ try (Statement stmt = c1.createStatement()) {
91+ stmt.executeUpdate("UPDATE " + TABLE + " SET name='ROLLBACK_TEST' WHERE id=2");
92+ c1.rollback();
93+ System.out.println("[INFO] 连接1 已回滚更新");
94+ }
95+ }
96+ 
97+ // 连接2:查询,应看到旧值
98+ try (Connection c2 = getConnection();
99+ Statement stmt = c2.createStatement();
100+ ResultSet rs = stmt.executeQuery("SELECT name FROM " + TABLE + " WHERE id=2")) {
101+ assertTrue(rs.next());
102+ String name = rs.getString("name");
103+ assertEquals("rollback 后应保持旧值", "Bob", name);
104+ System.out.println("[验证] 连接2 查询到: " + name);
105+ }
106+ 
107+ System.out.println("========== test03_rollback后跨连接不可见 通过 ==========");
108+ }
109+ 
110+ @Test
111+ public void test04_池化连接事务行为() throws Exception {
112+ System.out.println("========== test04_池化连接事务行为 开始 ==========");
113+ 
114+ // 从池里借连接、开事务、回滚、归还
115+ try (Connection conn = getConnection()) {
116+ conn.setAutoCommit(false);
117+ try (Statement stmt = conn.createStatement()) {
118+ stmt.executeUpdate("UPDATE " + TABLE + " SET name='POOL_TX' WHERE id=1");
119+ conn.rollback();
120+ } finally {
121+ conn.setAutoCommit(true);
122+ }
123+ }
124+ 
125+ // 再从池里借一个连接(可能是复用的),确认事务回滚生效
126+ try (Connection conn = getConnection();
127+ Statement stmt = conn.createStatement();
128+ ResultSet rs = stmt.executeQuery("SELECT name FROM " + TABLE + " WHERE id=1")) {
129+ assertTrue(rs.next());
130+ assertEquals("池化连接回滚后应保持旧值", "Alice", rs.getString("name"));
131+ System.out.println("[验证] 池化连接回滚后 id=1 仍为: " + rs.getString("name"));
132+ }
133+ 
134+ System.out.println("========== test04_池化连接事务行为 通过 ==========");
135+ }
136+ 
137+ @AfterClass
138+ public static void cleanup() {
139+ try {
140+ ProxoolFacade.removeConnectionPool("openGaussPool");
141+ System.out.println("[INFO] 连接池已关闭");
142+ } catch (Exception e) {
143+ System.err.println("[WARN] 关闭连接池失败: " + e.getMessage());
144+ }
145+ }
146+}