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