diff --git a/docs/guides/database-validation.md b/docs/guides/database-validation.md index 25a160812..7717db533 100644 --- a/docs/guides/database-validation.md +++ b/docs/guides/database-validation.md @@ -1,6 +1,6 @@ # Database service acceptance -This record separates public-module service/failure checks from positive workflows through actual plugin DSOs. Each scope has its own tested revisions and environment; a passing result applies only to that scope. +This record separates public-module service/failure checks, positive workflows through actual plugin DSOs, and server-observed request cancellation through those DSOs. Each scope has its own tested revisions and environment; a passing result applies only to that scope. ## Public modules against real services @@ -60,6 +60,40 @@ cmake --build database-build --target ark_database_public_plugin_service_tests - These are standalone C++ test modes. They do not create or stop services; missing configuration or an unavailable protocol fails rather than becoming a successful skip. `check-database` builds this program and its DSOs without running the real-service modes. +## Real plugin request cancellation + +Both modes of `ark_database_public_plugin_cancellation_tests` passed on **2026-10-26**, each with exit code `0`. The host uses only public interfaces and loads the actual hidden-symbol database plugin DSOs. + +| Environment | Value | +| --- | --- | +| Machine | Mac16,11 · Apple M4 Pro · 14 cores · 48 GiB | +| System | macOS 27.0.1 (26A434) · arm64 | +| Build | Apple Clang 21.0.0 · C++20 · Boost.Asio · Debug · SSL enabled · OpenSSL 3.6.4 | +| Executed ark | `dba34762dd58348784e46a2518f7505e52063d55` | +| arknet | CI pin `a0dfe09cd543e75cfb5996e537ce2fa49f2a758f` | +| Services | Redis 7.4.2 over authenticated TCP; MySQL Community Server 8.4.13 with required verified TLS | + +Cancellation is requested through the existing `task_group::cancel(id)` only after an independent pool observes the uniquely identified operation on the server. Polling timers only throttle observations; elapsed time is not the active-operation barrier. + +| Mode | Server observation before cancellation | Original native result and observed effect | +| --- | --- | --- | +| `redis` | `CLIENT LIST` identified the named pipeline blocked in BLPOP after its preceding INCR counter became `1` | `client_errc::cancelled`, `command_may_have_run=true`, no transport error and retained integer partial reply `1`. Explicit PING recovered; the observed block retired and the exclusive counter remained `1`. | +| `mysql` | `PROCESSLIST` identified the tagged UPDATE in `User sleep` | `connection_errc::cancelled`, `outcome_unknown=true` and `operation_aborted` transport status. An explicit query recovered; the observer waited for the tagged statement to disappear across all states and a bounded quiet window before reading the final counter, which was `1`. | + +The cancelled values remain service results, not coroutine exceptions. Request and observer roots each complete once on the owner thread/shared executor. Repeated cancellation of a pending task is accepted; its retired ID rejects cancellation. Both controlled tests complete natural managed/native shutdown within their observation and pumping bounds; repeated stop succeeds, and owning cancelled results remain readable after plugin/module/manager/runtime destruction. These are test guards, not a hard or preemptive production deadline: `manager.stop()` remains cooperative and cannot interrupt blocking hooks. No production timeout or cancellation policy is added, and `cancel_all` or forced frame destruction is not used for teardown. + +The final counters describe only the observed operations and finite observation window. The MySQL case permits `0` or `1` while the outcome is unknown; this run observed `1`. Cancellation does not establish rollback, global exactly-once execution or every possible retry race. + +Use the [standalone C++ cancellation guide](../../tests/database/cancellation.md) to prepare exclusively owned services, private authentication and the required database privileges. These modes write exclusive Redis keys or a private MySQL counter table; do not use shared services. With the SSL-enabled database build configured: + +```sh +cmake --build database-build --target ark_database_public_plugin_cancellation_tests --parallel 1 +./database-build/tests/database/ark_database_public_plugin_cancellation_tests redis +./database-build/tests/database/ark_database_public_plugin_cancellation_tests mysql +``` + +`check-database` compiles the program without running the real-service modes. Both owned servers were shut down normally using authenticated commands; the exact processes were independently confirmed absent and private data, credentials and TLS keys were removed. Server termination was not a test action. + ## Acceptance boundaries -The DSO workflows exercise normal plugin teardown and native close calls. Successful `FreeLibrary` or `dlclose` does not prove that the operating system physically removed the library image. Request cancellation, server termination, OS image removal, other deployment environments and cross-machine capacity still require separate acceptance. Neither test scope establishes arbitrary hot-update safety or production capacity. +The DSO workflows exercise normal plugin teardown and native close calls. Successful `FreeLibrary` or `dlclose` does not prove that the operating system physically removed the library image. Request cancellation acceptance is limited to the selected server-observed operations on this host. Transaction cancellation, Redis TLS, other cancellation/retry races, server termination, OS image removal, other platforms/deployment environments and cross-machine capacity remain unverified. These records do not establish arbitrary hot-update safety or production capacity. diff --git a/docs/guides/database-validation_CN.md b/docs/guides/database-validation_CN.md index 52dd15e4d..2f1def0cc 100644 --- a/docs/guides/database-validation_CN.md +++ b/docs/guides/database-validation_CN.md @@ -1,6 +1,6 @@ # 数据库服务验收 -本页分别记录公开模块的真实服务正常/故障检查,以及通过真实插件 DSO 执行的正常流程。每类检查保留各自的测试版本与环境;通过结果仅适用于对应范围。 +本页分别记录公开模块的真实服务正常/故障检查、真实插件 DSO 正常流程,以及经服务端观察确认的 DSO 请求取消。每类检查保留各自的测试版本与环境;通过结果仅适用于对应范围。 ## 公开模块与真实服务 @@ -60,6 +60,40 @@ cmake --build database-build --target ark_database_public_plugin_service_tests - 这些模式由独立 C++ 程序执行,不创建或停止服务。缺少配置、构建中没有所选协议时,程序返回失败,不会跳过并报告成功。`check-database` 仅构建该程序及其 DSO,不运行真实服务模式。 +## 真实插件请求取消 + +**2026-10-26**,`ark_database_public_plugin_cancellation_tests` 的两种模式均通过,退出码均为 `0`。宿主仅使用公开接口,加载真实的隐藏符号数据库插件 DSO。 + +| 环境 | 配置 | +| --- | --- | +| 机器 | Mac16,11 · Apple M4 Pro · 14 核 · 48 GiB | +| 系统 | macOS 27.0.1(26A434)· arm64 | +| 构建 | Apple Clang 21.0.0 · C++20 · Boost.Asio · Debug · 启用 SSL · OpenSSL 3.6.4 | +| 实际执行的 ark | `dba34762dd58348784e46a2518f7505e52063d55` | +| arknet | CI 固定版本 `a0dfe09cd543e75cfb5996e537ce2fa49f2a758f` | +| 服务 | Redis 7.4.2:TCP 与密码认证;MySQL Community Server 8.4.13:强制 TLS 与证书校验 | + +独立连接池先在服务端观察到唯一标识的操作,再通过现有 `task_group::cancel(id)` 请求取消。定时器仅降低轮询频率,等待时间不能替代操作已在服务端运行的证据。 + +| 模式 | 取消前的服务端观察 | 原生结果与实际影响 | +| --- | --- | --- | +| `redis` | `CLIENT LIST` 确认具名流水线在 BLPOP 中阻塞,此前的 INCR 已使计数变为 `1` | 保留 `client_errc::cancelled`、`command_may_have_run=true`、无传输错误与整数部分回复 `1`。显式 PING 恢复成功;观察到的阻塞命令退出后,独占计数仍为 `1`。 | +| `mysql` | `PROCESSLIST` 确认带标记的 UPDATE 处于 `User sleep` | 保留 `connection_errc::cancelled`、`outcome_unknown=true` 与 `operation_aborted` 传输状态。显式新查询恢复成功;观察者确认该语句在所有状态下均已消失,并经过有限静默观察期后,读取最终计数,本次为 `1`。 | + +取消值仍作为服务结果返回,不转换为协程异常。请求与观察根任务分别在所属线程/共享执行器完成一次。重复取消尚未退出的任务会被接收,取消已退出 ID 则失败。两个受控测试均在各自的观察与驱动时限内完成托管任务和原生资源的自然停机,重复 stop 成功;插件/模块/管理器/运行时销毁后,拥有型取消结果仍可读取。这些时限仅用于测试,不构成强制或抢占式生产期限:`manager.stop()` 仍为协作式调用,不能中断阻塞中的钩子。本次不增加生产超时或取消策略,停机不使用 `cancel_all` 或强制销毁协程帧。 + +最终计数仅描述本次观察到的操作及有限观察窗口。MySQL 用例在结果未知时允许 `0` 或 `1`,本次观察到 `1`。取消不证明回滚、全局恰好执行一次,也不覆盖全部重试竞争。 + +按[独立 C++ 取消验收指南](../../tests/database/cancellation_CN.md)准备独占服务、私有认证配置与数据库权限。这些模式会写入独占 Redis 键或私有 MySQL 计数表,不应对共享服务运行。完成启用 SSL 的数据库构建配置后,执行: + +```sh +cmake --build database-build --target ark_database_public_plugin_cancellation_tests --parallel 1 +./database-build/tests/database/ark_database_public_plugin_cancellation_tests redis +./database-build/tests/database/ark_database_public_plugin_cancellation_tests mysql +``` + +`check-database` 仅编译该程序,不运行真实服务模式。两个自有服务均通过认证命令正常关闭;精确进程已独立确认不存在,私有数据、凭据与 TLS 密钥已删除。服务端终止不是本次测试动作。 + ## 验收边界 -DSO 流程覆盖正常插件销毁与原生关闭调用。`FreeLibrary` 或 `dlclose` 成功不证明操作系统已物理移除库映像。请求取消、服务端终止、操作系统库映像移除、其他部署环境与跨机器容量仍需单独验收。这两类检查均不证明任意热更新安全或生产容量。 +DSO 流程覆盖正常插件销毁与原生关闭调用。`FreeLibrary` 或 `dlclose` 成功不证明操作系统已物理移除库映像。本机请求取消验收仅覆盖记录中经服务端观察确认的操作。事务取消、Redis TLS、其他取消/重试竞争、服务端终止、操作系统库映像移除、其他平台/部署环境与跨机器容量仍未验收。这些记录均不证明任意热更新安全或生产容量。 diff --git a/docs/project/roadmap.md b/docs/project/roadmap.md index 268ef5277..5d246c8c2 100644 --- a/docs/project/roadmap.md +++ b/docs/project/roadmap.md @@ -8,13 +8,13 @@ This page distinguishes supported source behavior from remaining design and depl - Public [MySQL](../reference/mysql.md) and [Redis](../reference/redis.md) modules use arknet native clients through Asio coroutine entry points. Inputs and results own their storage; manager-owned task groups resume on the module owner thread. MySQL includes prepared statements and transactions; Redis includes commands and pipelines. Legacy database drivers are no longer the public plugin interface. - Startup callbacks propagate the first failure. The launcher reports the failed phase and exits with code `1` before the update loop. Shutdown stops managed task admission, drains accepted tasks and waits for module readiness. A configured cooperative wait timeout retains the process/resources and disables unloading. These behaviors do not provide partial-startup rollback or a hard process-stop deadline. See [application startup and shutdown](../guides/applications.md) and [managed tasks](../guides/tasks.md). -Public database service/failure checks and the [real-plugin DSO positive workflows](../guides/database-validation.md#real-plugin-dsos-against-real-services) passed against isolated Redis and MySQL servers on macOS. The [database service acceptance record](../guides/database-validation.md) keeps their distinct tested revisions, environments and scope; those results do not cover every deployment or source revision. +Public database service/failure checks, [real-plugin DSO positive workflows](../guides/database-validation.md#real-plugin-dsos-against-real-services) and [bounded server-observed request cancellation](../guides/database-validation.md#real-plugin-request-cancellation) passed against isolated Redis and MySQL servers on macOS. The [database service acceptance record](../guides/database-validation.md) keeps their distinct tested revisions, environments and scope; those results do not cover every deployment or source revision. ## Remaining acceptance and design | Area | Remaining work and acceptance boundary | | --- | --- | -| Database deployment | Run separate real-service acceptance for request cancellation, server termination and OS library-image removal. Add target-environment and cross-machine capacity evidence before making deployment or capacity claims. Local protocol endpoints and construction checks do not replace these runs. | +| Database deployment | Transaction cancellation, Redis TLS, other cancellation/retry races, server termination, other platforms and OS library image removal still need acceptance. Add target-environment and cross-machine capacity evidence before making deployment or capacity claims. Local protocol endpoints and construction checks do not replace these runs. | | Startup and shutdown | Partial-startup rollback is not provided. A common dependency-aware shutdown protocol remains design work; managed task drain alone does not prove that arbitrary callbacks, native workers or background cleanup have ended. Define ownership and admission phases before extending unload guarantees. | | Optional plugins | Kafka construction/export checks do not test broker delivery, flush behavior or service interoperability; its driver build needs independent platform acceptance. OSS SDK remains a scaffold without client/request functionality. Physic retains the explicit unsupported legacy PhysX 3 configuration failure. See [optional plugin boundaries](../reference/directory-structure.md). | | C++20 modernization | Review each remaining module and optional integration against its own API, build and runtime requirements. The language baseline, header layout and focused construction checks do not establish completion of the whole modernization. | diff --git a/docs/project/roadmap_CN.md b/docs/project/roadmap_CN.md index 3f66f18b9..c10ed938b 100644 --- a/docs/project/roadmap_CN.md +++ b/docs/project/roadmap_CN.md @@ -8,13 +8,13 @@ - 公开 [MySQL](../reference/mysql_CN.md) 与 [Redis](../reference/redis_CN.md) 模块通过 Asio 协程入口使用 arknet 原生客户端。输入与结果拥有自身存储;管理器拥有的任务组在模块所属线程恢复。MySQL 提供预处理语句与事务,Redis 提供命令与流水线。旧数据库驱动已不再作为公开插件接口。 - 启动回调的首个失败会向上传递。启动器报告失败阶段,在进入更新循环前以退出码 `1` 终止。停止时关闭托管任务接收,排空已接收任务并等待模块就绪。配置的协作式等待超时会保留进程和资源,并禁止卸载。这些行为不提供部分启动回滚,也不构成强制进程退出期限。见[应用启动与停止](../guides/applications_CN.md)和[托管任务](../guides/tasks_CN.md)。 -公开数据库接口的正常/故障检查,以及[真实插件 DSO 正常流程](../guides/database-validation_CN.md#真实插件动态库与真实服务),已在 macOS 上通过隔离 Redis 与 MySQL 真实服务验收。[数据库服务验收记录](../guides/database-validation_CN.md)分别保留各自的测试版本、环境与范围;这些结果不覆盖所有部署环境或源码版本。 +公开数据库接口的正常/故障检查、[真实插件 DSO 正常流程](../guides/database-validation_CN.md#真实插件动态库与真实服务),以及[经服务端观察确认的有界请求取消](../guides/database-validation_CN.md#真实插件请求取消),已在 macOS 上通过隔离 Redis 与 MySQL 真实服务验收。[数据库服务验收记录](../guides/database-validation_CN.md)分别保留各自的测试版本、环境与范围;这些结果不覆盖所有部署环境或源码版本。 ## 剩余验收与设计 | 领域 | 剩余工作与验收边界 | | --- | --- | -| 数据库部署 | 为请求取消、服务端终止和操作系统库映像移除分别执行真实服务验收。发布部署或容量结论前,补充目标环境与跨机器容量证据。本机协议端点与构造检查不能替代这些验收。 | +| 数据库部署 | 事务取消、Redis TLS、其他取消/重试竞争、服务端终止、其他平台与操作系统库映像移除仍需验收。发布部署或容量结论前,补充目标环境与跨机器容量证据。本机协议端点与构造检查不能替代这些验收。 | | 启动与停机 | 框架不提供部分启动回滚。通用的依赖感知停机协议仍需设计;托管任务排空不代表任意回调、原生工作线程或后台清理均已结束。扩展卸载保证前,先明确所有权与接收阶段。 | | 可选插件 | Kafka 构造/导出检查不验证 broker 投递、flush 行为或服务互通;驱动构建仍需独立平台验收。OSS SDK 保持骨架状态,不提供客户端/请求功能。Physic 保留旧版 PhysX 3 不受支持的显式配置失败。见[可选插件边界](../reference/directory-structure_CN.md)。 | | C++20 改造 | 按各模块与可选集成的 API、构建和运行要求继续审查。语言基线、头文件布局和局部构造检查不足以证明全部改造完成。 | diff --git a/tests/database/CMakeLists.txt b/tests/database/CMakeLists.txt index 494b929e1..1d8271e16 100644 --- a/tests/database/CMakeLists.txt +++ b/tests/database/CMakeLists.txt @@ -63,6 +63,13 @@ set_property(TARGET ark_database_public_plugin_service_tests PROPERTY LINK_LIBRA target_compile_definitions(ark_database_public_plugin_service_tests PRIVATE ARK_DATABASE_PLUGIN_DIR="$/") add_dependencies(ark_database_public_plugin_service_tests ark_database_redis_plugin) +# Explicit real-service cancellation; compile in CI, execute only against owned services. +ark_database_test(ark_database_public_plugin_cancellation_tests public_plugin_cancellation.cpp) +set_property(TARGET ark_database_public_plugin_cancellation_tests PROPERTY INCLUDE_DIRECTORIES "") +set_property(TARGET ark_database_public_plugin_cancellation_tests PROPERTY LINK_LIBRARIES ark::redis Threads::Threads) +target_compile_definitions(ark_database_public_plugin_cancellation_tests PRIVATE + ARK_DATABASE_PLUGIN_DIR="$/") +add_dependencies(ark_database_public_plugin_cancellation_tests ark_database_redis_plugin) # Compile and load each sample's real module and plugin assembly with a local # provider that rejects configuration before coroutine or network work. function(ark_database_sample_check database) @@ -108,7 +115,8 @@ add_custom_target(check-database # Compile real-service acceptance in ordinary CI without running any server work. add_dependencies(check-database ark_database_redis_sample_check ark_database_public_service_tests - ark_database_public_failure_service_tests ark_database_public_plugin_service_tests check-database-samples) + ark_database_public_failure_service_tests ark_database_public_plugin_service_tests + ark_database_public_plugin_cancellation_tests check-database-samples) if(ARKNET_ENABLE_SSL) target_link_libraries(ark_database_public_failure_service_tests PRIVATE ark::mysql) @@ -140,6 +148,8 @@ if(ARKNET_ENABLE_SSL) add_dependencies(ark_database_public_mysql_plugin_tests ark_database_mysql_plugin) add_dependencies(ark_database_public_plugin_service_tests ark_database_mysql_plugin) target_link_libraries(ark_database_public_plugin_service_tests PRIVATE ark::mysql) + target_link_libraries(ark_database_public_plugin_cancellation_tests PRIVATE ark::mysql) + add_dependencies(ark_database_public_plugin_cancellation_tests ark_database_mysql_plugin) ark_database_sample_check(mysql) add_custom_target(check-database-samples-mysql COMMAND $ diff --git a/tests/database/README.md b/tests/database/README.md index a13692f90..3a75fd3a7 100644 --- a/tests/database/README.md +++ b/tests/database/README.md @@ -288,6 +288,10 @@ text. Building it and checking those failures do not establish successful real-service DSO acceptance; that requires explicitly running each selected mode against isolated services. +## Real plugin request cancellation + +`ark_database_public_plugin_cancellation_tests` is a separate opt-in runner for real Redis/MySQL plugin DSOs and owned services. It observes tagged server work before `task_group::cancel(id)`, retains native cancellation/unknown results, and checks owner-thread completion and natural retirement. The evidence covers the observed IDs, effects and bounded observation window; MySQL's final zero-or-one counter is not proof of rollback or every possible retry race. `check-database` compiles this runner without starting services. See the [cancellation guide](cancellation.md) for build commands, trusted TLS, exclusive write data and precise limits. + ## Public real-service failure checks The separate `ark_database_public_failure_service_tests` executable checks wrong diff --git a/tests/database/README_CN.md b/tests/database/README_CN.md index 126da945a..8506d3643 100644 --- a/tests/database/README_CN.md +++ b/tests/database/README_CN.md @@ -227,6 +227,10 @@ MySQL 检查有符号整数、文本、NULL 查询字段,二进制预处理结 程序不启动服务,也不打印凭据或原生服务端文本。编译与这些失败检查不能证明真实服务 DSO 正向验收通过;需要显式选择模式,在隔离服务上实际运行。 +## 真实插件请求取消 + +独立的 `ark_database_public_plugin_cancellation_tests` 手动启用模式使用真实 Redis/MySQL 插件 DSO 与自有服务,先观察带标记的服务端工作,再调用 `task_group::cancel(id)`,保留原生取消/未知结果,并检查所属线程完成和自然退出。证据仅覆盖观察到的 ID、影响及有限观察窗口;MySQL 最终计数为零或一,不证明回滚,也不覆盖全部重试竞争。`check-database` 只编译该程序,不启动服务。构建命令、可信 TLS、独占写入数据与准确边界见[取消验收指南](cancellation_CN.md)。 + ## 公开模块真实服务负面验收 独立的 `ark_database_public_failure_service_tests` 验证 Redis/MySQL 错误密码、 diff --git a/tests/database/cancellation.md b/tests/database/cancellation.md new file mode 100644 index 000000000..56c34a6f3 --- /dev/null +++ b/tests/database/cancellation.md @@ -0,0 +1,30 @@ +# Public database plugin cancellation acceptance + +[简体中文](cancellation_CN.md) + +`ark_database_public_plugin_cancellation_tests` loads the actual hidden-symbol database plugin through its DLL entry points. Its host links public interfaces only. The existing `check-database` target compiles this program but never starts services or executes its real-service modes. + +Use only owned, authenticated services bound to `127.0.0.1`, fresh private directories and exclusive keys/schema. Verify executable path, child PID, PID file and loopback listener before use and before normal authenticated shutdown. Do not run these writes against a shared or production service. Credentials belong in private files/environment, never command arguments or evidence uploads. + +```sh +cmake -S . -B database-build -G Ninja \ + -DARKNET_SOURCE_DIR=/path/to/the-pinned-arknet \ + -DARK_BUILD_APPLICATIONS=OFF -DARK_BUILD_PROTO=OFF \ + -DARK_BUILD_CORE_TESTS=OFF -DARK_BUILD_TESTS=OFF \ + -DARK_BUILD_CONFIG=OFF -DARK_BUILD_DATABASE_TESTS=ON \ + -DARKNET_ENABLE_SSL=ON +cmake --build database-build --target ark_database_public_plugin_cancellation_tests --parallel 1 +# After supplying private ARK_DATABASE_* environment variables: +database-build/tests/database/ark_database_public_plugin_cancellation_tests redis +database-build/tests/database/ark_database_public_plugin_cancellation_tests mysql +``` + +Redis uses `ARK_DATABASE_REDIS_HOST`, `PORT`, `PASSWORD`, and optional `USER`, each with the `ARK_DATABASE_REDIS_` prefix. MySQL uses `ARK_DATABASE_MYSQL_HOST`, `PORT`, `USER`, `PASSWORD`, `DATABASE`, and `CA`. MySQL requires a trusted CA covering the IP, TLS enabled in the build/server, and an isolated account requiring SSL with `CREATE`, `INSERT`, `UPDATE`, `SELECT` and `DROP` rights confined to the fresh schema. Its observer sees the same account's connections; no global PROCESS privilege is needed. + +The Redis task sends one `INCR`/`BLPOP` pipeline. An independent named pool observes `CLIENT LIST` with the unique client name, blocking flag and command, and confirms the counter is one before calling the existing `task_group::cancel(id)`. The test requires the original `client_errc::cancelled`, `command_may_have_run=true`, no synthesized transport error, and valid retained partial replies if any. An explicit new PING proves recovery; the tagged command must retire and the counter must remain one through the bounded observation. Exclusive keys are then deleted. + +MySQL first creates a private one-row counter table. Its task executes one tagged `UPDATE ... SET n=n+1 WHERE id=1 AND SLEEP(10)=0`. The independent pool observes that statement in `information_schema.processlist` with `STATE='User sleep'` before cancelling. The original result must remain `connection_errc::cancelled`, `outcome_unknown=true` and `operation_aborted` transport status. The observer waits for the original server statement to retire before checking its final effect: zero or one is legitimate, more than one fails the no-replay check. This does not infer rollback from cancellation. An explicit new query verifies recovery; the private table is dropped afterward. + +Both cases require one owner-thread/shared-executor continuation and exactly one root completion with no coroutine exception, retirement of the managed task, bounded natural manager/native shutdown, repeated stop, and an owning cancelled result surviving plugin/module/manager destruction. Repeated cancellation of a pending task succeeds; cancellation of its retired ID fails. Throttling timers do not establish that a command is active: only server observations do. No forced frame destruction, `cancel_all`, server-termination mode, automatic write retry or production policy change is introduced. Native close success does not prove physical image unmapping. + +These are opt-in cancellation scenarios, separate from positive natural-drain modes and deadline/error fixtures. They do not establish Redis TLS, transaction cancellation, every failure race, external-service interoperability, cross-platform acceptance, hot update or capacity. Exact executed revision/runtime identities and cleanup evidence belong to the local handoff; this test description alone is not a passing run. diff --git a/tests/database/cancellation_CN.md b/tests/database/cancellation_CN.md new file mode 100644 index 000000000..26c62d6a1 --- /dev/null +++ b/tests/database/cancellation_CN.md @@ -0,0 +1,30 @@ +# 公开数据库插件取消验收 + +[English](cancellation.md) + +`ark_database_public_plugin_cancellation_tests` 通过 DLL 入口加载真实的隐藏符号数据库插件,宿主仅链接公开接口。现有 `check-database` 目标编译该程序,但不启动服务,也不执行真实服务模式。 + +只使用绑定 `127.0.0.1` 的自有认证服务、全新私有目录和独占键/数据库。运行前及正常认证停服前,核实可执行文件路径、子进程 PID、PID 文件与回环监听。不要对共享服务或生产服务执行这些写入。凭据仅通过私有文件/环境变量传递,不放入命令参数或上传证据。 + +```sh +cmake -S . -B database-build -G Ninja \ + -DARKNET_SOURCE_DIR=/path/to/the-pinned-arknet \ + -DARK_BUILD_APPLICATIONS=OFF -DARK_BUILD_PROTO=OFF \ + -DARK_BUILD_CORE_TESTS=OFF -DARK_BUILD_TESTS=OFF \ + -DARK_BUILD_CONFIG=OFF -DARK_BUILD_DATABASE_TESTS=ON \ + -DARKNET_ENABLE_SSL=ON +cmake --build database-build --target ark_database_public_plugin_cancellation_tests --parallel 1 +# 提供私有 ARK_DATABASE_* 环境变量后运行: +database-build/tests/database/ark_database_public_plugin_cancellation_tests redis +database-build/tests/database/ark_database_public_plugin_cancellation_tests mysql +``` + +Redis 使用带 `ARK_DATABASE_REDIS_` 前缀的 `HOST`、`PORT`、`PASSWORD`,以及可选的 `USER`。MySQL 使用带 `ARK_DATABASE_MYSQL_` 前缀的 `HOST`、`PORT`、`USER`、`PASSWORD`、`DATABASE`、`CA`。MySQL 要求可信 CA 覆盖该 IP、构建与服务均开启 TLS,并为独立账户设置 SSL 要求;`CREATE`、`INSERT`、`UPDATE`、`SELECT`、`DROP` 权限只授予新建的私有数据库。观察连接属于同一账户,无需全局 PROCESS 权限。 + +Redis 任务提交一次 `INCR`/`BLPOP` 流水线。独立命名连接池通过 `CLIENT LIST` 确认唯一客户端名称、阻塞标志与命令,并先验证计数为一,再调用现有 `task_group::cancel(id)`。用例要求保留原生 `client_errc::cancelled`、`command_may_have_run=true`、没有人为合成的传输错误;若已有部分回复,回复仍须有效。显式提交新的 PING 验证恢复;带标记的命令须退出,有限观察期内计数仍为一,随后删除独占键。 + +MySQL 先创建私有单行计数表,任务只执行一次带标记的 `UPDATE ... SET n=n+1 WHERE id=1 AND SLEEP(10)=0`。独立连接池通过 `information_schema.processlist` 中的 `STATE='User sleep'` 确认该语句正在服务端运行,再取消。原结果须保留 `connection_errc::cancelled`、`outcome_unknown=true` 与 `operation_aborted` 传输状态。观察者等待原服务端语句退出后才检查最终影响:零或一均符合未知结果,超过一则未通过不重放检查。这不把取消推断为回滚。显式提交新查询验证恢复,随后删除私有表。 + +两个模式均要求协程在所属线程/共享执行器继续、根任务恰好完成一次且没有协程异常、托管任务自然退出、管理器与原生资源在有界时间内正常停机、重复停机成功,以及拥有型取消结果在插件/模块/管理器销毁后仍可读取。重复取消未退出任务成功,取消已退出 ID 失败。定时器只降低轮询频率,服务端观察才是命令已运行的证据。本次不增加强制销毁协程帧、`cancel_all`、终止服务模式、自动写重试或生产策略变更。原生关闭成功不证明库映像已从物理地址空间移除。 + +这些是手动启用的取消场景,与正常排空模式及超时/错误 fixture 分开。结果不覆盖 Redis TLS、事务取消、全部错误竞争、外部服务互通、跨平台验收、热更新或容量。实际执行的精确源码/运行时身份与清理证据由本地交接记录提供;测试说明本身不代表执行通过。 diff --git a/tests/database/public_plugin_cancellation.cpp b/tests/database/public_plugin_cancellation.cpp new file mode 100644 index 000000000..9a5d221fe --- /dev/null +++ b/tests/database/public_plugin_cancellation.cpp @@ -0,0 +1,552 @@ +// Copyright (c) 2026 OpenArkStudio. Licensed under Apache-2.0. See LICENSE. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ark/plugin_manager.hpp" +#include "ark/redis/redis_module.hpp" +#include "ark/task.hpp" + +#if defined(ARKNET_ENABLE_SSL) +#include "ark/mysql/mysql_module.hpp" +#endif + +namespace { + +using namespace std::chrono_literals; + +class acceptance_failure : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + +void require(bool condition, const char* message) { + if (!condition) + { + throw acceptance_failure(message); + } +} + +std::string environment(const char* name, bool allow_empty = false) { + const auto* value = std::getenv(name); + require(value && (allow_empty || *value), "Missing database cancellation acceptance environment"); + return value; +} + +std::uint16_t port(const char* name) { + const auto text = environment(name); + unsigned number{}; + const auto parsed = std::from_chars(text.data(), text.data() + text.size(), number); + require(parsed.ec == std::errc{} && parsed.ptr == text.data() + text.size() && number && number <= 65535, + "Database cancellation port must be an integer between 1 and 65535"); + return static_cast(number); +} + +struct completion +{ + unsigned count{}; + bool on_owner{}; + std::exception_ptr error; +}; + +// Public interfaces only: no implementation/factory object is linked into this host. +class plugin_host +{ +public: + ark::plugin_manager manager; + ark::task_group* tasks{}; + + explicit plugin_host(const char* library) { + ark::logger::init(nullptr); + ark::logger::instance()->set_log_func([](spdlog::level::level_enum, const std::string&) {}); + require(manager.start(0, ARK_DATABASE_PLUGIN_DIR, "", {{library, ""}}), "Database DSO startup failed"); + require(manager.add_module("cancel_consumer", &consumer_), "Cannot register cancellation consumer"); + runtime_ = manager.get_task_runtime(); + tasks = &manager.create_task_group(consumer_); + } + + ~plugin_host() { + if (!stopped_) + { + try + { stop(); } + catch (...) + { + std::cerr << "Database cancellation acceptance could not naturally drain\n" << std::flush; + std::_Exit(1); + } + } + } + + void check_executor() const { + require(std::this_thread::get_id() == owner_ && runtime_->get_executor().running_in_this_thread(), + "DSO cancellation continuation escaped owner thread/shared executor"); + } + + ark::task_group::task_id start(ark::task<> workflow, completion& record) { + const auto id = tasks->start(std::move(workflow), [this, &record](std::exception_ptr error) { + record.on_owner = std::this_thread::get_id() == owner_ && runtime_->get_executor().running_in_this_thread(); + record.error = std::move(error); + ++record.count; + }); + require(id != 0, "Database cancellation workflow admission failed"); + return id; + } + + static void check_completion(const completion& record) { + if (record.error) + { + std::rethrow_exception(record.error); + } + require(record.count == 1 && record.on_owner, "DSO root completion repeated or escaped its owner"); + } + + void pump_until(const std::function& done) { + const auto deadline = std::chrono::steady_clock::now() + 25s; + while (!done()) + { + require(std::chrono::steady_clock::now() < deadline, "Database cancellation acceptance exceeded bound"); + require(manager.update(0), "Database cancellation owner update failed"); + // Throttling only; server-visible state establishes cancellation readiness. + std::this_thread::sleep_for(1ms); + } + } + + void preflight(ark::task<> workflow) { + auto& record = preflight_completion_; + start(std::move(workflow), record); + pump_until([&] { return record.count != 0; }); + check_completion(record); + require(tasks->pending() == 0, "Preflight did not retire naturally"); + } + + void stop() { + const auto deadline = std::chrono::steady_clock::now() + 30s; + while (!(stopped_ = manager.stop(0))) + { + require(manager.get_shutdown_state() != ark::plugin_manager::shutdown_state::failed && + std::chrono::steady_clock::now() < deadline, + "Cancelled DSO requests did not naturally retire within the shutdown bound"); + require(manager.update(0), "Database cancellation drain update failed"); + std::this_thread::sleep_for(1ms); + } + require(manager.get_shutdown_state() == ark::plugin_manager::shutdown_state::stopped, + "Database cancellation shutdown did not reach stopped"); + require(tasks->closed() && tasks->pending() == 0 && manager.pending_tasks() == 0, + "DSO cancellation stop retained pending managed tasks"); + require(manager.stop(0), "Repeated database cancellation stop was not idempotent"); + manager.remove_module("cancel_consumer"); + } + +private: + completion preflight_completion_; + ark::module consumer_; + std::shared_ptr runtime_; + const std::thread::id owner_{std::this_thread::get_id()}; + bool stopped_{}; +}; + +ark::task<> observer_interval() { + asio::steady_timer timer(co_await asio::this_coro::executor); + timer.expires_after(20ms); + co_await timer.async_wait(asio::use_awaitable); +} + +struct cancellation_state +{ + ark::task_group::task_id request_id{}; + completion request, observer; + bool observed{}; + bool cancelled{}; + bool recovered{}; + bool retired_on_server{}; +}; + +void cancel_observed(plugin_host& host, cancellation_state& state) { + require(!state.cancelled && state.observed && state.request.count == 0, "Cancellation preceded active observation"); + require(host.tasks->cancel(state.request_id) && host.tasks->cancel(state.request_id), + "Accepted task cancellation or repeated pending cancellation failed"); + require(state.request.count == 0, "Cancellation fabricated synchronous root completion"); + state.cancelled = true; +} + +void redis_success(const arknet::redis::result& result) { + require(bool(result) && result.replies.size() == 1, "Explicit Redis observer/recovery operation failed"); +} + +std::string client_field(std::string_view line, std::string_view name) { + const auto marker = std::string(name) + "="; + for (std::size_t position{}; position < line.size();) + { + const auto end = line.find(' ', position); + const auto field = line.substr(position, end == std::string_view::npos ? end : end - position); + if (field.starts_with(marker)) + { + return std::string(field.substr(marker.size())); + } + if (end == std::string_view::npos) + { + break; + } + position = end + 1; + } + return {}; +} + +std::optional blocked_client(const arknet::redis::result& listing, const std::string& tag) { + redis_success(listing); + const auto& reply = listing.replies[0]; + require(reply.type == arknet::redis::value_type::bulk_string || + reply.type == arknet::redis::value_type::verbatim_string, + "Redis CLIENT LIST result was not text"); + std::string_view remaining = reply.text; + if (reply.type == arknet::redis::value_type::verbatim_string) + { + require(remaining.starts_with("txt:"), "Redis CLIENT LIST verbatim result had an unexpected format"); + remaining.remove_prefix(4); + } + std::optional found; + while (!remaining.empty()) + { + const auto end = remaining.find('\n'); + const auto line = remaining.substr(0, end); + if (client_field(line, "name") == tag && client_field(line, "cmd") == "blpop" && + client_field(line, "flags").find('b') != std::string::npos) + { + require(!found, "Redis observer saw concurrent replay of its tagged pipeline"); + found = client_field(line, "id"); + require(!found->empty(), "Redis observer could not identify the blocked client"); + } + if (end == std::string_view::npos) + { + break; + } + remaining.remove_prefix(end + 1); + } + return found; +} + +ark::task<> redis_preflight(plugin_host& host, ark::redis_module& database, const std::string& tag) { + arknet::redis::command remove_keys{"DEL", tag + "_counter", tag + "_list"}; + auto removed = co_await database.command("observer", std::move(remove_keys), 5s); + host.check_executor(); + redis_success(removed); + require(removed.replies[0].type == arknet::redis::value_type::integer && removed.replies[0].integer == 0, + "Exclusive Redis cancellation keys already existed"); + for (const auto* name : {"primary", "observer"}) + { + arknet::redis::command ping_command{"PING"}; + auto ping = co_await database.command(name, std::move(ping_command), 5s); + host.check_executor(); + redis_success(ping); + require(ping.replies[0].text == "PONG", "Redis cancellation preflight failed"); + } +} + +ark::task<> redis_request( + plugin_host& host, ark::redis_module& database, std::string tag, std::optional& result) { + std::vector commands{{"INCR", tag + "_counter"}, {"BLPOP", tag + "_list", "30"}}; + result = co_await database.pipeline("primary", std::move(commands), 20s); + host.check_executor(); +} + +ark::task<> redis_observe(plugin_host& host, ark::redis_module& database, cancellation_state& state, + std::optional& result, std::string tag) { + std::optional original; + std::optional quiet_until; + const auto deadline = std::chrono::steady_clock::now() + 15s; + for (;;) + { + require(!state.request.error && std::chrono::steady_clock::now() < deadline, + "Redis cancellation raised a coroutine exception or exceeded the observation bound"); + arknet::redis::command clients{"CLIENT", "LIST"}; + auto listing = co_await database.command("observer", std::move(clients), 3s); + host.check_executor(); + const auto blocked = blocked_client(listing, tag); + if (blocked) + { + require(!quiet_until && (!original || *original == *blocked), "Redis replayed its tagged pipeline"); + original = *blocked; + state.observed = true; + if (!state.cancelled) + { + arknet::redis::command get_counter{"GET", tag + "_counter"}; + auto counter = co_await database.command("observer", std::move(get_counter), 3s); + host.check_executor(); + redis_success(counter); + require(counter.replies[0].text == "1", "Redis pre-cancel write was not applied exactly once"); + cancel_observed(host, state); + require(!result, "Redis cancellation fabricated an immediate native result"); + std::cout << "redis server-visible tagged BLPOP active; task cancellation requested\n"; + } + } + if (result && !state.recovered) + { + std::cout << "redis native cancellation code=" << static_cast(result->error) + << " may_have_run=" << result->command_may_have_run + << " transport=" << result->transport_error.value() << " partial=" << result->replies.size() + << '\n'; + require(state.observed && state.cancelled && result->error == arknet::redis::client_errc::cancelled && + result->command_may_have_run && !result->transport_error && result->replies.size() <= 1, + "Redis cancellation lost its original sent/uncertain native result semantics"); + if (!result->replies.empty()) + { + require( + result->replies[0].type == arknet::redis::value_type::integer && result->replies[0].integer == 1, + "Redis cancellation changed its retained partial reply"); + } + arknet::redis::command recovery_ping{"PING"}; + auto recovered = co_await database.command("primary", std::move(recovery_ping), 5s); + host.check_executor(); + redis_success(recovered); + require(recovered.replies[0].text == "PONG", "Redis explicit follow-up could not recover"); + state.recovered = true; + } + if (!blocked && state.recovered) + { + state.retired_on_server = true; + if (!quiet_until) + { + quiet_until = std::chrono::steady_clock::now() + 500ms; + } + if (std::chrono::steady_clock::now() >= *quiet_until) + { + break; + } + } + co_await observer_interval(); + host.check_executor(); + } + arknet::redis::command get_counter{"GET", tag + "_counter"}; + auto counter = co_await database.command("observer", std::move(get_counter), 3s); + host.check_executor(); + redis_success(counter); + require(counter.replies[0].text == "1", "Redis implicitly replayed the cancelled pipeline write"); + arknet::redis::command remove_keys{"DEL", tag + "_counter", tag + "_list"}; + auto removed = co_await database.command("observer", std::move(remove_keys), 3s); + host.check_executor(); + redis_success(removed); + std::cout << "redis cancelled result, server retirement and counter=1 verified\n"; +} + +void check_redis() { + ark::redis_options options; + options.pool.connection.host = environment("ARK_DATABASE_REDIS_HOST"); + require(options.pool.connection.host == "127.0.0.1", "Redis cancellation requires an owned IPv4 loopback service"); + options.pool.connection.port = std::to_string(port("ARK_DATABASE_REDIS_PORT")); + options.pool.connection.password = environment("ARK_DATABASE_REDIS_PASSWORD"); + if (const auto* user = std::getenv("ARK_DATABASE_REDIS_USER")) + { + options.pool.connection.username = user; + } + options.pool.max_connections = 1; + options.pool.client_configuration.connect_timeout = 3s; + options.pool.client_configuration.drain_timeout = 100ms; + const auto tag = "ark_dso_cancel_" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + options.pool.connection.client_name = tag; + cancellation_state state; + std::optional result; + { + plugin_host host("ark_database_redis_plugin"); + auto* database = host.manager.find_module(); + require(database && database->configure("primary", options), "Cannot configure Redis cancellation primary"); + options.pool.connection.client_name = tag + "_observer"; + require(database->configure("observer", options), "Cannot configure Redis cancellation observer"); + host.preflight(redis_preflight(host, *database, tag)); + state.request_id = host.start(redis_request(host, *database, tag, result), state.request); + host.start(redis_observe(host, *database, state, result, tag), state.observer); + host.pump_until([&] { return state.request.count && state.observer.count; }); + plugin_host::check_completion(state.request); + plugin_host::check_completion(state.observer); + require(!host.tasks->cancel(state.request_id), "Retired Redis task still accepted cancellation"); + host.stop(); + require(state.request.count == 1 && state.observer.count == 1, "Redis completion repeated during shutdown"); + } + require(state.observed && state.cancelled && state.recovered && state.retired_on_server && result && + result->error == arknet::redis::client_errc::cancelled && result->command_may_have_run, + "Redis native cancellation result did not survive module/manager destruction"); +} + +#if defined(ARKNET_ENABLE_SSL) +void mysql_success(const ark::mysql_reply& reply) { + require(bool(reply), "Explicit MySQL observer/recovery operation failed"); +} + +ark::task<> mysql_preflight(plugin_host& host, ark::mysql_module& database, std::string table) { + auto created = + co_await database.query("primary", "CREATE TABLE `" + table + "` (id INT PRIMARY KEY,n BIGINT NOT NULL)", 5s); + host.check_executor(); + mysql_success(created); + auto seeded = co_await database.query("primary", "INSERT INTO `" + table + "` VALUES (1,0)", 5s); + host.check_executor(); + mysql_success(seeded); + auto observer = co_await database.query("observer", "SELECT 1", 5s); + host.check_executor(); + mysql_success(observer); +} + +ark::task<> mysql_request( + plugin_host& host, ark::mysql_module& database, std::string tag, std::optional& result) { + result = co_await database.query( + "primary", "UPDATE `" + tag + "` SET n=n+1 WHERE id=1 AND SLEEP(10)=0 /*" + tag + "*/", 20s); + host.check_executor(); +} + +ark::task<> mysql_observe(plugin_host& host, ark::mysql_module& database, cancellation_state& state, + std::optional& result, std::string tag) { + std::optional original; + std::optional quiet_until; + const auto deadline = std::chrono::steady_clock::now() + 18s; + const auto sql = "SELECT CAST(ID AS SIGNED), STATE FROM information_schema.processlist " + "WHERE ID <> CONNECTION_ID() AND INFO LIKE '%" + + tag + "%'"; + for (;;) + { + require(!state.request.error && std::chrono::steady_clock::now() < deadline, + "MySQL cancellation raised a coroutine exception or exceeded the server-retirement bound"); + auto listing = co_await database.query("observer", sql, 3s); + host.check_executor(); + mysql_success(listing); + require(listing.result.rows.size() <= 1, "MySQL observer saw concurrent replay of its tagged UPDATE"); + if (!listing.result.rows.empty()) + { + require(listing.result.rows[0].size() == 2, "MySQL observer lost its ID/state fields"); + const auto id = std::get(listing.result.rows[0][0]); + require(!quiet_until && (!original || *original == id), "MySQL replayed its tagged UPDATE"); + original = id; + const auto* phase = std::get_if(&listing.result.rows[0][1]); + if (!state.cancelled && phase && *phase == "User sleep") + { + state.observed = true; + cancel_observed(host, state); + require(!result, "MySQL cancellation fabricated an immediate native result"); + std::cout << "mysql server-visible tagged UPDATE active; task cancellation requested\n"; + } + } + if (result && !state.recovered) + { + std::cout << "mysql native cancellation code=" << static_cast(result->result.status.code) + << " outcome_unknown=" << result->result.status.outcome_unknown + << " transport=" << result->result.status.transport_error.value() << '\n'; + require(state.observed && state.cancelled && + result->result.status.code == arknet::mysql::connection_errc::cancelled && + result->result.status.outcome_unknown && + result->result.status.transport_error == asio::error::operation_aborted, + "MySQL cancellation lost its original sent/unknown native result semantics"); + auto recovered = co_await database.query("primary", "SELECT 9", 5s); + host.check_executor(); + mysql_success(recovered); + require(recovered.result.rows.size() == 1 && std::get(recovered.result.rows[0][0]) == 9, + "MySQL explicit follow-up could not recover"); + state.recovered = true; + } + if (listing.result.rows.empty() && state.recovered) + { + state.retired_on_server = true; + if (!quiet_until) + { + quiet_until = std::chrono::steady_clock::now() + 500ms; + } + if (std::chrono::steady_clock::now() >= *quiet_until) + { + break; + } + } + co_await observer_interval(); + host.check_executor(); + } + // Cancellation is not rollback. Inspect effects only after the server query retired. + auto counter = co_await database.query("observer", "SELECT n FROM `" + tag + "` WHERE id=1", 3s); + host.check_executor(); + mysql_success(counter); + require(state.retired_on_server && counter.result.rows.size() == 1 && counter.result.rows[0].size() == 1, + "MySQL final effect was inspected before tagged query retirement"); + const auto value = std::get(counter.result.rows[0][0]); + require(value == 0 || value == 1, "MySQL implicitly replayed the cancelled UPDATE"); + auto dropped = co_await database.query("observer", "DROP TABLE `" + tag + "`", 3s); + host.check_executor(); + mysql_success(dropped); + std::cout << "mysql cancelled unknown result, server retirement and final counter=" << value << " verified\n"; +} + +void check_mysql() { + ark::mysql_options options; + options.pool.connection.host = environment("ARK_DATABASE_MYSQL_HOST"); + require(options.pool.connection.host == "127.0.0.1", "MySQL cancellation requires an owned IPv4 loopback service"); + options.pool.connection.port = port("ARK_DATABASE_MYSQL_PORT"); + options.pool.connection.username = environment("ARK_DATABASE_MYSQL_USER"); + options.pool.connection.password = environment("ARK_DATABASE_MYSQL_PASSWORD", true); + options.pool.connection.database = environment("ARK_DATABASE_MYSQL_DATABASE"); + options.pool.connection.ca_file = environment("ARK_DATABASE_MYSQL_CA"); + options.pool.connection.tls = arknet::mysql::tls_mode::required; + options.pool.connection.timeout = 5s; + options.pool.max_connections = 1; + const auto tag = "ark_dso_cancel_" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + cancellation_state state; + std::optional result; + { + plugin_host host("ark_database_mysql_plugin"); + auto* database = host.manager.find_module(); + require(database && database->configure("primary", options), "Cannot configure MySQL cancellation primary"); + require(database->configure("observer", options), "Cannot configure MySQL cancellation observer"); + host.preflight(mysql_preflight(host, *database, tag)); + state.request_id = host.start(mysql_request(host, *database, tag, result), state.request); + host.start(mysql_observe(host, *database, state, result, tag), state.observer); + host.pump_until([&] { return state.request.count && state.observer.count; }); + plugin_host::check_completion(state.request); + plugin_host::check_completion(state.observer); + require(!host.tasks->cancel(state.request_id), "Retired MySQL task still accepted cancellation"); + host.stop(); + require(state.request.count == 1 && state.observer.count == 1, "MySQL completion repeated during shutdown"); + } + require(state.observed && state.cancelled && state.recovered && state.retired_on_server && result && + result->result.status.code == arknet::mysql::connection_errc::cancelled && + result->result.status.outcome_unknown, + "MySQL native cancellation result did not survive module/manager destruction"); +} +#endif + +} // namespace + +int main(int argc, char** argv) { + try + { + require(argc == 2, "Usage: ark_database_public_plugin_cancellation_tests redis|mysql"); + const std::string_view protocol(argv[1]); + if (protocol == "redis") + { + check_redis(); + } +#if defined(ARKNET_ENABLE_SSL) + else if (protocol == "mysql") + { + check_mysql(); + } +#endif + else + { + throw acceptance_failure("Requested database cancellation protocol is not enabled"); + } + std::cout << protocol << " real DSO request cancellation, owner completion and natural retirement passed\n"; + return 0; + } + catch (const acceptance_failure& error) + { std::cerr << error.what() << '\n'; } + catch (...) + { std::cerr << "Database DSO cancellation acceptance raised an unexpected coroutine/native exception\n"; } + return 1; +}