Skip to content

feat: per-worker proxy for table function execute callback - #1367

Merged
suketa merged 1 commit into
suketa:mainfrom
otegami:feature/worker-proxy-table
Jun 8, 2026
Merged

feat: per-worker proxy for table function execute callback#1367
suketa merged 1 commit into
suketa:mainfrom
otegami:feature/worker-proxy-table

Conversation

@otegami

@otegami otegami commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

GitHub: close GH-1136

Last step of the per-worker proxy work: on DuckDB >= 1.5.0, table function
execute callbacks now run on one proxy thread per DuckDB worker, same as scalar
functions since GH-1366. bind and init stay on the global executor — they run
once per query, not per worker, so there is no per-worker hook and nothing to
parallelize.

Why

Same ceiling as the scalar case: with one global executor, execute callbacks
from different workers can never overlap, even when they release the GVL.
Measured with the table section added to sample/issue1136.rb (GVL-releasing
emitter, 200 chunks):

SET threads=1 SET threads=4
before 1.225s, 1 callback thread 0.615s, 2 callback threads
after 1.214s, 1 callback thread 0.311s, 4 callback threads

The before run caps at 2 threads (calling thread + global executor) no
matter how many workers DuckDB spawns. Note the emitter needs both planner
hints (set_cardinality in bind, max_threads= in init) — without them
DuckDB assigns a single worker and nothing distributes.

Notes for review:

  • DuckDB 1.4.x LTS is untouched: duckdb_table_function_set_local_init is a
    1.5.0 API, so everything is behind HAVE_DUCKDB_H_GE_V1_5_0.
  • The test asserts execute callbacks ran on more than two distinct Ruby
    threads. The old path structurally caps at two, so the test fails without
    this change. Timing/simultaneity assertions were deliberately avoided as
    scheduler-dependent.

Summary by CodeRabbit

  • New Features

    • Improved table function execution with DuckDB 1.5.0+ through per-worker threading support for better multi-threaded performance and resource utilization.
  • Tests

    • Added comprehensive integration test for multi-threaded table function execution scenarios.

Wire the table execute path to per-worker proxy threads on DuckDB
>= 1.5.0. A local_init callback registered via
duckdb_table_function_set_local_init runs once per worker thread, creates
a proxy (allocating its Ruby thread under the GVL through the global
executor, since local_init runs on a non-Ruby thread), and stores it as
thread-local init data via duckdb_init_set_init_data. The execute
callback retrieves that proxy with duckdb_function_get_local_init_data
and dispatches through it via
rbduckdb_function_executor_dispatch_via_proxy, so callbacks from
different workers run concurrently instead of serializing on the single
global executor. bind and init stay on the global executor. DuckDB frees
each proxy through rbduckdb_worker_proxy_destroy.

The proxy-creating wrapper runs rbduckdb_worker_proxy_create under
rb_protect, implementing the raise contract documented on that function:
the executor runs callbacks unprotected, so an uncaught raise would
longjmp past its done-signaling and block the waiting DuckDB worker
forever. On failure the proxy stays NULL and the execute callback falls
back to the global executor.

On DuckDB < 1.5.0 the local_init hook is absent and the execute callback
keeps using the global executor unchanged.

The added test records which Ruby threads run the execute callback and
asserts more than two distinct threads, which the old implementation can
never produce (calling thread plus the single global executor), in
addition to result correctness. Verified to fail against a build without
this change. Simultaneity assertions are avoided as scheduler-dependent.

sample/issue1136.rb gains a table UDF section: a GVL-releasing emitter
with both planner hints (set_cardinality, max_threads) that demonstrates
the throughput win (locally about 4x at SET threads=4, execute callbacks
on 4 distinct Ruby threads instead of 1).
@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR enables DuckDB ≥1.5.0 per-worker proxy dispatch for table-function execute callbacks. It adds thread-detection, registers a per-worker local-init callback that creates a worker proxy on non-Ruby threads, and modifies the callback dispatch to retrieve and use that proxy. The changes include an integration test validating multi-thread execution and sample benchmarks demonstrating the feature for both scalar and table UDFs.

Changes

Per-Worker Proxy Table Function Dispatch

Layer / File(s) Summary
Thread-detection and local-init callback registration
ext/duckdb/table_function.c
Declares ruby_native_thread_p thread-detection dependency and registers table_function_local_init_callback as the per-worker hook for DuckDB ≥1.5.0.
Per-worker proxy dispatch in execute callback
ext/duckdb/table_function.c
Adds proxy variable to table_function_execute_callback and conditionally retrieves the per-worker proxy from DuckDB local init data; implements per-worker local-init callback with proxy creation, exception protection, and worker-proxy lifecycle management.
Integration test for per-worker proxy execution
test/duckdb_test/table_function_test.rb
Multi-threaded test that configures 4 DuckDB threads, creates a table function with cardinality hints and max_threads initialization, records executing Ruby threads during emission, and validates query results and multi-thread callback execution.
Sample benchmarks for scalar and table function proxies
sample/issue1136.rb
Generalizes timing measurement to test both scalar and table UDF callbacks; adds table-function UDF with chunked emission and worker distribution, measures elapsed time and distinct Ruby thread counts for both function types.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

  • suketa/ruby-duckdb#1365: Introduces the per-worker worker_proxy creation and dispatch primitives in function_executor.c that this PR extends to table-function callbacks.
  • suketa/ruby-duckdb#1366: Applies the same per-worker proxy mechanism to scalar-function callbacks, providing the parallel pattern this PR follows for table functions.
  • suketa/ruby-duckdb#1280: Prior modifications to table_function.c that add GVL-safe executor dispatch, which this PR builds upon for per-worker scenarios.

