Skip to content

fix(js): use try_lock in sync setters to avoid event-loop stalls - #41

Open
fornwall wants to merge 1 commit into
mainfrom
fix/js-sync-setters-try-lock
Open

fix(js): use try_lock in sync setters to avoid event-loop stalls#41
fornwall wants to merge 1 commit into
mainfrom
fix/js-sync-setters-try-lock

Conversation

@fornwall

Copy link
Copy Markdown
Owner

Problem

Synchronous #[napi] setters run on the Node.js main (JS) thread, while async Task::compute implementations run on the libuv worker pool. Both acquire the same per-object mutex:

  • the Mutex<ManagedConnection> inside AdbcConnectionCore, and
  • the Arc<Mutex<CoreStatement>> of a statement.

These setters previously used a blocking lock(). When a long-running async operation (e.g. ExecuteQueryTask::compute) holds the mutex on a worker thread, a synchronous setter called from JS blocks the entire Node event loop until the async driver call finishes.

Fix

Switch the synchronous setters to try_lock(). When the object is currently busy (mutex held by an in-flight async op), throw a clear JS exception instead of blocking:

  • "connection is busy with another operation"
  • "statement is busy with another operation"

Poisoned-mutex handling keeps parity with the prior behavior. The async Task implementations are intentionally left unchanged — they legitimately block on the worker pool, off the JS thread.

Methods changed

  • AdbcConnectionCore::set_option (client.rs) — backs _NativeAdbcConnection::set_option
  • _NativeAdbcStatement::set_sql_query (lib.rs)
  • _NativeAdbcStatement::set_option (lib.rs)

A dedicated ClientError::Busy variant carries the busy message without the generic "Internal Error:" prefix.

Verification

cd javascript && cargo build succeeds.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XNCrC87g9MkppGpL4MDgh5

Synchronous `#[napi]` setters run on the Node.js main (JS) thread, while
async `Task::compute` implementations run on the libuv worker pool. Both
acquire the same per-object mutex (the `Mutex<ManagedConnection>` inside
`AdbcConnectionCore`, and the `Arc<Mutex<CoreStatement>>` of a statement).

Previously these setters used a blocking `lock()`. If a long-running async
operation (e.g. `ExecuteQueryTask::compute`) held the mutex on a worker
thread, a synchronous setter called from JS would block the entire Node
event loop until the async driver call finished.

Switch the synchronous setters to `try_lock()` and surface a clear JS
exception ("connection is busy with another operation" / "statement is
busy with another operation") when the object is currently in use, instead
of blocking. Poisoned-mutex handling keeps parity with the prior behavior.
The async `Task` implementations are unchanged: they legitimately block on
the worker pool, off the JS thread.

Methods changed:
- `AdbcConnectionCore::set_option` (client.rs) — backs
  `_NativeAdbcConnection::set_option`
- `_NativeAdbcStatement::set_sql_query` (lib.rs)
- `_NativeAdbcStatement::set_option` (lib.rs)

A dedicated `ClientError::Busy` variant carries the busy message without
the generic "Internal Error:" prefix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNCrC87g9MkppGpL4MDgh5
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.

1 participant