Open
[开源实习]: Proxool 连接 openGauss B 兼容模式测试代码及自测报告 #126
wsyAdd on Sep 23
[开源实习]: Proxool 连接 openGauss B 兼容模式测试代码及自测报告 #126
Open
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 | |||
| 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 | + | ||
| 70 | + | ||
| 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; | ||
| 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 = | ||
| 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 " + | ||
| 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); | ||
| 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 | +} | ||


[P2] 自测结果要跟这次提交一起更新
新增 test06 后,当前是 39 个测试,ProxoolPoolParamTest 有 6 个;报告、README 和两张截图还停留在 38 个。两张截图文件与上一版相同,不能说明这次改完后的运行结果。
报告 4.2 还在引用旧的 test04 方法名和已经删掉的轮询步骤,4.8 也没同步隔离池、池外管理连接的改法。
请修好后完整跑一次 mvn clean test,再把结果、截图和场景说明一起更新。有失败就把实际结果和原因写出来。图片路径和 README 的报告链接这次已经改对了。