代码打桩

代码打桩,是指在 FE 或 BE 源码中插入一段代码,当程序执行到这里时,可以改变程序的变量或行为,这样的一段代码称为一个木桩

主要用于单元测试或回归测试,用来构造正常方法无法实现的异常。

每一个木桩都有一个名称,可以随便取名,可以通过一些机制控制木桩的开关,还可以向木桩传递参数。

FE 和 BE 都支持代码打桩,打桩完后要重新编译 BE 或 FE。

木桩代码示例

FE 桩子示例代码

  1. private Status foo() {
  2. // dbug_fe_foo_do_nothing 是一个木桩名字,
  3. // 打开这个木桩之后,DebugPointUtil.isEnable("dbug_fe_foo_do_nothing") 将会返回 true
  4. if (DebugPointUtil.isEnable("dbug_fe_foo_do_nothing")) {
  5. return Status.Nothing;
  6. }
  7. do_foo_action();
  8. return Status.Ok;
  9. }

BE 桩子示例代码

  1. void Status foo() {
  2. // dbug_be_foo_do_nothing 是一个木桩名字,
  3. // 打开这个木桩之后,DBUG_EXECUTE_IF 将会执行宏参数中的代码块
  4. DBUG_EXECUTE_IF("dbug_be_foo_do_nothing", { return Status.Nothing; });
  5. do_foo_action();
  6. return Status.Ok;
  7. }

总开关

需要把木桩总开关 enable_debug_points 打开之后,才能激活木桩。默认情况下,木桩总开关是关闭的。

总开关enable_debug_points 分别在 FE 的 fe.conf 和 BE 的 be.conf 中配置。

打开木桩

打开总开关后,还需要通过向 FE 或 BE 发送 http 请求的方式,打开或关闭指定名称的木桩,只有这样当代码执行到这个木桩时,相关代码才会被执行。

API

  1. POST /api/debug_point/add/{debug_point_name}[?timeout=<int>&execute=<int>]

参数

  • debug_point_name 木桩名字。必填。

  • timeout 超时时间,单位为秒。超时之后,木桩失活。默认值 -1 表示永远不超时。可选。

  • execute 木桩最大执行次数。默认值 -1 表示不限执行次数。可选。

Request body

Response

  1. {
  2. msg: "OK",
  3. code: 0
  4. }

Examples

打开木桩 foo,最多执行 5 次。

  1. curl -X POST "http://127.0.0.1:8030/api/debug_point/add/foo?execute=5"

向木桩传递参数

激活木桩时,除了前文所述的 timeout 和 execute,还可以传递其它自定义参数。
一个参数是一个形如 key=value 的 key-value 对,在 url 的路径部分,紧跟在木桩名称后,以字符 ‘?’ 开头。

API

  1. POST /api/debug_point/add/{debug_point_name}[?k1=v1&k2=v2&k3=v3...]
  • k1=v1 k1 为参数名称,v1 为参数值,多个参数用&分隔。

Request body

Response

  1. {
  2. msg: "OK",
  3. code: 0
  4. }

Examples

假设 FE 在 fe.conf 中有配置 http_port=8030,则下面的请求激活 FE 中的木桩foo,并传递了两个参数 percentduration

  1. curl -u root: -X POST "http://127.0.0.1:8030/api/debug_point/add/foo?percent=0.5&duration=3"
  1. 注意:
  2. 1、在 FE BE 的代码中,参数名和参数值都是字符串。
  3. 2、在 FE BE 的代码中和 http 请求中,参数名称和值都是大小写敏感的。
  4. 3、发给 FE BE http 请求,路径部分格式是相同的,只是 IP 地址和端口号不同。

在 FE 和 BE 代码中使用参数

激活 FE 中的木桩OlapTableSink.write_random_choose_sink并传递参数 needCatchUpsinkNum:

注意:可能需要用户名和密码

  1. curl -u root: -X POST "http://127.0.0.1:8030/api/debug_point/add/OlapTableSink.write_random_choose_sink?needCatchUp=true&sinkNum=3"

在 FE 代码中使用木桩 OlapTableSink.write_random_choose_sink 的参数 needCatchUpsinkNum

  1. private void debugWriteRandomChooseSink(Tablet tablet, long version, Multimap<Long, Long> bePathsMap) {
  2. DebugPoint debugPoint = DebugPointUtil.getDebugPoint("OlapTableSink.write_random_choose_sink");
  3. if (debugPoint == null) {
  4. return;
  5. }
  6. boolean needCatchup = debugPoint.param("needCatchUp", false);
  7. int sinkNum = debugPoint.param("sinkNum", 0);
  8. ...
  9. }

