diff --git a/docs/cn/rdma.md b/docs/cn/rdma.md index fd70686bb1..7dd2689156 100644 --- a/docs/cn/rdma.md +++ b/docs/cn/rdma.md @@ -1,3 +1,8 @@ +# 运行期故障恢复 + +模式切换、端口与对端冷却及 TCP 回落见 +[RDMA 与 URMA 运行期故障恢复](rdma_runtime_recovery.md)。 + # 编译 由于RDMA对驱动与硬件有要求,目前仅支持在Linux系统编译并运行RDMA功能。 diff --git a/docs/cn/rdma_runtime_recovery.md b/docs/cn/rdma_runtime_recovery.md new file mode 100644 index 0000000000..0a2f71e313 --- /dev/null +++ b/docs/cn/rdma_runtime_recovery.md @@ -0,0 +1,78 @@ + + +# RDMA 与 URMA 运行期故障恢复 + +运行期故障后,恢复策略控制后续连接的传输选择: + +故障上报 → 对端或端口冷却 → 新连接使用 TCP → 冷却结束后恢复准入。 + +## 模式 + +| 模式 | 新连接行为 | +| --- | --- | +| LEGACY(默认) | 沿用原有握手和错误处理 | +| AUTO | 按端口和对端状态准入,冷却期间选择 TCP | +| TCP_ONLY | 直接选择 TCP | + +模式切换作用于新连接,存量连接继续使用已协商的传输。 +当前 RPC 沿用 Controller 的重试次数和 deadline。 + +## 冷却与错误处理 + +默认冷却 5 秒,使用单调时钟。客户端按服务端 IP 和端口记录对端状态, +缓存上限为 1024 项,满时淘汰最早到期项。RDMA 和 URMA 独立维护状态与指标, +模式切换递增 epoch,过滤旧连接的故障上报。 + +| 事件 | 处理 | +| --- | --- | +| RDMA PORT_ERR | 暂停当前端口的新 RDMA 连接,记录端口冷却 | +| RDMA PORT_ACTIVE | 更新端口状态,冷却结束后恢复准入 | +| 重试耗尽、RNR、QP/CQ/JFC 错误、WR 提交失败 | 上报故障,AUTO 下冷却客户端对端 | +| 访问错误 | 单独计数 | +| WR flush | 沿用已记录的根因 | +| SQ 或接收 credit 耗尽 | 返回 EAGAIN,等待 completion 或 credit | + +同一连接只记录首个运行期故障。QP/CQ 异步事件核对资源归属,故障资源释放。 +URMA 握手支持 v2/v3;短连接关闭后回收队列和接收缓冲,TCP 解析与关闭检查串行执行。 + +## 配置 + +```cpp +#include "brpc/rdma/rdma_recovery_policy.h" +#include "brpc/urma/urma_recovery_policy.h" + +brpc::rdma::SetRdmaRecoveryCooldownMs(5000); +brpc::rdma::SetRdmaRecoveryMode(brpc::rdma::RdmaRecoveryMode::AUTO); + +brpc::urma::SetUrmaRecoveryCooldownMs(5000); +brpc::urma::SetUrmaRecoveryMode(brpc::urma::UrmaRecoveryMode::AUTO); +``` + +运行时调用对应 setter 切换 `LEGACY`、`AUTO` 或 `TCP_ONLY`。 +URMA 使用 CTP/RTP 路径,设备选择见 [URMA 配置](urma.md#传输路径选择)。 + +## 指标与验证 + +`GetRdmaRecoveryStatus()`、`GetUrmaRecoveryStatus()` 及 `/vars/rdma_recovery_*`、 +`/vars/urma_recovery_*` 提供模式、epoch、分原因故障计数、冷却状态、 +传输协商数和 TCP 准入数。TCP 准入按连接累计。 + +- [RDMA 双机测试](../../example/rdma_performance/RECOVERY.md):模式切换、端口和对端故障。 +- [URMA 定向注入](urma.md#定向发送失败测试):一次 SEND 提交失败、TCP 冷却和 URMA 恢复。 diff --git a/docs/cn/urma.md b/docs/cn/urma.md index 62e10389a8..48c822c622 100644 --- a/docs/cn/urma.md +++ b/docs/cn/urma.md @@ -11,7 +11,7 @@ UrmaTransport 是使用 openEuler ## 技术背景 URMA 在 UMDK 支持的设备上提供 verbs 风格接口。当前实现创建可靠消息 -(`URMA_TM_RM`)Jetty,并使用 CTP 传输路径;通过 +(`URMA_TM_RM`)Jetty,并使用 CTP 或 RTP 传输路径;通过 `urma_post_jetty_send_wr` 提交发送 WR,通过 `urma_post_jfr_wr` 提交接收 WR。完成事件既可由 JFC 忙轮询获取,也可通过 JFCE 事件 fd 获取。 @@ -20,22 +20,17 @@ WR。完成事件既可由 JFC 忙轮询获取,也可通过 JFCE 事件 fd 获 ### CMake 编译 ```bash -# 带 URMA 支持编译 brpc -cmake -B build -DWITH_URMA=ON -make -C build -j$(nproc) - -# 编译 urma_performance 示例 -cd example/urma_performance -cmake -B build -make -C build -j$(nproc) +cmake -S . -B build -DWITH_URMA=ON +cmake --build build -j8 +cmake --install build --prefix "$PWD/build/install" +cmake -S example/urma_performance -B build/urma-example \ + -DCMAKE_PREFIX_PATH="$PWD/build/install" +cmake --build build/urma-example -j8 ``` -`WITH_URMA=ON` 使用上游 UMDK 头文件进行编译。CMake 优先使用系统安装的 -SDK;找不到头文件时,会参照 Mooncake 的 mock 构建方式下载固定版本的 -UMDK commit,可通过 `DOWNLOAD_URMA_HEADERS=OFF` 禁止下载。找到 `liburma` 时使用 -真实硬件数据通路,否则链接 brpc 的 mock,使 URMA 代码和测试仍可在无硬件 -环境编译。`URMA_USE_MOCK` 默认为 `AUTO`;设为 `ON` 可强制使用 mock,设为 -`OFF` 则要求找到 `liburma` 并排除 mock。 +CMake 优先使用系统 UMDK 头文件,缺失时下载固定版本; +`DOWNLOAD_URMA_HEADERS=OFF` 关闭下载。`URMA_USE_MOCK=AUTO` 按 `liburma` 选择实现, +`ON` 使用 mock,`OFF` 使用硬件库。示例通过 `URMA_ROOT` 或 `URMA_INCLUDE_PATH` 指定 SDK 头文件。 ### Make 编译 @@ -60,7 +55,7 @@ sh config_brpc.sh --headers=/usr/include --libs=/usr/lib \ # 编译带 URMA 支持的 brpc bazel build --define=BRPC_WITH_URMA=true //:brpc -# 仅展示关闭下载的开关;头文件仍须由工具链或本地 BUILD 目标声明 +# 使用本地声明的 SDK 头文件 bazel build --define=BRPC_WITH_URMA=true \ --define=BRPC_DOWNLOAD_URMA_HEADERS=false //:brpc @@ -73,14 +68,9 @@ bazel build --define=BRPC_WITH_URMA=true \ --define=BRPC_URMA_USE_MOCK=false //:brpc ``` -Bazel 会自动获取固定 commit 的 UMDK 头文件,并通过 `@umdk//:urma_headers` -提供给 URMA 目标。设置 `BRPC_DOWNLOAD_URMA_HEADERS=false` 后,Bazel 不再依赖 -该外部仓库;实际构建仍需通过工具链或本地 BUILD 目标声明 -`urma_api.h`、`urma_types.h` 和 `urma_ubagg.h`。仅通过 -`--copt=-I/path` 指向工作区外目录,在默认 Bazel 沙箱下不会把未声明的头文件 -带入编译输入。设置 -`BRPC_URMA_USE_MOCK=true` 强制加入 mock;设置为 `false` 则排除 mock 并链接 -`liburma`。未指定时,URMA 构建默认使用 mock。 +Bazel 通过 `@umdk//:urma_headers` 使用固定版本的 UMDK 头文件。 +关闭下载时,由工具链或本地 BUILD 目标声明 `urma_api.h`、`urma_types.h` 和 `urma_ubagg.h`。 +`BRPC_URMA_USE_MOCK=true` 使用 mock,`false` 链接 `liburma`;默认使用 mock。 ## 使用 @@ -130,19 +120,15 @@ urma::UrmaEndpoint : public SocketUser (urma/urma_endpoint.{h,cpp}) 3. 双方在 TCP fd 上交换 `UrmaHello` 消息(v2 二进制 magic `URMA`, v3 protobuf magic `URM3`),携带本地 EID、jetty id、recv buffer 数量、 以及扁平化的 buffer 池 segment。 -4. 双方先调用 `urma_import_seg` **再**调用 `urma_import_jetty`,为远端 EID - 建立传输路径(TP)路由。跳过 `import_seg` 会导致首个 SEND 被硬件以 - `URMA_CR_RNR_RETRY_CNT_EXC_ERR` 拒绝。 +4. 双方依次调用 `urma_import_seg` 和 `urma_import_jetty`,建立远端路由。 5. 4 字节 ACK(`HELLO_ACK_URMA_OK = 0x1`)确认双方均要 URMA。 6. 成功后 TCP fd 仅保留用于 epoll 生命周期和回退,数据走 URMA。 ### 内存管理 -申请一大段 `mmap` 内存,用 `urma_register_seg` 一次性注册,再切成固定大小 -buffer(默认 8KB)。劫持 `butil::iobuf::blockmem_allocate` 使每个 IOBuf -block 都由注册 segment 支撑,发送路径可直接从 IOBuf block refs 构建 -`urma_sge_t`,无需逐消息注册(与 RDMA `block_pool` 设计一致)。用户注册 -内存通过 `urma::RegisterMemoryForUrma` / `DeregisterMemoryForUrma` 支持。 +通过 `mmap` 分配内存,调用 `urma_register_seg` 注册后切为固定大小的缓冲区(默认 8KB)。 +IOBuf 使用注册池分配器,发送路径从 block refs 构建 `urma_sge_t`。 +用户内存通过 `RegisterMemoryForUrma` / `DeregisterMemoryForUrma` 管理。 ## 配置 @@ -157,8 +143,10 @@ block 都由注册 segment 支撑,发送路径可直接从 IOBuf block refs | `--urma_rq_size` | 128 | 本地 JFR 深度 [16, 4096] | | `--urma_cqe_poll_once` | 32 | 每次 `urma_poll_jfc` 的上限 | | `--urma_recv_zerocopy` | true | 大于 `--urma_zerocopy_min_size` 的接收零拷贝 | +| `--urma_copy_send` | false | 将主机发送数据复制合并到 64 字节对齐的注册缓冲区 | | `--urma_zerocopy_min_size` | 512 | 小于此值的接收拷贝 | | `--urma_device` | "" | URMA 设备名(空=首个) | +| `--urma_tp_type` | auto | RM 路径:auto 优先 CTP,其次 RTP;ctp 或 rtp 显式选择路径 | | `--urma_max_sge` | 0 | 每 WR SGE 上限(0=设备上限) | | `--urma_bonding_mode` | 0 | bonding 模式:0=standalone,1=active-backup,2=balance | | `--urma_bonding_level` | 0 | bonding 层级:0=IODIE,1=port | @@ -172,6 +160,15 @@ block 都由注册 segment 支撑,发送路径可直接从 IOBuf block refs 前配置 provider。默认 standalone+IODIE 配置与 UMDK 性能工具保持一致。 bonding 支持需要 provider 扩展头文件 `urma_ubagg.h`。 +### 传输路径选择 + +`--urma_tp_type=auto` 按设备能力优先选择 CTP,其次 RTP;`ctp`、`rtp` 显式指定路径。 +两端使用相同路径,进程初始化后保持固定。Jetty 取首个匹配优先级,支持组合能力位; +设备映射决定 SL,例如 priority 0 映射 SL 2。日志记录实际 `tp_type` 和 `priority`。 + +设备未提供能力表时,auto/ctp 使用 CTP priority 6,rtp 使用 RTP priority 0; +路径选择失败时初始化回落 TCP。 + ## 与 UBRing 协同 UrmaTransport 推荐用于**大包和跨节点**高吞吐路径,而 UBRing @@ -185,11 +182,70 @@ UrmaTransport 推荐用于**大包和跨节点**高吞吐路径,而 UBRing | 跨节点大包(>= 64KB) | `SOCKET_MODE_URMA` | | 传统 RoCE/IB 数据中心 | `SOCKET_MODE_URMA` 或 `SOCKET_MODE_RDMA` | -单连接内按包大小自动分流的方案(路线 C 方案 B)作为后续演进方向。 +## 运行环境 + +Linux、`baidu_std` 协议,以及支持目标设备的 UMDK provider 和 `liburma`。 + +## 运行期故障恢复 + +恢复策略按进程配置,作用于新连接: + +| 模式 | 行为 | +| --- | --- | +| LEGACY(默认) | 沿用原有握手和错误处理 | +| AUTO | 对端故障后冷却,期间使用 TCP,到期恢复 URMA 准入 | +| TCP_ONLY | 直接使用 TCP | + +```cpp +#include "brpc/urma/urma_recovery_policy.h" + +brpc::urma::SetUrmaRecoveryCooldownMs(5000); +brpc::urma::SetUrmaRecoveryMode(brpc::urma::UrmaRecoveryMode::AUTO); +``` + +默认冷却 5 秒,故障来源包括重试耗尽、RNR、JFC 错误和 WR 提交失败。 +同一连接记录首个运行期故障;访问错误单独计数,窗口耗尽保持 EAGAIN。 +URMA 与 RDMA 独立维护模式、epoch 和冷却缓存,模式切换过滤旧连接上报。 + +`GetUrmaRecoveryStatus()` 和 `/vars/urma_recovery_status` 提供故障原因、冷却状态及协商数; +`/vars/urma_recovery_tcp_fallback_admissions` 按连接累计 TCP 准入。 + +### 功能验证 + +使用上文构建的 `urma_performance_server` 和 `urma_recovery_client`,两端指定相同设备路径。 +客户端示例: + +```bash +./build/urma-example/urma_recovery_client --transport=urma \ + --server=SERVER_IP:8003 --urma_device=URMA_DEVICE --urma_tp_type=rtp \ + --mode=auto --connection_type=short --seconds=15 --interval_ms=200 +``` + +健康 AUTO 检查 `failure=0`、URMA 协商增加、TCP 准入为 0; +TCP_ONLY 检查 `failure=0`、TCP 准入增加。运行中通过 `--mode_file` 切换模式。 +使用 `--connection_type=single` 或 `pooled` 验证连接复用,`--attachment_size=1048576` +验证 1 MiB 回显附件;客户端逐字节校验,汇总输出 `validation_failures`。 + +### 定向发送失败测试 + +启用 Linux 注入模块,在指定客户端模拟一次数据 SEND 提交失败(EIO): + +```bash +cmake -S example/urma_performance -B build/urma-example -DBUILD_URMA_FAULT_INJECTOR=ON +cmake --build build/urma-example --target urma_send_failure_injector -j8 +python3 example/urma_performance/run_recovery_injection.py \ + --server=SERVER_IP:8003 --device=URMA_DEVICE --tp-type=rtp +``` + +运行器等待 URMA RPC 成功后注入,校验指标 PID,保存 CSV、日志和三阶段快照。 +默认 v3 握手、运行 40 秒、冷却 10 秒;验收 `send_post=1` 和冷却期间 TCP 准入。 +`short` 检查一次预期 RPC 失败和冷却后 URMA 协商恢复;`--connection-type=single` 或 +`pooled` 检查重连后 RPC 持续成功,复用已建立的 TCP 连接。 +运行器支持 `--attachment-size` 回显校验和 `--polling` 轮询模式。 +构建 `urma_send_failure_injector_test` 后,通过示例目录的 CTest 执行工具自测。 -## 限制 +### 发送访问错误对照 -- 仅支持 `baidu_std` 协议(与 RDMA 一致)。SSL、RTMP、NSHEAD、MONGO 在 - `ContextInitOrDie` 阶段拒绝。 -- 硬件数据通路需要受支持的 UMDK provider 和 `liburma`。 -- 当前实现面向 Linux。 +`--urma_copy_send=true` 将每个数据 WR 合并到 64 字节对齐的注册缓冲区,并持有至完成, +增加一次 CPU 拷贝。两端保持设备、队列和消息大小一致,对比原发送路径与复制路径。 +初始化日志记录 `copy_send`,失败日志记录完成方向、槽位及注册段范围。 diff --git a/docs/en/urma.md b/docs/en/urma.md index a5b80f45b9..03cfc19098 100644 --- a/docs/en/urma.md +++ b/docs/en/urma.md @@ -10,7 +10,7 @@ for large-packet / cross-node scenarios (Route C, "double backend"). ## Technical Background URMA exposes a verbs-style API over devices supported by UMDK. This -implementation creates a reliable-message (`URMA_TM_RM`) Jetty with a CTP +implementation creates a reliable-message (`URMA_TM_RM`) Jetty with a CTP or RTP transport path, posts send work requests with `urma_post_jetty_send_wr`, and posts receive work requests with `urma_post_jfr_wr`. Completions are consumed from a JFC either by busy polling or through a JFCE event fd. @@ -20,24 +20,18 @@ from a JFC either by busy polling or through a JFCE event fd. ### Build with CMake ```bash -# Build brpc with URMA support -cmake -B build -DWITH_URMA=ON -make -C build -j$(nproc) - -# Build the urma_performance example -cd example/urma_performance -cmake -B build -make -C build -j$(nproc) +cmake -S . -B build -DWITH_URMA=ON +cmake --build build -j8 +cmake --install build --prefix "$PWD/build/install" +cmake -S example/urma_performance -B build/urma-example \ + -DCMAKE_PREFIX_PATH="$PWD/build/install" +cmake --build build/urma-example -j8 ``` -`WITH_URMA=ON` compiles against upstream UMDK headers. CMake prefers an -installed SDK and, following Mooncake's mock setup, downloads UMDK at a pinned -commit when the headers are unavailable. Set `DOWNLOAD_URMA_HEADERS=OFF` to -disable downloading. -When `liburma` is found it is linked for the hardware data path. Otherwise, -brpc uses its link-time mock so URMA code and tests can still be built without -hardware. `URMA_USE_MOCK` defaults to `AUTO`; set it to `ON` to force the mock, -or to `OFF` to require `liburma` and exclude the mock. +CMake uses installed UMDK headers or downloads a pinned version. +`DOWNLOAD_URMA_HEADERS=OFF` disables downloading. `URMA_USE_MOCK=AUTO` selects by +`liburma` availability; `ON` uses the mock and `OFF` uses the hardware library. +Set `URMA_ROOT` or `URMA_INCLUDE_PATH` to select SDK headers for the example. ### Build with Make @@ -63,7 +57,7 @@ requires `liburma` in the configured library paths. # Build brpc with URMA support bazel build --define=BRPC_WITH_URMA=true //:brpc -# This only shows the download switch; headers must still be declared by the toolchain or a local BUILD target +# Use locally declared SDK headers bazel build --define=BRPC_WITH_URMA=true \ --define=BRPC_DOWNLOAD_URMA_HEADERS=false //:brpc @@ -76,15 +70,10 @@ bazel build --define=BRPC_WITH_URMA=true \ --define=BRPC_URMA_USE_MOCK=false //:brpc ``` -Bazel fetches the pinned UMDK commit and exposes its headers through -`@umdk//:urma_headers` by default. With -`BRPC_DOWNLOAD_URMA_HEADERS=false`, Bazel does not depend on that external -repository; the actual build must still declare `urma_api.h`, `urma_types.h`, -and `urma_ubagg.h` through the toolchain or a local BUILD target. Passing only -`--copt=-I/path` to a directory outside the workspace does not make undeclared -headers available inside Bazel's default sandbox. -`BRPC_URMA_USE_MOCK=true` forces the link-time mock, while `false` excludes it -and links `liburma`. If unspecified, URMA builds use the mock by default. +Bazel uses pinned UMDK headers through `@umdk//:urma_headers`. +With downloading disabled, declare `urma_api.h`, `urma_types.h` and `urma_ubagg.h` +through the toolchain or a local BUILD target. `BRPC_URMA_USE_MOCK=true` uses the +mock; `false` links `liburma`. The default is the mock. ## Usage @@ -137,23 +126,17 @@ Like RDMA / UBRing, the control plane is TCP and the data plane is URMA: 3. Both sides exchange a `UrmaHello` message (magic `URMA` for v2 binary, `URM3` for v3 protobuf) over the TCP fd. The message carries the local EID, jetty id, recv buffer count, and the flattened buffer-pool segment. -4. Each side calls `urma_import_seg` **before** `urma_import_jetty` to - establish transport-path (TP) routing for the remote EID. Skipping the - `import_seg` step causes the first SEND to be rejected by hardware with - `URMA_CR_RNR_RETRY_CNT_EXC_ERR`. +4. Each side calls `urma_import_seg`, then `urma_import_jetty`, to establish the remote route. 5. A 4-byte ACK (`HELLO_ACK_URMA_OK = 0x1`) confirms both sides want URMA. 6. On success, the TCP fd is kept only for the epoll lifecycle and fallback; payloads flow through URMA. ### Memory management -A single large region is `mmap`-ed and registered once with -`urma_register_seg`, then sliced into fixed-size buffers (default 8 KB). -`butil::iobuf::blockmem_allocate` is hijacked so every IOBuf block is backed -by the registered segment, allowing the send path to build `urma_sge_t` -directly from IOBuf block refs without per-message registration (mirroring the -RDMA `block_pool` design). User-registered memory is supported via -`urma::RegisterMemoryForUrma` / `DeregisterMemoryForUrma`. +A region allocated with `mmap` is registered with `urma_register_seg` and split +into fixed-size buffers (default 8 KB). IOBuf uses the registered-pool allocator; +the send path builds `urma_sge_t` from block refs. Manage user memory with +`RegisterMemoryForUrma` / `DeregisterMemoryForUrma`. ## Configuration @@ -168,8 +151,10 @@ All flags use the `urma_` prefix (mirroring RDMA's `rdma_` prefix): | `--urma_rq_size` | 128 | Local JFR depth [16, 4096] | | `--urma_cqe_poll_once` | 32 | Max CQEs per `urma_poll_jfc` | | `--urma_recv_zerocopy` | true | Zero-copy receives above `--urma_zerocopy_min_size` | +| `--urma_copy_send` | false | Copy and coalesce host payloads into a 64-byte aligned registered send buffer | | `--urma_zerocopy_min_size` | 512 | Receives smaller than this are copied | | `--urma_device` | "" | URMA device name (empty = first) | +| `--urma_tp_type` | auto | RM path: auto prefers CTP, then RTP; ctp or rtp selects a path explicitly | | `--urma_max_sge` | 0 | Max SGEs per WR (0 = device max) | | `--urma_bonding_mode` | 0 | Bonding mode: 0=standalone, 1=active-backup, 2=balance | | `--urma_bonding_level` | 0 | Bonding level: 0=IODIE, 1=port | @@ -184,6 +169,17 @@ immediately after context creation and before creating segments or queues. The default standalone+IODIE combination matches the UMDK performance tool. Bonding support requires the provider extension header `urma_ubagg.h`. +### Transport path selection + +`--urma_tp_type=auto` prefers CTP, then RTP, from device capabilities; `ctp` and +`rtp` select a path explicitly. Both peers use the same path, fixed at process +initialization. Jettys use the first matching priority, including combined +capability bits. The device maps priority to SL, such as priority 0 to SL 2. +Logs report the selected `tp_type` and `priority`. + +Providers without a capability table use CTP priority 6 for auto/ctp or RTP +priority 0 for rtp. Path selection failure uses initialization's TCP fallback. + ## Coexistence with UBRing UrmaTransport is the recommended transport for **large packets and @@ -198,12 +194,79 @@ syscalls). For a mixed workload, select the transport per service: | Cross-node large packets (>= 64 KB) | `SOCKET_MODE_URMA` | | Traditional RoCE / IB datacenter | `SOCKET_MODE_URMA` or `SOCKET_MODE_RDMA` | -A single-connection hybrid that auto-routes by packet size (Route C, scheme B) -is tracked as a future enhancement. +## Runtime environment + +Linux, the `baidu_std` protocol, and a UMDK provider with `liburma` for the target device. + +## Runtime recovery + +Configure recovery per process for new connections: + +| Mode | Behavior | +| --- | --- | +| LEGACY (default) | Existing handshake and error handling | +| AUTO | Cool a failed peer, use TCP during cooldown, then resume URMA admission | +| TCP_ONLY | Select TCP | + +```cpp +#include "brpc/urma/urma_recovery_policy.h" + +brpc::urma::SetUrmaRecoveryCooldownMs(5000); +brpc::urma::SetUrmaRecoveryMode(brpc::urma::UrmaRecoveryMode::AUTO); +``` -## Limitations +The default cooldown is five seconds. Sources include retry exhaustion, RNR, +JFC errors and WR submission failures. Each connection records its first runtime +failure; access errors have a separate counter, and exhausted windows return EAGAIN. +URMA and RDMA keep separate modes, epochs and cooldown caches. Mode changes filter +reports from older connections. + +`GetUrmaRecoveryStatus()` and `/vars/urma_recovery_status` expose failure reasons, +cooldown state and negotiation counts. `/vars/urma_recovery_tcp_fallback_admissions` +counts TCP admissions per connection. + +### Functional checks + +Use the `urma_performance_server` and `urma_recovery_client` built above with the +same path on both peers. Client example: + +```bash +./build/urma-example/urma_recovery_client --transport=urma \ + --server=SERVER_IP:8003 --urma_device=URMA_DEVICE --urma_tp_type=rtp \ + --mode=auto --connection_type=short --seconds=15 --interval_ms=200 +``` + +Healthy AUTO requires `failure=0`, increasing URMA negotiations and zero TCP +admissions. TCP_ONLY requires `failure=0` and increasing TCP admissions. +Use `--mode_file` to switch modes while running. +Use `--connection_type=single` or `pooled` for connection reuse and +`--attachment_size=1048576` for a 1 MiB echo attachment. The client checks every +byte and reports `validation_failures` in the summary. + +### Targeted send failure test + +Enable the Linux injector to reject one data SEND with EIO in the selected client: + +```bash +cmake -S example/urma_performance -B build/urma-example -DBUILD_URMA_FAULT_INJECTOR=ON +cmake --build build/urma-example --target urma_send_failure_injector -j8 +python3 example/urma_performance/run_recovery_injection.py \ + --server=SERVER_IP:8003 --device=URMA_DEVICE --tp-type=rtp +``` -- `baidu_std` protocol only (same as RDMA). SSL, RTMP, NSHEAD, MONGO are - rejected at `ContextInitOrDie`. -- A hardware data path requires a supported UMDK provider and `liburma`. -- The current implementation targets Linux. +The runner waits for successful URMA RPCs, checks the metric listener PID, and +saves CSV, logs and three phase snapshots. Defaults are v3, a 40-second run and +10-second cooldown. Checks require `send_post=1` and TCP admission during cooldown. +`short` requires one expected RPC failure and resumed URMA negotiations; +`--connection-type=single` or `pooled` checks sustained RPC recovery after +reconnecting and reusing TCP. The runner supports `--attachment-size` for echo +validation and `--polling` for polling mode. +Build `urma_send_failure_injector_test` and run CTest in the example build +directory for the tool self-tests. + +### Comparing send access failures + +`--urma_copy_send=true` coalesces each data WR into a 64-byte aligned registered +buffer held until completion, adding one CPU copy. Keep device, queue and payload +settings consistent when comparing send paths. Startup logs report `copy_send`; +failure logs report direction, send slot and registered segment range. diff --git a/example/BUILD.bazel b/example/BUILD.bazel index 05eda2080b..9049cf07d7 100644 --- a/example/BUILD.bazel +++ b/example/BUILD.bazel @@ -123,6 +123,14 @@ cc_binary( ], ) +cc_binary( + name = "rdma_recovery_client", + srcs = ["rdma_performance/recovery_client.cpp"], + includes = ["rdma_performance"], + copts = COPTS + ["-DBRPC_WITH_RDMA=1"], + deps = [":cc_rdma_performance_proto", "//:brpc"], +) + cc_binary( name = "ubring_performance_server", srcs = [ @@ -162,4 +170,4 @@ cc_binary( deps = [ "//:brpc", ], -) \ No newline at end of file +) diff --git a/example/rdma_performance/CMakeLists.txt b/example/rdma_performance/CMakeLists.txt index 213cff0f24..7e0e70f724 100644 --- a/example/rdma_performance/CMakeLists.txt +++ b/example/rdma_performance/CMakeLists.txt @@ -40,3 +40,7 @@ brpc_example_configure_target(server) target_link_libraries(client PRIVATE ${BRPC_LIB} ${DYNAMIC_LIB}) target_link_libraries(server PRIVATE ${BRPC_LIB} ${DYNAMIC_LIB}) + +add_executable(recovery_client recovery_client.cpp ${PROTO_SRC} ${PROTO_HEADER}) +brpc_example_configure_target(recovery_client) +target_link_libraries(recovery_client PRIVATE ${BRPC_LIB} ${DYNAMIC_LIB}) diff --git a/example/rdma_performance/Makefile b/example/rdma_performance/Makefile index c112f1c330..58b40acf8b 100644 --- a/example/rdma_performance/Makefile +++ b/example/rdma_performance/Makefile @@ -59,7 +59,7 @@ endif .PHONY:all ifdef WITH_RDMA -all: client server +all: client server recovery_client else all: endif @@ -67,7 +67,7 @@ endif .PHONY:clean clean: @echo "> Cleaning" - rm -rf client server $(PROTO_GENS) $(PROTO_OBJS) $(CLIENT_OBJS) $(SERVER_OBJS) + rm -rf client server recovery_client recovery_client.o $(PROTO_GENS) $(PROTO_OBJS) $(CLIENT_OBJS) $(SERVER_OBJS) client:$(PROTO_OBJS) $(CLIENT_OBJS) @echo "> Linking $@" @@ -85,6 +85,13 @@ else $(CXX) $(LIBPATHS) $(LINK_OPTIONS) -o $@ endif +recovery_client:$(PROTO_OBJS) recovery_client.o +ifneq ("$(LINK_SO)", "") + $(CXX) $(LIBPATHS) $(SOPATHS) $(LINK_OPTIONS_SO) -o $@ +else + $(CXX) $(LIBPATHS) $(LINK_OPTIONS) -o $@ +endif + %.pb.cc %.pb.h:%.proto @echo "> Generating $@" $(PROTOC) --cpp_out=. --proto_path=. $(PROTOC_EXTRA_ARGS) $< diff --git a/example/rdma_performance/RECOVERY.md b/example/rdma_performance/RECOVERY.md new file mode 100644 index 0000000000..cf3a18497e --- /dev/null +++ b/example/rdma_performance/RECOVERY.md @@ -0,0 +1,84 @@ + + +# RDMA 双机故障恢复测试 + +两台 Linux 主机运行服务端和 recovery_client,TCP 控制地址保持可达。 +记录设备、端口、GID、驱动和固件版本。 + +## 编译 + +在仓库根目录执行: + +```bash +cmake -S . -B build -DWITH_RDMA=ON -DBUILD_SHARED_LIBS=OFF +cmake --build build -j8 +cmake --install build --prefix "$PWD/build/install" +cmake -S example/rdma_performance -B build/recovery -DCMAKE_PREFIX_PATH="$PWD/build/install" +cmake --build build/recovery --target server recovery_client -j8 +``` + +## 运行与切换 + +服务端: + +```bash +./build/recovery/server --port=8002 --use_rdma=true --rdma_device=mlx5_0 --rdma_gid_index=3 +``` + +客户端,将 B_TCP_IP 替换为服务端地址: + +```bash +printf 'auto\n' > /tmp/brpc-rdma-mode +./build/recovery/recovery_client --server=B_TCP_IP:8002 --seconds=180 \ + --mode_file=/tmp/brpc-rdma-mode --connection_type=short \ + --rdma_device=mlx5_0 --rdma_gid_index=3 +``` + +在客户端另一终端切换模式并查看指标: + +```bash +printf 'tcp_only\n' > /tmp/brpc-rdma-mode +curl --noproxy '*' 'http://127.0.0.1:8001/vars/rdma_recovery_*' +printf 'auto\n' > /tmp/brpc-rdma-mode +``` + +`short` 每次调用建立连接,`single` 复用已协商的传输。 +CSV 每秒输出累计成功、失败、传输协商、TCP 准入及冷却状态。 +健康阶段检查 `failure=0`;注入阶段记录新增失败和恢复时延。 +`rdma_recovery_status` 提供分原因故障计数,TCP 准入指标按连接累计。 + +## 验收 + +| 场景 | 操作 | 预期 | +| --- | --- | --- | +| 正常通信 | AUTO 运行 | RPC 成功,RDMA 协商数增加 | +| 运行中降级 | 切换 TCP_ONLY | TCP 准入增加,RDMA 协商暂停 | +| 恢复 RDMA | 切回 AUTO | short 连接的 RDMA 协商恢复 | +| 端口故障 | 关闭 RDMA 测试端口 | port_active=false,后续连接使用 TCP | +| 端口恢复 | 恢复端口并等待冷却 | RDMA 准入恢复 | +| 对端故障 | 中断服务端 RDMA 数据路径 | 故障计数增加,对端冷却,后续连接使用 TCP | +| RNR | 暂停接收 WR 补充并突破 credit 保护 | rnr 计数增加,对端冷却 | + +使用 `single` 观察在途失败,使用 `short` 检查新连接的回落和恢复。 + +## URMA + +[URMA 测试入口](../../docs/cn/urma.md#运行期故障恢复)提供恢复客户端及 SEND 失败注入运行器, +支持 v2/v3 握手、模式切换、对端冷却、TCP 准入和 URMA 恢复。 diff --git a/example/rdma_performance/recovery_client.cpp b/example/rdma_performance/recovery_client.cpp new file mode 100644 index 0000000000..c0673620b7 --- /dev/null +++ b/example/rdma_performance/recovery_client.cpp @@ -0,0 +1,160 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include +#include +#include +#include +#include +#include "brpc/channel.h" +#include "brpc/controller.h" +#include "brpc/server.h" +#include "brpc/rdma/rdma_recovery_policy.h" +#include "brpc/urma/urma_recovery_policy.h" +#include "butil/time.h" +#include "test.pb.h" + +DEFINE_string(server, "127.0.0.1:8002", "Performance test server address"); +DEFINE_string(mode, "auto", "Initial mode: legacy, auto, tcp_only"); +DEFINE_string(mode_file, "", "Read a recovery mode from this file while running"); +DEFINE_string(connection_type, "short", "short exercises new admission on each RPC"); +DEFINE_bool(use_rdma, true, "Use RDMA; false runs a TCP smoke test"); +DEFINE_string(transport, "", "rdma, urma or tcp; empty uses --use_rdma"); +DEFINE_int32(seconds, 60, "Duration of the fault-injection run"); +DEFINE_int32(interval_ms, 100, "Pause between RPCs"); +DEFINE_int32(timeout_ms, 1000, "RPC deadline"); +DEFINE_int32(cooldown_ms, 5000, "Path cooldown"); +DEFINE_int32(dummy_port, 8001, "Local /vars port; 0 disables it"); +DEFINE_int32(attachment_size, 0, "Echo attachment bytes and verify the returned content"); + +static bool SetMode(const std::string& value) { + using brpc::rdma::RdmaRecoveryMode; + RdmaRecoveryMode mode; + if (value == "legacy") { + mode = RdmaRecoveryMode::LEGACY; + } else if (value == "auto") { + mode = RdmaRecoveryMode::AUTO; + } else if (value == "tcp_only") { + mode = RdmaRecoveryMode::TCP_ONLY; + } else { + return false; + } + return FLAGS_transport == "urma" ? brpc::urma::SetUrmaRecoveryMode(mode) : + brpc::rdma::SetRdmaRecoveryMode(mode); +} + +int main(int argc, char* argv[]) { + GFLAGS_NAMESPACE::ParseCommandLineFlags(&argc, &argv, true); + if (FLAGS_transport.empty()) { + FLAGS_transport = FLAGS_use_rdma ? "rdma" : "tcp"; + } + if (FLAGS_transport != "rdma" && FLAGS_transport != "urma" && + FLAGS_transport != "tcp") { + std::cerr << "Invalid transport\n"; + return 1; + } + const bool cooldown_ok = FLAGS_transport == "urma" ? + brpc::urma::SetUrmaRecoveryCooldownMs(FLAGS_cooldown_ms) : + brpc::rdma::SetRdmaRecoveryCooldownMs(FLAGS_cooldown_ms); + if (FLAGS_seconds <= 0 || FLAGS_interval_ms < 0 || FLAGS_timeout_ms <= 0 || + FLAGS_attachment_size < 0 || + !cooldown_ok || + !SetMode(FLAGS_mode)) { + std::cerr << "Invalid duration, interval, timeout, cooldown, mode or attachment size\n"; + return 1; + } + brpc::Channel channel; + brpc::ChannelOptions options; + options.socket_mode = FLAGS_transport == "urma" ? brpc::SOCKET_MODE_URMA : + FLAGS_transport == "rdma" ? brpc::SOCKET_MODE_RDMA : brpc::SOCKET_MODE_TCP; + options.connection_type = FLAGS_connection_type; + options.timeout_ms = FLAGS_timeout_ms; + options.connect_timeout_ms = FLAGS_timeout_ms; + options.max_retry = 0; + if (channel.Init(FLAGS_server.c_str(), &options) != 0) { + return 1; + } + if (FLAGS_dummy_port && brpc::StartDummyServerAt(FLAGS_dummy_port) != 0) { + return 1; + } + std::string mode = FLAGS_mode; + uint64_t succeeded = 0; + uint64_t failed = 0; + uint64_t validation_failed = 0; + std::string payload(static_cast(FLAGS_attachment_size), '\0'); + for (size_t i = 0; i < payload.size(); ++i) { + payload[i] = static_cast(i % 251); + } + const int64_t start = butil::monotonic_time_us(); + int64_t next_report = start; + std::cout << "seconds,mode,success,failure,transport_negotiated,tcp_fallback_admissions," + "cooled_peers,port_active,port_cooldown_ms\n"; + while (!brpc::IsAskedToQuit() && + butil::monotonic_time_us() - start < static_cast(FLAGS_seconds) * 1000000) { + if (!FLAGS_mode_file.empty()) { + std::ifstream input(FLAGS_mode_file); + std::string value; + if (input >> value && value != mode) { + if (SetMode(value)) { + mode = value; + } else { + std::cerr << "Ignoring invalid recovery mode: " << value << '\n'; + } + } + } + test::PerfTestRequest request; + request.set_echo_attachment(!payload.empty()); + test::PerfTestResponse response; + brpc::Controller cntl; + const uint64_t sequence = succeeded + failed; + for (size_t i = 0; i < payload.size() && i < sizeof(sequence); ++i) { + payload[i] = static_cast(sequence >> (i * 8)); + } + cntl.request_attachment().append(payload); + test::PerfTestService_Stub(&channel).Test(&cntl, &request, &response, nullptr); + if (cntl.Failed()) { + ++failed; + std::cerr << "RPC failure: " << cntl.ErrorText() << '\n'; + } else if (!payload.empty() && cntl.response_attachment().to_string() != payload) { + ++failed; + ++validation_failed; + std::cerr << "Attachment mismatch: expected=" << payload.size() + << " actual=" << cntl.response_attachment().size() << '\n'; + } else { + ++succeeded; + } + const int64_t now = butil::monotonic_time_us(); + if (now >= next_report) { + const auto s = FLAGS_transport == "urma" ? + brpc::urma::GetUrmaRecoveryStatus() : brpc::rdma::GetRdmaRecoveryStatus(); + std::cout << (now - start) / 1000000 << ',' << mode << ',' << succeeded + << ',' << failed << ',' << s.negotiated_connections << ',' + << s.tcp_only_admissions + s.port_cooldown_admissions + + s.peer_cooldown_admissions + << ',' << s.cooled_peers << ',' << s.port_active << ',' + << s.port_cooldown_ms << std::endl; + next_report = now + 1000000; + } + usleep(static_cast(FLAGS_interval_ms) * 1000); + } + std::cout << "completed success=" << succeeded << " failure=" << failed; + if (!payload.empty()) { + std::cout << " validation_failures=" << validation_failed; + } + std::cout << std::endl; + return succeeded ? 0 : 2; +} diff --git a/example/urma_performance/CMakeLists.txt b/example/urma_performance/CMakeLists.txt index 4dbc989a45..7ae1d26316 100644 --- a/example/urma_performance/CMakeLists.txt +++ b/example/urma_performance/CMakeLists.txt @@ -21,6 +21,7 @@ project(urma_performance C CXX) include(${CMAKE_CURRENT_LIST_DIR}/../cmake/BrpcExample.cmake) option(LINK_SO "Whether examples are linked dynamically" OFF) +option(BUILD_URMA_FAULT_INJECTOR "Build the process-local SEND failure test module" OFF) brpc_example_find_common_deps(DYNAMIC_LIB) @@ -52,3 +53,39 @@ brpc_example_configure_target(urma_performance_server) target_link_libraries(urma_performance_client PRIVATE ${BRPC_LIB} ${DYNAMIC_LIB}) target_link_libraries(urma_performance_server PRIVATE ${BRPC_LIB} ${DYNAMIC_LIB}) + +add_executable(urma_recovery_client + ../rdma_performance/recovery_client.cpp ${PROTO_SRC} ${PROTO_HEADER}) +brpc_example_configure_target(urma_recovery_client) +target_link_libraries(urma_recovery_client PRIVATE ${BRPC_LIB} ${DYNAMIC_LIB}) + +if(BUILD_URMA_FAULT_INJECTOR) + if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux") + message(FATAL_ERROR "The URMA fault injector uses Linux LD_PRELOAD") + endif() + add_library(urma_send_failure_injector MODULE send_failure_injector.c) + set_target_properties(urma_send_failure_injector PROPERTIES + C_STANDARD 11 C_STANDARD_REQUIRED ON) + target_include_directories(urma_send_failure_injector PRIVATE ${URMA_INCLUDE_PATH}) + target_link_libraries(urma_send_failure_injector PRIVATE ${CMAKE_DL_LIBS} Threads::Threads) + include(CTest) + if(BUILD_TESTING) + add_library(urma_injector_test_provider SHARED test_send_failure_injector.c) + target_compile_definitions(urma_injector_test_provider PRIVATE URMA_INJECTOR_TEST_PROVIDER) + target_include_directories(urma_injector_test_provider PRIVATE ${URMA_INCLUDE_PATH}) + add_executable(urma_send_failure_injector_test test_send_failure_injector.c) + target_include_directories(urma_send_failure_injector_test PRIVATE ${URMA_INCLUDE_PATH}) + target_link_libraries(urma_send_failure_injector_test PRIVATE urma_injector_test_provider) + add_dependencies(urma_send_failure_injector_test urma_send_failure_injector) + add_test(NAME urma_send_failure_injector_test + COMMAND ${CMAKE_COMMAND} -E env + "LD_PRELOAD=$" + $) + find_package(Python3 COMPONENTS Interpreter QUIET) + if(Python3_Interpreter_FOUND) + add_test(NAME urma_recovery_injection_runner_test + COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/test_run_recovery_injection.py) + endif() + endif() +endif() diff --git a/example/urma_performance/run_recovery_injection.py b/example/urma_performance/run_recovery_injection.py new file mode 100644 index 0000000000..4e172c80ff --- /dev/null +++ b/example/urma_performance/run_recovery_injection.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python3 +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +"""Check one SEND failure, peer cooldown, TCP admission and URMA recovery.""" + +import argparse +import csv +import json +import os +from pathlib import Path +import re +import socket +import subprocess +import tempfile +import time +import urllib.request + + +def decode_bvar(text): + match = re.match(r"^\w+\s*:\s*(.*)$", text.strip(), re.DOTALL) + value = match.group(1) if match else text.strip() + try: + result = json.loads(value) + return json.loads(result) if isinstance(result, str) else result + except json.JSONDecodeError: + # Text /vars may quote the JSON object without escaping its quotes. + return json.loads(value[value.index("{"):value.rindex("}") + 1]) + + +def read_samples(path): + with path.open() as stream: + return [list(map(int, row[:1] + row[2:])) for row in csv.reader(stream) + if len(row) == 9 and row[0].isdigit()] + + +def wait_for(proc, predicate, timeout, description): + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + value = predicate() + if value: + return value + if proc.poll() is not None: + raise RuntimeError("Client exited while waiting for " + description) + time.sleep(0.1) + raise RuntimeError("Timed out waiting for " + description) + + +def validate_phases(baseline, cooldown, recovered, require_urma_reconnect=True): + if any(s["mode"] != "auto" for s in (baseline, cooldown, recovered)): + raise RuntimeError("Expected AUTO throughout the run") + if len({s["epoch"] for s in (baseline, cooldown, recovered)}) != 1: + raise RuntimeError("Recovery epoch changed during injection") + if (baseline["negotiated_connections"] <= 0 or baseline["cooled_peers"] != 0 + or baseline["failures"]["send_post"] != 0): + raise RuntimeError("Baseline must use URMA with no injected failure") + if (cooldown["failures"]["send_post"] != 1 or cooldown["cooled_peers"] != 1 + or cooldown["peer_cooldown_admissions"] <= 0): + raise RuntimeError("Expected one SEND failure and TCP peer-cooldown admission") + negotiated = recovered["negotiated_connections"] + previous = cooldown["negotiated_connections"] + if (recovered["failures"]["send_post"] != 1 or recovered["cooled_peers"] != 0 + or negotiated < previous or (require_urma_reconnect and negotiated == previous) + or recovered["peer_cooldown_admissions"] < cooldown["peer_cooldown_admissions"]): + raise RuntimeError("Expected URMA negotiations to resume after cooldown") + + +def run(args): + client = Path(args.client).resolve() + injector = Path(args.injector).resolve() + if not client.is_file() or not injector.is_file(): + raise RuntimeError("Build urma_recovery_client and urma_send_failure_injector first") + output = Path(tempfile.mkdtemp(prefix="pr92-injection-", dir=".")).resolve() + marker = output / "inject.once" + csv_path = output / "client.csv" + log_path = output / "client.log" + with socket.socket() as probe: + probe.bind(("127.0.0.1", 0)) + port = probe.getsockname()[1] + # All metric requests go directly to this client's loopback listener. + opener = urllib.request.build_opener(urllib.request.ProxyHandler({})) + + def read_var(name): + request = urllib.request.Request( + "http://127.0.0.1:{}/vars/{}?console=1".format(port, name), + headers={"Accept": "text/plain"}) + with opener.open(request, timeout=2) as response: + text = response.read().decode() + (output / ("latest-" + name + ".txt")).write_text(text) + return decode_bvar(text) + + def record_phase(name, status): + (output / (name + ".json")).write_text(json.dumps(status, indent=2)) + print("{}: send_post={} cooled_peers={} tcp={} negotiated={}".format( + name, status["failures"]["send_post"], status["cooled_peers"], + status["peer_cooldown_admissions"], status["negotiated_connections"]), flush=True) + return status + + command = [str(client), "--transport=urma", "--mode=auto", + "--server=" + args.server, "--urma_device=" + args.device, + "--urma_tp_type=" + args.tp_type, + "--urma_client_handshake_version=" + str(args.handshake_version), + "--urma_buffer_count=4096", "--connection_type=" + args.connection_type, + "--attachment_size=" + str(args.attachment_size), + "--urma_use_polling=" + str(args.polling).lower(), + "--interval_ms=200", "--timeout_ms=3000", + "--seconds=" + str(args.seconds), + "--cooldown_ms=" + str(args.cooldown_ms), "--dummy_port=" + str(port)] + env = dict(os.environ, LD_PRELOAD=str(injector), + BRPC_URMA_FAIL_ONCE_FILE=str(marker)) + proc = None + print("Artifacts: " + str(output), flush=True) + try: + with csv_path.open("w") as stdout, log_path.open("w") as stderr: + proc = subprocess.Popen(command, stdout=stdout, stderr=stderr, env=env) + print("Client PID={}; waiting for successful URMA RPCs".format(proc.pid), flush=True) + + def ready(): + rows = read_samples(csv_path) + if rows and rows[-1][2] != 0: + raise RuntimeError("RPC failed before injection") + return rows and rows[-1][1] > 0 and rows[-1][3] > 0 + + wait_for(proc, ready, args.startup_timeout, "successful URMA baseline") + if read_var("pid") != proc.pid: + raise RuntimeError("Metric listener PID does not match the test client") + time.sleep(args.baseline_seconds) + phases = {"baseline": record_phase("baseline", read_var("urma_recovery_status"))} + baseline = phases["baseline"] + if (baseline["failures"]["send_post"] or baseline["cooled_peers"] + or baseline["negotiated_connections"] <= 0): + raise RuntimeError("Unexpected baseline metrics") + marker.touch() + + def cooling(): + status = read_var("urma_recovery_status") + return status if (status["failures"]["send_post"] == 1 + and status["cooled_peers"] == 1 + and status["peer_cooldown_admissions"] > 0) else None + + phases["cooldown"] = record_phase("cooldown", wait_for( + proc, cooling, args.cooldown_ms / 1000, "peer cooldown and TCP admission")) + require_urma_reconnect = args.connection_type == "short" + recovery_success = None + + def recovering(): + nonlocal recovery_success + status = read_var("urma_recovery_status") + if status["cooled_peers"] != 0: + return None + if require_urma_reconnect: + return status if (status["negotiated_connections"] > + phases["cooldown"]["negotiated_connections"]) else None + rows = read_samples(csv_path) + if recovery_success is None: + recovery_success = rows[-1][1] + return status if rows[-1][1] > recovery_success else None + + phases["recovered"] = record_phase("recovered", wait_for( + proc, recovering, args.cooldown_ms / 1000 + 5, "URMA recovery")) + validate_phases(**phases, require_urma_reconnect=require_urma_reconnect) + failures_at_recovery = read_samples(csv_path)[-1][2] + (output / "snapshots.json").write_text(json.dumps( + {"pid": proc.pid, "port": port, "phases": phases}, indent=2)) + rc = proc.wait(timeout=args.seconds + 10) + summary = re.search(r"completed success=(\d+) failure=(\d+)", csv_path.read_text()) + expected_failures = 1 if require_urma_reconnect else failures_at_recovery + if (rc != 0 or not summary or int(summary[1]) == 0 + or expected_failures < 1 or int(summary[2]) != expected_failures): + raise RuntimeError("Expected RPC recovery with no further failures afterward") + if args.attachment_size: + validation = re.search(r"validation_failures=(\d+)", csv_path.read_text()) + if not validation or int(validation[1]) != 0: + raise RuntimeError("Attachment content validation failed") + if log_path.read_text().count("injected one data SEND post failure") != 1: + raise RuntimeError("Expected exactly one injection log") + last = read_samples(csv_path)[-1] + if last[5] != 0 or last[4] != phases["recovered"]["peer_cooldown_admissions"]: + raise RuntimeError("TCP cooldown admissions must stop after URMA recovery") + print(csv_path.read_text().splitlines()[-1]) + recovered_transport = "URMA" if require_urma_reconnect else "RPC" + print("PASS: one SEND failure, TCP cooldown, and " + recovered_transport + " recovery") + finally: + if proc is not None and proc.poll() is None: + proc.terminate() + try: + proc.wait(timeout=5) + except subprocess.TimeoutExpired: + proc.kill() + proc.wait(timeout=5) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--server", required=True) + parser.add_argument("--device", required=True) + parser.add_argument("--client", default="build/urma-example/urma_recovery_client") + parser.add_argument("--injector", default="build/urma-example/liburma_send_failure_injector.so") + parser.add_argument("--tp-type", choices=("rtp", "ctp", "auto"), default="rtp") + parser.add_argument("--handshake-version", type=int, choices=(2, 3), default=3) + parser.add_argument("--connection-type", choices=("short", "single", "pooled"), default="short") + parser.add_argument("--attachment-size", type=int, default=0) + parser.add_argument("--polling", action="store_true") + parser.add_argument("--seconds", type=int, default=40) + parser.add_argument("--cooldown-ms", type=int, default=10000) + parser.add_argument("--baseline-seconds", type=float, default=5) + parser.add_argument("--startup-timeout", type=float, default=120) + args = parser.parse_args() + if (args.cooldown_ms <= 0 or args.attachment_size < 0 + or args.baseline_seconds < 0 or args.startup_timeout <= 0 + or args.seconds < args.baseline_seconds + args.cooldown_ms / 1000 + 5): + parser.error("Duration must cover baseline, cooldown and at least five seconds of recovery") + try: + run(args) + except (OSError, ValueError, KeyError, TypeError, RuntimeError, subprocess.TimeoutExpired) as error: + parser.exit(1, "FAIL: {}\n".format(error)) + + +if __name__ == "__main__": + main() diff --git a/example/urma_performance/send_failure_injector.c b/example/urma_performance/send_failure_injector.c new file mode 100644 index 0000000000..321fa5c85a --- /dev/null +++ b/example/urma_performance/send_failure_injector.c @@ -0,0 +1,67 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include "urma_api.h" + +typedef urma_status_t (*PostSend)(urma_jetty_t*, urma_jfs_wr_t*, urma_jfs_wr_t**); +static PostSend g_post_send; +static pthread_once_t g_resolve_once = PTHREAD_ONCE_INIT; +static atomic_bool g_injected = ATOMIC_VAR_INIT(0); + +static void ResolvePostSend(void) { + g_post_send = (PostSend)dlsym(RTLD_NEXT, "urma_post_jetty_send_wr"); + if (!g_post_send) { + fprintf(stderr, "URMA injector: cannot resolve real post_send: %s\n", dlerror()); + } +} + +// LD_PRELOAD this module into the recovery client. Creating the marker named +// by BRPC_URMA_FAIL_ONCE_FILE rejects one data SEND before it reaches liburma. +// Credit ACKs and all subsequent sends are forwarded to the provider. +urma_status_t urma_post_jetty_send_wr(urma_jetty_t* jetty, urma_jfs_wr_t* wr, + urma_jfs_wr_t** bad_wr) { + pthread_once(&g_resolve_once, ResolvePostSend); + if (!g_post_send) { + if (bad_wr) { + *bad_wr = wr; + } + errno = ENOSYS; + return ENOSYS; + } + const char* marker = getenv("BRPC_URMA_FAIL_ONCE_FILE"); + if (wr && wr->opcode == URMA_OPC_SEND && marker && marker[0] && + access(marker, F_OK) == 0 && !atomic_exchange(&g_injected, 1)) { + if (unlink(marker) != 0) { + fprintf(stderr, "URMA injector: cannot remove marker: %s\n", marker); + } + if (bad_wr) { + *bad_wr = wr; + } + fprintf(stderr, "URMA injector: injected one data SEND post failure, errno=%d\n", EIO); + errno = EIO; + return EIO; + } + return g_post_send(jetty, wr, bad_wr); +} diff --git a/example/urma_performance/test_run_recovery_injection.py b/example/urma_performance/test_run_recovery_injection.py new file mode 100644 index 0000000000..e1d42b3d83 --- /dev/null +++ b/example/urma_performance/test_run_recovery_injection.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import copy +import json +from pathlib import Path +import sys +import tempfile +import unittest +from unittest import mock + +sys.dont_write_bytecode = True +import run_recovery_injection as runner + + +class RecoveryInjectionTest(unittest.TestCase): + def test_decode_text_and_json_bvar_formats(self): + value = {"mode": "auto", "failures": {"send_post": 1}} + raw = json.dumps(value) + for body in (raw, 'urma_recovery_status : "' + raw + '"', + "urma_recovery_status : " + json.dumps(raw)): + self.assertEqual(value, runner.decode_bvar(body)) + self.assertEqual(123, runner.decode_bvar("pid : 123\n")) + + def test_samples_skip_headers_summary_and_partial_rows(self): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "client.csv" + path.write_text("seconds,mode,success,failure\n" + "0,auto,1,0,1,0,0,1,0\n1,auto," + "\ncompleted success=1 failure=0\n") + self.assertEqual([[0, 1, 0, 1, 0, 0, 1, 0]], runner.read_samples(path)) + + def phases(self): + baseline = {"mode": "auto", "epoch": 2, "negotiated_connections": 10, + "cooled_peers": 0, "peer_cooldown_admissions": 0, + "failures": {"send_post": 0}} + cooldown = copy.deepcopy(baseline) + cooldown.update(cooled_peers=1, peer_cooldown_admissions=3) + cooldown["failures"]["send_post"] = 1 + recovered = copy.deepcopy(cooldown) + recovered.update(cooled_peers=0, negotiated_connections=12) + return baseline, cooldown, recovered + + def test_validate_complete_recovery(self): + runner.validate_phases(*self.phases()) + + def test_persistent_connections_can_reuse_tcp_after_cooldown(self): + baseline, cooldown, recovered = self.phases() + recovered["negotiated_connections"] = cooldown["negotiated_connections"] + runner.validate_phases(baseline, cooldown, recovered, require_urma_reconnect=False) + with self.assertRaises(RuntimeError): + runner.validate_phases(baseline, cooldown, recovered) + + def test_reject_stale_zero_failure_snapshot(self): + baseline, cooldown, recovered = self.phases() + cooldown["failures"]["send_post"] = 0 + with self.assertRaises(RuntimeError): + runner.validate_phases(baseline, cooldown, recovered) + + def test_reject_no_recovery_and_epoch_changes(self): + for field, value in (("negotiated_connections", 10), ("epoch", 3)): + baseline, cooldown, recovered = self.phases() + recovered[field] = value + with self.assertRaises(RuntimeError): + runner.validate_phases(baseline, cooldown, recovered) + + def test_wait_for_readiness_instead_of_fixed_startup_delay(self): + proc = mock.Mock() + proc.poll.return_value = None + ready = mock.Mock(side_effect=[False, False, "ready"]) + with mock.patch.object(runner.time, "sleep"): + self.assertEqual("ready", runner.wait_for(proc, ready, 1, "baseline")) + self.assertEqual(3, ready.call_count) + + def test_exit_before_readiness_is_a_failure(self): + proc = mock.Mock() + proc.poll.return_value = 1 + with self.assertRaises(RuntimeError): + runner.wait_for(proc, lambda: False, 1, "baseline") + + +if __name__ == "__main__": + unittest.main() diff --git a/example/urma_performance/test_send_failure_injector.c b/example/urma_performance/test_send_failure_injector.c new file mode 100644 index 0000000000..42269c90b4 --- /dev/null +++ b/example/urma_performance/test_send_failure_injector.c @@ -0,0 +1,90 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include "urma_api.h" + +#ifdef URMA_INJECTOR_TEST_PROVIDER + +static int g_posts; +int UrmaInjectorTestPostCount(void) { return g_posts; } + +urma_status_t urma_post_jetty_send_wr(urma_jetty_t* jetty, urma_jfs_wr_t* wr, + urma_jfs_wr_t** bad_wr) { + (void)jetty; + (void)wr; + if (bad_wr) { + *bad_wr = NULL; + } + ++g_posts; + return URMA_SUCCESS; +} + +#else + +int UrmaInjectorTestPostCount(void); +#define CHECK(expr) do { \ + if (!(expr)) { \ + fprintf(stderr, "Check failed: %s at line %d\n", #expr, __LINE__); \ + return 1; \ + } \ +} while (0) + +int main(void) { + char marker[] = "/tmp/brpc-urma-injector-XXXXXX"; + int fd = mkstemp(marker); + CHECK(fd >= 0); + close(fd); + CHECK(unlink(marker) == 0); + CHECK(setenv("BRPC_URMA_FAIL_ONCE_FILE", marker, 1) == 0); + urma_jfs_wr_t wr = {0}; + urma_jfs_wr_t* bad = NULL; + wr.opcode = URMA_OPC_SEND; + CHECK(urma_post_jetty_send_wr(NULL, &wr, &bad) == URMA_SUCCESS); + CHECK(UrmaInjectorTestPostCount() == 1); + + FILE* file = fopen(marker, "w"); + CHECK(file != NULL); + fclose(file); + wr.opcode = URMA_OPC_SEND_IMM; + CHECK(urma_post_jetty_send_wr(NULL, &wr, &bad) == URMA_SUCCESS); + CHECK(UrmaInjectorTestPostCount() == 2); + CHECK(access(marker, F_OK) == 0); + + wr.opcode = URMA_OPC_SEND; + errno = 0; + CHECK(urma_post_jetty_send_wr(NULL, &wr, &bad) == EIO); + CHECK(errno == EIO && bad == &wr && UrmaInjectorTestPostCount() == 2); + CHECK(access(marker, F_OK) != 0); + CHECK(urma_post_jetty_send_wr(NULL, &wr, &bad) == URMA_SUCCESS); + CHECK(UrmaInjectorTestPostCount() == 3); + + file = fopen(marker, "w"); + CHECK(file != NULL); + fclose(file); + CHECK(urma_post_jetty_send_wr(NULL, &wr, &bad) == URMA_SUCCESS); + CHECK(UrmaInjectorTestPostCount() == 4); + CHECK(unlink(marker) == 0); + puts("PASS: forwarding, credit ACK, single failure, and resumed forwarding"); + return 0; +} + +#endif diff --git a/src/brpc/rdma/rdma_endpoint.cpp b/src/brpc/rdma/rdma_endpoint.cpp index 0dc41c9dd1..cb0fcc9d01 100644 --- a/src/brpc/rdma/rdma_endpoint.cpp +++ b/src/brpc/rdma/rdma_endpoint.cpp @@ -29,6 +29,7 @@ #include "brpc/reloadable_flags.h" #include "brpc/rdma/block_pool.h" #include "brpc/rdma/rdma_helper.h" +#include "brpc/rdma/rdma_recovery_policy.h" #include "brpc/rdma/rdma_endpoint.h" #include "brpc/rdma_transport.h" #include "brpc/rdma/rdma_handshake.h" @@ -131,6 +132,8 @@ RdmaEndpoint::RdmaEndpoint(Socket* s) : _socket(s) , _state(UNINIT) , _handshake_version(0) + , _recovery_epoch(0) + , _runtime_failure(RdmaFailureReason::NONE) , _resource(nullptr) , _send_cq_events(0) , _recv_cq_events(0) @@ -178,6 +181,8 @@ void RdmaEndpoint::Reset() { _state.store(UNINIT, butil::memory_order_relaxed); _handshake_version = 0; + _recovery_epoch = 0; + _runtime_failure.store(RdmaFailureReason::NONE, butil::memory_order_relaxed); _outgoing_ece.reset(); _resource = nullptr; _send_cq_events = 0; @@ -212,7 +217,10 @@ void RdmaConnect::StartConnect(const Socket* socket, if (Socket::Address(socket->id(), &s) != 0) { return; } - if (!IsRdmaAvailable()) { + if (!IsRdmaAvailable() || + !GlobalRdmaRecoveryPolicy().AllowNewConnection( + butil::endpoint2str(socket->remote_side()).c_str(), + &rdma_transport->_rdma_ep->_recovery_epoch)) { rdma_transport->_rdma_state = RdmaTransport::RDMA_OFF; rdma_transport->_rdma_ep->_state.store( RdmaEndpoint::FALLBACK_TCP, butil::memory_order_release); @@ -236,6 +244,50 @@ void RdmaConnect::StartConnect(const Socket* socket, void RdmaConnect::StopConnect(Socket* socket) { } +void RdmaEndpoint::ReportFailure(RdmaFailureReason reason) { + RdmaFailureReason expected = RdmaFailureReason::NONE; + if (reason == RdmaFailureReason::NONE || + !_runtime_failure.compare_exchange_strong(expected, reason)) { + return; + } + if (_state.load(butil::memory_order_acquire) != ESTABLISHED) { + return; + } + GlobalRdmaRecoveryPolicy().ReportPeerFailure( + _socket->CreatedByConnect() ? + butil::endpoint2str(_socket->remote_side()).c_str() : "", + _recovery_epoch, reason); + LOG(WARNING) << "RDMA runtime failure=" << RdmaFailureReasonName(reason) + << " socket=" << _socket->id() + << " port=" << static_cast(GetRdmaPortNum()) + << " epoch=" << _recovery_epoch; +} + +void RdmaEndpoint::OnAsyncError(Socket* socket, const ibv_async_event& event) { + auto* transport = dynamic_cast(socket->_transport.get()); + RdmaEndpoint* ep = transport ? transport->GetRdmaEp() : nullptr; + if (!ep || !ep->_resource) { + return; + } + RdmaFailureReason reason; + if (event.event_type == IBV_EVENT_CQ_ERR) { + if (event.element.cq != ep->_resource->send_cq && + event.element.cq != ep->_resource->recv_cq && + event.element.cq != ep->_resource->polling_cq) { + return; + } + reason = RdmaFailureReason::CQ_ERROR; + } else { + if (event.element.qp != ep->_resource->qp) { + return; + } + reason = event.event_type == IBV_EVENT_QP_FATAL ? + RdmaFailureReason::QP_FATAL : RdmaFailureReason::ACCESS_ERROR; + } + ep->ReportFailure(reason); + socket->SetFailed(ERDMA, "RDMA async error: %s", RdmaFailureReasonName(reason)); +} + void RdmaConnect::Run() { _done(errno, _data); } @@ -581,6 +633,7 @@ void* RdmaEndpoint::ProcessHandshakeAtClient(void* arg) { if (rdma_transport->_rdma_state == RdmaTransport::RDMA_ON) { ep->_state.store(ESTABLISHED, butil::memory_order_release); + GlobalRdmaRecoveryPolicy().RecordNegotiatedConnection(ep->_recovery_epoch); // The handshake is over, so the QP stream may start parsing now. if (ep->StartCqEvents() < 0) { const int saved_errno = errno; @@ -683,7 +736,9 @@ ParseResult RdmaEndpoint::ExecuteServerHandshake(butil::IOBuf* source, Socket* s } // Negotiate + allocate resources. - bool negotiated = r == RemoteHelloResult::NEGOTIATED; + bool negotiated = r == RemoteHelloResult::NEGOTIATED && + GlobalRdmaRecoveryPolicy().AllowNewConnection( + "", &ep->_recovery_epoch); if (negotiated) { ep->ApplyRemoteHello(remote); ep->_state.store(S_ALLOC_QPCQ, butil::memory_order_relaxed); @@ -765,6 +820,7 @@ ParseResult RdmaEndpoint::ExecuteServerHandshake(butil::IOBuf* source, Socket* s << ") on " << s->description(); rdma_transport->_rdma_state = RdmaTransport::RDMA_ON; ep->_state.store(ESTABLISHED, butil::memory_order_release); + GlobalRdmaRecoveryPolicy().RecordNegotiatedConnection(ep->_recovery_epoch); s->reset_parsing_context(nullptr); // Two things are deliberately not done here. @@ -977,6 +1033,9 @@ ssize_t RdmaEndpoint::CutFromIOBufList(butil::IOBuf** from, size_t ndata) { std::ostringstream oss; DebugInfo(oss, ", "); LOG(WARNING) << "Fail to ibv_post_send: " << berror(err) << " " << oss.str(); + if (err == EIO || err == ENETDOWN || err == ENETUNREACH || err == ETIMEDOUT) { + ReportFailure(RdmaFailureReason::SEND_POST); + } errno = err; return -1; } @@ -1027,6 +1086,10 @@ int RdmaEndpoint::SendImm(uint32_t imm) { // We use other way to guarantee the Send Queue is not full. // So we just consider this error as an unrecoverable error. LOG(WARNING) << "Fail to ibv_post_send: " << berror(err) << " " << oss.str(); + if (err == EIO || err == ENETDOWN || err == ENETUNREACH || err == ETIMEDOUT) { + ReportFailure(RdmaFailureReason::SEND_POST); + } + errno = err; return -1; } @@ -1044,8 +1107,7 @@ ssize_t RdmaEndpoint::HandleCompletion(ibv_wc& wc) { if (0 == wc.wr_id) { _sq_imm_window_size += 1; // If there are any unacknowledged recvs, send an ack. - SendAck(0); - return 0; + return SendAck(0); } // Update SQ window. uint16_t wnd_to_update = wc.wr_id; @@ -1098,8 +1160,8 @@ ssize_t RdmaEndpoint::HandleCompletion(ibv_wc& wc) { if (PostRecv(1, zerocopy) < 0) { return -1; } - if (wc.byte_len > 0) { - SendAck(1); + if (wc.byte_len > 0 && SendAck(1) < 0) { + return -1; } return wc.byte_len; } @@ -1127,6 +1189,8 @@ int RdmaEndpoint::DoPostRecv(void* block, size_t block_size) { int err = ibv_post_recv(_resource->qp, &wr, &bad); if (err != 0) { LOG(WARNING) << "Fail to ibv_post_recv: " << berror(err); + ReportFailure(RdmaFailureReason::RECV_POST); + errno = err; return -1; } return 0; @@ -1334,6 +1398,8 @@ int RdmaEndpoint::DoAllocateResources() { return -1; } + BindResourceToSocket(); + if (!FLAGS_rdma_use_polling) { if (0 != ReqNotifyCq(true, false)) { return -1; @@ -1359,6 +1425,20 @@ int RdmaEndpoint::DoAllocateResources() { return 0; } +void RdmaEndpoint::BindResourceToSocket() { + void* owner = reinterpret_cast(_socket->id()); + _resource->qp->qp_context = owner; + if (_resource->send_cq) { + _resource->send_cq->cq_context = owner; + } + if (_resource->recv_cq) { + _resource->recv_cq->cq_context = owner; + } + if (_resource->polling_cq) { + _resource->polling_cq->cq_context = owner; + } +} + int RdmaEndpoint::StartCqEvents() { if (InputMessengerProcessor::STREAM_NONE != _socket->parsing_stream_type()) { LOG(WARNING) << "StartCqEvents() called while " << *_socket << " is parsing"; @@ -1544,7 +1624,8 @@ void RdmaEndpoint::DeallocateResources() { PollerRemoveCqSid(); } bool move_to_rdma_resource_list = false; - if (_sq_size <= FLAGS_rdma_prepared_qp_size && + if (_runtime_failure.load(butil::memory_order_relaxed) == RdmaFailureReason::NONE && + _sq_size <= FLAGS_rdma_prepared_qp_size && _rq_size <= FLAGS_rdma_prepared_qp_size && FLAGS_rdma_prepared_qp_cnt > 0) { ibv_qp_attr attr; @@ -1651,6 +1732,7 @@ int RdmaEndpoint::GetAndAckEvents(SocketUniquePtr& s) { if (errno != EAGAIN) { const int saved_errno = errno; PLOG(ERROR) << "Fail to get cq event from " << s->description(); + ReportFailure(RdmaFailureReason::CQ_EVENT); s->SetFailed(saved_errno, "Fail to get cq event from %s: %s", s->description().c_str(), berror(saved_errno)); return -1; @@ -1691,6 +1773,7 @@ int RdmaEndpoint::ReqNotifyCq(bool send_cq, bool fatal_on_error) { PLOG(WARNING) << "Fail to arm " << (send_cq ? "send" : "recv") << " CQ comp channel from " << _socket->description(); if (fatal_on_error) { + ReportFailure(RdmaFailureReason::CQ_NOTIFY); _socket->SetFailed(err, "Fail to arm %s CQ channel from %s: %s", send_cq ? "send" : "recv", _socket->description().c_str(), berror(err)); @@ -1744,6 +1827,7 @@ void RdmaEndpoint::PollCq(Socket* m) { if (cnt < 0) { const int saved_errno = errno; PLOG(WARNING) << "Fail to poll cq: " << s->description(); + ep->ReportFailure(RdmaFailureReason::CQ_POLL); s->SetFailed(saved_errno, "Fail to poll cq from %s: %s", s->description().c_str(), berror(saved_errno)); return; @@ -1812,6 +1896,7 @@ void RdmaEndpoint::PollCq(Socket* m) { } if (wc[i].status != IBV_WC_SUCCESS) { + ep->ReportFailure(ClassifyRdmaCompletion(wc[i].status)); PLOG(WARNING) << "Fail to handle RDMA completion, error status(" << wc[i].status << "): " << s->description(); s->SetFailed(ERDMA, "RDMA completion error(%d) from %s: %s", diff --git a/src/brpc/rdma/rdma_endpoint.h b/src/brpc/rdma/rdma_endpoint.h index eda5e558ef..85d3b280c9 100644 --- a/src/brpc/rdma/rdma_endpoint.h +++ b/src/brpc/rdma/rdma_endpoint.h @@ -32,6 +32,7 @@ #include "butil/containers/optional.h" #include "brpc/socket.h" #include "brpc/rdma/rdma_handshake_server.h" +#include "brpc/rdma/rdma_recovery_policy.h" namespace brpc { @@ -136,6 +137,9 @@ friend int v3_wire::WriteV3Hello(RdmaEndpoint*, const RdmaHello&); // Real handshake for RDMA-mode sockets. static ParseResult ExecuteServerHandshake(butil::IOBuf* source, Socket* socket); + // Called with an addressed Socket held until its async event is acknowledged. + static void OnAsyncError(Socket* socket, const ibv_async_event& event); + // Initialize polling mode static int PollingModeInitialize(bthread_tag_t tag, std::function callback, @@ -145,6 +149,9 @@ friend int v3_wire::WriteV3Hello(RdmaEndpoint*, const RdmaHello&); static void PollingModeRelease(bthread_tag_t tag); private: + void ReportFailure(RdmaFailureReason reason); + void BindResourceToSocket(); + enum State { UNINIT = 0x0, C_ALLOC_QPCQ = 0x1, @@ -307,6 +314,10 @@ friend int v3_wire::WriteV3Hello(RdmaEndpoint*, const RdmaHello&); // 3 = v3 "RDM3" int _handshake_version; + // Admission epoch used to discard failures from an earlier recovery mode. + uint64_t _recovery_epoch; + butil::atomic _runtime_failure; + // ECE payload to advertise in the next local hello: // Client: the locally queried ECE capabilities (filled // before C_HELLO_SEND); diff --git a/src/brpc/rdma/rdma_helper.cpp b/src/brpc/rdma/rdma_helper.cpp index 7e671a85ba..dc0e622a75 100644 --- a/src/brpc/rdma/rdma_helper.cpp +++ b/src/brpc/rdma/rdma_helper.cpp @@ -30,6 +30,7 @@ #include "brpc/rdma/block_pool.h" #include "brpc/rdma/rdma_endpoint.h" #include "brpc/rdma/rdma_helper.h" +#include "brpc/rdma/rdma_recovery_policy.h" namespace butil { @@ -268,7 +269,7 @@ static void OnRdmaAsyncEvent(Socket* m) { SocketId sid = (SocketId)event.element.qp->qp_context; SocketUniquePtr s; if (Socket::Address(sid, &s) == 0) { - s->SetFailed(ERDMA, "QP fatal error"); + RdmaEndpoint::OnAsyncError(s.get(), event); LOG(WARNING) << "Receive a QP fatal error on " << s->description(); } // NOTE: @@ -280,7 +281,11 @@ static void OnRdmaAsyncEvent(Socket* m) { break; } case IBV_EVENT_CQ_ERR: { - LOG(WARNING) << "CQ overruns, the connection will be stopped."; + SocketUniquePtr s; + const SocketId sid = reinterpret_cast(event.element.cq->cq_context); + if (Socket::Address(sid, &s) == 0) { + RdmaEndpoint::OnAsyncError(s.get(), event); + } IbvAckAsyncEvent(&event); break; } @@ -313,8 +318,10 @@ static void OnRdmaAsyncEvent(Socket* m) { } case IBV_EVENT_PORT_ACTIVE: case IBV_EVENT_PORT_ERR: { - // Port up/down will lead these two events. - // The port error is recoverable. + if (event.element.port_num == g_port_num) { + GlobalRdmaRecoveryPolicy().ReportPortState( + event.event_type == IBV_EVENT_PORT_ACTIVE); + } IbvAckAsyncEvent(&event); break; } @@ -699,6 +706,31 @@ bool IsRdmaAvailable() { return g_rdma_available.load(butil::memory_order_acquire); } +bool IsRdmaPathFailure(ibv_wc_status status) { + const auto reason = ClassifyRdmaCompletion(status); + return reason != RdmaFailureReason::NONE && + reason != RdmaFailureReason::ACCESS_ERROR; +} + +RdmaFailureReason ClassifyRdmaCompletion(ibv_wc_status status) { + switch (status) { + case IBV_WC_SUCCESS: + case IBV_WC_WR_FLUSH_ERR: + return RdmaFailureReason::NONE; + case IBV_WC_RETRY_EXC_ERR: + return RdmaFailureReason::RETRY_EXHAUSTED; + case IBV_WC_RNR_RETRY_EXC_ERR: + return RdmaFailureReason::RNR; + case IBV_WC_LOC_PROT_ERR: + case IBV_WC_LOC_ACCESS_ERR: + case IBV_WC_REM_ACCESS_ERR: + case IBV_WC_MW_BIND_ERR: + return RdmaFailureReason::ACCESS_ERROR; + default: + return RdmaFailureReason::TRANSPORT_ERROR; + } +} + void GlobalDisableRdma() { if (g_rdma_available.exchange(false, butil::memory_order_acquire)) { LOG(FATAL) << "RDMA is disabled due to some unrecoverable problem"; diff --git a/src/brpc/rdma/rdma_helper.h b/src/brpc/rdma/rdma_helper.h index 2383d2d8b4..5f49f5779f 100644 --- a/src/brpc/rdma/rdma_helper.h +++ b/src/brpc/rdma/rdma_helper.h @@ -29,6 +29,8 @@ namespace brpc { namespace rdma { +enum class RdmaFailureReason; + // Initialize RDMA environment // Exit if failed void GlobalRdmaInitializeOrDie(); @@ -89,6 +91,10 @@ bool IsRdmaAvailable(); // Disable RDMA in the remaining lifetime of the process void GlobalDisableRdma(); +// Completion failures that indicate an unusable path or exhausted receive credits. +bool IsRdmaPathFailure(ibv_wc_status status); +RdmaFailureReason ClassifyRdmaCompletion(ibv_wc_status status); + // If the given protocol supported by RDMA bool SupportedByRdma(std::string protocol); diff --git a/src/brpc/rdma/rdma_recovery_policy.cpp b/src/brpc/rdma/rdma_recovery_policy.cpp new file mode 100644 index 0000000000..c8329a95a1 --- /dev/null +++ b/src/brpc/rdma/rdma_recovery_policy.cpp @@ -0,0 +1,273 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include "brpc/rdma/rdma_recovery_policy.h" +#include "brpc/urma/urma_recovery_policy.h" +#include "butil/time.h" +#include "bvar/passive_status.h" + +namespace brpc { +namespace rdma { + +const char* RdmaFailureReasonName(RdmaFailureReason reason) { + switch (reason) { + case RdmaFailureReason::NONE: return "none"; + case RdmaFailureReason::RETRY_EXHAUSTED: return "retry_exhausted"; + case RdmaFailureReason::RNR: return "rnr"; + case RdmaFailureReason::QP_FATAL: return "qp_fatal"; + case RdmaFailureReason::CQ_ERROR: return "cq_error"; + case RdmaFailureReason::CQ_POLL: return "cq_poll"; + case RdmaFailureReason::CQ_EVENT: return "cq_event"; + case RdmaFailureReason::CQ_NOTIFY: return "cq_notify"; + case RdmaFailureReason::SEND_POST: return "send_post"; + case RdmaFailureReason::ACCESS_ERROR: return "access_error"; + case RdmaFailureReason::TRANSPORT_ERROR: return "transport_error"; + case RdmaFailureReason::RECV_POST: return "recv_post"; + default: return "unknown"; + } +} + +RdmaRecoveryPolicy::RdmaRecoveryPolicy(Clock clock, size_t max_peers) + : _clock(clock) + , _max_peers(max_peers) + , _mode(RdmaRecoveryMode::LEGACY) + , _epoch(1) + , _cooldown_us(5000000) + , _port_active(true) + , _port_retry_at(0) { +} + +bool RdmaRecoveryPolicy::SetMode(RdmaRecoveryMode mode) { + if (mode != RdmaRecoveryMode::LEGACY && mode != RdmaRecoveryMode::AUTO && + mode != RdmaRecoveryMode::TCP_ONLY) { + return false; + } + std::lock_guard lock(_mutex); + if (_mode != mode) { + _mode = mode; + ++_epoch; + _peers.clear(); + } + return true; +} + +RdmaRecoveryMode RdmaRecoveryPolicy::GetMode() { + std::lock_guard lock(_mutex); + return _mode; +} + +bool RdmaRecoveryPolicy::SetCooldownMs(int64_t cooldown_ms) { + if (cooldown_ms <= 0 || cooldown_ms > INT32_MAX) { + return false; + } + std::lock_guard lock(_mutex); + _cooldown_us = cooldown_ms * 1000; + return true; +} + +bool RdmaRecoveryPolicy::AllowNewConnection(const std::string& peer, + uint64_t* epoch) { + std::lock_guard lock(_mutex); + if (epoch != nullptr) { + *epoch = _epoch; + } + if (_mode == RdmaRecoveryMode::LEGACY) { + return true; + } + if (_mode == RdmaRecoveryMode::TCP_ONLY) { + ++_status.tcp_only_admissions; + return false; + } + const int64_t now = _clock(); + if (!_port_active || now < _port_retry_at) { + ++_status.port_cooldown_admissions; + return false; + } + auto it = _peers.find(peer); + if (it == _peers.end()) { + return true; + } + if (now < it->second) { + ++_status.peer_cooldown_admissions; + return false; + } + _peers.erase(it); + return true; +} + +void RdmaRecoveryPolicy::ReportPeerFailure(const std::string& peer, + uint64_t epoch, + RdmaFailureReason reason) { + std::lock_guard lock(_mutex); + if (epoch != _epoch || reason <= RdmaFailureReason::NONE || + reason >= RdmaFailureReason::COUNT) { + return; + } + ++_status.failures[static_cast(reason)]; + if (_mode != RdmaRecoveryMode::AUTO || peer.empty() || _max_peers == 0 || + reason == RdmaFailureReason::ACCESS_ERROR) { + return; + } + const int64_t now = _clock(); + for (auto it = _peers.begin(); it != _peers.end();) { + if (it->second <= now) { + it = _peers.erase(it); + } else { + ++it; + } + } + if (_peers.count(peer) == 0 && _peers.size() == _max_peers) { + auto oldest = _peers.begin(); + for (auto it = _peers.begin(); it != _peers.end(); ++it) { + if (it->second < oldest->second) { + oldest = it; + } + } + _peers.erase(oldest); + ++_status.cache_evictions; + } + _peers[peer] = now + _cooldown_us; +} + +void RdmaRecoveryPolicy::ReportPortState(bool active) { + std::lock_guard lock(_mutex); + _port_active = active; + if (!active) { + ++_status.port_errors; + _port_retry_at = _clock() + _cooldown_us; + } +} + +RdmaRecoveryStatus RdmaRecoveryPolicy::GetStatus() { + std::lock_guard lock(_mutex); + RdmaRecoveryStatus result = _status; + result.mode = _mode; + result.epoch = _epoch; + result.port_active = _port_active; + const int64_t now = _clock(); + result.port_cooldown_ms = _port_retry_at > now ? + (_port_retry_at - now + 999) / 1000 : 0; + for (const auto& peer : _peers) { + result.cooled_peers += peer.second > now; + } + return result; +} + +void RdmaRecoveryPolicy::RecordNegotiatedConnection(uint64_t epoch) { + std::lock_guard lock(_mutex); + if (epoch == _epoch) { + ++_status.negotiated_connections; + } +} + +static void DescribeRecovery(std::ostream& os, void* arg) { + const auto s = static_cast(arg)->GetStatus(); + os << "{\"mode\":\"" + << (s.mode == RdmaRecoveryMode::AUTO ? "auto" : + s.mode == RdmaRecoveryMode::TCP_ONLY ? "tcp_only" : "legacy") + << "\",\"epoch\":" << s.epoch + << ",\"port_active\":" << (s.port_active ? "true" : "false") + << ",\"port_cooldown_ms\":" << s.port_cooldown_ms + << ",\"cooled_peers\":" << s.cooled_peers + << ",\"tcp_only_admissions\":" << s.tcp_only_admissions + << ",\"port_cooldown_admissions\":" << s.port_cooldown_admissions + << ",\"peer_cooldown_admissions\":" << s.peer_cooldown_admissions + << ",\"port_errors\":" << s.port_errors + << ",\"cache_evictions\":" << s.cache_evictions + << ",\"negotiated_connections\":" << s.negotiated_connections + << ",\"failures\":{"; + for (size_t i = 1; i < static_cast(RdmaFailureReason::COUNT); ++i) { + if (i > 1) { + os << ','; + } + os << '"' << RdmaFailureReasonName(static_cast(i)) + << "\":" << s.failures[i]; + } + os << "}}"; +} + +struct RecoveryMetrics { + RecoveryMetrics(RdmaRecoveryPolicy* policy, const char* status_name, + const char* fallback_name) + : status(status_name, DescribeRecovery, policy) + , fallbacks(fallback_name, [](void* arg) { + const auto s = static_cast(arg)->GetStatus(); + return s.tcp_only_admissions + s.port_cooldown_admissions + + s.peer_cooldown_admissions; + }, policy) { + } + bvar::PassiveStatus status; + bvar::PassiveStatus fallbacks; +}; + +RdmaRecoveryPolicy& GlobalRdmaRecoveryPolicy() { + // Async event callbacks may outlive ordinary static destructors. + static RdmaRecoveryPolicy* policy = + new RdmaRecoveryPolicy(butil::monotonic_time_us); + static RecoveryMetrics* metrics = new RecoveryMetrics( + policy, "rdma_recovery_status", "rdma_recovery_tcp_fallback_admissions"); + (void)metrics; + return *policy; +} + +bool SetRdmaRecoveryMode(RdmaRecoveryMode mode) { + return GlobalRdmaRecoveryPolicy().SetMode(mode); +} + +RdmaRecoveryMode GetRdmaRecoveryMode() { + return GlobalRdmaRecoveryPolicy().GetMode(); +} + +bool SetRdmaRecoveryCooldownMs(int64_t cooldown_ms) { + return GlobalRdmaRecoveryPolicy().SetCooldownMs(cooldown_ms); +} + +RdmaRecoveryStatus GetRdmaRecoveryStatus() { + return GlobalRdmaRecoveryPolicy().GetStatus(); +} + +} // namespace rdma + +namespace urma { + +UrmaRecoveryPolicy& GlobalUrmaRecoveryPolicy() { + static UrmaRecoveryPolicy* policy = + new UrmaRecoveryPolicy(butil::monotonic_time_us); + static rdma::RecoveryMetrics* metrics = new rdma::RecoveryMetrics( + policy, "urma_recovery_status", "urma_recovery_tcp_fallback_admissions"); + (void)metrics; + return *policy; +} + +bool SetUrmaRecoveryMode(UrmaRecoveryMode mode) { + return GlobalUrmaRecoveryPolicy().SetMode(mode); +} + +UrmaRecoveryMode GetUrmaRecoveryMode() { + return GlobalUrmaRecoveryPolicy().GetMode(); +} + +bool SetUrmaRecoveryCooldownMs(int64_t cooldown_ms) { + return GlobalUrmaRecoveryPolicy().SetCooldownMs(cooldown_ms); +} + +UrmaRecoveryStatus GetUrmaRecoveryStatus() { + return GlobalUrmaRecoveryPolicy().GetStatus(); +} + +} // namespace urma +} // namespace brpc diff --git a/src/brpc/rdma/rdma_recovery_policy.h b/src/brpc/rdma/rdma_recovery_policy.h new file mode 100644 index 0000000000..c46c8a0705 --- /dev/null +++ b/src/brpc/rdma/rdma_recovery_policy.h @@ -0,0 +1,94 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#ifndef BRPC_RDMA_RECOVERY_POLICY_H +#define BRPC_RDMA_RECOVERY_POLICY_H + +#include +#include +#include +#include +#include + +namespace brpc { +namespace rdma { + +enum class RdmaRecoveryMode { LEGACY, AUTO, TCP_ONLY }; + +enum class RdmaFailureReason { + NONE, RETRY_EXHAUSTED, RNR, QP_FATAL, CQ_ERROR, CQ_POLL, + CQ_EVENT, CQ_NOTIFY, SEND_POST, ACCESS_ERROR, TRANSPORT_ERROR, RECV_POST, COUNT +}; + +const char* RdmaFailureReasonName(RdmaFailureReason reason); + +struct RdmaRecoveryStatus { + RdmaRecoveryMode mode = RdmaRecoveryMode::LEGACY; + uint64_t epoch = 0; + bool port_active = true; + int64_t port_cooldown_ms = 0; + size_t cooled_peers = 0; + uint64_t failures[static_cast(RdmaFailureReason::COUNT)] = {}; + uint64_t tcp_only_admissions = 0; + uint64_t port_cooldown_admissions = 0; + uint64_t peer_cooldown_admissions = 0; + uint64_t port_errors = 0; + uint64_t cache_evictions = 0; + uint64_t negotiated_connections = 0; +}; + +// Admission policy for new connections on the process's selected RDMA port. +// An empty peer selects server admission, which only checks the local port. +class RdmaRecoveryPolicy { +public: + typedef int64_t (*Clock)(); + explicit RdmaRecoveryPolicy(Clock clock, size_t max_peers = 1024); + bool SetMode(RdmaRecoveryMode mode); + RdmaRecoveryMode GetMode(); + // Positive milliseconds, at most INT32_MAX. Applies to future failures. + bool SetCooldownMs(int64_t cooldown_ms); + bool AllowNewConnection(const std::string& peer, uint64_t* epoch = nullptr); + void ReportPeerFailure(const std::string& peer, uint64_t epoch, + RdmaFailureReason reason = RdmaFailureReason::RETRY_EXHAUSTED); + void ReportPortState(bool active); + RdmaRecoveryStatus GetStatus(); + void RecordNegotiatedConnection(uint64_t epoch); + +private: + std::mutex _mutex; + const Clock _clock; + const size_t _max_peers; + RdmaRecoveryMode _mode; + uint64_t _epoch; + int64_t _cooldown_us; + bool _port_active; + int64_t _port_retry_at; + std::map _peers; + RdmaRecoveryStatus _status; +}; + +// Process-lifetime policy, shared by client and server handshake admission. +RdmaRecoveryPolicy& GlobalRdmaRecoveryPolicy(); +bool SetRdmaRecoveryMode(RdmaRecoveryMode mode); +RdmaRecoveryMode GetRdmaRecoveryMode(); +bool SetRdmaRecoveryCooldownMs(int64_t cooldown_ms); +RdmaRecoveryStatus GetRdmaRecoveryStatus(); + +} // namespace rdma +} // namespace brpc + +#endif // BRPC_RDMA_RECOVERY_POLICY_H diff --git a/src/brpc/urma/mock_urma.cpp b/src/brpc/urma/mock_urma.cpp index b0572c2e86..f6b89db1ec 100644 --- a/src/brpc/urma/mock_urma.cpp +++ b/src/brpc/urma/mock_urma.cpp @@ -244,6 +244,10 @@ urma_status_t urma_query_device(urma_device_t *device, mock_device_attr.dev_cap.max_jetty = 1024; mock_device_attr.dev_cap.max_jfs_sge = 8; mock_device_attr.dev_cap.max_jfr_sge = 8; + mock_device_attr.dev_cap.priority_info[0].tp_type.bs.rtp = 1; + mock_device_attr.dev_cap.priority_info[0].SL = 2; + mock_device_attr.dev_cap.priority_info[6].tp_type.bs.ctp = 1; + mock_device_attr.dev_cap.priority_info[6].SL = 6; memcpy(attr, &mock_device_attr, sizeof(urma_device_attr_t)); return URMA_SUCCESS; } @@ -390,13 +394,19 @@ urma_target_seg_t *urma_register_seg(urma_context_t *ctx, urma_seg_cfg_t *cfg) { if (!ctx || !cfg || context_map.find(ctx) == context_map.end()) { return nullptr; } - urma_target_seg_t *seg = new urma_target_seg_t; + urma_target_seg_t *seg = new urma_target_seg_t{}; memset(&seg->seg.ubva.eid, 0, sizeof(urma_eid_t)); seg->seg.ubva.eid.raw[0] = 1; seg->seg.ubva.uasid = 0; seg->seg.ubva.va = cfg->va; seg->seg.len = cfg->len; seg->seg.token_id = cfg->token_value.token; + seg->seg.attr.bs.access = cfg->flag.bs.access; + seg->seg.attr.bs.token_policy = cfg->flag.bs.token_policy; + seg->seg.attr.bs.cacheable = cfg->flag.bs.cacheable; + seg->user_ctx = cfg->user_ctx; + seg->urma_ctx = ctx; + seg->token_id = cfg->token_id; seg_map[seg] = 1; return seg; } diff --git a/src/brpc/urma/urma_endpoint.cpp b/src/brpc/urma/urma_endpoint.cpp index 26c7a1b9dc..a2f163a349 100644 --- a/src/brpc/urma/urma_endpoint.cpp +++ b/src/brpc/urma/urma_endpoint.cpp @@ -64,6 +64,7 @@ DECLARE_int32(urma_sq_size); DECLARE_int32(urma_rq_size); DECLARE_int32(urma_cqe_poll_once); DECLARE_bool(urma_recv_zerocopy); +DECLARE_bool(urma_copy_send); DECLARE_int32(urma_zerocopy_min_size); DECLARE_int32(urma_prepared_jetty_cnt); DECLARE_bool(urma_poller_yield); @@ -168,6 +169,8 @@ UrmaEndpoint::~UrmaEndpoint() { void UrmaEndpoint::Reset() { DeallocateResources(); _state = UNINIT; + _recovery_epoch = 0; + _runtime_failure.store(UrmaFailureReason::NONE, butil::memory_order_relaxed); _handshake_version = 0; _remote_recv_block_size = 0; _local_window_capacity = 0; @@ -262,7 +265,7 @@ void UrmaEndpoint::MakeLocalParsedHello(ParsedHello* out) const { : _resource->jetty->jetty_id.eid.raw; std::memcpy(out->eid, advertised_eid, 16); } - out->tp_type = static_cast(URMA_CTP); + out->tp_type = static_cast(GetUrmaTpType()); // Pool segment: flatten g_pool_seg's seg fields. urma_target_seg_t* pool = GetPoolSegFor(nullptr); if (pool) { @@ -539,6 +542,12 @@ void UrmaEndpoint::DeallocateResources() { // ============================================================================ int UrmaEndpoint::ImportPeer(const ParsedHello& peer) { + if (peer.tp_type != static_cast(GetUrmaTpType())) { + LOG(ERROR) << "URMA peer path mismatch: local=" << GetUrmaTpType() + << " peer=" << static_cast(peer.tp_type); + errno = EPROTO; + return -1; + } urma_context_t* ctx = GetUrmaContext(); if (!ctx) { errno = ENODEV; @@ -572,10 +581,6 @@ int UrmaEndpoint::ImportPeer(const ParsedHello& peer) { remote.jetty_id.id = peer.jetty_id; remote.trans_mode = URMA_TM_RM; remote.type = URMA_JETTY; - if (peer.tp_type > static_cast(URMA_UTP)) { - errno = EPROTO; - return -1; - } remote.tp_type = static_cast(peer.tp_type); urma_token_t token{}; @@ -661,9 +666,16 @@ class UrmaIOBuf : private butil::IOBuf { if (len + this_len > max_len) { this_len = max_len - len; } - sglist[*sge_index].addr = reinterpret_cast(start); - sglist[*sge_index].len = static_cast(this_len); - sglist[*sge_index].tseg = tseg; + if (!InitUrmaLocalSge(&sglist[*sge_index], + reinterpret_cast(start), this_len, tseg)) { + LOG(ERROR) << "Invalid URMA send SGE: address=" + << reinterpret_cast(start) + << " length=" << this_len + << " segment_va=" << tseg->seg.ubva.va + << " segment_len=" << tseg->seg.len + << " context_matches=" << (tseg->urma_ctx == GetUrmaContext()); + return -1; + } cutn(to, this_len); len += this_len; (*sge_index)++; @@ -677,6 +689,7 @@ ssize_t UrmaEndpoint::CutFromIOBufList(butil::IOBuf** from, size_t ndata) { errno = ENOTCONN; return -1; } + const bool copy_send = FLAGS_urma_copy_send; int max_sge = GetUrmaMaxSge(); if (max_sge < 1) { max_sge = 1; @@ -707,6 +720,10 @@ ssize_t UrmaEndpoint::CutFromIOBufList(butil::IOBuf** from, size_t ndata) { size_t max_len = _remote_recv_block_size > 0 ? _remote_recv_block_size : GetUrmaRecvBlockSize(); + if (copy_send) { + max_len = std::min(max_len, GetUrmaRecvBlockSize() - + (URMA_SEND_BUFFER_ALIGNMENT - 1)); + } while (sge_index < static_cast(max_sge) && this_len < max_len && current < ndata) { auto* data = reinterpret_cast(from[current]); @@ -714,16 +731,30 @@ ssize_t UrmaEndpoint::CutFromIOBufList(butil::IOBuf** from, size_t ndata) { ++current; continue; } - ssize_t n = data->cut_into_sglist(sglist, &sge_index, to, - max_sge, max_len - this_len); + ssize_t n = copy_send ? + static_cast(data->cutn(to, max_len - this_len)) : + data->cut_into_sglist(sglist, &sge_index, to, + max_sge, max_len - this_len); if (n < 0) { + if (errno == ERDMAMEM) { + ReportFailure(UrmaFailureReason::ACCESS_ERROR); + } return -1; } this_len += n; } - if (sge_index == 0) { + if (this_len == 0) { break; } + if (copy_send) { + if (!CopyUrmaSendBuffer(to, &sglist[0])) { + LOG(ERROR) << "Fail to prepare aligned URMA send buffer: length=" + << this_len << " errno=" << errno; + ReportFailure(UrmaFailureReason::ACCESS_ERROR); + return -1; + } + sge_index = 1; + } urma_sg_t sg{sglist, static_cast(sge_index)}; urma_jfs_wr_t wr{}; @@ -736,7 +767,8 @@ ssize_t UrmaEndpoint::CutFromIOBufList(butil::IOBuf** from, size_t ndata) { wr.flag.bs.complete_enable = 1; wr.tjetty = _resource->remote_jetty; wr.send.src = sg; - wr.user_ctx = 1; + // Zero identifies credit ACKs; data completions identify their SQ slot. + wr.user_ctx = static_cast(_sq_current) + 1; urma_jfs_wr_t* bad_wr = nullptr; const uint16_t sq_slot = _sq_current; const uint32_t local_jetty_id = _resource->jetty->jetty_id.id; @@ -770,6 +802,7 @@ ssize_t UrmaEndpoint::CutFromIOBufList(butil::IOBuf** from, size_t ndata) { << ", configured_max_sge=" << GetUrmaMaxSge() << ", payload_size=" << this_len << " on " << _socket->description(); + ReportFailure(UrmaFailureReason::SEND_POST); errno = rc; return -1; } @@ -797,7 +830,8 @@ int UrmaEndpoint::DoPostRecv(void* block, size_t block_size) { urma_sge_t sge{reinterpret_cast(block), static_cast(block_size), tseg, nullptr}; urma_sg_t sg{&sge, 1}; - urma_jfr_wr_t wr{sg, 0, nullptr}; + // Preserve the receive address in completions for access-error diagnostics. + urma_jfr_wr_t wr{sg, reinterpret_cast(block), nullptr}; urma_jfr_wr_t* bad = nullptr; // Use the shared-JFR path on every device, including bonding. The bonding // provider owns physical receive scheduling for the JFR; a local @@ -815,6 +849,7 @@ int UrmaEndpoint::DoPostRecv(void* block, size_t block_size) { _resource->jetty->remote_jetty) << " state=" << GetStateStr() << " on " << _socket->description(); + ReportFailure(UrmaFailureReason::RECV_POST); errno = status; return -1; } @@ -908,6 +943,7 @@ int UrmaEndpoint::SendImm(uint32_t imm) { << _resource->remote_jetty->id.id << " state=" << GetStateStr() << " on " << _socket->description(); + ReportFailure(UrmaFailureReason::SEND_POST); errno = status; return -1; } @@ -927,7 +963,63 @@ int UrmaEndpoint::SendAck(int num) { ssize_t UrmaEndpoint::HandleCompletion(const urma_cr_t& cr) { bool zerocopy = FLAGS_urma_recv_zerocopy; if (cr.status != URMA_CR_SUCCESS) { - LOG(WARNING) << "URMA completion failed, status=" << cr.status; + LOG(WARNING) << "URMA completion failed, status=" << cr.status + << " direction=" << (cr.flag.bs.s_r ? "recv" : "send") + << " flags=" << static_cast(cr.flag.value) + << " opcode=" << cr.opcode + << " local_id=" << cr.local_id + << " tpn=" << cr.tpn + << " user_ctx=" << cr.user_ctx + << " completion_len=" << cr.completion_len + << " jetty_id=" + << (_resource && _resource->jetty ? + _resource->jetty->jetty_id.id : 0) + << " jfr_id=" + << (_resource && _resource->jfr ? + _resource->jfr->jfr_id.id : 0) + << " state=" << GetStateStr() + << " on " << _socket->description(); + if (!cr.flag.bs.s_r && cr.user_ctx > 0 && cr.user_ctx <= _sbuf.size()) { + const size_t slot = static_cast(cr.user_ctx - 1); + const auto& buffer = _sbuf[slot]; + LOG(WARNING) << "URMA failed SEND buffer: slot=" << slot + << " bytes=" << buffer.size() + << " blocks=" << buffer.backing_block_num() + << " on " << _socket->description(); + for (size_t i = 0; i < buffer.backing_block_num(); ++i) { + const auto block = buffer.backing_block(i); + auto* segment = GetPoolSegFor(const_cast(block.data())); + LOG(WARNING) << "URMA failed SEND block: slot=" << slot + << " block=" << i + << " address=" << reinterpret_cast(block.data()) + << " length=" << block.size() + << " pool_segment=" << (segment != nullptr) + << " segment_va=" << (segment ? segment->seg.ubva.va : 0) + << " segment_len=" << (segment ? segment->seg.len : 0) + << " segment_token_id=" << (segment ? segment->seg.token_id : 0) + << " on " << _socket->description(); + } + } + switch (cr.status) { + case URMA_CR_ACK_TIMEOUT_ERR: + ReportFailure(UrmaFailureReason::RETRY_EXHAUSTED); + break; + case URMA_CR_RNR_RETRY_CNT_EXC_ERR: + ReportFailure(UrmaFailureReason::RNR); + break; + case URMA_CR_LOC_ACCESS_ERR: + case URMA_CR_REM_ACCESS_ABORT_ERR: + ReportFailure(UrmaFailureReason::ACCESS_ERROR); + break; + case URMA_CR_WR_FLUSH_ERR: + case URMA_CR_WR_FLUSH_ERR_DONE: + case URMA_CR_WR_SUSPEND_DONE: + case URMA_CR_WR_UNHANDLED: + break; + default: + ReportFailure(UrmaFailureReason::TRANSPORT_ERROR); + break; + } errno = EIO; return -1; } @@ -946,8 +1038,7 @@ ssize_t UrmaEndpoint::HandleCompletion(const urma_cr_t& cr) { return -1; } _sq_imm_window_size += 1; - SendAck(0); - return 0; + return SendAck(0); } uint16_t wnd = 1; // We signal every WR (complete_enable=1). uint16_t old = @@ -1035,8 +1126,8 @@ ssize_t UrmaEndpoint::HandleCompletion(const urma_cr_t& cr) { if (PostRecv(1, zerocopy) < 0) { return -1; } - if (cr.completion_len > 0) { - SendAck(1); + if (cr.completion_len > 0 && SendAck(1) < 0) { + return -1; } return static_cast(cr.completion_len); } @@ -1118,6 +1209,7 @@ void UrmaEndpoint::PollCq(Socket* m) { const int cnt = urma_poll_jfc(ep->_resource->jfc, n, crs); if (cnt < 0) { + ep->ReportFailure(UrmaFailureReason::CQ_POLL); return EIO; } if (cnt == 0) { @@ -1203,8 +1295,6 @@ void UrmaEndpoint::ApplyRemoteHello(const ParsedHello& remote) { // OnNewDataFromTcp: edge-triggered dispatcher. // ============================================================================ -static void TryReadOnTcpDuringUrmaEst(Socket* socket); - void UrmaEndpoint::OnNewDataFromTcp(Socket* m) { auto* tp = static_cast(m->_transport.get()); if (!tp) { @@ -1228,7 +1318,7 @@ void UrmaEndpoint::OnNewDataFromTcp(Socket* m) { if (!IsUrmaAvailable()) { ep->_state = FALLBACK_TCP; tp->_urma_state = UrmaTransport::URMA_OFF; - InputMessenger::OnNewMessages(m); + ep->TryReadOnTcp(); return; } SocketUniquePtr s; @@ -1244,35 +1334,43 @@ void UrmaEndpoint::OnNewDataFromTcp(Socket* m) { } else { s.release(); } - return; } // Client side: handled by ProcessHandshakeAtClient. - return; } else if (state < ESTABLISHED) { // During handshake: wake the handshake bthread parked in ReadFromFd. ep->_read_butex->fetch_add(1, butil::memory_order_release); bthread::butex_wake(ep->_read_butex); - return; } else if (state == FALLBACK_TCP) { - InputMessenger::OnNewMessages(m); + ep->TryReadOnTcp(); return; } else if (state == ESTABLISHED) { - TryReadOnTcpDuringUrmaEst(m); + ep->DrainEstablishedTcp(); return; } + // Return handshake events to the dispatcher as well. Leaving _nevent + // nonzero suppresses later EOF callbacks and retains server resources + // after a short-connection client closes its TCP fd. if (!m->MoreReadEvents(&progress)) { break; } } } -inline void UrmaEndpoint::TryReadOnTcp() { - if (_state.load(butil::memory_order_acquire) == FALLBACK_TCP) { +void UrmaEndpoint::TryReadOnTcp() { + std::unique_lock lock(_tcp_read_mutex); + if (_state.load(butil::memory_order_acquire) == FALLBACK_TCP && + !_socket->Failed()) { InputMessenger::OnNewMessages(_socket); } } -static void TryReadOnTcpDuringUrmaEst(Socket* socket) { +void UrmaEndpoint::DrainEstablishedTcp() { + std::unique_lock lock(_tcp_read_mutex); + if (_state.load(butil::memory_order_acquire) != ESTABLISHED || + _socket->Failed()) { + return; + } + Socket* socket = _socket; int progress = Socket::PROGRESS_INIT; while (true) { uint8_t byte = 0; @@ -1307,6 +1405,21 @@ void UrmaEndpoint::FallbackToTcp(UrmaTransport* transport, bool process_tcp) { } } +void UrmaEndpoint::ReportFailure(UrmaFailureReason reason) { + if (_state.load(butil::memory_order_acquire) != ESTABLISHED) { + return; + } + UrmaFailureReason expected = UrmaFailureReason::NONE; + if (reason == UrmaFailureReason::NONE || + !_runtime_failure.compare_exchange_strong(expected, reason)) { + return; + } + GlobalUrmaRecoveryPolicy().ReportPeerFailure( + _socket->CreatedByConnect() ? + butil::endpoint2str(_socket->remote_side()).c_str() : "", + _recovery_epoch, reason); +} + void UrmaEndpoint::FailHandshake(UrmaTransport* transport, int error, const char* reason) { LOG(ERROR) << "URMA handshake failed in state=" << GetStateStr() @@ -1333,7 +1446,9 @@ void* UrmaEndpoint::ProcessHandshakeAtClient(void* arg) { SocketUniquePtr s(ep->_socket); auto* tp = static_cast(s->_transport.get()); UrmaConnect::RunGuard guard(static_cast(s->_app_connect.get())); - if (!IsUrmaAvailable()) { + if (!IsUrmaAvailable() || + !GlobalUrmaRecoveryPolicy().AllowNewConnection( + butil::endpoint2str(s->remote_side()).c_str(), &ep->_recovery_epoch)) { ep->FallbackToTcp(tp, true); return nullptr; } @@ -1385,7 +1500,9 @@ void* UrmaEndpoint::ProcessHandshakeAtClient(void* arg) { } tp->_urma_state = UrmaTransport::URMA_ON; ep->_state = ESTABLISHED; + GlobalUrmaRecoveryPolicy().RecordNegotiatedConnection(ep->_recovery_epoch); ep->DispatchReceivedBytes(s, 0); + ep->DrainEstablishedTcp(); return nullptr; } @@ -1420,6 +1537,16 @@ void* UrmaEndpoint::ProcessHandshakeAtServer(void* arg) { ep->FailHandshake(tp, EPROTO, "invalid client hello"); return nullptr; } + if (!IsUrmaAvailable() || + !GlobalUrmaRecoveryPolicy().AllowNewConnection("", &ep->_recovery_epoch)) { + tp->_urma_state = UrmaTransport::URMA_OFF; + if (hs->SendLocalHello() < 0) { + ep->FailHandshake(tp, errno ? errno : EIO, "send TCP fallback hello"); + return nullptr; + } + ep->FallbackToTcp(tp, true); + return nullptr; + } ep->_state = S_ALLOC_RES; if (ep->AllocateResources() < 0) { const int saved_errno = errno ? errno : EIO; @@ -1470,7 +1597,9 @@ void* UrmaEndpoint::ProcessHandshakeAtServer(void* arg) { } tp->_urma_state = UrmaTransport::URMA_ON; ep->_state = ESTABLISHED; + GlobalUrmaRecoveryPolicy().RecordNegotiatedConnection(ep->_recovery_epoch); ep->DispatchReceivedBytes(s, 0); + ep->DrainEstablishedTcp(); } else { ep->FallbackToTcp(tp, true); } @@ -1560,6 +1689,8 @@ void UrmaEndpoint::DebugInfo(std::ostream& os, butil::StringPiece) const { << " remote_recv_block_size=" << _remote_recv_block_size << " sq_window=" << _sq_window_size.load(butil::memory_order_relaxed) << " remote_rq_window=" << _remote_rq_window_size.load(butil::memory_order_relaxed) + << " recovery_epoch=" << _recovery_epoch + << " runtime_failure=" << rdma::RdmaFailureReasonName(_runtime_failure.load()) << " handshake_version=" << _handshake_version << " read_buf=" << _input_processor.read_buf().size(); } @@ -1579,6 +1710,7 @@ int UrmaEndpoint::WaitCqEvent(SocketUniquePtr& s, const int saved_errno = errno; PLOG(ERROR) << "Fail to wait URMA JFC event from " << s->description(); + ReportFailure(UrmaFailureReason::CQ_EVENT); s->SetFailed(saved_errno, "Fail to wait URMA JFC event: %s", berror(saved_errno)); return -1; @@ -1589,6 +1721,7 @@ int UrmaEndpoint::WaitCqEvent(SocketUniquePtr& s, if (*event_jfc != _resource->jfc) { LOG(ERROR) << "Unexpected URMA JFC event on " << s->description(); errno = EPROTO; + ReportFailure(UrmaFailureReason::CQ_EVENT); s->SetFailed(EPROTO, "Unexpected URMA JFC event"); return -1; } @@ -1604,6 +1737,7 @@ int UrmaEndpoint::ReqNotifyCq() { if (rc != URMA_SUCCESS) { errno = rc; PLOG(WARNING) << "Fail to rearm URMA JFC"; + ReportFailure(UrmaFailureReason::CQ_NOTIFY); _socket->SetFailed(rc, "Fail to rearm URMA JFC: %s", berror(rc)); return -1; } diff --git a/src/brpc/urma/urma_endpoint.h b/src/brpc/urma/urma_endpoint.h index e5aed5ac66..033f7c9d45 100644 --- a/src/brpc/urma/urma_endpoint.h +++ b/src/brpc/urma/urma_endpoint.h @@ -18,6 +18,8 @@ #ifndef BRPC_URMA_ENDPOINT_H #define BRPC_URMA_ENDPOINT_H +#include "brpc/urma/urma_recovery_policy.h" + #include #include #include @@ -100,6 +102,7 @@ struct UrmaResource { // windows, and the registered buffer bookkeeping. Cache-line padded to avoid // false sharing between connections. class BAIDU_CACHELINE_ALIGNMENT UrmaEndpoint : public SocketUser { + friend class UrmaRecoveryTestPeer; friend class UrmaConnect; friend class Socket; friend class UrmaTransport; @@ -259,8 +262,10 @@ class BAIDU_CACHELINE_ALIGNMENT UrmaEndpoint : public SocketUser { void PollerAddCqSid(); void PollerRemoveCqSid(); - inline void TryReadOnTcp(); + void TryReadOnTcp(); + void DrainEstablishedTcp(); void FallbackToTcp(UrmaTransport* transport, bool process_tcp); + void ReportFailure(UrmaFailureReason reason); void FailHandshake(UrmaTransport* transport, int error, const char* reason); @@ -272,11 +277,16 @@ class BAIDU_CACHELINE_ALIGNMENT UrmaEndpoint : public SocketUser { // Not owned. Socket* _socket; + uint64_t _recovery_epoch{0}; + butil::atomic _runtime_failure{UrmaFailureReason::NONE}; + // Handshake state. butil::atomic _state{UNINIT}; int _handshake_version{0}; // 0 = unnegotiated; 2 = v2; 3 = v3 butil::atomic _pending_received_bytes{0}; butil::Mutex _dispatch_mutex; + // Serialize TCP parsing / EOF checks across event and handshake bthreads. + butil::Mutex _tcp_read_mutex; // The URMA resources (jetty / jfc / jfr / jfce / imported peer objects). UrmaResource* _resource{nullptr}; diff --git a/src/brpc/urma/urma_handshake.cpp b/src/brpc/urma/urma_handshake.cpp index b7018606e0..ac7eb115ce 100644 --- a/src/brpc/urma/urma_handshake.cpp +++ b/src/brpc/urma/urma_handshake.cpp @@ -328,7 +328,12 @@ int UrmaHandshakeServerV3::ReceiveAndParseRemoteHello(ParsedHello* out, int UrmaHandshakeServerV3::SendLocalHello() { UrmaHello msg; _ep->FillLocalHelloV3(&msg); - // v3 has no zero-out path; the client rejects jetty_id==0 via ValidHello. + auto* tp = static_cast(_ep->_socket->_transport.get()); + if (tp->_urma_state.load(butil::memory_order_acquire) == + UrmaTransport::URMA_OFF) { + msg.set_jetty_id(0); + msg.set_buffer_size(0); + } return _ep->WriteHelloV3(msg); } diff --git a/src/brpc/urma/urma_helper.cpp b/src/brpc/urma/urma_helper.cpp index ded662fb22..b9aa4b84c0 100644 --- a/src/brpc/urma/urma_helper.cpp +++ b/src/brpc/urma/urma_helper.cpp @@ -28,6 +28,7 @@ #include #include +#include #include #include #include @@ -75,11 +76,15 @@ DEFINE_int32(urma_cqe_poll_once, 32, "Max completion entries polled per urma_poll_jfc call"); DEFINE_bool(urma_recv_zerocopy, true, "Use zero-copy for receives larger than --urma_zerocopy_min_size"); +DEFINE_bool(urma_copy_send, false, + "Copy host send payloads into a 64-byte aligned registered buffer"); DEFINE_int32(urma_zerocopy_min_size, 512, "Receives smaller than this many bytes are copied (not zero-copy)"); DEFINE_string(urma_device, "", "The name of the URMA device to use. Empty means the first one."); +DEFINE_string(urma_tp_type, "auto", + "RM transport path: auto (prefer CTP, then RTP), ctp, or rtp"); DEFINE_int32(urma_max_sge, 0, "Max SGEs per WR. 0 means the device maximum."); DEFINE_int32(urma_bonding_mode, 0, @@ -123,6 +128,7 @@ static bool g_is_bonding_device = false; // Prefer the device capability table and retain priority 6 as a compatibility // fallback for CTP providers that do not report a priority. static uint8_t g_jetty_priority = 6; +static urma_tp_type_t g_tp_type = URMA_CTP; // urma_init/urma_uninit manage process-global liburma state. Only uninitialize // it when this helper performed the successful initialization; URMA_EEXIST // means another component owns that state. @@ -383,6 +389,12 @@ bool InitPool() { g_pool_base = nullptr; return false; } + LOG(INFO) << "URMA registered buffer pool: base=" << g_pool_base + << " size=" << g_pool_size + << " segment_va=" << g_pool_seg->seg.ubva.va + << " segment_len=" << g_pool_seg->seg.len + << " segment_access=" << g_pool_seg->seg.attr.bs.access + << " segment_token_id=" << g_pool_seg->seg.token_id; g_pool->in_use.assign(count, 0); for (size_t i = 0; i < count; ++i) { auto shard = i % kShardCount; @@ -412,6 +424,65 @@ urma_target_seg_t* GetPoolSegFor(void* buf) { return nullptr; } +bool InitUrmaLocalSge(urma_sge_t* sge, uint64_t address, size_t length, + urma_target_seg_t* segment) { + if (!sge || !segment || length == 0 || + length > std::numeric_limits::max() || + segment->urma_ctx != GetUrmaContext()) { + errno = ERDMAMEM; + return false; + } + const uint64_t base = segment->seg.ubva.va; + const uint64_t size = segment->seg.len; + if (size > std::numeric_limits::max() - base || + address < base || address - base > size || + length > size - (address - base)) { + errno = ERDMAMEM; + return false; + } + *sge = urma_sge_t{address, static_cast(length), segment, nullptr}; + return true; +} + +bool CopyUrmaSendBuffer(butil::IOBuf* buffer, urma_sge_t* sge, + urma_target_seg_t* (*lookup)(void*)) { + const size_t length = buffer->size(); + const size_t capacity = GetUrmaRecvBlockSize(); + if (length == 0 || capacity < URMA_SEND_BUFFER_ALIGNMENT || + length > capacity - (URMA_SEND_BUFFER_ALIGNMENT - 1)) { + errno = EMSGSIZE; + return false; + } + butil::IOBuf copied; + void* data = nullptr; + size_t padding = 0; + { + butil::IOBufAsZeroCopyOutputStream stream( + &copied, capacity + sizeof(butil::IOBuf::Block)); + int available = 0; + if (!stream.Next(&data, &available)) { + errno = ENOMEM; + return false; + } + padding = (URMA_SEND_BUFFER_ALIGNMENT - + reinterpret_cast(data) % URMA_SEND_BUFFER_ALIGNMENT) % + URMA_SEND_BUFFER_ALIGNMENT; + if (padding + length > static_cast(available)) { + errno = ENOMEM; + return false; + } + data = static_cast(data) + padding; + buffer->copy_to(data, length); + stream.BackUp(available - static_cast(padding + length)); + } + copied.pop_front(padding); + if (!InitUrmaLocalSge(sge, reinterpret_cast(data), length, lookup(data))) { + return false; + } + buffer->swap(copied); + return true; +} + static void GlobalRelease() { g_urma_available.store(false, butil::memory_order_release); if (g_mem_alloc_orig) { @@ -450,6 +521,7 @@ static void GlobalRelease() { g_has_local_eid = false; g_is_bonding_device = false; g_jetty_priority = 6; + g_tp_type = URMA_CTP; if (g_owns_urma_init) { const urma_status_t status = urma_uninit(); if (status != URMA_SUCCESS) { @@ -525,13 +597,22 @@ static bool GlobalUrmaInitializeImpl() { g_device = nullptr; return false; } - const int ctp_priority = - FindUrmaPriorityForTpType(g_device_attr, URMA_CTP); - if (ctp_priority >= 0) { - g_jetty_priority = static_cast(ctp_priority); - } else { - LOG(WARNING) << "URMA device does not report a CTP priority; " - "falling back to compatibility priority " + if (!SelectUrmaTransportPath(g_device_attr, FLAGS_urma_tp_type, + &g_tp_type, &g_jetty_priority)) { + LOG(ERROR) << "URMA path selection failed: requested=" + << FLAGS_urma_tp_type << " device=" << device_name + << " ctp_priority=" + << FindUrmaPriorityForTpType(g_device_attr, URMA_CTP) + << " rtp_priority=" + << FindUrmaPriorityForTpType(g_device_attr, URMA_RTP); + urma_free_device_list(devices); + g_device = nullptr; + errno = EINVAL; + return false; + } + if (FindUrmaPriorityForTpType(g_device_attr, g_tp_type) < 0) { + LOG(WARNING) << "URMA device does not report path capabilities; " + "using compatibility priority " << static_cast(g_jetty_priority); } uint32_t eid_cnt = 0; @@ -628,7 +709,10 @@ static bool GlobalUrmaInitializeImpl() { // resources on exit; GlobalRelease remains available for init rollback. LOG(INFO) << "URMA initialized: device=" << device_name << " bonding=" << g_is_bonding_device + << " tp_type=" << (g_tp_type == URMA_CTP ? "ctp" : "rtp") + << " priority=" << static_cast(g_jetty_priority) << " max_sge=" << g_max_sge + << " copy_send=" << FLAGS_urma_copy_send << " buffer_size=" << g_pool_buffer_size << " buffer_count=" << g_pool->buffer_count(); return true; @@ -678,14 +762,46 @@ bool IsUrmaBondingDevice() { return g_is_bonding_device; } int FindUrmaPriorityForTpType(const urma_device_attr_t& attr, urma_tp_type_t tp_type) { const union urma_tp_type_en expected = TpTypeCapability(tp_type); + if (expected.value == 0) { + return -1; + } for (int priority = 0; priority <= URMA_MAX_PRIORITY; ++priority) { - if (attr.dev_cap.priority_info[priority].tp_type.value == - expected.value) { + if ((attr.dev_cap.priority_info[priority].tp_type.value & + expected.value) != 0) { return priority; } } return -1; } +bool SelectUrmaTransportPath(const urma_device_attr_t& attr, + const std::string& requested, + urma_tp_type_t* tp_type, uint8_t* priority) { + if (!tp_type || !priority || + (requested != "auto" && requested != "ctp" && requested != "rtp")) { + return false; + } + for (auto candidate : {URMA_CTP, URMA_RTP}) { + if ((requested == "ctp" && candidate != URMA_CTP) || + (requested == "rtp" && candidate != URMA_RTP)) { + continue; + } + const int match = FindUrmaPriorityForTpType(attr, candidate); + if (match >= 0) { + *tp_type = candidate; + *priority = static_cast(match); + return true; + } + } + for (int i = 0; i <= URMA_MAX_PRIORITY; ++i) { + if (attr.dev_cap.priority_info[i].tp_type.value != 0) { + return false; + } + } + *tp_type = requested == "rtp" ? URMA_RTP : URMA_CTP; + *priority = *tp_type == URMA_CTP ? 6 : 0; + return true; +} +urma_tp_type_t GetUrmaTpType() { return g_tp_type; } uint8_t GetUrmaJettyPriority() { return g_jetty_priority; } int GetUrmaMaxSge() { return g_max_sge; } size_t GetUrmaRecvBlockSize() { return g_recv_block_size; } diff --git a/src/brpc/urma/urma_helper.h b/src/brpc/urma/urma_helper.h index 871f1a6354..b46475f3da 100644 --- a/src/brpc/urma/urma_helper.h +++ b/src/brpc/urma/urma_helper.h @@ -26,6 +26,10 @@ #include "bthread/types.h" #include "butil/atomicops.h" +namespace butil { +class IOBuf; +} + #if BRPC_WITH_URMA #include "urma_api.h" @@ -75,14 +79,30 @@ const urma_eid_t* GetUrmaLocalEid(); // Return true when the selected URMA device is a bonding provider device. bool IsUrmaBondingDevice(); -// Find the priority whose advertised transport-path capability exactly -// matches @tp_type. Returns -1 when the device does not report one. +// Find the first priority advertising @tp_type, including combined capabilities. +// Returns -1 when the device does not report one. int FindUrmaPriorityForTpType(const urma_device_attr_t& attr, urma_tp_type_t tp_type); -// Return the priority selected for the CTP jettys created by brpc. +// Select an RM path from "auto", "ctp", or "rtp". Auto prefers CTP, then RTP. +// Providers without a capability table retain CTP/6 or explicit RTP/0. +bool SelectUrmaTransportPath(const urma_device_attr_t& attr, + const std::string& requested, + urma_tp_type_t* tp_type, uint8_t* priority); + +// Return the process-wide path and priority selected at initialization. +urma_tp_type_t GetUrmaTpType(); uint8_t GetUrmaJettyPriority(); +// Initialize a local SGE whose complete byte range belongs to this context. +bool InitUrmaLocalSge(urma_sge_t* sge, uint64_t address, size_t length, + urma_target_seg_t* segment); + +constexpr size_t URMA_SEND_BUFFER_ALIGNMENT = 64; +// Copy host payloads into one aligned block and transfer its ownership to buffer. +bool CopyUrmaSendBuffer(butil::IOBuf* buffer, urma_sge_t* sge, + urma_target_seg_t* (*lookup)(void*) = GetPoolSegFor); + // If the URMA environment is available. bool IsUrmaAvailable(); diff --git a/src/brpc/urma/urma_recovery_policy.h b/src/brpc/urma/urma_recovery_policy.h new file mode 100644 index 0000000000..da2792fd54 --- /dev/null +++ b/src/brpc/urma/urma_recovery_policy.h @@ -0,0 +1,41 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#ifndef BRPC_URMA_RECOVERY_POLICY_H +#define BRPC_URMA_RECOVERY_POLICY_H + +#include "brpc/rdma/rdma_recovery_policy.h" + +namespace brpc { +namespace urma { + +// Share the admission algorithm while keeping transport state independent. +using UrmaRecoveryMode = rdma::RdmaRecoveryMode; +using UrmaFailureReason = rdma::RdmaFailureReason; +using UrmaRecoveryStatus = rdma::RdmaRecoveryStatus; +using UrmaRecoveryPolicy = rdma::RdmaRecoveryPolicy; + +UrmaRecoveryPolicy& GlobalUrmaRecoveryPolicy(); +bool SetUrmaRecoveryMode(UrmaRecoveryMode mode); +UrmaRecoveryMode GetUrmaRecoveryMode(); +bool SetUrmaRecoveryCooldownMs(int64_t cooldown_ms); +UrmaRecoveryStatus GetUrmaRecoveryStatus(); + +} // namespace urma +} // namespace brpc + +#endif // BRPC_URMA_RECOVERY_POLICY_H diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a0fc2d0536..971fc91752 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -295,6 +295,13 @@ foreach(BRPC_UT ${BRPC_UNITTESTS}) brpc_test_config ${GPERFTOOLS_LIBRARIES}) add_test(NAME ${BRPC_UT_WE} COMMAND ${BRPC_UT_WE}) + if(WITH_URMA AND BRPC_UT_WE STREQUAL "brpc_urma_transport_unittest") + add_test(NAME brpc_urma_transport_rtp_unittest + COMMAND ${BRPC_UT_WE} --urma_tp_type=rtp) + add_test(NAME brpc_urma_transport_rtp_stress_unittest + COMMAND ${BRPC_UT_WE} --urma_tp_type=rtp + --gtest_shuffle --gtest_random_seed=92 --gtest_repeat=20) + endif() endforeach() if(BUILD_FUZZ_TESTS) diff --git a/test/brpc_rdma_error_recovery_unittest.cpp b/test/brpc_rdma_error_recovery_unittest.cpp new file mode 100644 index 0000000000..1eaaaf5d71 --- /dev/null +++ b/test/brpc_rdma_error_recovery_unittest.cpp @@ -0,0 +1,220 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include +#include +#include +#include + +#if BRPC_WITH_RDMA +#include "brpc/global.h" +#include "brpc/input_messenger.h" +#include "brpc/rdma/rdma_endpoint.h" +#include "brpc/rdma/rdma_helper.h" +#include "brpc/rdma/rdma_recovery_policy.h" +#include "brpc/rdma_transport.h" +#include "brpc/socket.h" + +namespace brpc { +namespace rdma { +extern bool g_skip_rdma_init; +extern butil::atomic g_rdma_available; +} // namespace rdma +} // namespace brpc + +namespace { +using namespace brpc; + +struct CompletionSource { + ibv_wc_status status; + bool delivered = false; +}; + +int PollError(ibv_cq* cq, int, ibv_wc* wc) { + auto* source = static_cast(cq->cq_context); + if (source->delivered) { + return 0; + } + source->delivered = true; + std::memset(wc, 0, sizeof(*wc)); + wc->status = source->status; + return 1; +} + +int FailRecvPost(ibv_qp*, ibv_recv_wr*, ibv_recv_wr**) { + return EIO; +} + +class RdmaErrorRecoveryTest : public testing::Test { +protected: + void SetUp() override { + GlobalInitializeOrDie(); + _saved_mode = rdma::GetRdmaRecoveryMode(); + ResetPolicy(); + } + + void TearDown() override { + rdma::SetRdmaRecoveryMode(rdma::RdmaRecoveryMode::LEGACY); + rdma::SetRdmaRecoveryMode(_saved_mode); + } + + void ResetPolicy() { + rdma::SetRdmaRecoveryMode(rdma::RdmaRecoveryMode::LEGACY); + rdma::SetRdmaRecoveryMode(rdma::RdmaRecoveryMode::AUTO); + } + + void CreateSocket(SocketUniquePtr* socket) { + SocketOptions options; + options.socket_mode = SOCKET_MODE_RDMA; + ASSERT_EQ(0, butil::str2endpoint("127.0.0.1:8123", &options.remote_side)); + options.user = get_client_side_messenger(); + SocketId id; + ASSERT_EQ(0, Socket::Create(options, &id)); + ASSERT_EQ(0, Socket::Address(id, socket)); + } + + static rdma::RdmaEndpoint* Endpoint(const SocketUniquePtr& socket) { + return static_cast(socket->_transport.get())->GetRdmaEp(); + } + + void CheckCompletion(ibv_wc_status status, rdma::RdmaFailureReason reason, + bool cooldown) { + ResetPolicy(); + SocketUniquePtr socket; + ASSERT_NO_FATAL_FAILURE(CreateSocket(&socket)); + auto* endpoint = Endpoint(socket); + auto& policy = rdma::GlobalRdmaRecoveryPolicy(); + ASSERT_TRUE(policy.AllowNewConnection(_peer, &endpoint->_recovery_epoch)); + const auto before = policy.GetStatus(); + SocketOptions carrier_options; + carrier_options.user = endpoint; + SocketId carrier_id; + ASSERT_EQ(0, Socket::Create(carrier_options, &carrier_id)); + SocketUniquePtr carrier; + ASSERT_EQ(0, Socket::Address(carrier_id, &carrier)); + + CompletionSource source{status}; + ibv_context context{}; + context.ops.poll_cq = PollError; + ibv_cq cq{}; + cq.context = &context; + cq.cq_context = &source; + rdma::RdmaResource resource; + resource.polling_cq = &cq; + endpoint->_resource = &resource; + endpoint->_cq_sid = carrier_id; + endpoint->_state.store(rdma::RdmaEndpoint::ESTABLISHED); + rdma::FLAGS_rdma_use_polling = true; + + rdma::RdmaEndpoint::PollCq(carrier.get()); + EXPECT_TRUE(source.delivered); + EXPECT_TRUE(socket->Failed()); + EXPECT_EQ(reason, endpoint->_runtime_failure.load()); + EXPECT_EQ(!cooldown, policy.AllowNewConnection(_peer)); + EXPECT_TRUE(policy.AllowNewConnection("127.0.0.1:9999")); + const auto after = policy.GetStatus(); + for (size_t i = 1; i < static_cast(rdma::RdmaFailureReason::COUNT); ++i) { + EXPECT_EQ(before.failures[i] + (i == static_cast(reason)), + after.failures[i]); + } + if (reason != rdma::RdmaFailureReason::NONE) { + endpoint->ReportFailure(rdma::RdmaFailureReason::CQ_ERROR); + EXPECT_EQ(after.failures[static_cast(rdma::RdmaFailureReason::CQ_ERROR)], + policy.GetStatus().failures[ + static_cast(rdma::RdmaFailureReason::CQ_ERROR)]); + } + + endpoint->_resource = nullptr; + endpoint->_cq_sid = INVALID_SOCKET_ID; + resource.polling_cq = nullptr; + carrier->_user = nullptr; + carrier->SetFailed(); + } + + const std::string _peer = "127.0.0.1:8123"; + GFLAGS_NAMESPACE::FlagSaver _flags; + rdma::RdmaRecoveryMode _saved_mode; +}; + +TEST_F(RdmaErrorRecoveryTest, fatal_timeout_and_general_completions_cool_peer) { + for (auto status : {IBV_WC_FATAL_ERR, IBV_WC_RESP_TIMEOUT_ERR, IBV_WC_GENERAL_ERR}) { + ASSERT_NO_FATAL_FAILURE(CheckCompletion( + status, rdma::RdmaFailureReason::TRANSPORT_ERROR, true)); + } +} + +TEST_F(RdmaErrorRecoveryTest, retry_and_rnr_completions_cool_peer) { + ASSERT_NO_FATAL_FAILURE(CheckCompletion( + IBV_WC_RETRY_EXC_ERR, rdma::RdmaFailureReason::RETRY_EXHAUSTED, true)); + ASSERT_NO_FATAL_FAILURE(CheckCompletion( + IBV_WC_RNR_RETRY_EXC_ERR, rdma::RdmaFailureReason::RNR, true)); +} + +TEST_F(RdmaErrorRecoveryTest, access_and_flush_completions_preserve_admission) { + for (auto status : {IBV_WC_LOC_PROT_ERR, IBV_WC_LOC_ACCESS_ERR, + IBV_WC_REM_ACCESS_ERR, IBV_WC_MW_BIND_ERR}) { + ASSERT_NO_FATAL_FAILURE(CheckCompletion( + status, rdma::RdmaFailureReason::ACCESS_ERROR, false)); + } + ASSERT_NO_FATAL_FAILURE(CheckCompletion( + IBV_WC_WR_FLUSH_ERR, rdma::RdmaFailureReason::NONE, false)); +} + +TEST_F(RdmaErrorRecoveryTest, receive_post_error_preserves_errno_and_reports_once) { + for (bool established : {false, true}) { + ResetPolicy(); + SocketUniquePtr socket; + ASSERT_NO_FATAL_FAILURE(CreateSocket(&socket)); + auto* endpoint = Endpoint(socket); + auto& policy = rdma::GlobalRdmaRecoveryPolicy(); + ASSERT_TRUE(policy.AllowNewConnection(_peer, &endpoint->_recovery_epoch)); + const auto before = policy.GetStatus(); + ibv_context context{}; + context.ops.post_recv = FailRecvPost; + ibv_qp qp{}; + qp.context = &context; + rdma::RdmaResource resource; + resource.qp = &qp; + endpoint->_resource = &resource; + endpoint->_state.store(established ? rdma::RdmaEndpoint::ESTABLISHED : + rdma::RdmaEndpoint::C_ALLOC_QPCQ); + char data[32]{}; + for (int attempt = 0; attempt < 2; ++attempt) { + errno = EALREADY; + EXPECT_EQ(-1, endpoint->DoPostRecv(data, sizeof(data))); + EXPECT_EQ(EIO, errno); + } + EXPECT_EQ(!established, policy.AllowNewConnection(_peer)); + const size_t index = static_cast(rdma::RdmaFailureReason::RECV_POST); + EXPECT_EQ(before.failures[index] + established, policy.GetStatus().failures[index]); + endpoint->_resource = nullptr; + resource.qp = nullptr; + socket->SetFailed(); + } +} +} // namespace +#endif // BRPC_WITH_RDMA + +int main(int argc, char** argv) { + testing::InitGoogleTest(&argc, argv); + GFLAGS_NAMESPACE::ParseCommandLineFlags(&argc, &argv, true); +#if BRPC_WITH_RDMA + brpc::rdma::g_rdma_available.store(true, butil::memory_order_relaxed); + brpc::rdma::g_skip_rdma_init = true; +#endif + return RUN_ALL_TESTS(); +} diff --git a/test/brpc_rdma_recovery_unittest.cpp b/test/brpc_rdma_recovery_unittest.cpp new file mode 100644 index 0000000000..734fe794e2 --- /dev/null +++ b/test/brpc_rdma_recovery_unittest.cpp @@ -0,0 +1,186 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include +#include +#include +#include +#include "brpc/rdma/rdma_recovery_policy.h" + +namespace brpc { +namespace rdma { +namespace { + +std::atomic now_us(1000000); +int64_t FakeClock() { return now_us.load(); } + +class RdmaRecoveryTest : public testing::Test { +protected: + RdmaRecoveryTest() : policy(FakeClock, 2) {} + void SetUp() override { now_us = 1000000; } + RdmaRecoveryPolicy policy; +}; + +TEST_F(RdmaRecoveryTest, legacy_preserves_admission) { + policy.ReportPortState(false); + uint64_t epoch; + EXPECT_TRUE(policy.AllowNewConnection("a", &epoch)); + policy.ReportPeerFailure("a", epoch); + EXPECT_TRUE(policy.AllowNewConnection("a")); + EXPECT_TRUE(policy.SetMode(RdmaRecoveryMode::AUTO)); + EXPECT_FALSE(policy.AllowNewConnection("a")); +} + +TEST_F(RdmaRecoveryTest, peer_failure_only_cools_that_peer) { + policy.SetMode(RdmaRecoveryMode::AUTO); + policy.SetCooldownMs(10); + uint64_t epoch; + ASSERT_TRUE(policy.AllowNewConnection("a", &epoch)); + policy.ReportPeerFailure("a", epoch); + EXPECT_FALSE(policy.AllowNewConnection("a")); + EXPECT_TRUE(policy.AllowNewConnection("b")); + EXPECT_TRUE(policy.AllowNewConnection("")); + now_us += 9999; + EXPECT_FALSE(policy.AllowNewConnection("a")); + ++now_us; + EXPECT_TRUE(policy.AllowNewConnection("a")); +} + +TEST_F(RdmaRecoveryTest, port_requires_active_and_elapsed_cooldown) { + policy.SetMode(RdmaRecoveryMode::AUTO); + policy.SetCooldownMs(10); + policy.ReportPortState(false); + EXPECT_FALSE(policy.AllowNewConnection("a")); + EXPECT_FALSE(policy.AllowNewConnection("")); + policy.ReportPortState(true); + EXPECT_FALSE(policy.AllowNewConnection("a")); + now_us += 10000; + EXPECT_TRUE(policy.AllowNewConnection("a")); + policy.ReportPortState(false); + now_us += 10000; + EXPECT_FALSE(policy.AllowNewConnection("a")); + policy.ReportPortState(true); + EXPECT_TRUE(policy.AllowNewConnection("a")); +} + +TEST_F(RdmaRecoveryTest, tcp_only_and_stale_failure_after_mode_change) { + policy.SetMode(RdmaRecoveryMode::AUTO); + uint64_t old_epoch; + ASSERT_TRUE(policy.AllowNewConnection("a", &old_epoch)); + policy.SetMode(RdmaRecoveryMode::TCP_ONLY); + EXPECT_FALSE(policy.AllowNewConnection("a")); + EXPECT_FALSE(policy.AllowNewConnection("")); + policy.SetMode(RdmaRecoveryMode::AUTO); + policy.ReportPeerFailure("a", old_epoch); + EXPECT_TRUE(policy.AllowNewConnection("a")); + uint64_t epoch; + ASSERT_TRUE(policy.AllowNewConnection("a", &epoch)); + EXPECT_NE(old_epoch, epoch); + // Reapplying AUTO does not clear existing quarantine. + policy.ReportPeerFailure("a", epoch); + policy.SetMode(RdmaRecoveryMode::AUTO); + EXPECT_FALSE(policy.AllowNewConnection("a")); +} + +TEST_F(RdmaRecoveryTest, bounded_peer_cache_evicts_earliest_expiry) { + policy.SetMode(RdmaRecoveryMode::AUTO); + uint64_t epoch; + policy.AllowNewConnection("a", &epoch); + policy.ReportPeerFailure("a", epoch); + ++now_us; + policy.ReportPeerFailure("b", epoch); + ++now_us; + policy.ReportPeerFailure("c", epoch); + EXPECT_TRUE(policy.AllowNewConnection("a")); + EXPECT_FALSE(policy.AllowNewConnection("b")); + EXPECT_FALSE(policy.AllowNewConnection("c")); + now_us += 5000000; + policy.ReportPeerFailure("d", epoch); + EXPECT_TRUE(policy.AllowNewConnection("b")); + EXPECT_TRUE(policy.AllowNewConnection("c")); + EXPECT_FALSE(policy.AllowNewConnection("d")); +} + +TEST_F(RdmaRecoveryTest, validates_configuration) { + EXPECT_FALSE(policy.SetMode(static_cast(-1))); + EXPECT_EQ(RdmaRecoveryMode::LEGACY, policy.GetMode()); + EXPECT_FALSE(policy.SetCooldownMs(0)); + EXPECT_FALSE(policy.SetCooldownMs(-1)); + EXPECT_FALSE(policy.SetCooldownMs(static_cast(INT32_MAX) + 1)); + EXPECT_TRUE(policy.SetCooldownMs(INT32_MAX)); + policy.SetMode(RdmaRecoveryMode::AUTO); + uint64_t epoch; + policy.AllowNewConnection("a", &epoch); + policy.ReportPeerFailure("a", epoch); + EXPECT_FALSE(policy.AllowNewConnection("a")); +} + +TEST_F(RdmaRecoveryTest, concurrent_admission_failure_and_mode_change) { + std::vector workers; + for (int i = 0; i < 8; ++i) { + workers.emplace_back([this, i] { + for (int j = 0; j < 200; ++j) { + uint64_t epoch; + const std::string peer = std::to_string(i); + policy.SetMode(j % 2 ? RdmaRecoveryMode::AUTO : + RdmaRecoveryMode::TCP_ONLY); + policy.AllowNewConnection(peer, &epoch); + policy.ReportPeerFailure(peer, epoch); + } + }); + } + for (auto& worker : workers) { + worker.join(); + } + policy.SetMode(RdmaRecoveryMode::TCP_ONLY); + EXPECT_FALSE(policy.AllowNewConnection("a")); + EXPECT_FALSE(policy.AllowNewConnection("")); +} + +TEST_F(RdmaRecoveryTest, reports_reasons_and_admission_counters) { + policy.SetMode(RdmaRecoveryMode::AUTO); + uint64_t epoch; + policy.AllowNewConnection("a", &epoch); + policy.RecordNegotiatedConnection(epoch); + policy.ReportPeerFailure("a", epoch, RdmaFailureReason::QP_FATAL); + EXPECT_FALSE(policy.AllowNewConnection("a")); + auto status = policy.GetStatus(); + EXPECT_EQ(1u, status.cooled_peers); + EXPECT_EQ(1u, status.negotiated_connections); + EXPECT_EQ(1u, status.peer_cooldown_admissions); + EXPECT_EQ(1u, status.failures[static_cast(RdmaFailureReason::QP_FATAL)]); + policy.ReportPeerFailure("b", epoch, RdmaFailureReason::ACCESS_ERROR); + EXPECT_TRUE(policy.AllowNewConnection("b")); + policy.ReportPortState(false); + EXPECT_FALSE(policy.AllowNewConnection("b")); + status = policy.GetStatus(); + EXPECT_FALSE(status.port_active); + EXPECT_EQ(5000, status.port_cooldown_ms); + EXPECT_EQ(1u, status.port_errors); + EXPECT_EQ(1u, status.port_cooldown_admissions); + now_us += 5000000; + EXPECT_EQ(0u, policy.GetStatus().cooled_peers); + policy.SetMode(RdmaRecoveryMode::TCP_ONLY); + EXPECT_FALSE(policy.AllowNewConnection("b")); + EXPECT_EQ(1u, policy.GetStatus().tcp_only_admissions); + policy.ReportPeerFailure("b", epoch, RdmaFailureReason::CQ_ERROR); + EXPECT_EQ(0u, policy.GetStatus().failures[static_cast(RdmaFailureReason::CQ_ERROR)]); +} + +} // namespace +} // namespace rdma +} // namespace brpc diff --git a/test/brpc_rdma_runtime_unittest.cpp b/test/brpc_rdma_runtime_unittest.cpp new file mode 100644 index 0000000000..a1db214a5a --- /dev/null +++ b/test/brpc_rdma_runtime_unittest.cpp @@ -0,0 +1,510 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + + +#include +#include +#include +#include +#if BRPC_WITH_RDMA +#include +#include +#include +#include +#include +#include "butil/endpoint.h" +#include "butil/fd_guard.h" +#include "butil/iobuf.h" +#include "butil/sys_byteorder.h" +#include "butil/time.h" +#include "brpc/acceptor.h" +#include "brpc/channel.h" +#include "brpc/controller.h" +#include "brpc/input_messenger.h" +#include "brpc/server.h" +#include "brpc/socket.h" +#include "brpc/errno.pb.h" +#include "brpc/rdma_transport.h" +#include "brpc/rdma/rdma_endpoint.h" +#include "brpc/rdma/rdma_handshake.h" +#include "brpc/rdma/rdma_handshake_constants.h" +#include "brpc/rdma/rdma_handshake.pb.h" +#include "brpc/rdma/rdma_helper.h" +#include "brpc/rdma/rdma_recovery_policy.h" +#include "echo.pb.h" + +using namespace brpc; + +namespace brpc { +namespace rdma { +extern butil::atomic g_rdma_available; +extern bool g_skip_rdma_init; +} // namespace rdma +} // namespace brpc + +static butil::EndPoint g_ep; +static const int64_t WAIT_TIMEOUT_US = 5000000; +static const int64_t WAIT_INTERVAL_US = 1000; + +static bool WaitUntil(const std::function& pred, + int64_t timeout_us = WAIT_TIMEOUT_US) { + const int64_t deadline = butil::gettimeofday_us() + timeout_us; + while (!pred()) { + if (butil::gettimeofday_us() >= deadline) { + return false; + } + usleep((useconds_t)WAIT_INTERVAL_US); + } + return true; +} + +// write(2) may transfer less than asked for, so a test that cares about the +// whole buffer reaching the peer has to loop. +static bool WriteAll(int fd, const void* buf, size_t len) { + const uint8_t* p = (const uint8_t*)buf; + for (size_t done = 0; done < len; ) { + ssize_t n = write(fd, p + done, len - done); + if (n < 0) { + if (errno == EINTR) { + continue; + } + return false; + } + done += n; + } + return true; +} + +static void ConnectToServer(butil::fd_guard* sockfd) { + sockaddr_in addr; + bzero((char*)&addr, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(g_ep.port); + addr.sin_addr = g_ep.ip; + sockfd->reset(socket(AF_INET, SOCK_STREAM, 0)); + ASSERT_TRUE(*sockfd >= 0); + timeval tv; + tv.tv_sec = WAIT_TIMEOUT_US / 1000000; + tv.tv_usec = WAIT_TIMEOUT_US % 1000000; + ASSERT_EQ(0, setsockopt(*sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv))); + ASSERT_EQ(0, connect(*sockfd, (sockaddr*)&addr, sizeof(addr))); +} + +class RecoveryEchoService : public ::test::EchoService { + void Echo(google::protobuf::RpcController*, const ::test::EchoRequest*, + ::test::EchoResponse* response, + google::protobuf::Closure* done) override { + ClosureGuard done_guard(done); + response->set_message("MyEchoService"); + } +}; + +class RdmaRuntimeFixture : public ::testing::Test { +protected: + RdmaRuntimeFixture() { + EXPECT_EQ(0, butil::str2endpoint("127.0.0.1:8123", &g_ep)); + EXPECT_EQ(0, _server.AddService(&_service, SERVER_DOESNT_OWN_SERVICE)); + } + + void StartServer(bool use_rdma = true) { + ServerOptions options; + options.enabled_protocols = "baidu_std"; + options.socket_mode = use_rdma ? SOCKET_MODE_RDMA : SOCKET_MODE_TCP; + options.idle_timeout_sec = 5; + ASSERT_EQ(0, _server.Start("127.0.0.1:0", &options)); + g_ep = _server.listen_address(); + } + + void StopServer() { + _server.Stop(0); + _server.Join(); + } + + Socket* GetSocketFromServer(size_t index) { + std::vector sids; + _server._am->ListConnections(&sids); + if (index >= sids.size()) { + return nullptr; + } + SocketUniquePtr socket; + return Socket::Address(sids[index], &socket) == 0 ? socket.get() : nullptr; + } + + Socket* WaitForServerSocket() { + Socket* socket = nullptr; + WaitUntil([this, &socket] { + return (socket = GetSocketFromServer(0)) != nullptr; + }); + return socket; + } + + bool WaitForServerSocketGone() { + return WaitUntil([this] { return GetSocketFromServer(0) == nullptr; }); + } + + Server _server; + RecoveryEchoService _service; +}; + +static RdmaTransport* RdmaTransportOf(Socket* s) { + return static_cast(s->_transport.get()); +} +static RdmaTransport* RdmaTransportOf(const SocketUniquePtr& s) { + return RdmaTransportOf(s.get()); +} + +// Polls until the endpoint reaches `expected` and returns the last state seen, +// so that ASSERT_RDMA_STATE() reports what the endpoint actually settled on. +static rdma::RdmaEndpoint::State WaitForRdmaState( + RdmaTransport* transport, rdma::RdmaEndpoint::State expected) { + rdma::RdmaEndpoint::State state = transport->_rdma_ep->_state; + WaitUntil([transport, expected, &state] { + state = transport->_rdma_ep->_state; + return state == expected; + }); + return state; +} + +// Waits for `transport` to reach `expected`, failing the test if it does not. +#define ASSERT_RDMA_STATE(expected, transport) \ + ASSERT_EQ(expected, WaitForRdmaState(transport, expected)) + +static void MakeV2ClientHello(uint8_t (&data)[rdma::HELLO_V2_MSG_LEN_MIN]) { + rdma::v2_wire::HelloMessage msg{}; + msg.msg_len = rdma::HELLO_V2_MSG_LEN_MIN; + msg.hello_ver = rdma::HELLO_V2_VERSION; + msg.impl_ver = rdma::IMPL_V2_VERSION; + msg.sq_size = 16; + msg.rq_size = 16; + msg.block_size = 8192; + msg.qp_num = 0; + msg.gid = rdma::GetRdmaGid(); + memcpy(data, "RDMA", 4); + msg.Serialize(data + 4); +} + +std::string MakeV3Packet(const rdma::RdmaHello& msg) { + std::string body; + EXPECT_TRUE(msg.SerializeToString(&body)); + std::string packet; + packet.reserve(4 + 4 + body.size()); + packet.append("RDM3", 4); + uint32_t pb_size_be = + butil::HostToNet32(static_cast(body.size())); + packet.append(reinterpret_cast(&pb_size_be), 4); + packet.append(body); + return packet; +} + +// Build a fully-valid RdmaHello: all 6 required fields are set, with +// values that pass RdmaHelloV3Wire::RdmaHelloValid(). +// - block_size = 8192 (>= MIN_BLOCK_SIZE) +// - sq_size / rq_size = 16 (>= MIN_QP_SIZE) +// - gid = exactly 16B (sizeof(ibv_gid)) +// - qp_num = 0 (allowed because g_skip_rdma_init in UT) +rdma::RdmaHello MakeValidV3Hello() { + rdma::RdmaHello msg; + msg.set_block_size(8192); + msg.set_sq_size(16); + msg.set_rq_size(16); + msg.set_lid(0); + ibv_gid gid = rdma::GetRdmaGid(); + msg.set_gid(std::string(reinterpret_cast(gid.raw), + sizeof(gid.raw))); + msg.set_qp_num(0); + return msg; +} + + +class RdmaAdmissionTest : public RdmaRuntimeFixture { +protected: + void SetUp() override { + _saved_mode = rdma::GetRdmaRecoveryMode(); + rdma::SetRdmaRecoveryMode(rdma::RdmaRecoveryMode::LEGACY); + rdma::SetRdmaRecoveryMode(rdma::RdmaRecoveryMode::AUTO); + } + + void TearDown() override { + StopServer(); + rdma::SetRdmaRecoveryMode(rdma::RdmaRecoveryMode::LEGACY); + rdma::SetRdmaRecoveryMode(_saved_mode); + } + + void CallEchoOverFallback() { + Channel channel; + ChannelOptions options; + options.socket_mode = SOCKET_MODE_RDMA; + options.connect_timeout_ms = 1000; + options.timeout_ms = 1000; + options.max_retry = 0; + ASSERT_EQ(0, channel.Init(g_ep, &options)); + Controller cntl; + test::EchoRequest req; + test::EchoResponse res; + req.set_message("runtime recovery"); + test::EchoService::Stub(&channel).Echo(&cntl, &req, &res, nullptr); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + EXPECT_EQ("MyEchoService", res.message()); + SocketUniquePtr s; + ASSERT_EQ(0, Socket::Address(cntl._single_server_id, &s)); + EXPECT_EQ(RdmaTransport::RDMA_OFF, RdmaTransportOf(s)->_rdma_state); + EXPECT_EQ(rdma::RdmaEndpoint::FALLBACK_TCP, + RdmaTransportOf(s)->_rdma_ep->_state); + EXPECT_FALSE(s->Failed()); + } + +private: + rdma::RdmaRecoveryMode _saved_mode; +}; + +TEST_F(RdmaAdmissionTest, tcp_only_carries_rpc_to_tcp_server) { + StartServer(false); + rdma::SetRdmaRecoveryMode(rdma::RdmaRecoveryMode::TCP_ONLY); + ASSERT_NO_FATAL_FAILURE(CallEchoOverFallback()); +} + +TEST_F(RdmaAdmissionTest, cooled_peer_carries_rpc_over_tcp) { + StartServer(); + auto& policy = rdma::GlobalRdmaRecoveryPolicy(); + const std::string peer = butil::endpoint2str(g_ep).c_str(); + uint64_t epoch; + ASSERT_TRUE(policy.AllowNewConnection(peer, &epoch)); + policy.ReportPeerFailure(peer, epoch); + ASSERT_FALSE(policy.AllowNewConnection(peer)); + ASSERT_NO_FATAL_FAILURE(CallEchoOverFallback()); +} + +static int PollRnrCompletion(ibv_cq* cq, int, ibv_wc* wc) { + bool* delivered = static_cast(cq->cq_context); + if (*delivered) { + return 0; + } + *delivered = true; + memset(wc, 0, sizeof(*wc)); + wc->status = IBV_WC_RNR_RETRY_EXC_ERR; + return 1; +} + +TEST_F(RdmaAdmissionTest, rnr_completion_cools_peer_before_tcp_rpc) { + StartServer(); + SocketOptions options; + options.socket_mode = SOCKET_MODE_RDMA; + options.remote_side = g_ep; + options.user = get_client_side_messenger(); + SocketId sid; + ASSERT_EQ(0, Socket::Create(options, &sid)); + SocketUniquePtr socket; + ASSERT_EQ(0, Socket::Address(sid, &socket)); + auto* ep = RdmaTransportOf(socket)->_rdma_ep; + SocketOptions cq_options; + cq_options.user = ep; + SocketId cq_sid; + ASSERT_EQ(0, Socket::Create(cq_options, &cq_sid)); + SocketUniquePtr cq_socket; + ASSERT_EQ(0, Socket::Address(cq_sid, &cq_socket)); + + auto& policy = rdma::GlobalRdmaRecoveryPolicy(); + const std::string peer = butil::endpoint2str(g_ep).c_str(); + uint64_t epoch; + ASSERT_TRUE(policy.AllowNewConnection(peer, &epoch)); + bool delivered = false; + ibv_context context{}; + context.ops.poll_cq = PollRnrCompletion; + ibv_cq cq{}; + cq.context = &context; + cq.cq_context = &delivered; + rdma::RdmaResource resource; + resource.polling_cq = &cq; + ep->_resource = &resource; + ep->_cq_sid = cq_sid; + ep->_recovery_epoch = epoch; + ep->_state.store(rdma::RdmaEndpoint::ESTABLISHED); + const bool saved_polling = rdma::FLAGS_rdma_use_polling; + rdma::FLAGS_rdma_use_polling = true; + + rdma::RdmaEndpoint::PollCq(cq_socket.get()); + const auto first_report = rdma::GetRdmaRecoveryStatus(); + ep->ReportFailure(rdma::RdmaFailureReason::QP_FATAL); + EXPECT_EQ(first_report.failures[static_cast(rdma::RdmaFailureReason::QP_FATAL)], + rdma::GetRdmaRecoveryStatus().failures[ + static_cast(rdma::RdmaFailureReason::QP_FATAL)]); + EXPECT_TRUE(delivered); + EXPECT_TRUE(socket->Failed()); + EXPECT_FALSE(policy.AllowNewConnection(peer)); + EXPECT_TRUE(policy.AllowNewConnection("127.0.0.1:9999")); + + // The test owns the mock CQ and resource; ordinary recycling owns neither. + ep->_resource = nullptr; + ep->_cq_sid = INVALID_SOCKET_ID; + resource.polling_cq = nullptr; + cq_socket->_user = nullptr; + cq_socket->SetFailed(); + rdma::FLAGS_rdma_use_polling = saved_polling; + ASSERT_NO_FATAL_FAILURE(CallEchoOverFallback()); +} + +TEST_F(RdmaAdmissionTest, server_declines_v2_and_v3_when_tcp_only) { + StartServer(); + rdma::SetRdmaRecoveryMode(rdma::RdmaRecoveryMode::TCP_ONLY); + for (int version : {2, 3}) { + butil::fd_guard fd; + ASSERT_NO_FATAL_FAILURE(ConnectToServer(&fd)); + if (version == 2) { + uint8_t hello[rdma::HELLO_V2_MSG_LEN_MIN]; + MakeV2ClientHello(hello); + ASSERT_TRUE(WriteAll(fd, hello, sizeof(hello))); + } else { + const std::string hello = MakeV3Packet(MakeValidV3Hello()); + ASSERT_TRUE(WriteAll(fd, hello.data(), hello.size())); + } + Socket* s = WaitForServerSocket(); + ASSERT_TRUE(s != nullptr); + auto* transport = RdmaTransportOf(s); + ASSERT_RDMA_STATE(rdma::RdmaEndpoint::S_ACK_WAIT, transport); + EXPECT_EQ(RdmaTransport::RDMA_OFF, transport->_rdma_state); + EXPECT_EQ(nullptr, transport->_rdma_ep->_resource); + const uint32_t ack = 0; + ASSERT_TRUE(WriteAll(fd, &ack, sizeof(ack))); + ASSERT_RDMA_STATE(rdma::RdmaEndpoint::FALLBACK_TCP, transport); + EXPECT_FALSE(s->Failed()); + fd.reset(-1); + ASSERT_TRUE(WaitForServerSocketGone()); + } +} + +TEST(RdmaCompletionRecoveryTest, classifies_path_failures) { + EXPECT_TRUE(rdma::IsRdmaPathFailure(IBV_WC_RETRY_EXC_ERR)); + EXPECT_TRUE(rdma::IsRdmaPathFailure(IBV_WC_RNR_RETRY_EXC_ERR)); + EXPECT_FALSE(rdma::IsRdmaPathFailure(IBV_WC_SUCCESS)); + EXPECT_FALSE(rdma::IsRdmaPathFailure(IBV_WC_WR_FLUSH_ERR)); + EXPECT_FALSE(rdma::IsRdmaPathFailure(IBV_WC_LOC_PROT_ERR)); + EXPECT_FALSE(rdma::IsRdmaPathFailure(IBV_WC_REM_ACCESS_ERR)); +} + +TEST_F(RdmaAdmissionTest, async_errors_validate_owner_and_report_once) { + for (ibv_event_type type : {IBV_EVENT_QP_FATAL, IBV_EVENT_CQ_ERR, + IBV_EVENT_QP_ACCESS_ERR}) { + rdma::SetRdmaRecoveryMode(rdma::RdmaRecoveryMode::LEGACY); + rdma::SetRdmaRecoveryMode(rdma::RdmaRecoveryMode::AUTO); + SocketOptions options; + options.socket_mode = SOCKET_MODE_RDMA; + options.remote_side = g_ep; + options.user = get_client_side_messenger(); + SocketId sid; + ASSERT_EQ(0, Socket::Create(options, &sid)); + SocketUniquePtr socket; + ASSERT_EQ(0, Socket::Address(sid, &socket)); + auto* ep = RdmaTransportOf(socket)->_rdma_ep; + auto& policy = rdma::GlobalRdmaRecoveryPolicy(); + const std::string peer = butil::endpoint2str(g_ep).c_str(); + ASSERT_TRUE(policy.AllowNewConnection(peer, &ep->_recovery_epoch)); + ibv_qp qp{}; + ibv_qp other_qp{}; + ibv_cq cq{}; + ibv_cq other_cq{}; + rdma::RdmaResource resource; + resource.qp = &qp; + resource.polling_cq = &cq; + ep->_resource = &resource; + ep->BindResourceToSocket(); + EXPECT_EQ(sid, reinterpret_cast(qp.qp_context)); + EXPECT_EQ(sid, reinterpret_cast(cq.cq_context)); + ep->_state.store(rdma::RdmaEndpoint::ESTABLISHED); + const auto before = policy.GetStatus(); + ibv_async_event event{}; + event.event_type = type; + if (type == IBV_EVENT_CQ_ERR) { + event.element.cq = &other_cq; + } else { + event.element.qp = &other_qp; + } + rdma::RdmaEndpoint::OnAsyncError(socket.get(), event); + EXPECT_FALSE(socket->Failed()); + if (type == IBV_EVENT_CQ_ERR) { + event.element.cq = &cq; + } else { + event.element.qp = &qp; + } + rdma::RdmaEndpoint::OnAsyncError(socket.get(), event); + EXPECT_TRUE(socket->Failed()); + const auto reason = type == IBV_EVENT_CQ_ERR ? rdma::RdmaFailureReason::CQ_ERROR : + type == IBV_EVENT_QP_FATAL ? rdma::RdmaFailureReason::QP_FATAL : + rdma::RdmaFailureReason::ACCESS_ERROR; + EXPECT_EQ(before.failures[static_cast(reason)] + 1, + policy.GetStatus().failures[static_cast(reason)]); + EXPECT_EQ(type == IBV_EVENT_QP_ACCESS_ERR, policy.AllowNewConnection(peer)); + ep->ReportFailure(rdma::RdmaFailureReason::RNR); + EXPECT_EQ(before.failures[static_cast(rdma::RdmaFailureReason::RNR)], + policy.GetStatus().failures[static_cast(rdma::RdmaFailureReason::RNR)]); + const std::string exposed = bvar::Variable::describe_exposed("rdma_recovery_status"); + EXPECT_NE(std::string::npos, exposed.find("failures")); + EXPECT_NE(std::string::npos, exposed.find("qp_fatal")); + ep->_resource = nullptr; + resource.qp = nullptr; + resource.polling_cq = nullptr; + } +} + +static int FailAckPost(ibv_qp*, ibv_send_wr*, ibv_send_wr**) { + return EIO; +} + +TEST_F(RdmaAdmissionTest, ack_post_error_propagates_and_cools_peer) { + SocketOptions options; + options.socket_mode = SOCKET_MODE_RDMA; + options.remote_side = g_ep; + options.user = get_client_side_messenger(); + SocketId sid; + ASSERT_EQ(0, Socket::Create(options, &sid)); + SocketUniquePtr socket; + ASSERT_EQ(0, Socket::Address(sid, &socket)); + auto* ep = RdmaTransportOf(socket)->_rdma_ep; + auto& policy = rdma::GlobalRdmaRecoveryPolicy(); + const std::string peer = butil::endpoint2str(g_ep).c_str(); + ASSERT_TRUE(policy.AllowNewConnection(peer, &ep->_recovery_epoch)); + ibv_context context{}; + context.ops.post_send = FailAckPost; + ibv_qp qp{}; + qp.context = &context; + rdma::RdmaResource resource; + resource.qp = &qp; + ep->_resource = &resource; + ep->_state.store(rdma::RdmaEndpoint::ESTABLISHED); + ep->_remote_window_capacity = 0; + ep->_new_rq_wrs.store(1); + ibv_wc wc{}; + wc.opcode = IBV_WC_SEND; + EXPECT_EQ(-1, ep->HandleCompletion(wc)); + EXPECT_EQ(EIO, errno); + EXPECT_FALSE(policy.AllowNewConnection(peer)); + EXPECT_EQ(rdma::RdmaFailureReason::SEND_POST, ep->_runtime_failure.load()); + ep->_resource = nullptr; + resource.qp = nullptr; + socket->SetFailed(); +} + +#endif // BRPC_WITH_RDMA + +int main(int argc, char** argv) { + testing::InitGoogleTest(&argc, argv); + GFLAGS_NAMESPACE::ParseCommandLineFlags(&argc, &argv, true); +#if BRPC_WITH_RDMA + rdma::g_rdma_available.store(true, butil::memory_order_relaxed); + rdma::g_skip_rdma_init = true; +#endif + return RUN_ALL_TESTS(); +} diff --git a/test/brpc_urma_copy_unittest.cpp b/test/brpc_urma_copy_unittest.cpp new file mode 100644 index 0000000000..da403f608a --- /dev/null +++ b/test/brpc_urma_copy_unittest.cpp @@ -0,0 +1,134 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include +#include +#include +#include + +#if BRPC_WITH_URMA +#include "brpc/channel.h" +#include "brpc/controller.h" +#include "brpc/server.h" +#include "brpc/urma/urma_helper.h" +#include "brpc/urma/urma_recovery_policy.h" +#include "echo.pb.h" + +namespace brpc { +namespace urma { +DECLARE_bool(urma_copy_send); +DECLARE_int32(urma_buffer_count); +DECLARE_int32(urma_prepared_jetty_cnt); +DECLARE_int32(urma_max_sge); +DECLARE_int32(urma_client_handshake_version); +} // namespace urma +} // namespace brpc + +namespace { + +class CopiedEchoService : public test::EchoService { + void Echo(google::protobuf::RpcController* base, const test::EchoRequest* request, + test::EchoResponse* response, google::protobuf::Closure* done) override { + brpc::ClosureGuard guard(done); + auto* cntl = static_cast(base); + response->set_message(request->message()); + cntl->response_attachment().append(cntl->request_attachment()); + } +}; + +class UrmaCopiedRpcTest : public ::testing::TestWithParam { +protected: + static void SetUpTestSuite() { + urma_init_attr_t attr{}; + const auto status = urma_init(&attr); + if (status != URMA_SUCCESS && status != URMA_EEXIST) { + return; + } + int count = 0; + auto** devices = urma_get_device_list(&count); + _mock = devices && count > 0 && + strcmp(devices[0]->name, "mock_urma_device") == 0; + urma_free_device_list(devices); + if (status == URMA_SUCCESS) { + urma_uninit(); + } + if (!_mock) { + return; + } + brpc::urma::FLAGS_urma_copy_send = true; + brpc::urma::FLAGS_urma_buffer_count = 2048; + brpc::urma::FLAGS_urma_prepared_jetty_cnt = 0; + brpc::urma::FLAGS_urma_max_sge = 1; + brpc::urma::GlobalUrmaInitializeOrDie(); + } + void SetUp() override { + if (!_mock) { + GTEST_SKIP() << "Copied URMA RPC tests use the link-time mock"; + } + ASSERT_TRUE(brpc::urma::IsUrmaAvailable()); + ASSERT_NE(nullptr, brpc::urma::GetUrmaContext()); + brpc::urma::FLAGS_urma_client_handshake_version = GetParam(); + } + static bool _mock; +}; +bool UrmaCopiedRpcTest::_mock = false; + +TEST_P(UrmaCopiedRpcTest, copies_payload_and_attachment_across_wrs_and_credit_returns) { + CopiedEchoService service; + brpc::Server server; + ASSERT_EQ(0, server.AddService(&service, brpc::SERVER_DOESNT_OWN_SERVICE)); + brpc::ServerOptions so; + so.socket_mode = brpc::SOCKET_MODE_URMA; + ASSERT_EQ(0, server.Start("127.0.0.1:0", &so)); + brpc::Channel channel; + brpc::ChannelOptions co; + co.socket_mode = brpc::SOCKET_MODE_URMA; + co.connection_type = "single"; + co.timeout_ms = 3000; + co.max_retry = 0; + const auto before = brpc::urma::GetUrmaRecoveryStatus(); + ASSERT_EQ(0, channel.Init(server.listen_address(), &co)); + // Exceed the send window to exercise receive-credit ACKs as well as data WRs. + for (int round = 0; round < 40; ++round) { + const size_t size = round == 0 ? 0 : 20000; + const std::string payload(size, 'v'); + brpc::Controller cntl; + test::EchoRequest request; + test::EchoResponse response; + request.set_message(payload); + cntl.request_attachment().append(payload); + test::EchoService_Stub(&channel).Echo(&cntl, &request, &response, nullptr); + EXPECT_FALSE(cntl.Failed()) << cntl.ErrorText(); + EXPECT_EQ(payload, response.message()); + EXPECT_EQ(payload, cntl.response_attachment().to_string()); + } + EXPECT_EQ(before.negotiated_connections + 2, + brpc::urma::GetUrmaRecoveryStatus().negotiated_connections); + server.Stop(0); + server.Join(); +} + +INSTANTIATE_TEST_SUITE_P(HandshakeVersions, UrmaCopiedRpcTest, ::testing::Values(2, 3)); + +} // namespace +#endif // BRPC_WITH_URMA + +int main(int argc, char** argv) { + testing::InitGoogleTest(&argc, argv); + gflags::ParseCommandLineFlags(&argc, &argv, true); + return RUN_ALL_TESTS(); +} diff --git a/test/brpc_urma_runtime_unittest.cpp b/test/brpc_urma_runtime_unittest.cpp new file mode 100644 index 0000000000..29081ebb78 --- /dev/null +++ b/test/brpc_urma_runtime_unittest.cpp @@ -0,0 +1,622 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include +#include +#include +#include +#include +#include + +#if BRPC_WITH_URMA +#include "butil/atomicops.h" +#include "butil/sys_byteorder.h" +#include "urma_api.h" +#include "brpc/urma/urma_handshake.h" +#include "brpc/urma/urma_handshake.pb.h" +#include "brpc/urma/urma_helper.h" +#include "urma_types.h" +#include "brpc/channel.h" +#include "brpc/controller.h" +#include "brpc/server.h" +#include "brpc/input_messenger.h" +#include "brpc/urma/urma_endpoint.h" +#include "brpc/urma/urma_recovery_policy.h" +#include "butil/fd_guard.h" +#include "bvar/variable.h" +#include "echo.pb.h" + +using namespace brpc; + +namespace brpc { +namespace urma { + +DECLARE_int32(urma_client_handshake_version); +extern bool g_skip_urma_init; +extern butil::atomic g_urma_available; + +} // namespace urma +} // namespace brpc + +TEST(UrmaRuntimeHelperTest, combined_priority_and_invalid_transport_type) { + urma_device_attr_t attr{}; + attr.dev_cap.priority_info[3].tp_type.bs.rtp = 1; + attr.dev_cap.priority_info[6].tp_type.bs.ctp = 1; + attr.dev_cap.priority_info[6].tp_type.bs.rtp = 1; + EXPECT_EQ(6, urma::FindUrmaPriorityForTpType(attr, URMA_CTP)); + EXPECT_EQ(-1, urma::FindUrmaPriorityForTpType( + attr, static_cast(255))); +} + +TEST(UrmaRuntimeHelperTest, auto_path_prefers_ctp_and_selects_rtp_on_rtp_only_devices) { + urma_device_attr_t attr{}; + attr.dev_cap.priority_info[0].tp_type.bs.rtp = 1; + attr.dev_cap.priority_info[0].SL = 2; + urma_tp_type_t path = URMA_CTP; + uint8_t priority = 6; + ASSERT_TRUE(urma::SelectUrmaTransportPath(attr, "auto", &path, &priority)); + EXPECT_EQ(URMA_RTP, path); + EXPECT_EQ(0, priority); + attr.dev_cap.priority_info[7].tp_type.bs.ctp = 1; + ASSERT_TRUE(urma::SelectUrmaTransportPath(attr, "auto", &path, &priority)); + EXPECT_EQ(URMA_CTP, path); + EXPECT_EQ(7, priority); +} + +TEST(UrmaRuntimeHelperTest, explicit_path_uses_matching_priority_and_combined_capabilities) { + urma_device_attr_t attr{}; + attr.dev_cap.priority_info[2].tp_type.bs.rtp = 1; + attr.dev_cap.priority_info[2].tp_type.bs.ctp = 1; + attr.dev_cap.priority_info[6].tp_type.bs.ctp = 1; + urma_tp_type_t path = URMA_UTP; + uint8_t priority = 15; + for (const auto& requested : {"rtp", "ctp"}) { + ASSERT_TRUE(urma::SelectUrmaTransportPath(attr, requested, &path, &priority)); + EXPECT_EQ(std::string(requested) == "rtp" ? URMA_RTP : URMA_CTP, path); + EXPECT_EQ(2, priority); + } +} + +TEST(UrmaRuntimeHelperTest, rejects_unsupported_paths_in_a_reported_capability_table) { + urma_device_attr_t attr{}; + attr.dev_cap.priority_info[0].tp_type.bs.rtp = 1; + urma_tp_type_t path = URMA_UTP; + uint8_t priority = 15; + EXPECT_FALSE(urma::SelectUrmaTransportPath(attr, "ctp", &path, &priority)); + EXPECT_EQ(URMA_UTP, path); + EXPECT_EQ(15, priority); + attr.dev_cap.priority_info[0].tp_type.value = 0; + attr.dev_cap.priority_info[4].tp_type.bs.utp = 1; + EXPECT_FALSE(urma::SelectUrmaTransportPath(attr, "auto", &path, &priority)); + EXPECT_FALSE(urma::SelectUrmaTransportPath(attr, "rtp", &path, &priority)); +} + +TEST(UrmaRuntimeHelperTest, providers_without_path_capabilities_keep_compatibility_priorities) { + urma_device_attr_t attr{}; + urma_tp_type_t path = URMA_UTP; + uint8_t priority = 15; + for (const auto& requested : {"auto", "ctp", "rtp"}) { + ASSERT_TRUE(urma::SelectUrmaTransportPath(attr, requested, &path, &priority)); + const bool rtp = std::string(requested) == "rtp"; + EXPECT_EQ(rtp ? URMA_RTP : URMA_CTP, path); + EXPECT_EQ(rtp ? 0 : 6, priority); + } +} + +TEST(UrmaRuntimeHelperTest, rejects_invalid_path_configuration) { + urma_device_attr_t attr{}; + urma_tp_type_t path = URMA_CTP; + uint8_t priority = 6; + for (const auto& requested : {"", "utp", "RM", "typo"}) { + EXPECT_FALSE(urma::SelectUrmaTransportPath(attr, requested, &path, &priority)); + } + EXPECT_FALSE(urma::SelectUrmaTransportPath(attr, "auto", nullptr, &priority)); + EXPECT_FALSE(urma::SelectUrmaTransportPath(attr, "auto", &path, nullptr)); +} + +TEST(UrmaRuntimeHelperTest, local_sge_initializes_optional_fields_and_accepts_segment_edges) { + urma_target_seg_t segment{}; + segment.urma_ctx = urma::GetUrmaContext(); + segment.seg.ubva.va = 0x1000; + segment.seg.len = 8192; + urma_sge_t sge; + memset(&sge, 0xff, sizeof(sge)); + ASSERT_TRUE(urma::InitUrmaLocalSge(&sge, 0x1000, 8192, &segment)); + EXPECT_EQ(0x1000u, sge.addr); + EXPECT_EQ(8192u, sge.len); + EXPECT_EQ(&segment, sge.tseg); + EXPECT_EQ(nullptr, sge.user_tseg); + ASSERT_TRUE(urma::InitUrmaLocalSge(&sge, 0x2fff, 1, &segment)); + EXPECT_EQ(0x2fffu, sge.addr); + EXPECT_EQ(1u, sge.len); +} + +TEST(UrmaRuntimeHelperTest, local_sge_rejects_out_of_range_and_wrong_context) { + urma_target_seg_t segment{}; + segment.urma_ctx = urma::GetUrmaContext(); + segment.seg.ubva.va = 0x1000; + segment.seg.len = 8192; + urma_sge_t sge{}; + for (auto values : {std::pair(0xfff, 1), + {0x3000, 1}, {0x2fff, 2}, {0x1000, 0}, + {0x1000, static_cast(UINT32_MAX) + 1}}) { + EXPECT_FALSE(urma::InitUrmaLocalSge(&sge, values.first, values.second, &segment)); + EXPECT_EQ(ERDMAMEM, errno); + } + EXPECT_FALSE(urma::InitUrmaLocalSge(&sge, 0x1000, 1, nullptr)); + segment.urma_ctx = reinterpret_cast(1); + EXPECT_FALSE(urma::InitUrmaLocalSge(&sge, 0x1000, 1, &segment)); + segment.urma_ctx = urma::GetUrmaContext(); + segment.seg.ubva.va = UINT64_MAX - 7; + segment.seg.len = 16; + EXPECT_FALSE(urma::InitUrmaLocalSge(&sge, UINT64_MAX - 3, 2, &segment)); +} + +static urma_target_seg_t g_copy_test_segment; +static int g_copy_test_releases; + +static urma_target_seg_t* ResolveCopyTestSegment(void* address) { + g_copy_test_segment = urma_target_seg_t{}; + g_copy_test_segment.urma_ctx = urma::GetUrmaContext(); + g_copy_test_segment.seg.ubva.va = reinterpret_cast(address); + g_copy_test_segment.seg.len = urma::GetUrmaRecvBlockSize(); + return &g_copy_test_segment; +} + +static urma_target_seg_t* RejectCopyTestSegment(void*) { + return nullptr; +} + +static void DeleteCopyTestData(void* data) { + ++g_copy_test_releases; + delete[] static_cast(data); +} + +TEST(UrmaRuntimeHelperTest, copied_send_coalesces_host_fragments_and_retains_aligned_payload) { + g_copy_test_releases = 0; + butil::IOBuf buffer; + for (const char* text : {"first", "second"}) { + const size_t length = strlen(text); + char* data = new char[length]; + memcpy(data, text, length); + ASSERT_EQ(0, buffer.append_user_data(data, length, DeleteCopyTestData)); + } + ASSERT_EQ(2u, buffer.backing_block_num()); + urma_sge_t sge{}; + ASSERT_TRUE(urma::CopyUrmaSendBuffer(&buffer, &sge, ResolveCopyTestSegment)); + EXPECT_EQ(2, g_copy_test_releases); + EXPECT_EQ("firstsecond", buffer.to_string()); + ASSERT_EQ(1u, buffer.backing_block_num()); + EXPECT_EQ(0u, sge.addr % urma::URMA_SEND_BUFFER_ALIGNMENT); + EXPECT_EQ(reinterpret_cast(buffer.backing_block(0).data()), sge.addr); + EXPECT_EQ(buffer.size(), sge.len); + EXPECT_EQ(&g_copy_test_segment, sge.tseg); + EXPECT_EQ(nullptr, sge.user_tseg); + EXPECT_EQ("firstsecond", std::string(reinterpret_cast(sge.addr), sge.len)); + + buffer.clear(); + const std::string large(urma::GetUrmaRecvBlockSize() - + (urma::URMA_SEND_BUFFER_ALIGNMENT - 1), 'x'); + buffer.append(large); + ASSERT_TRUE(urma::CopyUrmaSendBuffer(&buffer, &sge, ResolveCopyTestSegment)); + EXPECT_EQ(0u, sge.addr % urma::URMA_SEND_BUFFER_ALIGNMENT); + EXPECT_EQ(large, buffer.to_string()); + EXPECT_EQ(large.size(), sge.len); +} + +TEST(UrmaRuntimeHelperTest, copied_send_failure_preserves_source_payload) { + butil::IOBuf buffer; + buffer.append("unchanged"); + urma_sge_t sge{}; + EXPECT_FALSE(urma::CopyUrmaSendBuffer(&buffer, &sge, RejectCopyTestSegment)); + EXPECT_EQ(ERDMAMEM, errno); + EXPECT_EQ("unchanged", buffer.to_string()); + const std::string large(urma::GetUrmaRecvBlockSize(), 'y'); + buffer.clear(); + buffer.append(large); + EXPECT_FALSE(urma::CopyUrmaSendBuffer(&buffer, &sge, ResolveCopyTestSegment)); + EXPECT_EQ(EMSGSIZE, errno); + EXPECT_EQ(large, buffer.to_string()); +} + +class UrmaRuntimeMockTest : public ::testing::Test { +protected: + void SetUp() override { + urma::g_skip_urma_init = false; + + urma_init_attr_t init_attr{}; + const urma_status_t status = urma_init(&init_attr); + ASSERT_TRUE(status == URMA_SUCCESS || status == URMA_EEXIST); + _owns_urma_init = status == URMA_SUCCESS; + + int num_devices = 0; + urma_device_t** devices = urma_get_device_list(&num_devices); + ASSERT_NE(nullptr, devices); + ASSERT_GT(num_devices, 0); + const bool using_mock = + strcmp(devices[0]->name, "mock_urma_device") == 0; + urma_free_device_list(devices); + if (!using_mock) { + if (_owns_urma_init) { + EXPECT_EQ(URMA_SUCCESS, urma_uninit()); + _owns_urma_init = false; + } + GTEST_SKIP() << "UrmaRuntimeMockTest requires the URMA link-time mock"; + } + } + + void TearDown() override { + if (_owns_urma_init) { + EXPECT_EQ(URMA_SUCCESS, urma_uninit()); + } + urma::g_skip_urma_init = true; + urma::g_urma_available.store(true, butil::memory_order_relaxed); + } + +private: + bool _owns_urma_init{false}; +}; + +namespace brpc { +namespace urma { + +class UrmaRecoveryTestPeer { +public: + static int ImportPeer(UrmaEndpoint* ep, const ParsedHello& peer) { + return ep->ImportPeer(peer); + } + static void Establish(UrmaEndpoint* ep) { + GlobalUrmaRecoveryPolicy().AllowNewConnection("", &ep->_recovery_epoch); + ep->_state = UrmaEndpoint::ESTABLISHED; + } + static ssize_t Complete(UrmaEndpoint* ep, urma_cr_status_t status) { + urma_cr_t cr{}; + cr.status = status; + return ep->HandleCompletion(cr); + } + static void Report(UrmaEndpoint* ep, UrmaFailureReason reason) { + ep->ReportFailure(reason); + } + static void PollFailure(UrmaEndpoint* ep) { + UrmaResource resource; + urma_jfc_t invalid_jfc{}; + resource.jfc = &invalid_jfc; + ep->_resource = &resource; + SocketOptions options; + options.user = ep; + SocketId sid; + if (Socket::Create(options, &sid) == 0) { + SocketUniquePtr carrier; + if (Socket::Address(sid, &carrier) == 0) { + const bool polling = FLAGS_urma_use_polling; + FLAGS_urma_use_polling = true; + UrmaEndpoint::PollCq(carrier.get()); + FLAGS_urma_use_polling = polling; + carrier->SetFailed(); + } + } + ep->_resource = nullptr; + resource.jfc = nullptr; + } + static bool Backpressure(UrmaEndpoint* ep) { + UrmaResource resource; + urma_jetty_t jetty{}; + urma_target_jetty_t remote{}; + resource.jetty = &jetty; + resource.remote_jetty = &remote; + ep->_resource = &resource; + ep->_sq_window_size = 0; + ep->_remote_rq_window_size = 1; + butil::IOBuf data; + data.append("payload"); + butil::IOBuf* buf = &data; + const ssize_t rc = ep->CutFromIOBufList(&buf, 1); + const bool ok = rc == -1 && errno == EAGAIN && data.size() == 7; + ep->_resource = nullptr; + resource.jetty = nullptr; + resource.remote_jetty = nullptr; + return ok; + } + static int FailedAck(UrmaEndpoint* ep) { + // A Jetty without an imported target fails provider submission. + UrmaResource resource; + urma_jetty_t jetty{}; + urma_target_jetty_t remote{}; + resource.jetty = &jetty; + resource.remote_jetty = &remote; + ep->_resource = &resource; + ep->_sq_imm_window_size = 1; + ep->_remote_window_capacity = 2; + ep->_new_rq_wrs = 2; + urma_cr_t cr{}; + cr.status = URMA_CR_SUCCESS; + cr.user_ctx = 0; + const int rc = ep->HandleCompletion(cr); + ep->_resource = nullptr; + resource.jetty = nullptr; + resource.remote_jetty = nullptr; + return rc; + } +}; + +} // namespace urma +} // namespace brpc + +class UrmaRecoveryTest : public UrmaRuntimeMockTest { +protected: + void SetUp() override { + UrmaRuntimeMockTest::SetUp(); + if (IsSkipped() || HasFatalFailure()) { + return; + } + urma::g_skip_urma_init = true; + urma::SetUrmaRecoveryMode(urma::UrmaRecoveryMode::LEGACY); + urma::SetUrmaRecoveryMode(urma::UrmaRecoveryMode::AUTO); + rdma::SetRdmaRecoveryMode(rdma::RdmaRecoveryMode::LEGACY); + urma::GlobalUrmaRecoveryPolicy().ReportPortState(true); + } + void TearDown() override { + urma::SetUrmaRecoveryMode(urma::UrmaRecoveryMode::LEGACY); + rdma::SetRdmaRecoveryMode(rdma::RdmaRecoveryMode::LEGACY); + UrmaRuntimeMockTest::TearDown(); + } + void CreateSocket(SocketUniquePtr* socket) { + SocketOptions opt; + butil::str2endpoint("127.0.0.1:8123", &opt.remote_side); + opt.user = get_client_side_messenger(); + SocketId sid; + ASSERT_EQ(0, Socket::Create(opt, &sid)); + ASSERT_EQ(0, Socket::Address(sid, socket)); + } +}; + +TEST_F(UrmaRecoveryTest, independent_transport_modes_and_metrics) { + ASSERT_TRUE(urma::SetUrmaRecoveryMode(urma::UrmaRecoveryMode::TCP_ONLY)); + EXPECT_FALSE(urma::GlobalUrmaRecoveryPolicy().AllowNewConnection("peer")); + EXPECT_TRUE(rdma::GlobalRdmaRecoveryPolicy().AllowNewConnection("peer")); + EXPECT_EQ(rdma::RdmaRecoveryMode::LEGACY, rdma::GetRdmaRecoveryMode()); + EXPECT_FALSE(urma::SetUrmaRecoveryCooldownMs(0)); + EXPECT_TRUE(urma::SetUrmaRecoveryCooldownMs(5000)); + EXPECT_NE(std::string::npos, + bvar::Variable::describe_exposed("urma_recovery_status").find("tcp_only")); + EXPECT_FALSE(bvar::Variable::describe_exposed( + "urma_recovery_tcp_fallback_admissions").empty()); +} + +TEST_F(UrmaRecoveryTest, rejects_mismatched_path_before_importing_resources) { + urma::UrmaEndpoint endpoint(nullptr); + urma::ParsedHello peer; + peer.tp_type = urma::GetUrmaTpType() == URMA_CTP ? URMA_RTP : URMA_CTP; + EXPECT_EQ(-1, urma::UrmaRecoveryTestPeer::ImportPeer(&endpoint, peer)); + EXPECT_EQ(EPROTO, errno); + peer.tp_type = URMA_UTP; + EXPECT_EQ(-1, urma::UrmaRecoveryTestPeer::ImportPeer(&endpoint, peer)); + EXPECT_EQ(EPROTO, errno); +} + +TEST_F(UrmaRecoveryTest, completion_errors_cool_peer_once) { + for (auto status : {URMA_CR_ACK_TIMEOUT_ERR, URMA_CR_RNR_RETRY_CNT_EXC_ERR, + URMA_CR_LOC_ACCESS_ERR, URMA_CR_REM_OPERATION_ERR}) { + urma::SetUrmaRecoveryMode(urma::UrmaRecoveryMode::LEGACY); + urma::SetUrmaRecoveryMode(urma::UrmaRecoveryMode::AUTO); + SocketUniquePtr socket; + ASSERT_NO_FATAL_FAILURE(CreateSocket(&socket)); + urma::UrmaEndpoint ep(socket.get()); + urma::UrmaRecoveryTestPeer::Establish(&ep); + const auto before = urma::GetUrmaRecoveryStatus(); + EXPECT_EQ(-1, urma::UrmaRecoveryTestPeer::Complete(&ep, status)); + const auto reason = status == URMA_CR_ACK_TIMEOUT_ERR ? + urma::UrmaFailureReason::RETRY_EXHAUSTED : + status == URMA_CR_RNR_RETRY_CNT_EXC_ERR ? urma::UrmaFailureReason::RNR : + status == URMA_CR_LOC_ACCESS_ERR ? urma::UrmaFailureReason::ACCESS_ERROR : + urma::UrmaFailureReason::TRANSPORT_ERROR; + EXPECT_EQ(before.failures[static_cast(reason)] + 1, + urma::GetUrmaRecoveryStatus().failures[static_cast(reason)]); + EXPECT_EQ(status == URMA_CR_LOC_ACCESS_ERR, + urma::GlobalUrmaRecoveryPolicy().AllowNewConnection("127.0.0.1:8123")); + urma::UrmaRecoveryTestPeer::Report(&ep, urma::UrmaFailureReason::CQ_POLL); + EXPECT_EQ(before.failures[static_cast(urma::UrmaFailureReason::CQ_POLL)], + urma::GetUrmaRecoveryStatus().failures[ + static_cast(urma::UrmaFailureReason::CQ_POLL)]); + socket->SetFailed(); + } +} + +TEST_F(UrmaRecoveryTest, flush_backpressure_and_old_epoch_do_not_cool_peer) { + SocketUniquePtr socket; + ASSERT_NO_FATAL_FAILURE(CreateSocket(&socket)); + urma::UrmaEndpoint ep(socket.get()); + urma::UrmaRecoveryTestPeer::Establish(&ep); + EXPECT_TRUE(urma::UrmaRecoveryTestPeer::Backpressure(&ep)); + EXPECT_EQ(-1, urma::UrmaRecoveryTestPeer::Complete(&ep, URMA_CR_WR_FLUSH_ERR)); + EXPECT_TRUE(urma::GlobalUrmaRecoveryPolicy().AllowNewConnection("127.0.0.1:8123")); + urma::SetUrmaRecoveryMode(urma::UrmaRecoveryMode::TCP_ONLY); + urma::SetUrmaRecoveryMode(urma::UrmaRecoveryMode::AUTO); + urma::UrmaRecoveryTestPeer::Report(&ep, urma::UrmaFailureReason::CQ_POLL); + EXPECT_TRUE(urma::GlobalUrmaRecoveryPolicy().AllowNewConnection("127.0.0.1:8123")); + socket->SetFailed(); +} + +TEST_F(UrmaRecoveryTest, ack_post_failure_propagates) { + SocketUniquePtr socket; + ASSERT_NO_FATAL_FAILURE(CreateSocket(&socket)); + urma::UrmaEndpoint ep(socket.get()); + urma::UrmaRecoveryTestPeer::Establish(&ep); + EXPECT_EQ(-1, urma::UrmaRecoveryTestPeer::FailedAck(&ep)); + EXPECT_FALSE(urma::GlobalUrmaRecoveryPolicy().AllowNewConnection("127.0.0.1:8123")); + socket->SetFailed(); +} + +TEST_F(UrmaRecoveryTest, jfc_poll_failure_fails_socket_and_cools_peer) { + SocketUniquePtr socket; + ASSERT_NO_FATAL_FAILURE(CreateSocket(&socket)); + urma::UrmaEndpoint ep(socket.get()); + urma::UrmaRecoveryTestPeer::Establish(&ep); + const auto before = urma::GetUrmaRecoveryStatus(); + urma::UrmaRecoveryTestPeer::PollFailure(&ep); + EXPECT_TRUE(socket->Failed()); + EXPECT_FALSE(urma::GlobalUrmaRecoveryPolicy().AllowNewConnection("127.0.0.1:8123")); + EXPECT_EQ(before.failures[static_cast(urma::UrmaFailureReason::CQ_POLL)] + 1, + urma::GetUrmaRecoveryStatus().failures[ + static_cast(urma::UrmaFailureReason::CQ_POLL)]); +} + +class UrmaRecoveryEcho : public ::test::EchoService { + void Echo(google::protobuf::RpcController*, const ::test::EchoRequest* request, + ::test::EchoResponse* response, google::protobuf::Closure* done) override { + response->set_message(request->message()); + done->Run(); + } +}; + +TEST_F(UrmaRecoveryTest, tcp_only_client_completes_rpc_on_urma_server) { + urma::SetUrmaRecoveryMode(urma::UrmaRecoveryMode::TCP_ONLY); + UrmaRecoveryEcho service; + Server server; + ASSERT_EQ(0, server.AddService(&service, SERVER_DOESNT_OWN_SERVICE)); + ServerOptions so; + so.socket_mode = SOCKET_MODE_URMA; + ASSERT_EQ(0, server.Start("127.0.0.1:0", &so)); + Channel channel; + ChannelOptions co; + co.socket_mode = SOCKET_MODE_URMA; + co.connection_type = "short"; + co.timeout_ms = 2000; + ASSERT_EQ(0, channel.Init(server.listen_address(), &co)); + ::test::EchoService_Stub stub(&channel); + Controller cntl; + ::test::EchoRequest request; + ::test::EchoResponse response; + request.set_message("recovery over TCP"); + stub.Echo(&cntl, &request, &response, nullptr); + EXPECT_FALSE(cntl.Failed()) << cntl.ErrorText(); + EXPECT_EQ(request.message(), response.message()); + urma::SetUrmaRecoveryMode(urma::UrmaRecoveryMode::AUTO); + SocketOptions failed_options; + failed_options.remote_side = server.listen_address(); + failed_options.user = get_client_side_messenger(); + SocketId failed_sid; + ASSERT_EQ(0, Socket::Create(failed_options, &failed_sid)); + SocketUniquePtr failed_socket; + ASSERT_EQ(0, Socket::Address(failed_sid, &failed_socket)); + { + urma::UrmaEndpoint failed_ep(failed_socket.get()); + urma::UrmaRecoveryTestPeer::Establish(&failed_ep); + EXPECT_EQ(-1, urma::UrmaRecoveryTestPeer::Complete( + &failed_ep, URMA_CR_RNR_RETRY_CNT_EXC_ERR)); + } + failed_socket->SetFailed(); + const auto before = urma::GetUrmaRecoveryStatus(); + cntl.Reset(); + response.Clear(); + stub.Echo(&cntl, &request, &response, nullptr); + EXPECT_FALSE(cntl.Failed()) << cntl.ErrorText(); + EXPECT_EQ(request.message(), response.message()); + EXPECT_EQ(before.peer_cooldown_admissions + 1, + urma::GetUrmaRecoveryStatus().peer_cooldown_admissions); + server.Stop(0); + server.Join(); +} + +TEST_F(UrmaRecoveryTest, server_declines_v2_and_v3_without_allocating_jetty) { + urma::SetUrmaRecoveryMode(urma::UrmaRecoveryMode::TCP_ONLY); + Server server; + ServerOptions so; + so.socket_mode = SOCKET_MODE_URMA; + ASSERT_EQ(0, server.Start("127.0.0.1:0", &so)); + for (int version : {2, 3}) { + butil::fd_guard fd(butil::tcp_connect(server.listen_address(), nullptr)); + ASSERT_GE(fd, 0); + timeval timeout{2, 0}; + ASSERT_EQ(0, setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout))); + urma::v2_wire::HelloMessage hello; + hello.msg_len = urma::v2_wire::HELLO_PACKET_LEN; + hello.hello_ver = urma::v2_wire::HELLO_V2_VERSION; + hello.impl_ver = urma::v2_wire::IMPL_V2_VERSION; + hello.buffer_size = 8192; + hello.recv_buffer_cnt = 127; + hello.jetty_id = 42; + memset(hello.eid, 1, 16); + memset(hello.seg_eid, 1, 16); + hello.tp_type = URMA_CTP; + hello.seg_va = 0x100000; + hello.seg_len = 1024 * 1024; + if (version == 2) { + uint8_t packet[urma::v2_wire::HELLO_PACKET_LEN]; + memcpy(packet, "URMA", 4); + hello.Serialize(packet + 4); + ASSERT_EQ(sizeof(packet), static_cast(write(fd, packet, sizeof(packet)))); + uint8_t reply[sizeof(packet)]; + ASSERT_EQ(sizeof(reply), static_cast(recv(fd, reply, sizeof(reply), MSG_WAITALL))); + EXPECT_EQ(0, memcmp(reply, "URMA", 4)); + urma::v2_wire::HelloMessage rejected; + rejected.Deserialize(reply + 4); + EXPECT_EQ(0, rejected.hello_ver); + EXPECT_EQ(0u, rejected.jetty_id); + } else { + urma::UrmaHello msg; + msg.set_buffer_size(8192); + msg.set_recv_buffer_cnt(127); + msg.set_jetty_id(42); + msg.set_uasid(0); + msg.set_seg_uasid(0); + msg.set_seg_token_id(0); + msg.set_eid(hello.eid, 16); + msg.set_seg_eid(hello.seg_eid, 16); + msg.set_tp_type(URMA_CTP); + msg.set_seg_va(hello.seg_va); + msg.set_seg_len(hello.seg_len); + const std::string body = msg.SerializeAsString(); + uint32_t size = butil::HostToNet32(body.size()); + std::string packet("URM3", 4); + packet.append(reinterpret_cast(&size), 4); + packet.append(body); + ASSERT_EQ(packet.size(), static_cast(write(fd, packet.data(), packet.size()))); + char header[8]; + ASSERT_EQ(8, recv(fd, header, 8, MSG_WAITALL)); + EXPECT_EQ(0, memcmp(header, "URM3", 4)); + memcpy(&size, header + 4, 4); + size = butil::NetToHost32(size); + ASSERT_LE(size, 4096u); + std::string reply(size, '\0'); + ASSERT_EQ(size, static_cast(recv(fd, &reply[0], size, MSG_WAITALL))); + urma::UrmaHello rejected; + ASSERT_TRUE(rejected.ParseFromString(reply)); + EXPECT_EQ(0u, rejected.jetty_id()); + } + const std::string request = "GET /version HTTP/1.1\r\nHost: localhost\r\n\r\n"; + ASSERT_EQ(request.size(), static_cast(write(fd, request.data(), request.size()))); + char response[4096]; + const ssize_t nr = recv(fd, response, sizeof(response), 0); + ASSERT_GT(nr, 0); + EXPECT_NE(std::string::npos, std::string(response, nr).find(" 200 ")); + } + server.Stop(0); + server.Join(); +} + +#endif // BRPC_WITH_URMA + +int main(int argc, char** argv) { + testing::InitGoogleTest(&argc, argv); + gflags::ParseCommandLineFlags(&argc, &argv, true); +#if BRPC_WITH_URMA + urma::g_skip_urma_init = true; + urma::g_urma_available.store(true, butil::memory_order_relaxed); +#endif + return RUN_ALL_TESTS(); +} diff --git a/test/brpc_urma_transport_unittest.cpp b/test/brpc_urma_transport_unittest.cpp new file mode 100644 index 0000000000..f7ca8e87b8 --- /dev/null +++ b/test/brpc_urma_transport_unittest.cpp @@ -0,0 +1,192 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include +#include +#include +#include + +#if BRPC_WITH_URMA +#include "brpc/channel.h" +#include "brpc/controller.h" +#include "brpc/server.h" +#include "brpc/urma/urma_endpoint.h" +#include "brpc/urma/urma_handshake.h" +#include "brpc/urma/urma_handshake.pb.h" +#include "brpc/urma/urma_helper.h" +#include "brpc/urma/urma_recovery_policy.h" +#include "echo.pb.h" + +namespace brpc { +namespace urma { +DECLARE_string(urma_tp_type); +DECLARE_int32(urma_buffer_count); +DECLARE_int32(urma_client_handshake_version); +} // namespace urma +} // namespace brpc + +namespace { + +class PathEchoService : public test::EchoService { + void Echo(google::protobuf::RpcController* base, const test::EchoRequest* request, + test::EchoResponse* response, google::protobuf::Closure* done) override { + brpc::ClosureGuard guard(done); + auto* cntl = static_cast(base); + response->set_message(request->message()); + cntl->response_attachment().append(cntl->request_attachment()); + } +}; + +class UrmaTransportTest : public ::testing::TestWithParam { +protected: + static void SetUpTestSuite() { + urma_init_attr_t attr{}; + const auto status = urma_init(&attr); + if (status != URMA_SUCCESS && status != URMA_EEXIST) { + return; + } + int count = 0; + auto** devices = urma_get_device_list(&count); + _mock = devices && count > 0 && + strcmp(devices[0]->name, "mock_urma_device") == 0; + urma_free_device_list(devices); + if (status == URMA_SUCCESS) { + urma_uninit(); + } + if (_mock) { + brpc::urma::FLAGS_urma_buffer_count = 4096; + brpc::urma::GlobalUrmaInitializeOrDie(); + } + } + void SetUp() override { + if (!_mock) { + GTEST_SKIP() << "Path RPC tests use the link-time URMA mock"; + } + ASSERT_TRUE(brpc::urma::IsUrmaAvailable()); + const bool rtp = brpc::urma::FLAGS_urma_tp_type == "rtp"; + ASSERT_EQ(rtp ? URMA_RTP : URMA_CTP, brpc::urma::GetUrmaTpType()); + ASSERT_EQ(rtp ? 0 : 6, brpc::urma::GetUrmaJettyPriority()); + brpc::urma::FLAGS_urma_client_handshake_version = GetParam(); + brpc::urma::SetUrmaRecoveryMode(brpc::urma::UrmaRecoveryMode::AUTO); + } + void TearDown() override { + brpc::urma::SetUrmaRecoveryMode(brpc::urma::UrmaRecoveryMode::LEGACY); + } + static bool _mock; +}; +bool UrmaTransportTest::_mock = false; + +TEST_P(UrmaTransportTest, advertises_selected_path_in_both_hello_formats) { + brpc::urma::UrmaEndpoint endpoint(nullptr); + brpc::urma::v2_wire::HelloMessage hello; + endpoint.FillLocalHelloV2(&hello); + EXPECT_EQ(brpc::urma::GetUrmaTpType(), hello.tp_type); + brpc::urma::UrmaHello protobuf_hello; + endpoint.FillLocalHelloV3(&protobuf_hello); + EXPECT_EQ(brpc::urma::GetUrmaTpType(), protobuf_hello.tp_type()); +} + +TEST_P(UrmaTransportTest, exchanges_zero_copy_payloads_and_receive_credits) { + PathEchoService service; + brpc::Server server; + ASSERT_EQ(0, server.AddService(&service, brpc::SERVER_DOESNT_OWN_SERVICE)); + brpc::ServerOptions so; + so.socket_mode = brpc::SOCKET_MODE_URMA; + ASSERT_EQ(0, server.Start("127.0.0.1:0", &so)); + brpc::Channel channel; + brpc::ChannelOptions co; + co.socket_mode = brpc::SOCKET_MODE_URMA; + co.connection_type = "single"; + co.timeout_ms = 3000; + co.max_retry = 0; + const auto before = brpc::urma::GetUrmaRecoveryStatus(); + ASSERT_EQ(0, channel.Init(server.listen_address(), &co)); + for (int round = 0; round < 40; ++round) { + const size_t size = round == 0 ? 0 : round == 1 ? 1024 * 1024 : 20000; + const std::string payload(size, 'r'); + brpc::Controller cntl; + test::EchoRequest request; + test::EchoResponse response; + request.set_message(payload); + cntl.request_attachment().append(payload); + test::EchoService_Stub(&channel).Echo(&cntl, &request, &response, nullptr); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + EXPECT_EQ(payload, response.message()); + EXPECT_EQ(payload, cntl.response_attachment().to_string()); + } + EXPECT_EQ(before.negotiated_connections + 2, + brpc::urma::GetUrmaRecoveryStatus().negotiated_connections); + server.Stop(0); + server.Join(); +} + +TEST_P(UrmaTransportTest, switches_new_connections_from_auto_to_tcp_only_and_back) { + PathEchoService service; + brpc::Server server; + ASSERT_EQ(0, server.AddService(&service, brpc::SERVER_DOESNT_OWN_SERVICE)); + brpc::ServerOptions so; + so.socket_mode = brpc::SOCKET_MODE_URMA; + ASSERT_EQ(0, server.Start("127.0.0.1:0", &so)); + brpc::Channel channel; + brpc::ChannelOptions co; + co.socket_mode = brpc::SOCKET_MODE_URMA; + co.connection_type = "short"; + co.timeout_ms = 3000; + co.max_retry = 0; + ASSERT_EQ(0, channel.Init(server.listen_address(), &co)); + // Cumulative receive buffers exceed the pool capacity unless closed + // short connections release their queues and buffers. + const int requests_per_mode = 40; + for (auto mode : {brpc::urma::UrmaRecoveryMode::AUTO, + brpc::urma::UrmaRecoveryMode::TCP_ONLY, + brpc::urma::UrmaRecoveryMode::AUTO}) { + ASSERT_TRUE(brpc::urma::SetUrmaRecoveryMode(mode)); + const auto before = brpc::urma::GetUrmaRecoveryStatus(); + for (int i = 0; i < requests_per_mode; ++i) { + brpc::Controller cntl; + test::EchoRequest request; + test::EchoResponse response; + request.set_message("switch"); + test::EchoService_Stub(&channel).Echo(&cntl, &request, &response, nullptr); + ASSERT_FALSE(cntl.Failed()) << cntl.ErrorText(); + EXPECT_EQ("switch", response.message()); + } + const auto after = brpc::urma::GetUrmaRecoveryStatus(); + if (mode == brpc::urma::UrmaRecoveryMode::TCP_ONLY) { + EXPECT_EQ(before.negotiated_connections, after.negotiated_connections); + EXPECT_EQ(before.tcp_only_admissions + requests_per_mode, + after.tcp_only_admissions); + } else { + EXPECT_EQ(before.negotiated_connections + 2 * requests_per_mode, + after.negotiated_connections); + EXPECT_EQ(before.tcp_only_admissions, after.tcp_only_admissions); + } + } + server.Stop(0); + server.Join(); +} + +INSTANTIATE_TEST_SUITE_P(HandshakeVersions, UrmaTransportTest, ::testing::Values(2, 3)); + +} // namespace +#endif // BRPC_WITH_URMA + +int main(int argc, char** argv) { + testing::InitGoogleTest(&argc, argv); + gflags::ParseCommandLineFlags(&argc, &argv, true); + return RUN_ALL_TESTS(); +}