From ba8cf2aefd4a324994e57de25ed41326e3093c47 Mon Sep 17 00:00:00 2001 From: Masaki Suketa Date: Sat, 15 Aug 2026 08:10:24 +0900 Subject: [PATCH] Bound the CI test step and drop the unusable dispatch guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit within_dispatch_timeout could never fire. A stranded UDF dispatcher wedges the VM with the GVL held, so Thread#join's timeout never runs and flunk is unreachable — a regression hung the run exactly as it would have with no guard at all. Verified on the unfixed build: the wedged process also ignores SIGTERM, so plain `timeout` cannot reclaim it either. Put the bound where it can actually be enforced, as timeout-minutes on the Ubuntu and macOS test steps. The step is the part that can wedge; keeping it off the job clock leaves checkout, setup and the duckdb download out of the budget. The suite runs in ~50s locally, so 3 minutes is ample. Windows already bounds its test step through nick-fields/retry. Job-level timeout-minutes stays as the backstop. Co-Authored-By: Claude Opus 5 --- .github/workflows/test_on_macos.yml | 3 ++ .github/workflows/test_on_ubuntu.yml | 3 ++ .../function_error_message_test.rb | 28 ++++++------------- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/.github/workflows/test_on_macos.yml b/.github/workflows/test_on_macos.yml index 5e500199..bd0d4901 100644 --- a/.github/workflows/test_on_macos.yml +++ b/.github/workflows/test_on_macos.yml @@ -88,7 +88,10 @@ jobs: puts "duckdb library #{actual}" ' + # A UDF callback that strands the dispatcher wedges the VM with the GVL + # held, so the run never returns. Bound the step: the suite takes ~1 minute. - name: test with Ruby ${{ matrix.ruby }} + timeout-minutes: 3 env: MYSQL_TEST: 1 DYLD_LIBRARY_PATH: ${{ github.workspace }}/libduckdb-osx-universal diff --git a/.github/workflows/test_on_ubuntu.yml b/.github/workflows/test_on_ubuntu.yml index 9356a7b6..5ca856e2 100644 --- a/.github/workflows/test_on_ubuntu.yml +++ b/.github/workflows/test_on_ubuntu.yml @@ -95,7 +95,10 @@ jobs: puts "duckdb library #{actual}" ' + # A UDF callback that strands the dispatcher wedges the VM with the GVL + # held, so the run never returns. Bound the step: the suite takes ~1 minute. - name: test with Ruby ${{ matrix.ruby }} + timeout-minutes: 3 env: MYSQL_TEST: 1 run: | diff --git a/test/duckdb_test/function_error_message_test.rb b/test/duckdb_test/function_error_message_test.rb index c9c9954a..a1d52ad7 100644 --- a/test/duckdb_test/function_error_message_test.rb +++ b/test/duckdb_test/function_error_message_test.rb @@ -36,25 +36,15 @@ def register_scalar(name, &block) end) end - # Bounds a query that stops returning once the dispatcher is stranded, so a - # regression fails the run instead of blocking it forever. - def within_dispatch_timeout(timeout = 60, &block) - thread = Thread.new do - Thread.current.report_on_exception = false - block.call - end - return thread.value if thread.join(timeout) - - thread.kill - - flunk 'UDF callback dispatch is stranded' - end - + # A regression here strands the dispatcher, and the queries below then never + # return. Nothing in Ruby can bound that: the stranded state holds the GVL, + # so Thread#join timeouts never fire and even SIGTERM is not delivered. The + # bound lives in CI instead, as timeout-minutes on the test step. def test_scalar_error_message_with_a_null_byte_is_reported_to_duckdb register_scalar('nul_boom') { |v| raise "bad token: \0#{v}" } error = assert_raises(DuckDB::Error) do - within_dispatch_timeout { @con.query('SELECT nul_boom(a) FROM t') } + @con.query('SELECT nul_boom(a) FROM t') end assert_match(/bad token:/, error.message) @@ -64,7 +54,7 @@ def test_scalar_error_message_that_cannot_be_read_is_reported_to_duckdb register_scalar('unreadable_boom') { |_v| raise MessageRaises } assert_raises(DuckDB::Error) do - within_dispatch_timeout { @con.query('SELECT unreadable_boom(a) FROM t') } + @con.query('SELECT unreadable_boom(a) FROM t') end end @@ -82,7 +72,7 @@ def test_aggregate_error_message_with_a_null_byte_is_reported_to_duckdb ) error = assert_raises(DuckDB::Error) do - within_dispatch_timeout { @con.query('SELECT nul_boom_agg(a) FROM t') } + @con.query('SELECT nul_boom_agg(a) FROM t') end assert_match(/bad token:/, error.message) @@ -95,10 +85,10 @@ def test_udf_dispatch_survives_an_error_message_duckdb_cannot_be_given # Deliberately loose: the reporting path is asserted above, and a regression # there must not fail this test before it reaches the recovery check. assert_raises(StandardError) do - within_dispatch_timeout { @con.query('SELECT nul_boom2(a) FROM t') } + @con.query('SELECT nul_boom2(a) FROM t') end - result = within_dispatch_timeout { @con.query('SELECT echo(a) FROM t LIMIT 1').to_a } + result = @con.query('SELECT echo(a) FROM t LIMIT 1').to_a assert_equal [['echo0']], result end