激活 BE 中的木桩TxnManager.prepare_txn.random_failed并传递参数 percent:

  1. curl -X POST "http://127.0.0.1:8040/api/debug_point/add/TxnManager.prepare_txn.random_failed?percent=0.7

在 BE 代码中使用木桩 TxnManager.prepare_txn.random_failed 的参数 percent

  1. DBUG_EXECUTE_IF("TxnManager.prepare_txn.random_failed",
  2. {if (rand() % 100 < (100 * dp->param("percent", 0.5))) {
  3. LOG_WARNING("TxnManager.prepare_txn.random_failed random failed");
  4. return Status::InternalError("debug prepare txn random failed");
  5. }}
  6. );

关闭木桩

API

  1. POST /api/debug_point/remove/{debug_point_name}

参数

  • debug_point_name 木桩名字。必填。

Request body

Response

  1. {
  2. msg: "OK",
  3. code: 0
  4. }

Examples

关闭木桩foo

  1. curl -X POST "http://127.0.0.1:8030/api/debug_point/remove/foo"

清除所有木桩

API

  1. POST /api/debug_point/clear

Request body

Response

  1. {
  2. msg: "OK",
  3. code: 0
  4. }

Examples

清除所有木桩。

  1. curl -X POST "http://127.0.0.1:8030/api/debug_point/clear"

在回归测试中使用木桩

提交 PR 时,社区 CI 系统默认开启 FE 和 BE 的enable_debug_points配置。

回归测试框架提供方法函数来开关指定的木桩,它们声明如下:

  1. // 打开木桩,name 是木桩名称,params 是一个key-value列表,是传给木桩的参数
  2. def enableDebugPointForAllFEs(String name, Map<String, String> params = null);
  3. def enableDebugPointForAllBEs(String name, Map<String, String> params = null);
  4. // 关闭木桩,name 是木桩的名称
  5. def disableDebugPointForAllFEs(String name);
  6. def disableDebugPointForAllFEs(String name);

需要在调用测试 action 之前调用 enableDebugPointForAllFEs()enableDebugPointForAllBEs() 来开启木桩,
这样执行到木桩代码时,相关代码才会被执行,
然后在调用测试 action 之后调用 disableDebugPointForAllFEs()disableDebugPointForAllBEs() 来关闭木桩。

并发问题

FE 或 BE 中开启的木桩是全局生效的,同一个 Pull Request 中,并发跑的其它测试,可能会受影响而意外失败。 为了避免这种情况,我们规定,使用木桩的回归测试,必须放在 regression-test/suites/fault_injection_p0 目录下, 且组名必须设置为 nonConcurrent,社区 CI 系统对于这些用例,会串行运行。

Examples

  1. // 测试用例的.groovy 文件必须放在 regression-test/suites/fault_injection_p0 目录下,
  2. // 且组名设置为 'nonConcurrent'
  3. suite('debugpoint_action', 'nonConcurrent') {
  4. try {
  5. // 打开所有FE中,名为 "PublishVersionDaemon.stop_publish" 的木桩
  6. // 传参数 timeout
  7. // 与上面curl调用时一样,execute 是执行次数,timeout 是超时秒数
  8. GetDebugPoint().enableDebugPointForAllFEs('PublishVersionDaemon.stop_publish', [timeout:1])
  9. // 打开所有BE中,名为 "Tablet.build_tablet_report_info.version_miss" 的木桩
  10. // 传参数 tablet_id, version_miss 和 timeout
  11. GetDebugPoint().enableDebugPointForAllBEs('Tablet.build_tablet_report_info.version_miss',
  12. [tablet_id:'12345', version_miss:true, timeout:1])
  13. // 测试用例,会触发木桩代码的执行
  14. sql """CREATE TABLE tbl_1 (k1 INT, k2 INT)
  15. DUPLICATE KEY (k1)
  16. DISTRIBUTED BY HASH(k1)
  17. BUCKETS 3
  18. PROPERTIES ("replication_allocation" = "tag.location.default: 1");
  19. """
  20. sql "INSERT INTO tbl_1 VALUES (1, 10)"
  21. sql "INSERT INTO tbl_1 VALUES (2, 20)"
  22. order_qt_select_1_1 'SELECT * FROM tbl_1'
  23. } finally {
  24. GetDebugPoint().disableDebugPointForAllFEs('PublishVersionDaemon.stop_publish')
  25. GetDebugPoint().disableDebugPointForAllBEs('Tablet.build_tablet_report_info.version_miss')
  26. }
  27. }