Suggested reviewers

  • suketa

Poem

🐰 Per workers fly on proxy threads so fine,
Table functions now with threads align!
No more the lock, let DuckDB workers run,
Ruby callbacks dancing in the sun. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: implementing per-worker proxy for table function execute callbacks. It is specific, concise, and directly reflects the primary objective of the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Infer (1.2.0)
ext/duckdb/table_function.c

In file included from ext/duckdb/table_function.c:1:
ext/duckdb/ruby-duckdb.h:7:10: fatal error: 'ruby.h' file not found
7 | #include "ruby.h"
| ^~~~~~~~
1 error generated.
Error: the following clang command did not run successfully:
/opt/infer-linux-x86_64-v1.2.0/lib/infer/facebook-clang-plugins/clang/install/bin/clang-18
@/tmp/coderabbit-infer/6b0fd95bd5941bc0885cdad10ab7df23b1b1f9db-80314d1aaa4b3fbf/tmp/clang_command_.tmp.a55174.txt
++Contents of '/tmp/coderabbit-infer/6b0fd95bd5941bc0885cdad10ab7df23b1b1f9db-80314d1aaa4b3fbf/tmp/clang_command_.tmp.a55174.txt':
"-cc1" "-load"
"/opt/infer-linux-x86_64-v1.2.0/lib/infer/infer/bin/../../facebook-clang-plugins/libtooling/build/FacebookClangPlugin.dylib"
"-add-plugin" "BiniouASTExporter" "-plugin-arg-BiniouASTExporter" "-"
"-plugin-arg-BiniouASTExporter" "PREPEND_CURRENT_DIR=1"
"-plugin-arg-BiniouASTExporter" "MAX_STRING_SIZE=65535" "-cc1" "-triple"
"x86_64-unknown-linux-gnu" "-emit-obj

... [truncated 743 characters] ...

r-linux-x86_64-v1.2.0/lib/infer/facebook-clang-plugins/clang/install/lib/clang/18/include"
"-internal-isystem" "/usr/local/include" "-internal-isystem"
"/usr/lib/gcc/x86_64-linux-gnu/12/../../../../x86_64-linux-gnu/include"
"-internal-externc-isystem" "/usr/include/x86_64-linux-gnu"
"-internal-externc-isystem" "/include" "-internal-externc-isystem"
"/usr/include" "-Wno-ignored-optimization-argument" "-Wno-everything"
"-ferror-limit" "19" "-fgnuc-version=4.2.1" "-fskip-odr-check-in-gmf"
"-D__GCC_HAVE_DWARF2_CFI_ASM=1" "-o"
"/tmp/coderabbit-infer/80314d1aaa4b3fbf/file.o" "-x" "c"
"ext/duckdb/table_function.c" "-O0" "-fno-builtin" "-include"
"/opt/infer-linux-x86_64-v1.2.0/lib/infer/infer/bin/../lib/clang_wrappers/global_defines.h"
"-Wno-everything"


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
sample/issue1136.rb (1)

79-87: 💤 Low value

Potential off-by-one: decrement-then-check may emit one fewer chunk than intended.

(remaining -= 1) >= 0 decrements first, so when remaining=1, it becomes 0 and emits; when remaining=0, it becomes -1 and stops. This yields exactly CHUNKS iterations, which is correct. However, this pattern is subtle—consider a comment or using remaining.positive? before decrement for clarity.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sample/issue1136.rb` around lines 79 - 87, The emitter_execute_block method's
decrement-then-check on remaining is subtle; update emitter_execute_block so the
mutex-synchronized block checks remaining.positive? before decrementing (or
atomically decrement after confirming >0) to make intent explicit, or at minimum
add a clarifying comment next to the (remaining -= 1) >= 0 expression; refer to
the emitter_execute_block function, the remaining variable and the
mutex.synchronize block when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@sample/issue1136.rb`:
- Around line 79-87: The emitter_execute_block method's decrement-then-check on
remaining is subtle; update emitter_execute_block so the mutex-synchronized
block checks remaining.positive? before decrementing (or atomically decrement
after confirming >0) to make intent explicit, or at minimum add a clarifying
comment next to the (remaining -= 1) >= 0 expression; refer to the
emitter_execute_block function, the remaining variable and the mutex.synchronize
block when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4817496d-0a96-4f23-939c-fe4ebf74ff6a

📥 Commits

Reviewing files that changed from the base of the PR and between 52eac0c and 6b0fd95.

📒 Files selected for processing (3)
  • ext/duckdb/table_function.c
  • sample/issue1136.rb
  • test/duckdb_test/table_function_test.rb

@suketa suketa left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thank you

@suketa
suketa merged commit 33065e9 into suketa:main Jun 8, 2026
35 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

make DuckDB::TableFunction work in multi thread duckdb

2 participants