Skip to content

Add worker loops: run and drive an ErlangEventLoop inside a context - #71

Merged
benoitc merged 10 commits into
mainfrom
worker-loops
Aug 15, 2026
Merged

Add worker loops: run and drive an ErlangEventLoop inside a context#71
benoitc merged 10 commits into
mainfrom
worker-loops

Conversation

@benoitc

@benoitc benoitc commented Aug 15, 2026

Copy link
Copy Markdown
Owner

Runtime API for gunicorn-style Python workers inside the VM (the arbiter itself will live in hornbeam).

Added

  • py_context:start_loop/1,2, stop_loop/1,2, loop_ref/1, submit/4,5, submit_await/4,5,6; {py_loop_exit, Ctx, Result} to the owner; {error, loop_running} for call/eval/exec while a loop runs; preload option on py_context:new/1.
  • erlang.server: serve(listen_fd, factory, udp=False), adopt(fd, factory), stop_serving.
  • Coroutine injection into owngil loops (process_ready_tasks attaches to the subinterpreter), wake on inject, task start failures reported to the caller.

Fixed

  • owngil contexts had no ErlangEventLoop; context_get_event_loop returned the main loop and re-pointed its worker.
  • owngil dispatch blocking on a dirty scheduler with a 30 s cap; now the async queue (11.6 us vs 15.6 us per call).
  • fd closed while still in the poll set under churn (erts_poll / stealing control reports); READ re-arm from the loop thread crashed in enif_select.

Tests: py_worker_loop_SUITE (22 cases), Python unit tests (test_server, test_transport_close, test_loop_helpers), py_worker_loop_stress_SUITE (STRESS=1). Bench: examples/bench_worker_loop.erl. Docs: docs/workers.md.

Full CT on Python 3.14: 619 passed, 14 expected skips. Python 3.12: owngil group skips, rest passes.

benoitc added 10 commits August 15, 2026 19:37
py_context:start_loop/stop_loop/loop_ref/submit/submit_await and the
erlang.server helper let Erlang run N owngil workers that serve TCP/UDP
on a socket Erlang bound (py:dup_fd per worker) or adopt accepted fds,
and inject coroutines into the running loop.

Fixes behind it: owngil contexts had no ErlangEventLoop and returned
the main loop from context_get_event_loop; owngil dispatch was blocking
on a dirty scheduler with a 30 s cap; transports closed fds still in
the poll set (erts_poll reports under churn); READ re-arm from the loop
thread crashed in enif_select; task start failures were dropped.

Includes CT suite, Python unit tests, gated stress suite, bench script
and docs/workers.md.
…ng loop

Tasks beyond MAX_TASK_BATCH queued before the worker ran were stranded:
submit_task sends no new wakeup while one is pending and the running-loop
branch returned ok. Seen as test_submit_ordering timeouts on slow CI.
The io queue can store several small task binaries in one iovec element;
dequeuing iov_len per task dropped every task after the first in that
element (every second submit lost on slow CI runners).
A loop running on its own thread consumes pending events itself; sending
task_ready for every timer or fd event made the worker attach to the
subinterpreter and take its GIL for nothing. Track that state in the loop
struct (_set_running_for) and only wake the loop thread.
asyncio cancels a handle after it ran (sleep's finally); a recycled handle
then cancels the callback that reused it. Only _dispatch fd handles are
pooled now. Explains the every-second-task loss on CI.
@benoitc
benoitc merged commit e4f3b61 into main Aug 15, 2026
20 checks passed
@benoitc
benoitc deleted the worker-loops branch August 15, 2026 19:29
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