Skip to content

Drain closing handles after the exit event - #160

Closed
bavulapati wants to merge 3 commits into
mainfrom
drain-closing-handles-on-exit
Closed

bavulapati wants to merge 3 commits into
mainfrom
drain-closing-handles-on-exit

Conversation

@bavulapati

Copy link
Copy Markdown
Contributor

Motivation

Comes out of holepunchto/bare-kit#83 / holepunchto/bare-kit#92. A bare-kit worklet ties Bare.IPC to the process lifetime: when the worklet exits, it closes Bare.IPC so the host's read reaches EOF and the host can detect a stopped worklet.

The problem is a chicken-and-egg around teardown. Closing a pipe is async (uv_close needs a loop turn). The exit event fires after the loop has already stopped (bare_runtime_run exits its uv_run loop, then calls bare_runtime__on_exit), so a close issued from exit is queued but never completes until bare_teardown pumps the loop. An embedder that parks between bare_run returning and bare_teardown (waiting for the host to tear it down) therefore never flushes the close - and the host never sees EOF, so it never tears down. Deadlock.

Change

After bare_runtime__on_exit, pump the loop once, non-blocking:

bare_runtime__on_exit(runtime);

uv_run(runtime->loop, UV_RUN_NOWAIT);

This drains handles closed during the exit event so they release before bare_run() returns. It's bounded - a single non-blocking iteration - and is the same pump bare_teardown already does (bare_runtime_teardown runs uv_run(loop, UV_RUN_NOWAIT)), just early enough that process-scoped resources release as the process exits.

Validation

Verified end to end in bare-kit: with this flush applied (and bare-kit's own loop-pump workaround removed), the IPC EOF tests pass on real Windows (VMware Fusion VM) and the equivalent pump is green on CI across win32-x64, win32-arm64, linux, darwin, ios, android.

Note on tests

I didn't add a bare-side test: the behavior is close-timing relative to bare_run returning, which isn't cleanly observable from a JS test (you can't run JS after exit). Happy to add a C test in whatever shape you prefer if you'd like one here - the downstream bare-kit EOF tests exercise it.

Refs holepunchto/bare-kit#92, holepunchto/bare-kit#83

Pump the loop once (non-blocking) after emitting 'exit' so handles closed
during the exit event release before bare_run() returns, rather than lingering
until bare_teardown(). This lets resources whose lifetime is tied to the
process - e.g. an IPC channel an embedder closes on exit - be seen as closed by
the peer without waiting for teardown.
@bavulapati
bavulapati requested a review from a team June 30, 2026 17:15

@kasperisager kasperisager left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This provides an escape hatch for I/O callbacks and microtasks to run between exit firing and teardown handlers running, which is strictly forbidden. That's also why exit event handlers are forbidden from queuing additional I/O.

Whatever fix we need must be made in Bare Kit, breaking the teardown contract is not the solution.

@kasperisager

Copy link
Copy Markdown
Contributor

The tests introduced in #161 show why this is problematic.

@kasperisager
kasperisager deleted the drain-closing-handles-on-exit branch July 2, 2026 09:35
bavulapati added a commit to holepunchto/bare-kit that referenced this pull request Aug 13, 2026
Bare Kit is in-process (worklets are threads), so the pipe-based IPC with its
async close-on-exit was the wrong tool and the root of the EOF and win32
problems (holepunchto/bare#160, #92). Move the transport onto the in-process
queue primitive:

- Bare.IPC becomes a bare-stream Duplex over native queue ops that worklet.c
  injects (read/write/close/ref/unref); end() closes the queue so the host
  reaches EOF.
- The host bare_ipc runs over the queue's thread port; the apple poll is a
  manual dispatch source fired by the queue signal, reporting readiness via
  bare_queue_readable/writable.
- The worklet closes its queue end synchronously after bare_run, so the host
  reaches EOF on every exit path (loop drain, Bare.exit, IPC.end, terminate)
  with no loop pump and no async scheduled from exit.
- bare_worklet_destroy joins the worklet thread (terminating and waking it
  first) so the runtime and queue finish tearing down before the host returns.
- Delete the pipe posix/ipc.c.

Darwin only so far: android/linux/win32 polls are not yet ported. All 14 darwin
worklet and queue tests pass.
bavulapati added a commit to holepunchto/bare-kit that referenced this pull request Aug 14, 2026
Bare Kit is in-process - worklets are threads - so the pipe transport, with its
async close on exit, was the wrong tool and the root of the EOF and win32
problems (holepunchto/bare#160, #92).

Bare.IPC becomes a bare-stream Duplex over the queue, and the worklet closes its
end synchronously after bare_run, so the host reaches EOF on every exit path with
no loop pump and nothing scheduled from the exit event. The host polls readiness
rather than reading a pipe: apple keeps its dispatch source, linux and win32 now
share one uv implementation instead of a hand-rolled epoll thread and a pipe
loop, and android keeps its looper with the ipc owning the eventfd.

bare_ipc_get_incoming and bare_ipc_get_outgoing go with the pipes; the win32
exports follow. The two EOF tests now run on win32, which is what this was for.
bavulapati added a commit to holepunchto/bare-kit that referenced this pull request Aug 14, 2026
Bare Kit is in-process - worklets are threads - so the pipe transport, with its
async close on exit, was the wrong tool and the root of the EOF and win32
problems (holepunchto/bare#160, #92).

Bare.IPC becomes a bare-stream Duplex over the queue, and the worklet closes its
end synchronously after bare_run, so the host reaches EOF on every exit path with
no loop pump and nothing scheduled from the exit event. The host polls readiness
rather than reading a pipe: apple keeps its dispatch source, linux and win32 now
share one uv implementation instead of a hand-rolled epoll thread and a pipe
loop, and android keeps its looper with the ipc owning the eventfd.

The two poll tests closed their loop while their poll was still armed. Pipes hid
that: their EOF arrived asynchronously at thread teardown, while the queue closes
synchronously, so the wake lands in the window and the callback posts to a closed
loop - a segfault on linux-x64. The poll is retired first now.

bare_ipc_get_incoming and bare_ipc_get_outgoing go with the pipes; the win32
exports follow. Both EOF tests now run on win32, which is what this was for.
bavulapati added a commit to holepunchto/bare-kit that referenced this pull request Aug 14, 2026
Bare Kit is in-process - worklets are threads - so the pipe transport, with its
async close on exit, was the wrong tool and the root of the EOF and win32
problems (holepunchto/bare#160, #92).

Bare.IPC becomes a bare-stream Duplex over the queue, and the worklet closes its
end synchronously after bare_run, so the host reaches EOF on every exit path with
no loop pump and nothing scheduled from the exit event. The host polls readiness
rather than reading a pipe: apple keeps its dispatch source, linux and win32 now
share one uv implementation instead of a hand-rolled epoll thread and a pipe
loop, and android keeps its looper with the ipc owning the eventfd.

The two poll tests closed their loop while their poll was still armed. Pipes hid
that: their EOF arrived asynchronously at thread teardown, while the queue closes
synchronously, so the wake lands in the window and the callback posts to a closed
loop - a segfault on linux-x64. The poll is retired first now.

bare_ipc_get_incoming and bare_ipc_get_outgoing go with the pipes; the win32
exports follow. Both EOF tests now run on win32, which is what this was for.
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.

2 participants