From e4b1dd410117305fa3b4a533ceefdc93af62d613 Mon Sep 17 00:00:00 2001 From: Benoit Chesneau Date: Sat, 29 Aug 2026 15:27:12 +0200 Subject: [PATCH] Remove the legacy worker API and its executor Contexts are the only execution path; the per-worker NIFs, the single executor thread they ran on, the deprecated async worker NIFs, the unused worker pool, the cancel aliases and the unreachable inline executor branches of the context NIFs are gone (about 3,000 lines). This removes public NIF functions, so the open release becomes 5.0.0. memory_stats and gc run under the GIL on the calling scheduler. The generator example uses py:stream. --- CHANGELOG.md | 15 +- c_src/README.md | 10 +- c_src/py_callback.c | 690 +-------------------------- c_src/py_event_loop.c | 14 - c_src/py_event_loop.h | 14 - c_src/py_exec.c | 793 ------------------------------- c_src/py_nif.c | 975 ++++---------------------------------- c_src/py_nif.h | 355 +------------- c_src/py_worker_pool.c | 921 ----------------------------------- c_src/py_worker_pool.h | 496 ------------------- docs/architecture.md | 22 +- docs/code-map.md | 17 +- docs/glossary.md | 4 +- docs/scalability.md | 2 +- examples/gen_test.erl | 91 ++-- src/erlang_python.app.src | 2 +- src/py_nif.erl | 231 --------- 17 files changed, 169 insertions(+), 4483 deletions(-) delete mode 100644 c_src/py_worker_pool.c delete mode 100644 c_src/py_worker_pool.h diff --git a/CHANGELOG.md b/CHANGELOG.md index a1434640..437a85c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 4.2.0 (2026-08-29) +## 5.0.0 (2026-08-29) ### Added @@ -49,6 +49,19 @@ storms, loop churn, 60 s mixed workload with resource counters checked. - Guide: `docs/isolated.md`, with what each of the three modes guarantees. +### Removed + +- The legacy worker API (`py_nif:worker_new/0,1`, `worker_call`, `worker_eval`, + `worker_exec`, `worker_next`, `worker_destroy`, `import_module/2`, + `get_attr/3`, `set_callback_handler/2`, `send_callback_response/2`, + `resume_callback/2`) and the single executor thread behind it, the + `async_worker_*`/`async_call`/`async_gather`/`async_stream` NIFs that only + returned `deprecated`, the unused worker pool (`pool_*` NIFs), the + `cancel_reader/writer` aliases, and the unreachable inline executor + branches of the context NIFs. Contexts (`py_context`, `py:call/3`) are the + only execution path. `py:memory_stats/0` and `py:gc/0,1` now run on the + calling scheduler under the GIL. + ### Fixed - `pthread_timedjoin_np` was called without `_GNU_SOURCE`, an implicit diff --git a/c_src/README.md b/c_src/README.md index a8f240ec..8d1dc06b 100644 --- a/c_src/README.md +++ b/c_src/README.md @@ -16,7 +16,7 @@ where things are. | `py_nif.h` | All shared types: `py_context_t` and its request queue, request types, callback and suspension state, runtime state machine, atoms, globals, declarations | 2.4k lines. The struct comments carry the locking rules; read `py_context_t` before touching threads | | `py_nif.c` | Runtime init and finalize, resource types, context create/destroy, the request queue, `worker_context_thread_main` and `owngil_context_thread_main`, `owngil_execute_*` (used by both thread kinds), the `nif_context_*` NIFs, process-local envs, `py_ref`, the NIF function table at the end | Sections are banner-separated; `grep -n '^ \* ===\|^/\* ==='` lists them | | `py_convert.c` | `py_to_term`, `term_to_py`, depth limits, tagged tuples (`{bytes, B}`, `{'$py_shm', ...}`), error tuples `{error, {Type, Msg}}` | The type mapping tables in the comments are the reference for `_etf.py` | -| `py_exec.c` | Executing a call/eval/exec with suspension support; the legacy single executor thread | | +| `py_exec.c` | Execution mode detection (free-threaded or GIL build) | | | `py_callback.c` | The `erlang` Python module: `call`, `send`, `whereis`, `schedule*`, `Atom`/`Pid`/`Ref` types, callback delivery paths (suspension, blocking pipe, async pipe), channel and shared-dict methods, callback name registry | `erlang_call_impl` documents the path precedence | | `py_thread_worker.c` | Python threads calling Erlang through `py_thread_handler` | | | `py_subinterp_thread.c/.h` | Thread pool of sub-interpreters (owngil contexts, loop pools) | | @@ -24,7 +24,6 @@ where things are. | `py_channel.c/.h`, `py_buffer.c/.h`, `py_reactor_buffer.c/.h`, `py_shared_dict.c` | Resources with a Python-facing object each | | | `py_logging.c` | Logging and tracing NIFs | | | `py_mem_limit.c` | obmalloc arena accounting for owngil memory caps | | -| `py_worker_pool.c/.h` | Legacy pool, no caller in `src/` | Candidate for removal | | `py_util.c/.h` | Macros, small helpers | | ## Where the live paths are @@ -62,10 +61,3 @@ where things are. 3. Add the stub and its `-spec` and doc to `src/py_nif.erl`. 4. Cover it in a suite; `rebar3 dialyzer` and `rebar3 xref` must stay clean. -## Legacy code, for orientation - -Not on the path of contexts created today: the `worker_*` NIFs and -"Worker management" section, `async_worker_*` NIFs (return `deprecated`), -the "Legacy mode" inline branches in `nif_context_call/eval/exec`, -`py_worker_pool.c`, and the `cancel_reader/writer` aliases. When in doubt, -follow `nif_context_call_async` and ignore the rest. diff --git a/c_src/py_callback.c b/c_src/py_callback.c index ba630a2f..b5606735 100644 --- a/c_src/py_callback.c +++ b/c_src/py_callback.c @@ -337,325 +337,6 @@ static void cleanup_callback_registry(void) { pthread_mutex_unlock(&g_callback_registry_mutex); } -/* ============================================================================ - * Suspended state management - * ============================================================================ */ - -/** - * Source type for suspended state creation. - * Indicates whether the source is a request or an existing suspended state. - */ -typedef enum { - SUSPENDED_SOURCE_REQUEST, /* Source is py_request_t */ - SUSPENDED_SOURCE_EXISTING /* Source is suspended_state_t */ -} suspended_source_type_t; - -/** - * Source union for suspended state creation. - * Contains pointers to either request or existing suspended state. - */ -typedef struct { - suspended_source_type_t type; - union { - py_request_t *req; /* For SUSPENDED_SOURCE_REQUEST */ - suspended_state_t *existing; /* For SUSPENDED_SOURCE_EXISTING */ - } data; -} suspended_source_t; - -/** - * Internal cleanup helper for suspended state creation failure. - */ -static void cleanup_suspended_state_partial(suspended_state_t *state, PyObject *callback_args) { - if (state->orig_env != NULL) { - enif_free_env(state->orig_env); - } - if (state->callback_args != NULL) { - Py_DECREF(state->callback_args); - } else if (callback_args != NULL) { - Py_DECREF(callback_args); - } - if (state->callback_func_name != NULL) { - enif_free(state->callback_func_name); - } - enif_release_resource(state); -} - -/** - * Create a suspended state resource from exception args. - * Args tuple format: (callback_id, func_name, args) - * - * This unified function handles both: - * - Creating from a request (initial suspension) - * - Creating from an existing suspended state (nested suspension during replay) - * - * @param env NIF environment - * @param exc_args Exception args tuple from erlang.call() - * @param source Source of original request data - * @return suspended_state_t* or NULL on error - */ -static suspended_state_t *create_suspended_state_ex( - ErlNifEnv *env, PyObject *exc_args, const suspended_source_t *source) { - - (void)env; /* Only needed for future extensions */ - - if (!PyTuple_Check(exc_args) || PyTuple_Size(exc_args) != 3) { - return NULL; - } - - PyObject *callback_id_obj = PyTuple_GetItem(exc_args, 0); - PyObject *func_name_obj = PyTuple_GetItem(exc_args, 1); - PyObject *callback_args = PyTuple_GetItem(exc_args, 2); - - if (!PyLong_Check(callback_id_obj) || !PyUnicode_Check(func_name_obj)) { - return NULL; - } - - /* Allocate the suspended state resource */ - suspended_state_t *state = enif_alloc_resource( - SUSPENDED_STATE_RESOURCE_TYPE, sizeof(suspended_state_t)); - if (state == NULL) { - return NULL; - } - - /* Initialize the state */ - memset(state, 0, sizeof(suspended_state_t)); - - /* Set worker based on source type */ - if (source->type == SUSPENDED_SOURCE_REQUEST) { - state->worker = tl_current_worker; - } else { - state->worker = source->data.existing->worker; - } - /* Keep the worker resource alive for as long as the suspended state exists. - * Without this the worker can be GC'd while a callback is suspended, and - * nif_resume_callback_dirty would dereference a freed worker (use-after-free - * with the GIL held). Mirrors the enif_keep_resource(ctx) on the context path; - * suspended_state_destructor releases it. */ - if (state->worker != NULL) { - enif_keep_resource(state->worker); - } - - state->callback_id = PyLong_AsUnsignedLongLong(callback_id_obj); - - /* Copy callback function name */ - Py_ssize_t len; - const char *func_name = PyUnicode_AsUTF8AndSize(func_name_obj, &len); - if (func_name == NULL) { - enif_release_resource(state); - return NULL; - } - state->callback_func_name = enif_alloc(len + 1); - if (state->callback_func_name == NULL) { - enif_release_resource(state); - return NULL; - } - memcpy(state->callback_func_name, func_name, len); - state->callback_func_name[len] = '\0'; - state->callback_func_len = len; - - /* Store reference to callback args */ - Py_INCREF(callback_args); - state->callback_args = callback_args; - - /* Get request type and timeout based on source */ - int request_type; - unsigned long timeout_ms; - - if (source->type == SUSPENDED_SOURCE_REQUEST) { - request_type = source->data.req->type; - timeout_ms = source->data.req->timeout_ms; - } else { - request_type = source->data.existing->request_type; - timeout_ms = source->data.existing->orig_timeout_ms; - } - - state->request_type = request_type; - state->orig_timeout_ms = timeout_ms; - - /* Create environment to hold copied terms */ - state->orig_env = enif_alloc_env(); - if (state->orig_env == NULL) { - cleanup_suspended_state_partial(state, NULL); - return NULL; - } - - /* Copy request-specific data based on source type and request type */ - if (request_type == PY_REQ_CALL) { - ErlNifBinary *src_module, *src_func; - ERL_NIF_TERM src_args, src_kwargs; - ErlNifEnv *src_env; - - if (source->type == SUSPENDED_SOURCE_REQUEST) { - src_module = &source->data.req->module_bin; - src_func = &source->data.req->func_bin; - src_args = source->data.req->args_term; - src_kwargs = source->data.req->kwargs_term; - src_env = source->data.req->env; - } else { - src_module = &source->data.existing->orig_module; - src_func = &source->data.existing->orig_func; - src_args = source->data.existing->orig_args; - src_kwargs = source->data.existing->orig_kwargs; - src_env = source->data.existing->orig_env; - } - - /* Copy module binary */ - if (!enif_alloc_binary(src_module->size, &state->orig_module)) { - cleanup_suspended_state_partial(state, NULL); - return NULL; - } - memcpy(state->orig_module.data, src_module->data, src_module->size); - - /* Copy function binary */ - if (!enif_alloc_binary(src_func->size, &state->orig_func)) { - enif_release_binary(&state->orig_module); - cleanup_suspended_state_partial(state, NULL); - return NULL; - } - memcpy(state->orig_func.data, src_func->data, src_func->size); - - /* Copy args and kwargs to our environment */ - state->orig_args = enif_make_copy(state->orig_env, src_args); - state->orig_kwargs = enif_make_copy(state->orig_env, src_kwargs); - (void)src_env; /* Used implicitly by enif_make_copy */ - - } else if (request_type == PY_REQ_EVAL) { - ErlNifBinary *src_code; - ERL_NIF_TERM src_locals; - ErlNifEnv *src_env; - - if (source->type == SUSPENDED_SOURCE_REQUEST) { - src_code = &source->data.req->code_bin; - src_locals = source->data.req->locals_term; - src_env = source->data.req->env; - } else { - src_code = &source->data.existing->orig_code; - src_locals = source->data.existing->orig_locals; - src_env = source->data.existing->orig_env; - } - - /* Copy code binary */ - if (!enif_alloc_binary(src_code->size, &state->orig_code)) { - cleanup_suspended_state_partial(state, NULL); - return NULL; - } - memcpy(state->orig_code.data, src_code->data, src_code->size); - - /* Copy locals */ - state->orig_locals = enif_make_copy(state->orig_env, src_locals); - (void)src_env; /* Used implicitly by enif_make_copy */ - } - - /* Initialize synchronization primitives */ - pthread_mutex_init(&state->mutex, NULL); - pthread_cond_init(&state->cond, NULL); - - state->result_data = NULL; - state->result_len = 0; - state->has_result = false; - state->is_error = false; - - return state; -} - -/** - * Create a suspended state resource from a request. - * Wrapper for create_suspended_state_ex for initial suspension. - */ -static suspended_state_t *create_suspended_state(ErlNifEnv *env, PyObject *exc_args, - py_request_t *req) { - suspended_source_t source = { - .type = SUSPENDED_SOURCE_REQUEST, - .data.req = req - }; - return create_suspended_state_ex(env, exc_args, &source); -} - -/** - * Create a new suspended state from an existing one (for nested suspensions). - * Wrapper for create_suspended_state_ex for nested suspension during replay. - */ -static suspended_state_t *create_suspended_state_from_existing( - ErlNifEnv *env, PyObject *exc_args, suspended_state_t *existing) { - suspended_source_t source = { - .type = SUSPENDED_SOURCE_EXISTING, - .data.existing = existing - }; - return create_suspended_state_ex(env, exc_args, &source); -} - -/** - * Build exception args tuple from thread-local pending callback state. - * - * This helper extracts the common pattern of building the exc_args tuple - * (callback_id, func_name, args) from thread-local storage. - * - * @return PyObject* tuple on success, NULL on failure - * @note On failure, tl_pending_callback is cleared - * @note Caller must Py_DECREF the returned tuple when done - */ -static PyObject *build_pending_callback_exc_args(void) { - PyObject *exc_args = PyTuple_New(3); - if (exc_args == NULL) { - tl_pending_callback = false; - Py_CLEAR(tl_pending_args); - return NULL; - } - - PyObject *callback_id_obj = PyLong_FromUnsignedLongLong(tl_pending_callback_id); - PyObject *func_name_obj = PyUnicode_FromStringAndSize( - tl_pending_func_name, tl_pending_func_name_len); - - if (callback_id_obj == NULL || func_name_obj == NULL) { - Py_XDECREF(callback_id_obj); - Py_XDECREF(func_name_obj); - Py_DECREF(exc_args); - tl_pending_callback = false; - Py_CLEAR(tl_pending_args); - return NULL; - } - - PyTuple_SET_ITEM(exc_args, 0, callback_id_obj); - PyTuple_SET_ITEM(exc_args, 1, func_name_obj); - Py_INCREF(tl_pending_args); /* Tuple takes ownership */ - PyTuple_SET_ITEM(exc_args, 2, tl_pending_args); - - return exc_args; -} - -/** - * Build the {suspended, ...} result term from a suspended state. - * - * Common helper for creating the suspension result after a callback - * is detected during Python execution. - * - * @param env NIF environment - * @param suspended Suspended state (resource will be released) - * @return ERL_NIF_TERM {suspended, CallbackId, StateRef, {FuncName, Args}} - * @note Clears tl_pending_callback - */ -static ERL_NIF_TERM build_suspended_result(ErlNifEnv *env, suspended_state_t *suspended) { - ERL_NIF_TERM state_ref = enif_make_resource(env, suspended); - enif_release_resource(suspended); - - ERL_NIF_TERM callback_id_term = enif_make_uint64(env, tl_pending_callback_id); - - ERL_NIF_TERM func_name_term; - unsigned char *fn_buf = enif_make_new_binary(env, tl_pending_func_name_len, &func_name_term); - memcpy(fn_buf, tl_pending_func_name, tl_pending_func_name_len); - - ERL_NIF_TERM args_term = py_to_term(env, tl_pending_args); - - tl_pending_callback = false; - Py_CLEAR(tl_pending_args); - - return enif_make_tuple4(env, - ATOM_SUSPENDED, - callback_id_term, - state_ref, - enif_make_tuple2(env, func_name_term, args_term)); -} - /* ============================================================================ * Context suspension helpers (for process-per-context architecture) * @@ -1881,8 +1562,7 @@ static PyObject *erlang_call_impl(PyObject *self, PyObject *args) { * Priority: * 1. tl_current_context with suspension enabled (new process-per-context API) * 2. tl_current_context with callback_handler (old blocking pipe mode) - * 3. tl_current_worker (legacy worker API) - * 4. thread_worker_call (spawned threads) + * 3. thread_worker_call (spawned threads) * * NOTE: In OWN_GIL mode, erlang.call() goes through thread_worker_call() * rather than using suspension/resume. This is because OWN_GIL contexts @@ -1893,9 +1573,8 @@ static PyObject *erlang_call_impl(PyObject *self, PyObject *args) { */ bool has_context_suspension = (tl_current_context != NULL && tl_allow_suspension); bool has_context_handler = (tl_current_context != NULL && tl_current_context->has_callback_handler); - bool has_worker_handler = (tl_current_worker != NULL && tl_current_worker->has_callback_handler); - if (!has_context_suspension && !has_context_handler && !has_worker_handler) { + if (!has_context_suspension && !has_context_handler) { /* * Not an executor thread - use thread worker path. * This enables any spawned Python thread to call erlang.call(): @@ -1951,22 +1630,6 @@ static PyObject *erlang_call_impl(PyObject *self, PyObject *args) { } size_t func_name_len = strlen(func_name); - /* Check if we have a suspended state with a cached result (replay case) */ - if (tl_current_suspended != NULL && tl_current_suspended->has_result) { - /* Verify this is the same callback */ - if (tl_current_suspended->callback_func_len == func_name_len && - memcmp(tl_current_suspended->callback_func_name, func_name, func_name_len) == 0) { - /* Return the cached result - parse using ast.literal_eval */ - PyObject *result = parse_callback_response( - tl_current_suspended->result_data, - tl_current_suspended->result_len); - /* Mark result as consumed (don't clear tl_current_suspended yet, - * as we might need it for nested callbacks in the future) */ - tl_current_suspended->has_result = false; - return result; - } - } - /* Check for context-based suspended state with cached results (context replay case) */ if (tl_current_context_suspended != NULL) { /* @@ -2022,23 +1685,6 @@ static PyObject *erlang_call_impl(PyObject *self, PyObject *args) { /* If we get here, this is a NEW callback - will suspend below */ } - /* - * FIX for multiple sequential erlang.call(): - * If we're in WORKER replay context (tl_current_suspended != NULL) but didn't get - * a cache hit above, this is a SUBSEQUENT call (e.g., second erlang.call() - * in the same Python function). For WORKER mode, the callback handler process - * is still running and will handle this via blocking pipe. - * - * For CONTEXT replay (tl_current_context_suspended != NULL), we CANNOT block - * because there's no callback handler process. Instead, we must suspend again - * and let the context process handle the subsequent callback. This works because - * the context process re-replays from the beginning, and each callback result - * is returned via the cached result mechanism on subsequent replays. - */ - bool force_blocking = (tl_current_suspended != NULL); - /* Note: tl_current_context_suspended is NOT included here - context mode - * always uses suspension for callbacks, allowing unlimited nesting via replay */ - /* Build args list (remaining args) */ PyObject *call_args = PyTuple_GetSlice(args, 1, nargs); if (call_args == NULL) { @@ -2051,9 +1697,8 @@ static PyObject *erlang_call_impl(PyObject *self, PyObject *args) { * executor (PY_REQ_CALL or PY_REQ_EVAL). For PY_REQ_EXEC or nested Python * code, we must block and wait for the result. * - * Also block if force_blocking is set (replay context with no cache hit). */ - if (!tl_allow_suspension || force_blocking) { + if (!tl_allow_suspension) { /* Fall back to blocking behavior - send message and wait on pipe */ ErlNifEnv *msg_env = enif_alloc_env(); if (msg_env == NULL) { @@ -2083,16 +1728,15 @@ static PyObject *erlang_call_impl(PyObject *self, PyObject *args) { uint32_t response_len = 0; int read_result; - /* Get callback handler and pipe from context or worker */ - ErlNifPid *handler_pid; - int read_fd; - if (has_context_handler) { - handler_pid = &tl_current_context->callback_handler; - read_fd = tl_current_context->callback_pipe[0]; - } else { - handler_pid = &tl_current_worker->callback_handler; - read_fd = tl_current_worker->callback_pipe[0]; + /* Callback handler and pipe of the context */ + if (!has_context_handler) { + Py_DECREF(call_args); + enif_free_env(msg_env); + PyErr_SetString(PyExc_RuntimeError, "erlang.call: no callback handler for this context"); + return NULL; } + ErlNifPid *handler_pid = &tl_current_context->callback_handler; + int read_fd = tl_current_context->callback_pipe[0]; Py_BEGIN_ALLOW_THREADS enif_send(NULL, handler_pid, msg_env, msg); @@ -4341,318 +3985,6 @@ static int create_erlang_module(void) { * event-driven operation without pthread polling. * ============================================================================ */ -/* ============================================================================ - * Resume callback NIFs - * ============================================================================ */ - -/* Forward declaration for the dirty resume NIF */ -static ERL_NIF_TERM nif_resume_callback_dirty(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]); - -/** - * Resume a suspended callback by storing the result and scheduling replay. - * - * Args: StateRef, ResultBinary - * - * This NIF stores the callback result in the suspended state and schedules - * a dirty NIF (nif_resume_callback_dirty) to replay the Python code. - */ -static ERL_NIF_TERM nif_resume_callback(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { - (void)argc; - suspended_state_t *state; - ErlNifBinary result_bin; - - if (!runtime_is_running()) { - return make_error(env, "python_not_running"); - } - - if (!enif_get_resource(env, argv[0], SUSPENDED_STATE_RESOURCE_TYPE, (void **)&state)) { - return make_error(env, "invalid_state_ref"); - } - - if (!enif_inspect_binary(env, argv[1], &result_bin)) { - return make_error(env, "invalid_result"); - } - - /* Store the result in the suspended state */ - pthread_mutex_lock(&state->mutex); - - /* Copy result data. Free any prior result first: a duplicate/raced resume - * would otherwise leak the previous buffer. (has_result is not a one-shot - * flag -- it toggles during nested replay -- so result_data is the real - * pending-result indicator.) */ - if (state->result_data != NULL) { - enif_free(state->result_data); - state->result_data = NULL; - } - state->result_data = enif_alloc(result_bin.size); - if (state->result_data == NULL) { - pthread_mutex_unlock(&state->mutex); - return make_error(env, "alloc_failed"); - } - memcpy(state->result_data, result_bin.data, result_bin.size); - state->result_len = result_bin.size; - state->has_result = true; - state->is_error = false; - - pthread_mutex_unlock(&state->mutex); - - /* - * Schedule the dirty resume NIF. - * This allows the current NIF to return immediately, and the dirty NIF - * will handle the Python replay on a dirty scheduler. - */ - ERL_NIF_TERM new_argv[1] = { argv[0] }; /* Pass StateRef to dirty NIF */ - return enif_schedule_nif(env, "resume_callback_dirty", - ERL_NIF_DIRTY_JOB_IO_BOUND, nif_resume_callback_dirty, 1, new_argv); -} - -/** - * Dirty NIF that replays Python code with the cached callback result. - * - * This is scheduled by nif_resume_callback and runs on a dirty I/O scheduler. - * It sets tl_current_suspended so erlang_call_impl can return the cached result, - * then re-runs the original Python code. When Python hits erlang.call() again, - * it gets the cached result and continues normally. - */ -static ERL_NIF_TERM nif_resume_callback_dirty(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { - (void)argc; - suspended_state_t *state; - - if (!runtime_is_running()) { - return make_error(env, "python_not_running"); - } - - if (!enif_get_resource(env, argv[0], SUSPENDED_STATE_RESOURCE_TYPE, (void **)&state)) { - return make_error(env, "invalid_state_ref"); - } - - /* Verify the state has a result */ - if (!state->has_result) { - return make_error(env, "no_result"); - } - - /* The worker is kept alive for the lifetime of the suspended state, but - * guard rather than dereference NULL in the replay below. */ - if (state->worker == NULL) { - return make_error(env, "no_worker"); - } - - /* Set up thread-local state for replay */ - tl_current_worker = state->worker; - tl_callback_env = env; - tl_current_suspended = state; /* erlang_call_impl will check this */ - tl_allow_suspension = true; - - ERL_NIF_TERM result; - - if (state->request_type == PY_REQ_CALL) { - /* Replay a py:call */ - char *module_name = enif_alloc(state->orig_module.size + 1); - char *func_name = enif_alloc(state->orig_func.size + 1); - - if (module_name == NULL || func_name == NULL) { - enif_free(module_name); - enif_free(func_name); - tl_current_suspended = NULL; - return make_error(env, "alloc_failed"); - } - - memcpy(module_name, state->orig_module.data, state->orig_module.size); - module_name[state->orig_module.size] = '\0'; - memcpy(func_name, state->orig_func.data, state->orig_func.size); - func_name[state->orig_func.size] = '\0'; - - PyGILState_STATE gstate = PyGILState_Ensure(); - - PyObject *func = NULL; - - /* Get the function (same logic as process_request) */ - if (strcmp(module_name, "__main__") == 0) { - func = PyDict_GetItemString(state->worker->locals, func_name); - if (func == NULL) { - func = PyDict_GetItemString(state->worker->globals, func_name); - } - if (func != NULL) { - Py_INCREF(func); - } else { - PyErr_Format(PyExc_NameError, "name '%s' is not defined", func_name); - result = make_py_error(env); - goto call_cleanup; - } - } else { - PyObject *module = PyImport_ImportModule(module_name); - if (module == NULL) { - result = make_py_error(env); - goto call_cleanup; - } - func = PyObject_GetAttrString(module, func_name); - Py_DECREF(module); - } - - if (func == NULL) { - result = make_py_error(env); - goto call_cleanup; - } - - /* Convert args */ - unsigned int args_len; - if (!enif_get_list_length(state->orig_env, state->orig_args, &args_len)) { - Py_DECREF(func); - result = make_error(env, "invalid_args"); - goto call_cleanup; - } - - PyObject *args = PyTuple_New(args_len); - if (args == NULL) { - Py_DECREF(func); - result = make_error(env, "alloc_failed"); - goto call_cleanup; - } - ERL_NIF_TERM head, tail = state->orig_args; - for (unsigned int i = 0; i < args_len; i++) { - enif_get_list_cell(state->orig_env, tail, &head, &tail); - PyObject *arg = term_to_py(state->orig_env, head); - if (arg == NULL) { - Py_DECREF(args); - Py_DECREF(func); - result = make_error(env, "arg_conversion_failed"); - goto call_cleanup; - } - PyTuple_SET_ITEM(args, i, arg); - } - - /* Convert kwargs */ - PyObject *kwargs = NULL; - if (enif_is_map(state->orig_env, state->orig_kwargs)) { - kwargs = term_to_py(state->orig_env, state->orig_kwargs); - } - - /* Call the function (this will hit erlang.call which returns cached result) */ - PyObject *py_result = PyObject_Call(func, args, kwargs); - - Py_DECREF(func); - Py_DECREF(args); - Py_XDECREF(kwargs); - - if (py_result == NULL) { - if (tl_pending_callback) { - /* - * Flag-based callback detection during replay. - * Check flag FIRST, not exception type - this works even if - * Python code caught and re-raised the exception. - */ - PyErr_Clear(); /* Clear whatever exception is set */ - - /* Build exc_args tuple from thread-local storage */ - PyObject *exc_args = build_pending_callback_exc_args(); - if (exc_args == NULL) { - result = make_error(env, "build_exc_args_failed"); - } else { - suspended_state_t *new_suspended = create_suspended_state_from_existing(env, exc_args, state); - Py_DECREF(exc_args); - if (new_suspended == NULL) { - tl_pending_callback = false; - Py_CLEAR(tl_pending_args); - result = make_error(env, "create_nested_suspended_state_failed"); - } else { - result = build_suspended_result(env, new_suspended); - } - } - } else { - result = make_py_error(env); - } - } else { - ERL_NIF_TERM term_result = py_to_term(env, py_result); - Py_DECREF(py_result); - result = enif_make_tuple2(env, ATOM_OK, term_result); - } - - call_cleanup: - PyGILState_Release(gstate); - enif_free(module_name); - enif_free(func_name); - - } else if (state->request_type == PY_REQ_EVAL) { - /* Replay a py:eval */ - char *code = enif_alloc(state->orig_code.size + 1); - if (code == NULL) { - tl_current_suspended = NULL; - return make_error(env, "alloc_failed"); - } - memcpy(code, state->orig_code.data, state->orig_code.size); - code[state->orig_code.size] = '\0'; - - PyGILState_STATE gstate = PyGILState_Ensure(); - - /* Update locals if provided */ - if (enif_is_map(state->orig_env, state->orig_locals)) { - PyObject *new_locals = term_to_py(state->orig_env, state->orig_locals); - if (new_locals != NULL && PyDict_Check(new_locals)) { - PyDict_Update(state->worker->locals, new_locals); - Py_DECREF(new_locals); - } - } - - /* Compile and evaluate */ - PyObject *compiled = Py_CompileString(code, "", Py_eval_input); - - if (compiled == NULL) { - result = make_py_error(env); - } else { - PyObject *py_result = PyEval_EvalCode(compiled, state->worker->globals, - state->worker->locals); - Py_DECREF(compiled); - - if (py_result == NULL) { - if (tl_pending_callback) { - /* - * Flag-based callback detection during eval replay. - * Check flag FIRST, not exception type - this works even if - * Python code caught and re-raised the exception. - */ - PyErr_Clear(); /* Clear whatever exception is set */ - - /* Build exc_args tuple from thread-local storage */ - PyObject *exc_args = build_pending_callback_exc_args(); - if (exc_args == NULL) { - result = make_error(env, "build_exc_args_failed"); - } else { - suspended_state_t *new_suspended = create_suspended_state_from_existing(env, exc_args, state); - Py_DECREF(exc_args); - if (new_suspended == NULL) { - tl_pending_callback = false; - Py_CLEAR(tl_pending_args); - result = make_error(env, "create_nested_suspended_state_failed"); - } else { - result = build_suspended_result(env, new_suspended); - } - } - } else { - result = make_py_error(env); - } - } else { - ERL_NIF_TERM term_result = py_to_term(env, py_result); - Py_DECREF(py_result); - result = enif_make_tuple2(env, ATOM_OK, term_result); - } - } - - PyGILState_Release(gstate); - enif_free(code); - - } else { - result = make_error(env, "unsupported_request_type"); - } - - /* Clear thread-local state */ - tl_current_worker = NULL; - tl_callback_env = NULL; - tl_current_suspended = NULL; - tl_allow_suspension = false; - - return result; -} - /* ============================================================================ * NIF functions for callback name registration * ============================================================================ */ diff --git a/c_src/py_event_loop.c b/c_src/py_event_loop.c index 198b048f..91b37ca3 100644 --- a/c_src/py_event_loop.c +++ b/c_src/py_event_loop.c @@ -4838,21 +4838,7 @@ ERL_NIF_TERM nif_start_writer(ErlNifEnv *env, int argc, } /* Legacy aliases for backward compatibility */ -ERL_NIF_TERM nif_cancel_reader(ErlNifEnv *env, int argc, - const ERL_NIF_TERM argv[]) { - /* cancel_reader(Loop, FdRef) -> stop_reader(FdRef) */ - (void)argc; - ERL_NIF_TERM new_argv[1] = {argv[1]}; /* Skip Loop arg */ - return nif_stop_reader(env, 1, new_argv); -} -ERL_NIF_TERM nif_cancel_writer(ErlNifEnv *env, int argc, - const ERL_NIF_TERM argv[]) { - /* cancel_writer(Loop, FdRef) -> stop_writer(FdRef) */ - (void)argc; - ERL_NIF_TERM new_argv[1] = {argv[1]}; /* Skip Loop arg */ - return nif_stop_writer(env, 1, new_argv); -} /** * close_fd(FdRef) -> ok diff --git a/c_src/py_event_loop.h b/c_src/py_event_loop.h index 2485e5b2..e9e9413c 100644 --- a/c_src/py_event_loop.h +++ b/c_src/py_event_loop.h @@ -922,21 +922,7 @@ ERL_NIF_TERM nif_stop_writer(ErlNifEnv *env, int argc, ERL_NIF_TERM nif_start_writer(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]); -/** - * @brief Cancel read monitoring (legacy alias for stop_reader) - * - * NIF: cancel_reader(LoopRef, FdRef) -> ok | {error, Reason} - */ -ERL_NIF_TERM nif_cancel_reader(ErlNifEnv *env, int argc, - const ERL_NIF_TERM argv[]); -/** - * @brief Cancel write monitoring (legacy alias for stop_writer) - * - * NIF: cancel_writer(LoopRef, FdRef) -> ok | {error, Reason} - */ -ERL_NIF_TERM nif_cancel_writer(ErlNifEnv *env, int argc, - const ERL_NIF_TERM argv[]); /** * @brief Explicitly close an FD with proper lifecycle cleanup diff --git a/c_src/py_exec.c b/c_src/py_exec.c index 8672664c..7430c0d5 100644 --- a/c_src/py_exec.c +++ b/c_src/py_exec.c @@ -65,87 +65,6 @@ * @note This file is included from py_nif.c (single compilation unit) */ -/* ============================================================================ - * Timeout Support - * - * Python execution timeout is implemented using PyEval_SetTrace(), which - * installs a callback invoked at each Python instruction. This allows - * cooperative timeout checking without requiring signal handlers. - * ============================================================================ */ - -/** - * @brief Trace callback for timeout checking - * - * Called by Python at each line/call/return event when tracing is enabled. - * Checks if the deadline has passed and raises TimeoutError if so. - * - * @param obj Trace function argument (unused) - * @param frame Current execution frame (unused) - * @param what Event type (call/line/return/exception) - * @param arg Event-specific argument (unused) - * - * @return 0 to continue, -1 to abort with exception - * - * @note Called with GIL held - * @note Uses thread-local storage for deadline - */ -static int python_trace_callback(PyObject *obj, PyFrameObject *frame, int what, PyObject *arg) { - (void)obj; - (void)frame; - (void)what; - (void)arg; - if (tl_timeout_enabled && tl_timeout_deadline > 0) { - if (get_monotonic_ns() > tl_timeout_deadline) { - PyErr_SetString(PyExc_TimeoutError, "execution timeout"); - return -1; /* Abort execution */ - } - } - return 0; -} - -/** - * @brief Enable timeout monitoring for Python execution - * - * Installs a trace callback that checks elapsed time against a deadline. - * If the deadline is exceeded, TimeoutError is raised in Python. - * - * @param timeout_ms Timeout in milliseconds (0 = no timeout) - * - * @par Implementation - * - * Uses thread-local storage to avoid global state: - * - `tl_timeout_deadline`: Absolute deadline (monotonic ns) - * - `tl_timeout_enabled`: Flag to enable checking - * - * @note Must be paired with stop_timeout() - * @note GIL must be held - * - * @see stop_timeout() - * @see python_trace_callback() - */ -static void start_timeout(unsigned long timeout_ms) { - if (timeout_ms > 0) { - tl_timeout_deadline = get_monotonic_ns() + (timeout_ms * 1000000ULL); - tl_timeout_enabled = true; - PyEval_SetTrace(python_trace_callback, NULL); - } -} - -static void stop_timeout(void) { - if (tl_timeout_enabled) { - tl_timeout_enabled = false; - tl_timeout_deadline = 0; - PyEval_SetTrace(NULL, NULL); - } -} - -static bool check_timeout_error(void) { - if (PyErr_Occurred() && PyErr_ExceptionMatches(PyExc_TimeoutError)) { - return true; - } - return false; -} - /* ============================================================================ * Execution mode detection * ============================================================================ */ @@ -158,715 +77,3 @@ static void detect_execution_mode(void) { #endif } -/* ============================================================================ - * Request processing - * ============================================================================ */ - -/** - * Initialize a request structure. - */ -static void request_init(py_request_t *req) { - memset(req, 0, sizeof(py_request_t)); - pthread_mutex_init(&req->mutex, NULL); - pthread_cond_init(&req->cond, NULL); - req->completed = false; -} - -/** - * Clean up a request structure. - */ -static void request_cleanup(py_request_t *req) { - pthread_mutex_destroy(&req->mutex); - pthread_cond_destroy(&req->cond); -} - -/** - * Process a single request in the executor thread (GIL held). - */ -static void process_request(py_request_t *req) { - ErlNifEnv *env = req->env; - py_worker_t *worker = req->worker; - py_context_t *context = req->context; - - /* Extract globals/locals from context or worker */ - PyObject *globals = context ? context->globals : (worker ? worker->globals : NULL); - PyObject *locals = context ? context->locals : (worker ? worker->locals : NULL); - - switch (req->type) { - case PY_REQ_CALL: { - /* Set thread-local worker/context for callbacks */ - tl_current_worker = worker; - tl_current_context = context; - tl_callback_env = env; - tl_allow_suspension = false; /* Blocking mode - code runs once, no replay */ - - char *module_name = binary_to_string(&req->module_bin); - char *func_name = binary_to_string(&req->func_bin); - if (module_name == NULL || func_name == NULL) { - enif_free(module_name); - enif_free(func_name); - req->result = make_error(env, "alloc_failed"); - break; - } - - PyObject *func = NULL; - - /* Special handling for __main__ - look in globals/locals namespace first */ - if (strcmp(module_name, "__main__") == 0) { - func = PyDict_GetItemString(locals, func_name); - if (func == NULL) { - func = PyDict_GetItemString(globals, func_name); - } - if (func != NULL) { - Py_INCREF(func); - } - /* If not found in namespace, fall through to module import below */ - } - - if (func == NULL) { - /* Import module and get attribute */ - PyObject *module = PyImport_ImportModule(module_name); - if (module == NULL) { - req->result = make_py_error(env); - goto call_cleanup; - } - func = PyObject_GetAttrString(module, func_name); - Py_DECREF(module); - } - - if (func == NULL) { - req->result = make_py_error(env); - goto call_cleanup; - } - - /* Convert args list to Python tuple */ - unsigned int args_len; - if (!enif_get_list_length(env, req->args_term, &args_len)) { - Py_DECREF(func); - req->result = make_error(env, "invalid_args"); - goto call_cleanup; - } - - PyObject *args = PyTuple_New(args_len); - if (args == NULL) { - Py_DECREF(func); - req->result = make_error(env, "alloc_failed"); - goto call_cleanup; - } - ERL_NIF_TERM head, tail = req->args_term; - for (unsigned int i = 0; i < args_len; i++) { - enif_get_list_cell(env, tail, &head, &tail); - PyObject *arg = term_to_py(env, head); - if (arg == NULL) { - Py_DECREF(args); - Py_DECREF(func); - req->result = make_error(env, "arg_conversion_failed"); - goto call_cleanup; - } - PyTuple_SET_ITEM(args, i, arg); - } - - /* Convert kwargs map to Python dict */ - PyObject *kwargs = NULL; - if (enif_is_map(env, req->kwargs_term)) { - kwargs = term_to_py(env, req->kwargs_term); - } - - /* Start timeout if specified */ - start_timeout(req->timeout_ms); - - /* Call the function */ - PyObject *py_result = PyObject_Call(func, args, kwargs); - - /* Stop timeout */ - stop_timeout(); - - Py_DECREF(func); - Py_DECREF(args); - Py_XDECREF(kwargs); - - if (py_result == NULL) { - if (check_timeout_error()) { - PyErr_Clear(); - req->result = enif_make_tuple2(env, ATOM_ERROR, ATOM_TIMEOUT); - } else if (tl_pending_callback) { - /* - * Flag-based callback detection: check flag FIRST, not exception type. - * This works even if Python code caught and re-raised the exception. - */ - PyErr_Clear(); /* Clear whatever exception is set */ - - /* Build exc_args tuple from thread-local storage */ - PyObject *exc_args = build_pending_callback_exc_args(); - if (exc_args == NULL) { - req->result = make_error(env, "build_exc_args_failed"); - } else { - suspended_state_t *suspended = create_suspended_state(env, exc_args, req); - Py_DECREF(exc_args); - if (suspended == NULL) { - clear_pending_callback_tls(); - req->result = make_error(env, "create_suspended_state_failed"); - } else { - req->result = build_suspended_result(env, suspended); - /* func_name/args are copied into the suspended state; clear - * the pending-callback TLS so a later request on this reused - * worker thread doesn't trip the stale-TLS entry invariant. */ - clear_pending_callback_tls(); - } - } - } else { - req->result = make_py_error(env); - } - } else if (PyGen_Check(py_result) || PyIter_Check(py_result)) { - py_object_t *wrapper = enif_alloc_resource(PYOBJ_RESOURCE_TYPE, sizeof(py_object_t)); - if (wrapper == NULL) { - Py_DECREF(py_result); - req->result = make_error(env, "alloc_failed"); - } else { - wrapper->obj = py_result; - wrapper->interp_id = 0; /* Main interpreter */ - atomic_fetch_add(&g_counters.pyobj_created, 1); - ERL_NIF_TERM gen_ref = enif_make_resource(env, wrapper); - enif_release_resource(wrapper); - req->result = enif_make_tuple2(env, ATOM_OK, - enif_make_tuple2(env, ATOM_GENERATOR, gen_ref)); - } - } else if (is_inline_schedule_marker(py_result)) { - /* Inline schedule marker not supported in legacy worker NIFs. - * Note: py:call() uses the context API (nif_context_call), which - * does support schedule_inline. This code path is only hit by - * direct py_nif:worker_call usage, which is rare. */ - Py_DECREF(py_result); - req->result = make_error(env, "schedule_inline_not_supported_in_worker_mode"); - } else if (is_schedule_marker(py_result)) { - /* Schedule marker: release dirty scheduler, continue via callback */ - ScheduleMarkerObject *marker = (ScheduleMarkerObject *)py_result; - ERL_NIF_TERM callback_name = py_to_term(env, marker->callback_name); - ERL_NIF_TERM callback_args = py_to_term(env, marker->args); - Py_DECREF(py_result); - req->result = enif_make_tuple3(env, ATOM_SCHEDULE, callback_name, callback_args); - } else { - ERL_NIF_TERM term_result = py_to_term(env, py_result); - Py_DECREF(py_result); - req->result = enif_make_tuple2(env, ATOM_OK, term_result); - } - - call_cleanup: - tl_current_worker = NULL; - tl_current_context = NULL; - tl_callback_env = NULL; - tl_allow_suspension = false; - enif_free(module_name); - enif_free(func_name); - break; - } - - case PY_REQ_EVAL: { - tl_current_worker = worker; - tl_current_context = context; - tl_callback_env = env; - tl_allow_suspension = true; /* Allow suspension - we replay on resume */ - - char *code = binary_to_string(&req->code_bin); - if (code == NULL) { - req->result = make_error(env, "alloc_failed"); - break; - } - - /* Update locals if provided */ - if (enif_is_map(env, req->locals_term)) { - PyObject *new_locals = term_to_py(env, req->locals_term); - if (new_locals != NULL && PyDict_Check(new_locals)) { - PyDict_Update(locals, new_locals); - Py_DECREF(new_locals); - } - } - - /* Start timeout if specified */ - start_timeout(req->timeout_ms); - - /* Compile and evaluate */ - PyObject *compiled = Py_CompileString(code, "", Py_eval_input); - - if (compiled == NULL) { - stop_timeout(); - req->result = make_py_error(env); - } else { - PyObject *py_result = PyEval_EvalCode(compiled, globals, locals); - Py_DECREF(compiled); - stop_timeout(); - - if (py_result == NULL) { - if (check_timeout_error()) { - PyErr_Clear(); - req->result = enif_make_tuple2(env, ATOM_ERROR, ATOM_TIMEOUT); - } else if (tl_pending_callback) { - /* Flag-based callback detection for eval */ - PyErr_Clear(); - - PyObject *exc_args = build_pending_callback_exc_args(); - if (exc_args == NULL) { - req->result = make_error(env, "build_exc_args_failed"); - } else { - suspended_state_t *suspended = create_suspended_state(env, exc_args, req); - Py_DECREF(exc_args); - if (suspended == NULL) { - clear_pending_callback_tls(); - req->result = make_error(env, "create_suspended_state_failed"); - } else { - req->result = build_suspended_result(env, suspended); - clear_pending_callback_tls(); - } - } - } else { - req->result = make_py_error(env); - } - } else if (PyGen_Check(py_result) || PyIter_Check(py_result)) { - py_object_t *wrapper = enif_alloc_resource(PYOBJ_RESOURCE_TYPE, sizeof(py_object_t)); - if (wrapper == NULL) { - Py_DECREF(py_result); - req->result = make_error(env, "alloc_failed"); - } else { - wrapper->obj = py_result; - wrapper->interp_id = 0; /* Main interpreter */ - atomic_fetch_add(&g_counters.pyobj_created, 1); - ERL_NIF_TERM gen_ref = enif_make_resource(env, wrapper); - enif_release_resource(wrapper); - req->result = enif_make_tuple2(env, ATOM_OK, - enif_make_tuple2(env, ATOM_GENERATOR, gen_ref)); - } - } else if (is_inline_schedule_marker(py_result)) { - /* Inline schedule marker not supported in legacy worker NIFs. - * Note: py:call() uses the context API, which supports schedule_inline. */ - Py_DECREF(py_result); - req->result = make_error(env, "schedule_inline_not_supported_in_worker_mode"); - } else if (is_schedule_marker(py_result)) { - /* Schedule marker: release dirty scheduler, continue via callback */ - ScheduleMarkerObject *marker = (ScheduleMarkerObject *)py_result; - ERL_NIF_TERM callback_name = py_to_term(env, marker->callback_name); - ERL_NIF_TERM callback_args = py_to_term(env, marker->args); - Py_DECREF(py_result); - req->result = enif_make_tuple3(env, ATOM_SCHEDULE, callback_name, callback_args); - } else { - ERL_NIF_TERM term_result = py_to_term(env, py_result); - Py_DECREF(py_result); - req->result = enif_make_tuple2(env, ATOM_OK, term_result); - } - } - - tl_current_worker = NULL; - tl_current_context = NULL; - tl_callback_env = NULL; - tl_allow_suspension = false; - enif_free(code); - break; - } - - case PY_REQ_EXEC: { - tl_current_worker = worker; - tl_current_context = context; - tl_callback_env = env; - /* Note: tl_allow_suspension stays false for exec - suspension not allowed */ - - char *code = binary_to_string(&req->code_bin); - if (code == NULL) { - req->result = make_error(env, "alloc_failed"); - break; - } - - PyObject *compiled = Py_CompileString(code, "", Py_file_input); - - if (compiled == NULL) { - req->result = make_py_error(env); - } else { - /* Use globals for both to ensure imports are visible to defined functions. - * When using separate dicts, imports go to locals but function closures - * only see globals, causing "name X is not defined" errors. */ - PyObject *py_result = PyEval_EvalCode(compiled, globals, globals); - Py_DECREF(compiled); - - if (py_result == NULL) { - req->result = make_py_error(env); - } else { - Py_DECREF(py_result); - req->result = ATOM_OK; - } - } - - tl_current_worker = NULL; - tl_current_context = NULL; - tl_callback_env = NULL; - enif_free(code); - break; - } - - case PY_REQ_NEXT: { - PyObject *item = PyIter_Next(req->gen_wrapper->obj); - - if (item == NULL) { - if (PyErr_Occurred()) { - if (PyErr_ExceptionMatches(PyExc_StopIteration)) { - PyErr_Clear(); - req->result = enif_make_tuple2(env, ATOM_ERROR, ATOM_STOP_ITERATION); - } else { - req->result = make_py_error(env); - } - } else { - req->result = enif_make_tuple2(env, ATOM_ERROR, ATOM_STOP_ITERATION); - } - } else if (PyGen_Check(item) || PyIter_Check(item)) { - py_object_t *wrapper = enif_alloc_resource(PYOBJ_RESOURCE_TYPE, sizeof(py_object_t)); - if (wrapper == NULL) { - Py_DECREF(item); - req->result = make_error(env, "alloc_failed"); - } else { - wrapper->obj = item; - wrapper->interp_id = 0; /* Main interpreter */ - atomic_fetch_add(&g_counters.pyobj_created, 1); - ERL_NIF_TERM gen_ref = enif_make_resource(env, wrapper); - enif_release_resource(wrapper); - req->result = enif_make_tuple2(env, ATOM_OK, - enif_make_tuple2(env, ATOM_GENERATOR, gen_ref)); - } - } else { - ERL_NIF_TERM term_result = py_to_term(env, item); - Py_DECREF(item); - req->result = enif_make_tuple2(env, ATOM_OK, term_result); - } - break; - } - - case PY_REQ_IMPORT: { - char *module_name = binary_to_string(&req->module_bin); - if (module_name == NULL) { - req->result = make_error(env, "alloc_failed"); - break; - } - - PyObject *module = PyImport_ImportModule(module_name); - enif_free(module_name); - - if (module == NULL) { - req->result = make_py_error(env); - } else { - py_object_t *wrapper = enif_alloc_resource(PYOBJ_RESOURCE_TYPE, sizeof(py_object_t)); - if (wrapper == NULL) { - Py_DECREF(module); - req->result = make_error(env, "alloc_failed"); - } else { - wrapper->obj = module; - wrapper->interp_id = 0; /* Main interpreter */ - atomic_fetch_add(&g_counters.pyobj_created, 1); - ERL_NIF_TERM mod_ref = enif_make_resource(env, wrapper); - enif_release_resource(wrapper); - req->result = enif_make_tuple2(env, ATOM_OK, mod_ref); - } - } - break; - } - - case PY_REQ_GETATTR: { - char *attr_name = binary_to_string(&req->attr_bin); - if (attr_name == NULL) { - req->result = make_error(env, "alloc_failed"); - break; - } - - PyObject *attr = PyObject_GetAttrString(req->obj_wrapper->obj, attr_name); - enif_free(attr_name); - - if (attr == NULL) { - req->result = make_py_error(env); - } else { - ERL_NIF_TERM term_result = py_to_term(env, attr); - Py_DECREF(attr); - req->result = enif_make_tuple2(env, ATOM_OK, term_result); - } - break; - } - - case PY_REQ_MEMORY_STATS: { - /* Import gc module */ - PyObject *gc_module = PyImport_ImportModule("gc"); - if (gc_module == NULL) { - req->result = make_error(env, "gc_import_failed"); - break; - } - - ERL_NIF_TERM result_map = enif_make_new_map(env); - - PyObject *stats = PyObject_CallMethod(gc_module, "get_stats", NULL); - if (stats != NULL && PyList_Check(stats)) { - Py_ssize_t num_gens = PyList_Size(stats); - if (num_gens > 0) { - ERL_NIF_TERM *gen_stats = enif_alloc(sizeof(ERL_NIF_TERM) * num_gens); - if (gen_stats != NULL) { - for (Py_ssize_t i = 0; i < num_gens; i++) { - PyObject *gen = PyList_GetItem(stats, i); - gen_stats[i] = py_to_term(env, gen); - } - ERL_NIF_TERM gc_stats_list = enif_make_list_from_array(env, gen_stats, num_gens); - enif_free(gen_stats); - enif_make_map_put(env, result_map, - enif_make_atom(env, "gc_stats"), gc_stats_list, &result_map); - } - /* If gen_stats alloc failed, we skip gc_stats but continue with other stats */ - } - Py_DECREF(stats); - } - - PyObject *counts = PyObject_CallMethod(gc_module, "get_count", NULL); - if (counts != NULL && PyTuple_Check(counts)) { - ERL_NIF_TERM count_term = py_to_term(env, counts); - enif_make_map_put(env, result_map, - enif_make_atom(env, "gc_count"), count_term, &result_map); - Py_DECREF(counts); - } - - PyObject *threshold = PyObject_CallMethod(gc_module, "get_threshold", NULL); - if (threshold != NULL && PyTuple_Check(threshold)) { - ERL_NIF_TERM threshold_term = py_to_term(env, threshold); - enif_make_map_put(env, result_map, - enif_make_atom(env, "gc_threshold"), threshold_term, &result_map); - Py_DECREF(threshold); - } - - Py_DECREF(gc_module); - - /* Try to get tracemalloc stats if available */ - PyObject *tracemalloc = PyImport_ImportModule("tracemalloc"); - if (tracemalloc != NULL) { - PyObject *is_tracing = PyObject_CallMethod(tracemalloc, "is_tracing", NULL); - if (is_tracing != NULL && PyObject_IsTrue(is_tracing)) { - PyObject *current_traced = PyObject_CallMethod(tracemalloc, "get_traced_memory", NULL); - if (current_traced != NULL && PyTuple_Check(current_traced)) { - ERL_NIF_TERM current = py_to_term(env, PyTuple_GetItem(current_traced, 0)); - ERL_NIF_TERM peak = py_to_term(env, PyTuple_GetItem(current_traced, 1)); - enif_make_map_put(env, result_map, - enif_make_atom(env, "traced_memory_current"), current, &result_map); - enif_make_map_put(env, result_map, - enif_make_atom(env, "traced_memory_peak"), peak, &result_map); - Py_DECREF(current_traced); - } - } - Py_XDECREF(is_tracing); - Py_DECREF(tracemalloc); - } - PyErr_Clear(); - - req->result = enif_make_tuple2(env, ATOM_OK, result_map); - break; - } - - case PY_REQ_GC: { - PyObject *gc_module = PyImport_ImportModule("gc"); - if (gc_module == NULL) { - req->result = make_error(env, "gc_import_failed"); - break; - } - - PyObject *result = PyObject_CallMethod(gc_module, "collect", "i", req->gc_generation); - Py_DECREF(gc_module); - - if (result == NULL) { - req->result = make_py_error(env); - } else { - long collected = PyLong_AsLong(result); - Py_DECREF(result); - req->result = enif_make_tuple2(env, ATOM_OK, enif_make_long(env, collected)); - } - break; - } - - case PY_REQ_SHUTDOWN: - /* Signal to exit the loop - nothing to do here */ - break; - } -} - -/* ============================================================================ - * Single executor thread implementation - * ============================================================================ */ - -/** - * Main function for the executor thread. - * Acquires GIL and processes requests until shutdown. - */ -static void *executor_thread_main(void *arg) { - (void)arg; - - /* Acquire GIL for this thread */ - PyGILState_STATE gstate = PyGILState_Ensure(); - - atomic_store(&g_executor_running, true); - - /* - * Main processing loop. - * We continue processing until we receive a PY_REQ_SHUTDOWN request. - * The shutdown flag is used to stop waiting when the queue is empty. - */ - bool should_exit = false; - while (!should_exit) { - py_request_t *req = NULL; - - /* Release GIL while waiting for work (like PyO3 allow_threads) */ - Py_BEGIN_ALLOW_THREADS - - pthread_mutex_lock(&g_executor_mutex); - while (g_executor_queue_head == NULL && !atomic_load(&g_executor_shutdown)) { - pthread_cond_wait(&g_executor_cond, &g_executor_mutex); - } - - /* Dequeue request if available */ - if (g_executor_queue_head != NULL) { - req = g_executor_queue_head; - g_executor_queue_head = req->next; - if (g_executor_queue_head == NULL) { - g_executor_queue_tail = NULL; - } - req->next = NULL; - } else if (atomic_load(&g_executor_shutdown)) { - /* Queue is empty and shutdown requested - exit */ - should_exit = true; - } - pthread_mutex_unlock(&g_executor_mutex); - - Py_END_ALLOW_THREADS - - if (req != NULL) { - if (req->type == PY_REQ_SHUTDOWN) { - /* Signal completion and exit */ - pthread_mutex_lock(&req->mutex); - req->completed = true; - pthread_cond_signal(&req->cond); - pthread_mutex_unlock(&req->mutex); - should_exit = true; - } else { - /* Process the request with GIL held */ - process_request(req); - - /* Track completed requests */ - atomic_fetch_add(&g_counters.complete_count, 1); - - /* Signal completion */ - pthread_mutex_lock(&req->mutex); - req->completed = true; - pthread_cond_signal(&req->cond); - pthread_mutex_unlock(&req->mutex); - } - } - } - - atomic_store(&g_executor_running, false); - PyGILState_Release(gstate); - - return NULL; -} - -/** - * Enqueue a request to the appropriate executor based on execution mode. - * Routes to multi-executor pool, single executor, or executes directly. - * - * @return 0 on success, -1 if shutting down (request rejected) - */ -static int executor_enqueue(py_request_t *req) { - /* Reject work if runtime is shutting down (except shutdown requests) */ - if (runtime_is_shutting_down() && req->type != PY_REQ_SHUTDOWN) { - atomic_fetch_add(&g_counters.rejected_count, 1); - return -1; - } - - /* Track enqueued requests */ - atomic_fetch_add(&g_counters.enqueue_count, 1); - -#ifdef HAVE_FREE_THREADED - if (g_execution_mode == PY_MODE_FREE_THREADED) { - /* Execute directly in free-threaded mode - no executor needed */ - PyGILState_STATE gstate = PyGILState_Ensure(); - process_request(req); - PyGILState_Release(gstate); - /* Signal completion immediately */ - pthread_mutex_lock(&req->mutex); - req->completed = true; - pthread_cond_signal(&req->cond); - pthread_mutex_unlock(&req->mutex); - return 0; - } -#endif - - /* Single coordinator executor queue */ - pthread_mutex_lock(&g_executor_mutex); - req->next = NULL; - if (g_executor_queue_tail == NULL) { - g_executor_queue_head = req; - g_executor_queue_tail = req; - } else { - g_executor_queue_tail->next = req; - g_executor_queue_tail = req; - } - pthread_cond_signal(&g_executor_cond); - pthread_mutex_unlock(&g_executor_mutex); - return 0; -} - -/** - * Wait for a request to complete. - */ -static void executor_wait(py_request_t *req) { - pthread_mutex_lock(&req->mutex); - while (!req->completed) { - pthread_cond_wait(&req->cond, &req->mutex); - } - pthread_mutex_unlock(&req->mutex); -} - -/** - * Start the executor thread. - * Called during Python initialization. - */ -static int executor_start(void) { - atomic_store(&g_executor_shutdown, false); - g_executor_queue_head = NULL; - g_executor_queue_tail = NULL; - - if (pthread_create(&g_executor_thread, NULL, executor_thread_main, NULL) != 0) { - return -1; - } - - /* Wait for executor to be ready */ - int max_wait = 100; /* 1 second max */ - while (!atomic_load(&g_executor_running) && max_wait-- > 0) { - usleep(10000); /* 10ms */ - } - - return atomic_load(&g_executor_running) ? 0 : -1; -} - -/** - * Stop the executor thread. - * Called during Python finalization. - */ -static void executor_stop(void) { - if (!atomic_load(&g_executor_running)) { - return; - } - - /* Send shutdown request */ - py_request_t shutdown_req; - request_init(&shutdown_req); - shutdown_req.type = PY_REQ_SHUTDOWN; - - atomic_store(&g_executor_shutdown, true); - executor_enqueue(&shutdown_req); - executor_wait(&shutdown_req); - request_cleanup(&shutdown_req); - - /* Wait for thread to finish */ - pthread_join(g_executor_thread, NULL); -} - -/* - * Note: Free-threaded execution (Python 3.13+ nogil) is handled inline - * in executor_enqueue() using PyGILState_Ensure/Release which are no-ops - * in free-threaded builds but still work correctly. - */ diff --git a/c_src/py_nif.c b/c_src/py_nif.c index 83f83df1..14c3b628 100644 --- a/c_src/py_nif.c +++ b/c_src/py_nif.c @@ -53,10 +53,8 @@ * Global state definitions * ============================================================================ */ -ErlNifResourceType *WORKER_RESOURCE_TYPE = NULL; ErlNifResourceType *PYOBJ_RESOURCE_TYPE = NULL; /* ASYNC_WORKER_RESOURCE_TYPE removed - async workers replaced by event loop model */ -ErlNifResourceType *SUSPENDED_STATE_RESOURCE_TYPE = NULL; /* Process-per-context resource type (no mutex) */ ErlNifResourceType *PY_CONTEXT_RESOURCE_TYPE = NULL; @@ -147,13 +145,6 @@ PyThreadState *g_main_thread_state = NULL; py_execution_mode_t g_execution_mode = PY_MODE_GIL; /* Single executor state */ -pthread_t g_executor_thread; -pthread_mutex_t g_executor_mutex = PTHREAD_MUTEX_INITIALIZER; -pthread_cond_t g_executor_cond = PTHREAD_COND_INITIALIZER; -py_request_t *g_executor_queue_head = NULL; -py_request_t *g_executor_queue_tail = NULL; -_Atomic bool g_executor_running = false; -_Atomic bool g_executor_shutdown = false; /* Global counter for callback IDs */ _Atomic uint64_t g_callback_id_counter = 1; @@ -168,10 +159,8 @@ PyObject *ProcessErrorException = NULL; PyObject *g_numpy_ndarray_type = NULL; /* Thread-local callback context */ -__thread py_worker_t *tl_current_worker = NULL; __thread py_context_t *tl_current_context = NULL; __thread ErlNifEnv *tl_callback_env = NULL; -__thread suspended_state_t *tl_current_suspended = NULL; __thread suspended_context_state_t *tl_current_context_suspended = NULL; __thread bool tl_allow_suspension = false; @@ -240,8 +229,6 @@ ERL_NIF_TERM ATOM_SPAN_EVENT; * ============================================================================ */ /* From py_callback.c - needed by py_exec.c */ -static PyObject *build_pending_callback_exc_args(void); -static ERL_NIF_TERM build_suspended_result(ErlNifEnv *env, suspended_state_t *suspended); /* Schedule marker type and helper - from py_callback.c, needed by py_exec.c */ typedef struct { @@ -276,8 +263,6 @@ static int is_inline_schedule_marker(PyObject *obj); #include "py_callback.c" #include "py_thread_worker.c" #include "py_event_loop.c" -#include "py_worker_pool.h" -#include "py_worker_pool.c" #include "py_subinterp_thread.c" #include "py_reactor_buffer.c" #include "py_channel.c" @@ -287,23 +272,6 @@ static int is_inline_schedule_marker(PyObject *obj); * Resource callbacks * ============================================================================ */ -static void worker_destructor(ErlNifEnv *env, void *obj) { - (void)env; - py_worker_t *worker = (py_worker_t *)obj; - - /* Close callback pipes */ - close_pipe_pair(worker->callback_pipe); - - /* Only clean up Python state if Python is still initialized */ - if (worker->thread_state != NULL && runtime_is_running()) { - PyEval_RestoreThread(worker->thread_state); - Py_XDECREF(worker->globals); - Py_XDECREF(worker->locals); - PyThreadState_Clear(worker->thread_state); - PyThreadState_DeleteCurrent(); - } -} - static void pyobj_destructor(ErlNifEnv *env, void *obj) { (void)env; py_object_t *wrapper = (py_object_t *)obj; @@ -524,53 +492,6 @@ static void suspended_context_state_destructor(ErlNifEnv *env, void *obj) { atomic_fetch_add(&g_counters.suspended_destroyed, 1); } -static void suspended_state_destructor(ErlNifEnv *env, void *obj) { - (void)env; - suspended_state_t *state = (suspended_state_t *)obj; - - /* Release the worker resource kept alive in create_suspended_state_ex. */ - if (state->worker != NULL) { - enif_release_resource(state->worker); - state->worker = NULL; - } - - /* Clean up Python objects if Python is still initialized. - * suspended_state_t is used with the worker-based API which runs in - * the main interpreter, so we always use PyGILState_Ensure. */ - if (runtime_is_running() && state->callback_args != NULL) { - if (PyGILState_GetThisThreadState() != NULL || PyGILState_Check()) { - Py_XDECREF(state->callback_args); - state->callback_args = NULL; - } else { - PyGILState_STATE gstate = PyGILState_Ensure(); - Py_XDECREF(state->callback_args); - state->callback_args = NULL; - PyGILState_Release(gstate); - } - } - - /* Free allocated memory */ - if (state->callback_func_name != NULL) { - enif_free(state->callback_func_name); - state->callback_func_name = NULL; - } - if (state->result_data != NULL) { - enif_free(state->result_data); - state->result_data = NULL; - } - - /* Free original context environment */ - if (state->orig_env != NULL) { - enif_free_env(state->orig_env); - state->orig_env = NULL; - } - - /* Destroy synchronization primitives */ - pthread_mutex_destroy(&state->mutex); - pthread_cond_destroy(&state->cond); - - atomic_fetch_add(&g_counters.suspended_destroyed, 1); -} /* ============================================================================ * Inline Continuation Support @@ -1142,22 +1063,6 @@ static ERL_NIF_TERM nif_py_init(ErlNifEnv *env, int argc, const ERL_NIF_TERM arg /* Save main thread state and release GIL for other threads */ g_main_thread_state = PyEval_SaveThread(); - /* Start single executor for coordinator operations. - * Context operations use per-context worker threads (see worker_context_init). - * The single executor handles legacy worker API and coordinator tasks. */ - int executor_result = 0; - if (g_execution_mode != PY_MODE_FREE_THREADED) { - executor_result = executor_start(); - } - - if (executor_result < 0) { - PyEval_RestoreThread(g_main_thread_state); - g_main_thread_state = NULL; - Py_Finalize(); - atomic_store(&g_runtime_state, PY_STATE_STOPPED); - return make_error(env, "executor_start_failed"); - } - /* Initialize thread worker system for ThreadPoolExecutor support */ if (thread_worker_init() < 0) { /* Non-fatal - thread worker support just won't be available */ @@ -1195,11 +1100,6 @@ static ERL_NIF_TERM nif_finalize(ErlNifEnv *env, int argc, const ERL_NIF_TERM ar * 3. Then clean up caches with GIL (no active work at this point) */ - /* Step 1: Stop executor - it will finish in-flight requests and exit */ - if (g_execution_mode != PY_MODE_FREE_THREADED) { - executor_stop(); - } - /* Step 2: Clean up thread worker system */ thread_worker_cleanup(); @@ -1246,280 +1146,6 @@ static ERL_NIF_TERM nif_finalize(ErlNifEnv *env, int argc, const ERL_NIF_TERM ar return ATOM_OK; } -/* ============================================================================ - * Worker management - * ============================================================================ */ - -static ERL_NIF_TERM nif_worker_new(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { - (void)argc; - (void)argv; - - if (!runtime_is_running()) { - return make_error(env, "python_not_running"); - } - - py_worker_t *worker = enif_alloc_resource(WORKER_RESOURCE_TYPE, sizeof(py_worker_t)); - if (worker == NULL) { - return make_error(env, "alloc_failed"); - } - - /* Acquire GIL to create thread state */ - PyGILState_STATE gstate = PyGILState_Ensure(); - - /* Create a new thread state for this worker */ - PyInterpreterState *interp = PyInterpreterState_Get(); - worker->thread_state = PyThreadState_New(interp); - - /* Create global/local namespaces */ - worker->globals = PyDict_New(); - worker->locals = PyDict_New(); - - /* Import __builtins__ into globals */ - PyObject *builtins = PyEval_GetBuiltins(); - PyDict_SetItemString(worker->globals, "__builtins__", builtins); - - /* Import erlang module into worker's namespace for callbacks */ - PyObject *erlang_module = PyImport_ImportModule("erlang"); - if (erlang_module != NULL) { - PyDict_SetItemString(worker->globals, "erlang", erlang_module); - Py_DECREF(erlang_module); - } - - worker->owns_gil = false; - - /* Initialize callback state */ - worker->callback_pipe[0] = -1; - worker->callback_pipe[1] = -1; - worker->has_callback_handler = false; - worker->callback_env = NULL; - - PyGILState_Release(gstate); - - ERL_NIF_TERM result = enif_make_resource(env, worker); - enif_release_resource(worker); - - return enif_make_tuple2(env, ATOM_OK, result); -} - -static ERL_NIF_TERM nif_worker_destroy(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { - (void)argc; - py_worker_t *worker; - - if (!enif_get_resource(env, argv[0], WORKER_RESOURCE_TYPE, (void **)&worker)) { - return make_error(env, "invalid_worker"); - } - - /* Resource destructor will handle cleanup */ - return ATOM_OK; -} - -/* ============================================================================ - * Python execution (dirty NIFs) - * ============================================================================ */ - -static ERL_NIF_TERM nif_worker_call(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { - py_worker_t *worker; - - if (!enif_get_resource(env, argv[0], WORKER_RESOURCE_TYPE, (void **)&worker)) { - return make_error(env, "invalid_worker"); - } - - /* Build request and route to executor */ - py_request_t req; - request_init(&req); - req.type = PY_REQ_CALL; - req.worker = worker; - req.env = env; - - if (!enif_inspect_binary(env, argv[1], &req.module_bin)) { - request_cleanup(&req); - return make_error(env, "invalid_module"); - } - if (!enif_inspect_binary(env, argv[2], &req.func_bin)) { - request_cleanup(&req); - return make_error(env, "invalid_func"); - } - - req.args_term = argv[3]; - req.kwargs_term = (argc > 4) ? argv[4] : 0; - req.timeout_ms = 0; - - if (argc > 5) { - enif_get_ulong(env, argv[5], &req.timeout_ms); - } - - /* Submit to executor and wait */ - if (executor_enqueue(&req) != 0) { - request_cleanup(&req); - return make_error(env, "runtime_shutting_down"); - } - executor_wait(&req); - - ERL_NIF_TERM result = req.result; - request_cleanup(&req); - return result; -} - -static ERL_NIF_TERM nif_worker_eval(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { - py_worker_t *worker; - - if (!enif_get_resource(env, argv[0], WORKER_RESOURCE_TYPE, (void **)&worker)) { - return make_error(env, "invalid_worker"); - } - - py_request_t req; - request_init(&req); - req.type = PY_REQ_EVAL; - req.worker = worker; - req.env = env; - - if (!enif_inspect_binary(env, argv[1], &req.code_bin)) { - request_cleanup(&req); - return make_error(env, "invalid_code"); - } - - req.locals_term = (argc > 2) ? argv[2] : 0; - req.timeout_ms = 0; - if (argc > 3) { - enif_get_ulong(env, argv[3], &req.timeout_ms); - } - - if (executor_enqueue(&req) != 0) { - request_cleanup(&req); - return make_error(env, "runtime_shutting_down"); - } - executor_wait(&req); - - ERL_NIF_TERM result = req.result; - request_cleanup(&req); - return result; -} - -static ERL_NIF_TERM nif_worker_exec(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { - (void)argc; - py_worker_t *worker; - - if (!enif_get_resource(env, argv[0], WORKER_RESOURCE_TYPE, (void **)&worker)) { - return make_error(env, "invalid_worker"); - } - - py_request_t req; - request_init(&req); - req.type = PY_REQ_EXEC; - req.worker = worker; - req.env = env; - - if (!enif_inspect_binary(env, argv[1], &req.code_bin)) { - request_cleanup(&req); - return make_error(env, "invalid_code"); - } - - if (executor_enqueue(&req) != 0) { - request_cleanup(&req); - return make_error(env, "runtime_shutting_down"); - } - executor_wait(&req); - - ERL_NIF_TERM result = req.result; - request_cleanup(&req); - return result; -} - -static ERL_NIF_TERM nif_worker_next(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { - (void)argc; - py_worker_t *worker; - py_object_t *gen_wrapper; - - if (!enif_get_resource(env, argv[0], WORKER_RESOURCE_TYPE, (void **)&worker)) { - return make_error(env, "invalid_worker"); - } - if (!enif_get_resource(env, argv[1], PYOBJ_RESOURCE_TYPE, (void **)&gen_wrapper)) { - return make_error(env, "invalid_generator"); - } - - py_request_t req; - request_init(&req); - req.type = PY_REQ_NEXT; - req.worker = worker; - req.env = env; - req.gen_wrapper = gen_wrapper; - - if (executor_enqueue(&req) != 0) { - request_cleanup(&req); - return make_error(env, "runtime_shutting_down"); - } - executor_wait(&req); - - ERL_NIF_TERM result = req.result; - request_cleanup(&req); - return result; -} - -static ERL_NIF_TERM nif_import_module(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { - (void)argc; - py_worker_t *worker; - - if (!enif_get_resource(env, argv[0], WORKER_RESOURCE_TYPE, (void **)&worker)) { - return make_error(env, "invalid_worker"); - } - - py_request_t req; - request_init(&req); - req.type = PY_REQ_IMPORT; - req.worker = worker; - req.env = env; - - if (!enif_inspect_binary(env, argv[1], &req.module_bin)) { - request_cleanup(&req); - return make_error(env, "invalid_module"); - } - - if (executor_enqueue(&req) != 0) { - request_cleanup(&req); - return make_error(env, "runtime_shutting_down"); - } - executor_wait(&req); - - ERL_NIF_TERM result = req.result; - request_cleanup(&req); - return result; -} - -static ERL_NIF_TERM nif_get_attr(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { - (void)argc; - py_worker_t *worker; - py_object_t *obj_wrapper; - - if (!enif_get_resource(env, argv[0], WORKER_RESOURCE_TYPE, (void **)&worker)) { - return make_error(env, "invalid_worker"); - } - if (!enif_get_resource(env, argv[1], PYOBJ_RESOURCE_TYPE, (void **)&obj_wrapper)) { - return make_error(env, "invalid_object"); - } - - py_request_t req; - request_init(&req); - req.type = PY_REQ_GETATTR; - req.worker = worker; - req.env = env; - req.obj_wrapper = obj_wrapper; - - if (!enif_inspect_binary(env, argv[2], &req.attr_bin)) { - request_cleanup(&req); - return make_error(env, "invalid_attr"); - } - - if (executor_enqueue(&req) != 0) { - request_cleanup(&req); - return make_error(env, "runtime_shutting_down"); - } - executor_wait(&req); - - ERL_NIF_TERM result = req.result; - request_cleanup(&req); - return result; -} - /* ============================================================================ * Info NIFs * ============================================================================ */ @@ -1545,20 +1171,65 @@ static ERL_NIF_TERM nif_memory_stats(ErlNifEnv *env, int argc, const ERL_NIF_TER return make_error(env, "python_not_running"); } - py_request_t req; - request_init(&req); - req.type = PY_REQ_MEMORY_STATS; - req.env = env; - - if (executor_enqueue(&req) != 0) { - request_cleanup(&req); - return make_error(env, "runtime_shutting_down"); + PyGILState_STATE gstate = PyGILState_Ensure(); + PyObject *gc_module = PyImport_ImportModule("gc"); + if (gc_module == NULL) { + PyErr_Clear(); + PyGILState_Release(gstate); + return make_error(env, "gc_import_failed"); + } + ERL_NIF_TERM result_map = enif_make_new_map(env); + PyObject *stats = PyObject_CallMethod(gc_module, "get_stats", NULL); + if (stats != NULL && PyList_Check(stats)) { + Py_ssize_t num_gens = PyList_Size(stats); + if (num_gens > 0) { + ERL_NIF_TERM *gen_stats = enif_alloc(sizeof(ERL_NIF_TERM) * num_gens); + if (gen_stats != NULL) { + for (Py_ssize_t i = 0; i < num_gens; i++) { + gen_stats[i] = py_to_term(env, PyList_GetItem(stats, i)); + } + ERL_NIF_TERM gc_stats_list = enif_make_list_from_array(env, gen_stats, num_gens); + enif_free(gen_stats); + enif_make_map_put(env, result_map, + enif_make_atom(env, "gc_stats"), gc_stats_list, &result_map); + } + } + } + Py_XDECREF(stats); + PyObject *counts = PyObject_CallMethod(gc_module, "get_count", NULL); + if (counts != NULL && PyTuple_Check(counts)) { + enif_make_map_put(env, result_map, + enif_make_atom(env, "gc_count"), py_to_term(env, counts), &result_map); + } + Py_XDECREF(counts); + PyObject *threshold = PyObject_CallMethod(gc_module, "get_threshold", NULL); + if (threshold != NULL && PyTuple_Check(threshold)) { + enif_make_map_put(env, result_map, + enif_make_atom(env, "gc_threshold"), py_to_term(env, threshold), &result_map); } - executor_wait(&req); + Py_XDECREF(threshold); + Py_DECREF(gc_module); - ERL_NIF_TERM result = req.result; - request_cleanup(&req); - return result; + /* tracemalloc stats when tracing is on */ + PyObject *tracemalloc = PyImport_ImportModule("tracemalloc"); + if (tracemalloc != NULL) { + PyObject *is_tracing = PyObject_CallMethod(tracemalloc, "is_tracing", NULL); + if (is_tracing != NULL && PyObject_IsTrue(is_tracing)) { + PyObject *traced = PyObject_CallMethod(tracemalloc, "get_traced_memory", NULL); + if (traced != NULL && PyTuple_Check(traced)) { + enif_make_map_put(env, result_map, enif_make_atom(env, "traced_memory_current"), + py_to_term(env, PyTuple_GetItem(traced, 0)), &result_map); + enif_make_map_put(env, result_map, enif_make_atom(env, "traced_memory_peak"), + py_to_term(env, PyTuple_GetItem(traced, 1)), &result_map); + } + Py_XDECREF(traced); + } + Py_XDECREF(is_tracing); + Py_DECREF(tracemalloc); + } + PyErr_Clear(); + PyGILState_Release(gstate); + return enif_make_tuple2(env, ATOM_OK, result_map); } /** @@ -1620,25 +1291,30 @@ static ERL_NIF_TERM nif_gc(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) if (!runtime_is_running()) { return make_error(env, "python_not_running"); } - - py_request_t req; - request_init(&req); - req.type = PY_REQ_GC; - req.env = env; - req.gc_generation = 2; /* Full collection by default */ + int generation = 2; /* Full collection by default */ if (argc > 0) { - enif_get_int(env, argv[0], &req.gc_generation); + enif_get_int(env, argv[0], &generation); } - if (executor_enqueue(&req) != 0) { - request_cleanup(&req); - return make_error(env, "runtime_shutting_down"); + PyGILState_STATE gstate = PyGILState_Ensure(); + PyObject *gc_module = PyImport_ImportModule("gc"); + if (gc_module == NULL) { + PyErr_Clear(); + PyGILState_Release(gstate); + return make_error(env, "gc_import_failed"); } - executor_wait(&req); - - ERL_NIF_TERM result = req.result; - request_cleanup(&req); - return result; + PyObject *result = PyObject_CallMethod(gc_module, "collect", "i", generation); + Py_DECREF(gc_module); + ERL_NIF_TERM term; + if (result == NULL) { + term = make_py_error(env); + } else { + long collected = PyLong_AsLong(result); + Py_DECREF(result); + term = enif_make_tuple2(env, ATOM_OK, enif_make_long(env, collected)); + } + PyGILState_Release(gstate); + return term; } static ERL_NIF_TERM nif_tracemalloc_start(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { @@ -1719,34 +1395,6 @@ static ERL_NIF_TERM nif_execution_mode(ErlNifEnv *env, int argc, const ERL_NIF_T * Callback support NIFs * ============================================================================ */ -static ERL_NIF_TERM nif_set_callback_handler(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { - (void)argc; - py_worker_t *worker; - - if (!enif_get_resource(env, argv[0], WORKER_RESOURCE_TYPE, (void **)&worker)) { - return make_error(env, "invalid_worker"); - } - - if (!enif_get_local_pid(env, argv[1], &worker->callback_handler)) { - return make_error(env, "invalid_pid"); - } - - /* Create pipe for callback responses */ - if (pipe(worker->callback_pipe) < 0) { - return make_error(env, "pipe_failed"); - } - /* Non-blocking write end so write_all_with_deadline can bound the write. */ - { - int wfl = fcntl(worker->callback_pipe[1], F_GETFL, 0); - if (wfl >= 0) (void)fcntl(worker->callback_pipe[1], F_SETFL, wfl | O_NONBLOCK); - } - - worker->has_callback_handler = true; - - /* Return the write end of the pipe as a file descriptor for Erlang to use */ - return enif_make_tuple2(env, ATOM_OK, - enif_make_int(env, worker->callback_pipe[1])); -} /* Bound for callback-response pipe writes: a stalled reader must not block a * dirty scheduler forever (the pipe write ends are set non-blocking). */ @@ -1756,71 +1404,6 @@ static ERL_NIF_TERM nif_set_callback_handler(ErlNifEnv *env, int argc, const ERL * block the dispatching dirty scheduler forever. */ #define OWNGIL_IO_TIMEOUT_MS 30000 -static ERL_NIF_TERM nif_send_callback_response(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { - (void)argc; - int fd; - ErlNifBinary response; - - if (!enif_get_int(env, argv[0], &fd)) { - return make_error(env, "invalid_fd"); - } - - if (!enif_inspect_binary(env, argv[1], &response)) { - return make_error(env, "invalid_response"); - } - - /* Write length then data with a timed, non-blocking writer (the pipe write - * end is O_NONBLOCK) so a stalled reader or a large payload can't block a - * dirty scheduler forever or desync the length-framed protocol on EINTR. */ - uint32_t len = (uint32_t)response.size; - if (write_all_with_deadline(fd, &len, sizeof(len), - CALLBACK_RESPONSE_IO_TIMEOUT_MS) != WRITE_OK) { - return make_error(env, "write_length_failed"); - } - if (write_all_with_deadline(fd, response.data, response.size, - CALLBACK_RESPONSE_IO_TIMEOUT_MS) != WRITE_OK) { - return make_error(env, "write_data_failed"); - } - - return ATOM_OK; -} - -/* ============================================================================ - * Async worker NIFs (deprecated - replaced by event loop model) - * - * These NIFs are deprecated and return errors. Use py_event_loop_pool and - * py_event_loop:run_async/2 instead. - * ============================================================================ */ - -static ERL_NIF_TERM nif_async_worker_new(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { - (void)argc; - (void)argv; - return make_error(env, "async_workers_deprecated_use_event_loop"); -} - -static ERL_NIF_TERM nif_async_worker_destroy(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { - (void)argc; - (void)argv; - return ATOM_OK; -} - -static ERL_NIF_TERM nif_async_call(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { - (void)argc; - (void)argv; - return make_error(env, "async_workers_deprecated_use_event_loop"); -} - -static ERL_NIF_TERM nif_async_gather(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { - (void)argc; - (void)argv; - return make_error(env, "async_workers_deprecated_use_event_loop"); -} - -static ERL_NIF_TERM nif_async_stream(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { - (void)argc; - (void)argv; - return make_error(env, "async_workers_deprecated_use_event_loop"); -} /* ============================================================================ * Sub-interpreter support (Python 3.12+) @@ -5097,35 +4680,7 @@ static ERL_NIF_TERM nif_context_destroy(ErlNifEnv *env, int argc, const ERL_NIF_ return ATOM_OK; } - /* Legacy mode (should not reach here with new architecture) */ - if (runtime_is_running()) { - PyGILState_STATE gstate = PyGILState_Ensure(); - Py_XDECREF(ctx->module_cache); - ctx->module_cache = NULL; - Py_XDECREF(ctx->globals); - ctx->globals = NULL; - Py_XDECREF(ctx->locals); - ctx->locals = NULL; -#ifndef HAVE_SUBINTERPRETERS - if (ctx->thread_state != NULL) { - PyThreadState_Clear(ctx->thread_state); - PyThreadState_Delete(ctx->thread_state); - ctx->thread_state = NULL; - } -#endif - PyGILState_Release(gstate); - } - - /* Close callback pipes */ - if (ctx->callback_pipe[0] >= 0) { - close(ctx->callback_pipe[0]); - ctx->callback_pipe[0] = -1; - } - if (ctx->callback_pipe[1] >= 0) { - close(ctx->callback_pipe[1]); - ctx->callback_pipe[1] = -1; - } - + /* Every context created by nif_context_create has a thread */ atomic_fetch_add(&g_counters.ctx_destroyed, 1); return ATOM_OK; } @@ -5209,179 +4764,8 @@ static ERL_NIF_TERM nif_context_call(ErlNifEnv *env, int argc, const ERL_NIF_TER return dispatch_to_worker_thread(env, ctx, CTX_REQ_CALL, request); } - /* Legacy mode: direct execution with py_context_acquire. - * For subinterpreters, py_context_acquire handles PyThreadState_Swap - * to switch to the pool slot's interpreter. */ - ErlNifBinary module_bin, func_bin; - if (!enif_inspect_binary(env, argv[1], &module_bin)) { - return make_error(env, "invalid_module"); - } - if (!enif_inspect_binary(env, argv[2], &func_bin)) { - return make_error(env, "invalid_func"); - } - - char *module_name = binary_to_string(&module_bin); - char *func_name = binary_to_string(&func_bin); - if (module_name == NULL || func_name == NULL) { - enif_free(module_name); - enif_free(func_name); - return make_error(env, "alloc_failed"); - } - - ERL_NIF_TERM result; - - /* Acquire thread state using centralized guard (worker mode only) */ - py_context_guard_t guard = py_context_acquire(ctx); - if (!guard.acquired) { - enif_free(module_name); - enif_free(func_name); - return make_error(env, "acquire_failed"); - } - - /* Set thread-local context for callback support */ - py_context_t *prev_context = tl_current_context; - tl_current_context = ctx; - - /* Enable suspension for callback support */ - bool prev_allow_suspension = tl_allow_suspension; - tl_allow_suspension = true; - - PyObject *module = NULL; - PyObject *func = NULL; - - /* Special handling for __main__ module - check ctx->globals first */ - if (strcmp(module_name, "__main__") == 0) { - func = PyDict_GetItemString(ctx->globals, func_name); /* Borrowed ref */ - if (func != NULL) { - Py_INCREF(func); - } - } - - if (func == NULL) { - /* Get or import module */ - module = context_get_module(ctx, module_name); - if (module == NULL) { - result = make_py_error(env); - goto cleanup; - } - - /* Get function */ - func = PyObject_GetAttrString(module, func_name); - if (func == NULL) { - result = make_py_error(env); - goto cleanup; - } - } - - /* Convert args */ - unsigned int args_len; - if (!enif_get_list_length(env, argv[3], &args_len)) { - Py_DECREF(func); - result = make_error(env, "invalid_args"); - goto cleanup; - } - - PyObject *args = PyTuple_New(args_len); - if (args == NULL) { - Py_DECREF(func); - result = make_error(env, "alloc_failed"); - goto cleanup; - } - ERL_NIF_TERM head, tail = argv[3]; - for (unsigned int i = 0; i < args_len; i++) { - enif_get_list_cell(env, tail, &head, &tail); - PyObject *arg = term_to_py(env, head); - if (arg == NULL) { - Py_DECREF(args); - Py_DECREF(func); - result = make_error(env, "arg_conversion_failed"); - goto cleanup; - } - PyTuple_SET_ITEM(args, i, arg); - } - - /* Convert kwargs */ - PyObject *kwargs = NULL; - if (argc > 4 && enif_is_map(env, argv[4])) { - kwargs = term_to_py(env, argv[4]); - } - - /* Call the function */ - PyObject *py_result = PyObject_Call(func, args, kwargs); - Py_DECREF(func); - Py_DECREF(args); - Py_XDECREF(kwargs); - - if (py_result == NULL) { - /* Check for pending callback (flag-based detection) */ - if (tl_pending_callback) { - PyErr_Clear(); /* Clear whatever exception is set */ - - /* Create suspended context state */ - suspended_context_state_t *suspended = create_suspended_context_state_for_call( - env, ctx, &module_bin, &func_bin, argv[3], - argc > 4 ? argv[4] : enif_make_new_map(env)); - - if (suspended == NULL) { - tl_pending_callback = false; - Py_CLEAR(tl_pending_args); - result = make_error(env, "create_suspended_state_failed"); - } else { - result = build_suspended_context_result(env, suspended); - } - } else { - result = make_py_error(env); - } - } else if (is_inline_schedule_marker(py_result)) { - /* Inline schedule marker: chain via enif_schedule_nif without Erlang messaging */ - inline_continuation_t *cont = create_inline_continuation(ctx, NULL, py_result, 0); - Py_DECREF(py_result); - - if (cont == NULL) { - result = make_error(env, "create_continuation_failed"); - } else { - ERL_NIF_TERM cont_ref = enif_make_resource(env, cont); - enif_release_resource(cont); - - /* Restore thread-local state before scheduling */ - tl_allow_suspension = prev_allow_suspension; - tl_current_context = prev_context; - clear_pending_callback_tls(); - enif_free(module_name); - enif_free(func_name); - py_context_release(&guard); - - return enif_schedule_nif(env, "inline_continuation", - ERL_NIF_DIRTY_JOB_IO_BOUND, nif_inline_continuation, 1, &cont_ref); - } - } else if (is_schedule_marker(py_result)) { - /* Schedule marker: release dirty scheduler, continue via callback */ - ScheduleMarkerObject *marker = (ScheduleMarkerObject *)py_result; - ERL_NIF_TERM callback_name = py_to_term(env, marker->callback_name); - ERL_NIF_TERM callback_args = py_to_term(env, marker->args); - Py_DECREF(py_result); - result = enif_make_tuple3(env, ATOM_SCHEDULE, callback_name, callback_args); - } else { - ERL_NIF_TERM term_result = py_to_term(env, py_result); - Py_DECREF(py_result); - result = enif_make_tuple2(env, ATOM_OK, term_result); - } - -cleanup: - /* Restore thread-local state */ - tl_allow_suspension = prev_allow_suspension; - tl_current_context = prev_context; - - /* Clear pending callback TLS before releasing context */ - clear_pending_callback_tls(); - - enif_free(module_name); - enif_free(func_name); - - /* Release thread state using centralized guard */ - py_context_release(&guard); - - return result; + /* Every context created by nif_context_create has a thread */ + return make_error(env, "context_has_no_thread"); } /** @@ -5691,115 +5075,8 @@ static ERL_NIF_TERM nif_context_eval(ErlNifEnv *env, int argc, const ERL_NIF_TER return dispatch_to_worker_thread(env, ctx, CTX_REQ_EVAL, request); } - /* Legacy mode: direct execution with py_context_acquire. - * For subinterpreters, py_context_acquire handles PyThreadState_Swap - * to switch to the pool slot's interpreter. */ - ErlNifBinary code_bin; - if (!enif_inspect_binary(env, argv[1], &code_bin)) { - return make_error(env, "invalid_code"); - } - - char *code = binary_to_string(&code_bin); - if (code == NULL) { - return make_error(env, "alloc_failed"); - } - - ERL_NIF_TERM result; - - /* Acquire thread state using centralized guard (worker mode only) */ - py_context_guard_t guard = py_context_acquire(ctx); - if (!guard.acquired) { - enif_free(code); - return make_error(env, "acquire_failed"); - } - - /* Set thread-local context for callback support */ - py_context_t *prev_context = tl_current_context; - tl_current_context = ctx; - - /* Enable suspension for callback support */ - bool prev_allow_suspension = tl_allow_suspension; - tl_allow_suspension = true; - - /* Update locals if provided */ - ERL_NIF_TERM locals_term = argc > 2 ? argv[2] : enif_make_new_map(env); - if (argc > 2 && enif_is_map(env, argv[2])) { - PyObject *new_locals = term_to_py(env, argv[2]); - if (new_locals != NULL && PyDict_Check(new_locals)) { - PyDict_Update(ctx->locals, new_locals); - Py_DECREF(new_locals); - } - } - - /* Compile and evaluate */ - PyObject *py_result = PyRun_String(code, Py_eval_input, ctx->globals, ctx->locals); - - if (py_result == NULL) { - /* Check for pending callback (flag-based detection) */ - if (tl_pending_callback) { - PyErr_Clear(); /* Clear whatever exception is set */ - - /* Create suspended context state */ - suspended_context_state_t *suspended = create_suspended_context_state_for_eval( - env, ctx, &code_bin, locals_term); - - if (suspended == NULL) { - tl_pending_callback = false; - Py_CLEAR(tl_pending_args); - result = make_error(env, "create_suspended_state_failed"); - } else { - result = build_suspended_context_result(env, suspended); - } - } else { - result = make_py_error(env); - } - } else if (is_inline_schedule_marker(py_result)) { - /* Inline schedule marker: chain via enif_schedule_nif without Erlang messaging */ - inline_continuation_t *cont = create_inline_continuation(ctx, NULL, py_result, 0); - Py_DECREF(py_result); - - if (cont == NULL) { - result = make_error(env, "create_continuation_failed"); - } else { - ERL_NIF_TERM cont_ref = enif_make_resource(env, cont); - enif_release_resource(cont); - - /* Restore thread-local state before scheduling */ - tl_allow_suspension = prev_allow_suspension; - tl_current_context = prev_context; - clear_pending_callback_tls(); - enif_free(code); - py_context_release(&guard); - - return enif_schedule_nif(env, "inline_continuation", - ERL_NIF_DIRTY_JOB_IO_BOUND, nif_inline_continuation, 1, &cont_ref); - } - } else if (is_schedule_marker(py_result)) { - /* Schedule marker: release dirty scheduler, continue via callback */ - ScheduleMarkerObject *marker = (ScheduleMarkerObject *)py_result; - ERL_NIF_TERM callback_name = py_to_term(env, marker->callback_name); - ERL_NIF_TERM callback_args = py_to_term(env, marker->args); - Py_DECREF(py_result); - result = enif_make_tuple3(env, ATOM_SCHEDULE, callback_name, callback_args); - } else { - ERL_NIF_TERM term_result = py_to_term(env, py_result); - Py_DECREF(py_result); - result = enif_make_tuple2(env, ATOM_OK, term_result); - } - - /* Restore thread-local state */ - tl_allow_suspension = prev_allow_suspension; - tl_current_context = prev_context; - - /* Clear pending callback TLS before releasing context */ - clear_pending_callback_tls(); - - enif_free(code); - - /* Release thread state using centralized guard */ - py_context_release(&guard); - - return result; + /* Every context created by nif_context_create has a thread */ + return make_error(env, "context_has_no_thread"); } /** @@ -5833,53 +5110,8 @@ static ERL_NIF_TERM nif_context_exec(ErlNifEnv *env, int argc, const ERL_NIF_TER return dispatch_to_worker_thread(env, ctx, CTX_REQ_EXEC, argv[1]); } - /* Legacy mode: direct execution with py_context_acquire. - * For subinterpreters, py_context_acquire handles PyThreadState_Swap - * to switch to the pool slot's interpreter. */ - ErlNifBinary code_bin; - if (!enif_inspect_binary(env, argv[1], &code_bin)) { - return make_error(env, "invalid_code"); - } - - char *code = binary_to_string(&code_bin); - if (code == NULL) { - return make_error(env, "alloc_failed"); - } - - ERL_NIF_TERM result; - - /* Acquire thread state using centralized guard (worker mode only) */ - py_context_guard_t guard = py_context_acquire(ctx); - if (!guard.acquired) { - enif_free(code); - return make_error(env, "acquire_failed"); - } - - /* Set thread-local context for callback support */ - py_context_t *prev_context = tl_current_context; - tl_current_context = ctx; - - /* Execute statements. - * Use globals for both globals and locals to simulate module-level execution. - * This ensures imports are accessible from function definitions. */ - PyObject *py_result = PyRun_String(code, Py_file_input, ctx->globals, ctx->globals); - - if (py_result == NULL) { - result = make_py_error(env); - } else { - Py_DECREF(py_result); - result = ATOM_OK; - } - - /* Restore previous context */ - tl_current_context = prev_context; - - enif_free(code); - - /* Release thread state using centralized guard */ - py_context_release(&guard); - - return result; + /* Every context created by nif_context_create has a thread */ + return make_error(env, "context_has_no_thread"); } /* ============================================================================ @@ -7937,20 +7169,12 @@ static int load(ErlNifEnv *env, void **priv_data, ERL_NIF_TERM load_info) { (void)load_info; /* Create resource types */ - WORKER_RESOURCE_TYPE = enif_open_resource_type( - env, NULL, "py_worker", worker_destructor, - ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER, NULL); - PYOBJ_RESOURCE_TYPE = enif_open_resource_type( env, NULL, "py_object", pyobj_destructor, ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER, NULL); /* ASYNC_WORKER_RESOURCE_TYPE removed - replaced by event loop model */ - SUSPENDED_STATE_RESOURCE_TYPE = enif_open_resource_type( - env, NULL, "py_suspended_state", suspended_state_destructor, - ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER, NULL); - /* Process-per-context resource type (no mutex) */ PY_CONTEXT_RESOURCE_TYPE = enif_open_resource_type( env, NULL, "py_context", context_destructor, @@ -7984,8 +7208,8 @@ static int load(ErlNifEnv *env, void **priv_data, ERL_NIF_TERM load_info) { env, NULL, "py_shared_dict", shared_dict_destructor, ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER, NULL); - if (WORKER_RESOURCE_TYPE == NULL || PYOBJ_RESOURCE_TYPE == NULL || - SUSPENDED_STATE_RESOURCE_TYPE == NULL || + if (PYOBJ_RESOURCE_TYPE == NULL || + PY_CONTEXT_RESOURCE_TYPE == NULL || PY_REF_RESOURCE_TYPE == NULL || PY_CONTEXT_SUSPENDED_RESOURCE_TYPE == NULL || PY_ENV_RESOURCE_TYPE == NULL || @@ -8023,7 +7247,6 @@ static int load(ErlNifEnv *env, void **priv_data, ERL_NIF_TERM load_info) { ATOM_SPAN_EVENT = enif_make_atom(env, "span_event"); /* Worker pool atoms */ - pool_atoms_init(env); /* Reactor buffer resource type for zero-copy read handling */ REACTOR_BUFFER_RESOURCE_TYPE = enif_open_resource_type( @@ -8112,22 +7335,10 @@ static ErlNifFunc nif_funcs[] = { {"init", 1, nif_py_init, 0}, {"finalize", 0, nif_finalize, 0}, - /* Worker management */ - {"worker_new", 0, nif_worker_new, 0}, - {"worker_new", 1, nif_worker_new, 0}, - {"worker_destroy", 1, nif_worker_destroy, 0}, /* Python execution - dirty I/O NIFs */ - {"worker_call", 5, nif_worker_call, ERL_NIF_DIRTY_JOB_IO_BOUND}, - {"worker_call", 6, nif_worker_call, ERL_NIF_DIRTY_JOB_IO_BOUND}, - {"worker_eval", 3, nif_worker_eval, ERL_NIF_DIRTY_JOB_IO_BOUND}, - {"worker_eval", 4, nif_worker_eval, ERL_NIF_DIRTY_JOB_IO_BOUND}, - {"worker_exec", 2, nif_worker_exec, ERL_NIF_DIRTY_JOB_IO_BOUND}, - {"worker_next", 2, nif_worker_next, ERL_NIF_DIRTY_JOB_IO_BOUND}, /* Module operations */ - {"import_module", 2, nif_import_module, ERL_NIF_DIRTY_JOB_IO_BOUND}, - {"get_attr", 3, nif_get_attr, ERL_NIF_DIRTY_JOB_IO_BOUND}, /* Info */ {"version", 0, nif_version, 0}, @@ -8142,18 +7353,10 @@ static ErlNifFunc nif_funcs[] = { {"tracemalloc_stop", 0, nif_tracemalloc_stop, 0}, /* Callback support */ - {"set_callback_handler", 2, nif_set_callback_handler, 0}, - {"send_callback_response", 2, nif_send_callback_response, ERL_NIF_DIRTY_JOB_IO_BOUND}, - {"resume_callback", 2, nif_resume_callback, 0}, /* Async worker management */ - {"async_worker_new", 0, nif_async_worker_new, 0}, - {"async_worker_destroy", 1, nif_async_worker_destroy, 0}, /* Async execution - dirty I/O NIFs */ - {"async_call", 6, nif_async_call, ERL_NIF_DIRTY_JOB_IO_BOUND}, - {"async_gather", 3, nif_async_gather, ERL_NIF_DIRTY_JOB_IO_BOUND}, - {"async_stream", 6, nif_async_stream, ERL_NIF_DIRTY_JOB_IO_BOUND}, /* Subinterpreter capability probes */ {"subinterp_supported", 0, nif_subinterp_supported, 0}, @@ -8240,8 +7443,6 @@ static ErlNifFunc nif_funcs[] = { {"start_reader", 1, nif_start_reader, 0}, {"stop_writer", 1, nif_stop_writer, 0}, {"start_writer", 1, nif_start_writer, 0}, - {"cancel_reader", 2, nif_cancel_reader, 0}, /* Legacy alias */ - {"cancel_writer", 2, nif_cancel_writer, 0}, /* Legacy alias */ {"close_fd", 1, nif_close_fd, 0}, /* Test helpers for fd monitoring (using pipes) */ {"create_test_pipe", 0, nif_create_test_pipe, 0}, @@ -8265,10 +7466,6 @@ static ErlNifFunc nif_funcs[] = { {"set_shared_worker", 1, nif_set_shared_worker, 0}, /* Worker pool */ - {"pool_start", 1, nif_pool_start, 0}, - {"pool_stop", 0, nif_pool_stop, 0}, - {"pool_submit", 5, nif_pool_submit, 0}, - {"pool_stats", 0, nif_pool_stats, 0}, /* Process-per-context API (no mutex) */ {"context_create", 1, nif_context_create, 0}, diff --git a/c_src/py_nif.h b/c_src/py_nif.h index 45020cb6..7dd3b18e 100644 --- a/c_src/py_nif.h +++ b/c_src/py_nif.h @@ -146,7 +146,7 @@ typedef enum { /** * @brief Conventional GIL mode (every other supported build) * - * Coordinator-side work runs through the single executor thread. + * Coordinator-side work (thread callbacks) runs on the thread-worker bridge. * Per-context worker / OWN_GIL pthreads handle the public context * APIs directly; this mode label only governs the coordinator path. */ @@ -300,52 +300,6 @@ extern py_invariant_counters_t g_counters; * @{ */ -/** - * @struct py_worker_t - * @brief Represents a Python worker with its own namespace - * - * A worker encapsulates a Python execution context with isolated - * global and local namespaces. Workers are created per-process in - * Erlang and can execute Python code independently. - * - * @note Workers should be created via `py:worker_new/0` and destroyed - * via `py:worker_destroy/1` or automatically via GC. - * - * @see nif_worker_new - * @see nif_worker_destroy - */ -typedef struct { - /** @brief Python thread state for this worker */ - PyThreadState *thread_state; - - /** @brief Global namespace dictionary (`__globals__`) */ - PyObject *globals; - - /** @brief Local namespace dictionary (`__locals__`) */ - PyObject *locals; - - /** @brief Whether this worker currently owns the GIL */ - bool owns_gil; - - /* Callback support fields */ - - /** - * @brief Pipe file descriptors for callback IPC - * - * - `callback_pipe[0]` - Read end (Python reads responses) - * - `callback_pipe[1]` - Write end (Erlang writes responses) - */ - int callback_pipe[2]; - - /** @brief PID of the Erlang callback handler process */ - ErlNifPid callback_handler; - - /** @brief Whether a callback handler is registered */ - bool has_callback_handler; - - /** @brief Environment for building callback messages */ - ErlNifEnv *callback_env; -} py_worker_t; /* async_pending_t and py_async_worker_t removed - async workers replaced by event loop model */ @@ -394,7 +348,7 @@ typedef struct { /** * @defgroup requests Request Handling - * @brief Structures for executor request processing + * @brief Request kinds shared by the context executors and callback replay * @{ */ @@ -403,7 +357,7 @@ typedef struct py_context py_context_t; /** * @enum py_request_type_t - * @brief Types of requests that can be submitted to the executor + * @brief Kinds of Python work a context can run */ typedef enum { PY_REQ_CALL, /**< Call a Python function */ @@ -414,98 +368,9 @@ typedef enum { PY_REQ_GETATTR, /**< Get attribute from Python object */ PY_REQ_MEMORY_STATS, /**< Get Python memory statistics */ PY_REQ_GC, /**< Trigger Python garbage collection */ - PY_REQ_SHUTDOWN /**< Signal executor shutdown */ + PY_REQ_SHUTDOWN /**< Shutdown marker */ } py_request_type_t; -/** - * @struct py_request_t - * @brief Request submitted to the executor thread for processing - * - * Encapsulates all information needed to execute a Python operation. - * The caller thread blocks on the condition variable until the - * executor signals completion. - * - * @note Requests are allocated on the stack by the caller NIF and - * passed to the executor. The executor processes them with - * the GIL held. - */ -typedef struct py_request { - /** @brief Type of operation to perform */ - py_request_type_t type; - - /* Synchronization primitives */ - - /** @brief Mutex for condition variable */ - pthread_mutex_t mutex; - - /** @brief Condition variable for completion signaling */ - pthread_cond_t cond; - - /** @brief Flag set when processing is complete */ - volatile bool completed; - - /* Common parameters */ - - /** @brief Worker context (may be NULL for global ops) */ - py_worker_t *worker; - - /** @brief Context for process-owned operations (may be NULL) */ - py_context_t *context; - - /** @brief Caller's NIF environment for term creation */ - ErlNifEnv *env; - - /* Call/Import parameters */ - - /** @brief Module name as binary */ - ErlNifBinary module_bin; - - /** @brief Function name as binary */ - ErlNifBinary func_bin; - - /** @brief Code string for eval/exec */ - ErlNifBinary code_bin; - - /** @brief Arguments list term */ - ERL_NIF_TERM args_term; - - /** @brief Keyword arguments map term */ - ERL_NIF_TERM kwargs_term; - - /** @brief Local variables map for eval */ - ERL_NIF_TERM locals_term; - - /** @brief Execution timeout in milliseconds (0 = no timeout) */ - unsigned long timeout_ms; - - /* Iterator parameters */ - - /** @brief Generator/iterator wrapper for PY_REQ_NEXT */ - py_object_t *gen_wrapper; - - /* Getattr parameters */ - - /** @brief Object wrapper for PY_REQ_GETATTR */ - py_object_t *obj_wrapper; - - /** @brief Attribute name as binary */ - ErlNifBinary attr_bin; - - /* GC parameters */ - - /** @brief Generation to collect (0, 1, or 2) */ - int gc_generation; - - /* Result */ - - /** @brief Result term set by executor */ - ERL_NIF_TERM result; - - /* Queue linkage */ - - /** @brief Next request in executor queue */ - struct py_request *next; -} py_request_t; /** @} */ @@ -519,94 +384,6 @@ typedef struct py_request { * @{ */ -/** - * @struct suspended_state_t - * @brief State for a suspended Python execution awaiting callback result - * - * When Python code calls `erlang.call()`, execution is suspended and - * this structure captures all state needed to resume after Erlang - * processes the callback. - * - * @par Suspension Flow: - * 1. Python calls `erlang.call('func', args)` - * 2. `erlang_call_impl` raises `SuspensionRequired` exception - * 3. `process_request` catches exception, creates `suspended_state_t` - * 4. Returns `{suspended, CallbackId, StateRef, {Func, Args}}` to Erlang - * 5. Erlang executes callback, calls `resume_callback(StateRef, Result)` - * 6. `nif_resume_callback_dirty` replays Python with cached result - * - * @see erlang_call_impl - * @see nif_resume_callback - */ -typedef struct { - /** @brief Worker context for replay */ - py_worker_t *worker; - - /** @brief Unique identifier for this callback */ - uint64_t callback_id; - - /* Callback invocation info */ - - /** @brief Name of Erlang function being called */ - char *callback_func_name; - - /** @brief Length of callback_func_name */ - size_t callback_func_len; - - /** @brief Arguments passed to the callback */ - PyObject *callback_args; - - /* Original request context for replay */ - - /** @brief Original module name binary */ - ErlNifBinary orig_module; - - /** @brief Original function name binary */ - ErlNifBinary orig_func; - - /** @brief Original arguments (copied to orig_env) */ - ERL_NIF_TERM orig_args; - - /** @brief Original keyword arguments */ - ERL_NIF_TERM orig_kwargs; - - /** @brief Environment owning copied terms */ - ErlNifEnv *orig_env; - - /** @brief Original timeout setting */ - int orig_timeout_ms; - - /** @brief Original request type (PY_REQ_CALL, PY_REQ_EVAL) */ - int request_type; - - /** @brief Original code for eval/exec replay */ - ErlNifBinary orig_code; - - /** @brief Original locals map for eval replay */ - ERL_NIF_TERM orig_locals; - - /* Callback result */ - - /** @brief Raw result data from Erlang callback */ - unsigned char *result_data; - - /** @brief Length of result_data */ - size_t result_len; - - /** @brief Flag: result is available for replay */ - _Atomic bool has_result; - - /** @brief Flag: result represents an error */ - _Atomic bool is_error; - - /* Synchronization */ - - /** @brief Mutex for result access */ - pthread_mutex_t mutex; - - /** @brief Condition for blocking callback mode */ - pthread_cond_t cond; -} suspended_state_t; /** @} */ @@ -1403,16 +1180,12 @@ typedef struct { * @{ */ -/** @brief Resource type for py_worker_t */ -extern ErlNifResourceType *WORKER_RESOURCE_TYPE; /** @brief Resource type for py_object_t */ extern ErlNifResourceType *PYOBJ_RESOURCE_TYPE; /* ASYNC_WORKER_RESOURCE_TYPE removed - async workers replaced by event loop model */ -/** @brief Resource type for suspended_state_t */ -extern ErlNifResourceType *SUSPENDED_STATE_RESOURCE_TYPE; /** @brief Resource type for py_context_t (process-per-context) */ extern ErlNifResourceType *PY_CONTEXT_RESOURCE_TYPE; @@ -1495,28 +1268,13 @@ extern PyThreadState *g_main_thread_state; /** @brief Current execution mode */ extern py_execution_mode_t g_execution_mode; -/* Single executor state */ -/** @brief Single executor thread handle */ -extern pthread_t g_executor_thread; -/** @brief Single executor queue mutex */ -extern pthread_mutex_t g_executor_mutex; -/** @brief Single executor queue condition */ -extern pthread_cond_t g_executor_cond; -/** @brief Single executor queue head */ -extern py_request_t *g_executor_queue_head; -/** @brief Single executor queue tail */ -extern py_request_t *g_executor_queue_tail; -/** @brief Single executor running flag (atomic for thread-safe access) */ -extern _Atomic bool g_executor_running; -/** @brief Single executor shutdown flag (atomic for thread-safe access) */ -extern _Atomic bool g_executor_shutdown; /** @brief Global counter for unique callback IDs */ extern _Atomic uint64_t g_callback_id_counter; @@ -1551,8 +1309,6 @@ extern PyObject *g_numpy_ndarray_type; /* Thread-local state */ -/** @brief Current worker for callback context (legacy) */ -extern __thread py_worker_t *tl_current_worker; /** @brief Current context for callback context (new process-per-context API) */ extern __thread py_context_t *tl_current_context; @@ -1560,8 +1316,6 @@ extern __thread py_context_t *tl_current_context; /** @brief Current NIF environment for callbacks */ extern __thread ErlNifEnv *tl_callback_env; -/** @brief Current suspended state (for replay) */ -extern __thread suspended_state_t *tl_current_suspended; /** @brief Flag: suspension is allowed in current context */ extern __thread bool tl_allow_suspension; @@ -1987,33 +1741,8 @@ static inline uint64_t get_monotonic_ns(void) { return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec; } -/** - * @brief Start timeout monitoring for Python execution - * - * Sets up a trace callback that checks elapsed time and raises - * `TimeoutError` if the deadline is exceeded. - * - * @param timeout_ms Timeout in milliseconds (0 = no timeout) - * - * @see stop_timeout - */ -static void start_timeout(unsigned long timeout_ms); -/** - * @brief Stop timeout monitoring - * - * Removes the trace callback and resets timeout state. - * - * @see start_timeout - */ -static void stop_timeout(void); -/** - * @brief Check if current Python exception is a timeout error - * - * @return true if TimeoutError is pending, false otherwise - */ -static bool check_timeout_error(void); /** @} */ @@ -2027,75 +1756,12 @@ static bool check_timeout_error(void); * @{ */ -/** - * @brief Process a single request with GIL held - * - * Main dispatch function called by executor threads. Handles all - * request types and stores results in the request structure. - * - * @param req Request to process (must not be NULL) - * - * @note Caller must hold the GIL - * @note Sets req->result on completion - */ -static void process_request(py_request_t *req); -/** - * @brief Submit a request to the executor - * - * Routes the request based on execution mode: - * - FREE_THREADED: Execute directly - * - MULTI_EXECUTOR: Route to executor pool - * - SUBINTERP: Use single executor - * - * @param req Request to submit - */ -static int executor_enqueue(py_request_t *req); -/** - * @brief Wait for a request to complete - * - * Blocks until the executor signals completion by setting - * req->completed and signaling req->cond. - * - * @param req Request to wait for - */ -static void executor_wait(py_request_t *req); -/** - * @brief Initialize a request structure - * - * Zeroes the structure and initializes mutex/condvar. - * - * @param req Request to initialize - */ -static void request_init(py_request_t *req); -/** - * @brief Clean up a request structure - * - * Destroys mutex and condvar. Does not free the request itself. - * - * @param req Request to clean up - */ -static void request_cleanup(py_request_t *req); -/** - * @brief Start the single executor thread - * - * Creates and starts the executor thread, waiting for it to - * become ready before returning. - * - * @return 0 on success, -1 on failure - */ -static int executor_start(void); -/** - * @brief Stop the single executor thread - * - * Sends shutdown request and waits for thread to terminate. - */ -static void executor_stop(void); /** @} */ @@ -2148,19 +1814,6 @@ static PyObject *erlang_module_getattr(PyObject *module, PyObject *name); /* async_event_loop_thread removed - replaced by event loop model */ -/** - * @brief Create suspended state for callback handling - * - * Captures all state needed to resume Python execution after - * Erlang processes the callback. - * - * @param env NIF environment - * @param exc_args Exception args tuple (callback_id, func_name, args) - * @param req Original request being processed - * @return New suspended state resource, or NULL on error - */ -static suspended_state_t *create_suspended_state(ErlNifEnv *env, PyObject *exc_args, - py_request_t *req); /** * @brief Parse callback response from Erlang diff --git a/c_src/py_worker_pool.c b/c_src/py_worker_pool.c deleted file mode 100644 index 7452c199..00000000 --- a/c_src/py_worker_pool.c +++ /dev/null @@ -1,921 +0,0 @@ -/* - * Copyright 2026 Benoit Chesneau - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * @file py_worker_pool.c - * @brief Worker thread pool implementation - * - * Implements a pool of worker threads for processing Python operations. - * Each worker can have its own subinterpreter (Python 3.12+) for true - * parallelism, or share the GIL with batching for older Python versions. - */ - -/* ============================================================================ - * Global Pool Instance - * ============================================================================ */ - -py_worker_pool_t g_pool = { - .num_workers = 0, - .initialized = false, - .shutting_down = false, - .request_id_counter = 1, - .use_subinterpreters = false, - .free_threaded = false -}; - -/* Atom for response message */ -static ERL_NIF_TERM ATOM_PY_RESPONSE; - -/* ============================================================================ - * Queue Operations - * ============================================================================ */ - -static void queue_init(py_pool_queue_t *queue) { - queue->head = NULL; - queue->tail = NULL; - atomic_store(&queue->pending_count, 0); - atomic_store(&queue->total_enqueued, 0); - pthread_mutex_init(&queue->mutex, NULL); - pthread_cond_init(&queue->cond, NULL); -} - -static void queue_destroy(py_pool_queue_t *queue) { - pthread_mutex_destroy(&queue->mutex); - pthread_cond_destroy(&queue->cond); -} - -static void queue_enqueue(py_pool_queue_t *queue, py_pool_request_t *req) { - pthread_mutex_lock(&queue->mutex); - - req->next = NULL; - if (queue->tail == NULL) { - queue->head = req; - queue->tail = req; - } else { - queue->tail->next = req; - queue->tail = req; - } - - atomic_fetch_add(&queue->pending_count, 1); - atomic_fetch_add(&queue->total_enqueued, 1); - - pthread_cond_signal(&queue->cond); - pthread_mutex_unlock(&queue->mutex); -} - -static py_pool_request_t *queue_dequeue(py_pool_queue_t *queue, bool wait) { - pthread_mutex_lock(&queue->mutex); - - while (queue->head == NULL) { - if (!wait) { - pthread_mutex_unlock(&queue->mutex); - return NULL; - } - pthread_cond_wait(&queue->cond, &queue->mutex); - - /* Check for shutdown after wakeup */ - if (g_pool.shutting_down) { - pthread_mutex_unlock(&queue->mutex); - return NULL; - } - } - - py_pool_request_t *req = queue->head; - queue->head = req->next; - if (queue->head == NULL) { - queue->tail = NULL; - } - req->next = NULL; - - atomic_fetch_sub(&queue->pending_count, 1); - pthread_mutex_unlock(&queue->mutex); - - return req; -} - -/* Wake up all workers waiting on the queue */ -static void queue_broadcast(py_pool_queue_t *queue) { - pthread_mutex_lock(&queue->mutex); - pthread_cond_broadcast(&queue->cond); - pthread_mutex_unlock(&queue->mutex); -} - -/* ============================================================================ - * Request Management - * ============================================================================ */ - -static py_pool_request_t *py_pool_request_new(py_pool_request_type_t type, - ErlNifPid caller_pid) { - py_pool_request_t *req = enif_alloc(sizeof(py_pool_request_t)); - if (req == NULL) { - return NULL; - } - - memset(req, 0, sizeof(py_pool_request_t)); - req->type = type; - req->caller_pid = caller_pid; - req->request_id = atomic_fetch_add(&g_pool.request_id_counter, 1); - req->msg_env = enif_alloc_env(); - - if (req->msg_env == NULL) { - enif_free(req); - return NULL; - } - - return req; -} - -static void py_pool_request_free(py_pool_request_t *req) { - if (req == NULL) { - return; - } - - if (req->module_name) { - enif_free(req->module_name); - } - if (req->func_name) { - enif_free(req->func_name); - } - if (req->code) { - enif_free(req->code); - } - if (req->msg_env) { - enif_free_env(req->msg_env); - } - - enif_free(req); -} - -/* ============================================================================ - * Module Caching - * ============================================================================ */ - -static PyObject *py_pool_get_module(py_pool_worker_t *worker, - const char *module_name) { - /* Check cache first */ - if (worker->module_cache != NULL) { - PyObject *key = PyUnicode_FromString(module_name); - if (key != NULL) { - PyObject *module = PyDict_GetItem(worker->module_cache, key); - Py_DECREF(key); - if (module != NULL) { - return module; /* Borrowed reference */ - } - } - } - - /* Import module */ - PyObject *module = PyImport_ImportModule(module_name); - if (module == NULL) { - return NULL; - } - - /* Cache it */ - if (worker->module_cache != NULL) { - PyObject *key = PyUnicode_FromString(module_name); - if (key != NULL) { - PyDict_SetItem(worker->module_cache, key, module); - Py_DECREF(key); - } - } - - Py_DECREF(module); /* Dict now owns it, return borrowed ref */ - return PyDict_GetItemString(worker->module_cache, module_name); -} - -/* ============================================================================ - * Response Sending - * ============================================================================ */ - -/* Debug: track sent responses */ -static _Atomic uint64_t g_responses_sent = 0; -static _Atomic uint64_t g_responses_failed = 0; - -static void py_pool_send_response(py_pool_request_t *req, ERL_NIF_TERM result) { - /* Build message: {py_response, RequestId, Result} */ - ERL_NIF_TERM request_id_term = enif_make_uint64(req->msg_env, req->request_id); - ERL_NIF_TERM msg = enif_make_tuple3(req->msg_env, - ATOM_PY_RESPONSE, - request_id_term, - result); - - int send_result = enif_send(NULL, &req->caller_pid, req->msg_env, msg); - if (send_result) { - atomic_fetch_add(&g_responses_sent, 1); - /* IMPORTANT: enif_send consumes/invalidates the msg_env on success. - * Set to NULL to prevent double-free in py_pool_request_free. */ - req->msg_env = NULL; - } else { - /* enif_send fails normally when the caller has already died; the - * g_responses_failed counter records it (no stderr spam). */ - atomic_fetch_add(&g_responses_failed, 1); - /* On failure, msg_env is still valid and will be freed in request_free */ - } -} - -/* ============================================================================ - * Request Processing - CALL/APPLY - * ============================================================================ */ - -static ERL_NIF_TERM py_pool_process_call(py_pool_worker_t *worker, - py_pool_request_t *req) { - ErlNifEnv *env = req->msg_env; - - /* Get module */ - PyObject *module = py_pool_get_module(worker, req->module_name); - if (module == NULL) { - ERL_NIF_TERM err = make_py_error(env); - return err; - } - - /* Get function */ - PyObject *func = PyObject_GetAttrString(module, req->func_name); - if (func == NULL) { - ERL_NIF_TERM err = make_py_error(env); - return err; - } - - /* Convert args to Python */ - PyObject *args = term_to_py(env, req->args_term); - if (args == NULL) { - Py_DECREF(func); - return make_error(env, "args_conversion_failed"); - } - - /* Ensure args is a tuple */ - if (!PyTuple_Check(args)) { - if (PyList_Check(args)) { - PyObject *tuple = PyList_AsTuple(args); - Py_DECREF(args); - args = tuple; - } else { - PyObject *tuple = PyTuple_Pack(1, args); - Py_DECREF(args); - args = tuple; - } - } - - /* Call function */ - PyObject *result = PyObject_Call(func, args, NULL); - Py_DECREF(func); - Py_DECREF(args); - - if (result == NULL) { - return make_py_error(env); - } - - /* Convert result to Erlang */ - ERL_NIF_TERM result_term = py_to_term(env, result); - Py_DECREF(result); - - return enif_make_tuple2(env, ATOM_OK, result_term); -} - -static ERL_NIF_TERM py_pool_process_apply(py_pool_worker_t *worker, - py_pool_request_t *req) { - ErlNifEnv *env = req->msg_env; - - /* Get module */ - PyObject *module = py_pool_get_module(worker, req->module_name); - if (module == NULL) { - return make_py_error(env); - } - - /* Get function */ - PyObject *func = PyObject_GetAttrString(module, req->func_name); - if (func == NULL) { - return make_py_error(env); - } - - /* Convert args to Python */ - PyObject *args = term_to_py(env, req->args_term); - if (args == NULL) { - Py_DECREF(func); - return make_error(env, "args_conversion_failed"); - } - - /* Ensure args is a tuple */ - if (!PyTuple_Check(args)) { - if (PyList_Check(args)) { - PyObject *tuple = PyList_AsTuple(args); - Py_DECREF(args); - args = tuple; - } else { - PyObject *tuple = PyTuple_Pack(1, args); - Py_DECREF(args); - args = tuple; - } - } - - /* Convert kwargs to Python dict */ - PyObject *kwargs = NULL; - if (enif_is_map(env, req->kwargs_term)) { - kwargs = term_to_py(env, req->kwargs_term); - if (kwargs != NULL && !PyDict_Check(kwargs)) { - Py_DECREF(kwargs); - kwargs = NULL; - } - } - - /* Call function with kwargs */ - PyObject *result = PyObject_Call(func, args, kwargs); - Py_DECREF(func); - Py_DECREF(args); - Py_XDECREF(kwargs); - - if (result == NULL) { - return make_py_error(env); - } - - /* Convert result to Erlang */ - ERL_NIF_TERM result_term = py_to_term(env, result); - Py_DECREF(result); - - return enif_make_tuple2(env, ATOM_OK, result_term); -} - -/* ============================================================================ - * Request Processing - EVAL/EXEC - * ============================================================================ */ - -static ERL_NIF_TERM py_pool_process_eval(py_pool_worker_t *worker, - py_pool_request_t *req) { - ErlNifEnv *env = req->msg_env; - - /* Compile code as expression */ - PyObject *code = Py_CompileString(req->code, "", Py_eval_input); - if (code == NULL) { - return make_py_error(env); - } - - /* Prepare locals if provided */ - PyObject *locals = worker->locals; - /* Check if locals_term was set (non-zero) before checking if it's a map */ - if (req->locals_term != 0 && enif_is_map(env, req->locals_term)) { - PyObject *new_locals = term_to_py(env, req->locals_term); - if (new_locals != NULL && PyDict_Check(new_locals)) { - /* Merge with existing locals */ - PyDict_Update(locals, new_locals); - Py_DECREF(new_locals); - } - } - - /* Evaluate */ - PyObject *result = PyEval_EvalCode(code, worker->globals, locals); - Py_DECREF(code); - - if (result == NULL) { - return make_py_error(env); - } - - /* Convert result to Erlang */ - ERL_NIF_TERM result_term = py_to_term(env, result); - Py_DECREF(result); - - return enif_make_tuple2(env, ATOM_OK, result_term); -} - -static ERL_NIF_TERM py_pool_process_exec(py_pool_worker_t *worker, - py_pool_request_t *req) { - ErlNifEnv *env = req->msg_env; - - /* Compile code as statements */ - PyObject *code = Py_CompileString(req->code, "", Py_file_input); - if (code == NULL) { - return make_py_error(env); - } - - /* Execute */ - PyObject *result = PyEval_EvalCode(code, worker->globals, worker->locals); - Py_DECREF(code); - - if (result == NULL) { - return make_py_error(env); - } - - Py_DECREF(result); - return enif_make_tuple2(env, ATOM_OK, ATOM_NONE); -} - -/* ============================================================================ - * Request Processing Dispatcher - * ============================================================================ */ - -static void py_pool_process_request(py_pool_worker_t *worker, - py_pool_request_t *req) { - uint64_t start_ns = get_monotonic_ns(); - ERL_NIF_TERM result; - - switch (req->type) { - case PY_POOL_REQ_CALL: - result = py_pool_process_call(worker, req); - break; - case PY_POOL_REQ_APPLY: - result = py_pool_process_apply(worker, req); - break; - case PY_POOL_REQ_EVAL: - result = py_pool_process_eval(worker, req); - break; - case PY_POOL_REQ_EXEC: - result = py_pool_process_exec(worker, req); - break; - case PY_POOL_REQ_SHUTDOWN: - /* Shutdown handled by worker thread */ - return; - default: - result = make_error(req->msg_env, "unknown_request_type"); - break; - } - - /* Send response */ - py_pool_send_response(req, result); - - /* Update stats */ - uint64_t elapsed_ns = get_monotonic_ns() - start_ns; - atomic_fetch_add(&worker->requests_processed, 1); - atomic_fetch_add(&worker->total_processing_ns, elapsed_ns); -} - -/* ============================================================================ - * Worker Thread - * ============================================================================ */ - -static void *py_pool_worker_thread(void *arg) { - py_pool_worker_t *worker = (py_pool_worker_t *)arg; - - /* Initialize Python state */ - gil_guard_t guard = {0}; - -#ifdef HAVE_SUBINTERPRETERS - if (g_pool.use_subinterpreters) { - /* Acquire GIL in main interpreter first */ - guard = gil_acquire(); - - /* Create sub-interpreter */ - PyInterpreterConfig config = { - .use_main_obmalloc = 0, - .allow_fork = 0, - .allow_exec = 0, - .allow_threads = 1, - .allow_daemon_threads = 0, - .check_multi_interp_extensions = 1, - .gil = PyInterpreterConfig_OWN_GIL, - }; - - PyStatus status = Py_NewInterpreterFromConfig(&worker->tstate, &config); - if (PyStatus_Exception(status)) { - gil_release(guard); - worker->running = false; - return NULL; - } - - worker->interp = PyThreadState_GetInterpreter(worker->tstate); - - /* Initialize event loop for this subinterpreter */ - if (init_subinterpreter_event_loop(NULL) < 0) { - gil_release(guard); - worker->running = false; - return NULL; - } - - /* Release main GIL - we now have our own */ - gil_release(guard); - - /* We're now attached to our sub-interpreter */ - } else -#endif - { - /* Non-subinterpreter mode: acquire the shared GIL */ - guard = gil_acquire(); - } - - /* Create per-worker state */ - worker->module_cache = PyDict_New(); - worker->globals = PyDict_New(); - worker->locals = PyDict_New(); - - if (worker->module_cache == NULL || - worker->globals == NULL || - worker->locals == NULL) { - goto cleanup; - } - - /* Add builtins to globals */ - PyObject *builtins = PyEval_GetBuiltins(); - if (builtins != NULL) { - PyDict_SetItemString(worker->globals, "__builtins__", builtins); - } - - worker->running = true; - - /* Main processing loop */ - while (!worker->shutdown) { - py_pool_request_t *req = NULL; - -#ifdef HAVE_SUBINTERPRETERS - if (g_pool.use_subinterpreters) { - /* Subinterpreter mode: we own our GIL, just dequeue and process */ - req = queue_dequeue(&g_pool.queue, true); - } else -#endif - { - /* Release GIL while waiting for work */ - Py_BEGIN_ALLOW_THREADS - req = queue_dequeue(&g_pool.queue, true); - Py_END_ALLOW_THREADS - } - - if (req == NULL || req->type == PY_POOL_REQ_SHUTDOWN) { - if (req != NULL) { - py_pool_request_free(req); - } - break; - } - - /* Process with GIL held (or in subinterpreter with own GIL) */ - py_pool_process_request(worker, req); - py_pool_request_free(req); - } - -cleanup: - /* Clean up Python state */ - Py_XDECREF(worker->module_cache); - Py_XDECREF(worker->globals); - Py_XDECREF(worker->locals); - worker->module_cache = NULL; - worker->globals = NULL; - worker->locals = NULL; - -#ifdef HAVE_SUBINTERPRETERS - if (g_pool.use_subinterpreters && worker->tstate != NULL) { - Py_EndInterpreter(worker->tstate); - worker->tstate = NULL; - worker->interp = NULL; - } else -#endif - { - gil_release(guard); - } - - worker->running = false; - return NULL; -} - -/* ============================================================================ - * Pool Lifecycle - * ============================================================================ */ - -static int py_pool_init(int num_workers) { - /* Init/shutdown are serialized by the single Erlang gen_server that owns the - * pool, so this check-then-init runs without a concurrent caller and needs no - * extra lock. */ - if (g_pool.initialized) { - return 0; /* Already initialized */ - } - - /* Determine number of workers */ - if (num_workers <= 0) { - /* Auto-detect: use number of CPUs */ - long ncpus = sysconf(_SC_NPROCESSORS_ONLN); - num_workers = (ncpus > 0) ? (int)ncpus : 4; - } - if (num_workers > POOL_MAX_WORKERS) { - num_workers = POOL_MAX_WORKERS; - } - - /* Detect execution mode */ -#ifdef HAVE_FREE_THREADED - g_pool.free_threaded = true; - g_pool.use_subinterpreters = false; -#elif defined(HAVE_SUBINTERPRETERS) - g_pool.free_threaded = false; - g_pool.use_subinterpreters = true; -#else - g_pool.free_threaded = false; - g_pool.use_subinterpreters = false; -#endif - - /* Initialize queue */ - queue_init(&g_pool.queue); - - /* Initialize workers */ - g_pool.num_workers = num_workers; - for (int i = 0; i < num_workers; i++) { - py_pool_worker_t *worker = &g_pool.workers[i]; - memset(worker, 0, sizeof(py_pool_worker_t)); - worker->worker_id = i; - worker->shutdown = false; - atomic_store(&worker->requests_processed, 0); - atomic_store(&worker->total_processing_ns, 0); - } - - /* Start worker threads */ - for (int i = 0; i < num_workers; i++) { - py_pool_worker_t *worker = &g_pool.workers[i]; - int rc = pthread_create(&worker->thread, NULL, - py_pool_worker_thread, worker); - if (rc != 0) { - /* Failed to create thread - shut down already created ones */ - g_pool.shutting_down = true; - queue_broadcast(&g_pool.queue); - for (int j = 0; j < i; j++) { - pthread_join(g_pool.workers[j].thread, NULL); - } - queue_destroy(&g_pool.queue); - return -1; - } - } - - /* Wait for workers to start */ - for (int i = 0; i < num_workers; i++) { - while (!g_pool.workers[i].running && !g_pool.workers[i].shutdown) { - usleep(1000); /* 1ms */ - } - } - - g_pool.initialized = true; - return 0; -} - -static void py_pool_shutdown(void) { - if (!g_pool.initialized) { - return; - } - - g_pool.shutting_down = true; - - /* Send shutdown requests to all workers */ - for (int i = 0; i < g_pool.num_workers; i++) { - g_pool.workers[i].shutdown = true; - - /* Enqueue shutdown request to wake up workers */ - py_pool_request_t *shutdown_req = py_pool_request_new( - PY_POOL_REQ_SHUTDOWN, (ErlNifPid){0}); - if (shutdown_req != NULL) { - queue_enqueue(&g_pool.queue, shutdown_req); - } - } - - /* Wake up all waiting workers */ - queue_broadcast(&g_pool.queue); - - /* Wait for workers to terminate */ - for (int i = 0; i < g_pool.num_workers; i++) { - pthread_join(g_pool.workers[i].thread, NULL); - } - - /* Drain and free remaining requests */ - py_pool_request_t *req; - while ((req = queue_dequeue(&g_pool.queue, false)) != NULL) { - /* Send error response for abandoned requests */ - if (req->type != PY_POOL_REQ_SHUTDOWN && req->msg_env != NULL) { - ERL_NIF_TERM error = make_error(req->msg_env, "pool_shutdown"); - py_pool_send_response(req, error); - } - py_pool_request_free(req); - } - - queue_destroy(&g_pool.queue); - g_pool.initialized = false; - g_pool.shutting_down = false; -} - -static int py_pool_enqueue(py_pool_request_t *req) { - if (!g_pool.initialized || g_pool.shutting_down) { - return -1; - } - - queue_enqueue(&g_pool.queue, req); - return 0; -} - -/* ============================================================================ - * Statistics - * ============================================================================ */ - -static void py_pool_get_stats(py_pool_stats_t *stats) { - memset(stats, 0, sizeof(py_pool_stats_t)); - - stats->num_workers = g_pool.num_workers; - stats->initialized = g_pool.initialized; - stats->use_subinterpreters = g_pool.use_subinterpreters; - stats->free_threaded = g_pool.free_threaded; - stats->pending_count = atomic_load(&g_pool.queue.pending_count); - stats->total_enqueued = atomic_load(&g_pool.queue.total_enqueued); - - for (int i = 0; i < g_pool.num_workers && i < POOL_MAX_WORKERS; i++) { - stats->worker_stats[i].requests_processed = - atomic_load(&g_pool.workers[i].requests_processed); - stats->worker_stats[i].total_processing_ns = - atomic_load(&g_pool.workers[i].total_processing_ns); - } -} - -/* ============================================================================ - * NIF Functions - * ============================================================================ */ - -static ERL_NIF_TERM nif_pool_start(ErlNifEnv *env, int argc, - const ERL_NIF_TERM argv[]) { - if (argc != 1) { - return enif_make_badarg(env); - } - - int num_workers; - if (!enif_get_int(env, argv[0], &num_workers)) { - return enif_make_badarg(env); - } - - if (py_pool_init(num_workers) != 0) { - return make_error(env, "failed_to_start_pool"); - } - - return ATOM_OK; -} - -static ERL_NIF_TERM nif_pool_stop(ErlNifEnv *env, int argc, - const ERL_NIF_TERM argv[]) { - (void)argc; - (void)argv; - - py_pool_shutdown(); - return ATOM_OK; -} - -static ERL_NIF_TERM nif_pool_submit(ErlNifEnv *env, int argc, - const ERL_NIF_TERM argv[]) { - if (argc != 5) { - return enif_make_badarg(env); - } - - if (!g_pool.initialized) { - return make_error(env, "pool_not_started"); - } - - /* Get request type atom */ - char type_buf[32]; - if (!enif_get_atom(env, argv[0], type_buf, sizeof(type_buf), ERL_NIF_LATIN1)) { - return enif_make_badarg(env); - } - - py_pool_request_type_t type; - if (strcmp(type_buf, "call") == 0) { - type = PY_POOL_REQ_CALL; - } else if (strcmp(type_buf, "apply") == 0) { - type = PY_POOL_REQ_APPLY; - } else if (strcmp(type_buf, "eval") == 0) { - type = PY_POOL_REQ_EVAL; - } else if (strcmp(type_buf, "exec") == 0) { - type = PY_POOL_REQ_EXEC; - } else { - return make_error(env, "unknown_request_type"); - } - - /* Get caller PID */ - ErlNifPid caller_pid; - if (!enif_self(env, &caller_pid)) { - return make_error(env, "cannot_get_self_pid"); - } - - /* Create request */ - py_pool_request_t *req = py_pool_request_new(type, caller_pid); - if (req == NULL) { - return make_error(env, "request_allocation_failed"); - } - - /* Parse arguments based on type */ - switch (type) { - case PY_POOL_REQ_CALL: - case PY_POOL_REQ_APPLY: { - /* argv[1] = Module, argv[2] = Func, argv[3] = Args, argv[4] = Kwargs/undefined */ - ErlNifBinary module_bin, func_bin; - if (!enif_inspect_binary(env, argv[1], &module_bin) || - !enif_inspect_binary(env, argv[2], &func_bin)) { - py_pool_request_free(req); - return enif_make_badarg(env); - } - - req->module_name = enif_alloc(module_bin.size + 1); - req->func_name = enif_alloc(func_bin.size + 1); - if (req->module_name == NULL || req->func_name == NULL) { - py_pool_request_free(req); - return make_error(env, "allocation_failed"); - } - - memcpy(req->module_name, module_bin.data, module_bin.size); - req->module_name[module_bin.size] = '\0'; - memcpy(req->func_name, func_bin.data, func_bin.size); - req->func_name[func_bin.size] = '\0'; - - req->args_term = enif_make_copy(req->msg_env, argv[3]); - - if (type == PY_POOL_REQ_APPLY && !enif_is_atom(env, argv[4])) { - req->kwargs_term = enif_make_copy(req->msg_env, argv[4]); - } - break; - } - - case PY_POOL_REQ_EVAL: - case PY_POOL_REQ_EXEC: { - /* argv[1] = Code, argv[2-4] = unused */ - ErlNifBinary code_bin; - if (!enif_inspect_binary(env, argv[1], &code_bin)) { - py_pool_request_free(req); - return enif_make_badarg(env); - } - - req->code = enif_alloc(code_bin.size + 1); - if (req->code == NULL) { - py_pool_request_free(req); - return make_error(env, "allocation_failed"); - } - - memcpy(req->code, code_bin.data, code_bin.size); - req->code[code_bin.size] = '\0'; - - if (!enif_is_atom(env, argv[2])) { - req->locals_term = enif_make_copy(req->msg_env, argv[2]); - } - break; - } - - default: - py_pool_request_free(req); - return make_error(env, "unknown_request_type"); - } - - /* IMPORTANT: Save request_id BEFORE enqueueing. - * Once enqueued, a worker can process and free the request at any time. - * Accessing req->request_id after enqueue is use-after-free. */ - uint64_t request_id = req->request_id; - - /* Enqueue request */ - if (py_pool_enqueue(req) != 0) { - py_pool_request_free(req); - return make_error(env, "enqueue_failed"); - } - - /* Return {ok, RequestId} - using saved ID to avoid use-after-free */ - return enif_make_tuple2(env, ATOM_OK, - enif_make_uint64(env, request_id)); -} - -static ERL_NIF_TERM nif_pool_stats(ErlNifEnv *env, int argc, - const ERL_NIF_TERM argv[]) { - (void)argc; - (void)argv; - - py_pool_stats_t stats; - py_pool_get_stats(&stats); - - /* Build result map */ - ERL_NIF_TERM keys[8], values[8]; - - keys[0] = enif_make_atom(env, "num_workers"); - values[0] = enif_make_int(env, stats.num_workers); - - keys[1] = enif_make_atom(env, "initialized"); - values[1] = stats.initialized ? ATOM_TRUE : ATOM_FALSE; - - keys[2] = enif_make_atom(env, "use_subinterpreters"); - values[2] = stats.use_subinterpreters ? ATOM_TRUE : ATOM_FALSE; - - keys[3] = enif_make_atom(env, "free_threaded"); - values[3] = stats.free_threaded ? ATOM_TRUE : ATOM_FALSE; - - keys[4] = enif_make_atom(env, "pending_count"); - values[4] = enif_make_uint64(env, stats.pending_count); - - keys[5] = enif_make_atom(env, "total_enqueued"); - values[5] = enif_make_uint64(env, stats.total_enqueued); - - keys[6] = enif_make_atom(env, "responses_sent"); - values[6] = enif_make_uint64(env, atomic_load(&g_responses_sent)); - - keys[7] = enif_make_atom(env, "responses_failed"); - values[7] = enif_make_uint64(env, atomic_load(&g_responses_failed)); - - ERL_NIF_TERM result; - enif_make_map_from_arrays(env, keys, values, 8, &result); - - return result; -} - -/* Initialize pool-specific atoms */ -static int pool_atoms_init(ErlNifEnv *env) { - ATOM_PY_RESPONSE = enif_make_atom(env, "py_response"); - return 0; -} diff --git a/c_src/py_worker_pool.h b/c_src/py_worker_pool.h deleted file mode 100644 index 2ca61471..00000000 --- a/c_src/py_worker_pool.h +++ /dev/null @@ -1,496 +0,0 @@ -/* - * Copyright 2026 Benoit Chesneau - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * @file py_worker_pool.h - * @brief Worker thread pool for Python operations - * @author Benoit Chesneau - * - * @section overview Overview - * - * This module implements a general-purpose worker thread pool for Python - * calls (py:call, py:eval). Each worker has its own subinterpreter - * (Python 3.12+) or dedicated GIL-holding thread, processing requests from a - * shared queue. - * - * @section architecture Architecture - * - * ``` - * Erlang Processes Lock-free Queue Python Workers - * [P1]--enqueue--+ +---------+ +-------------------------+ - * [P2]--enqueue--+---->| Request |<--poll---| Worker 0 (Subinterp+GIL)| - * [P3]--enqueue--+ | Queue | | - Holds GIL | - * ... | | (MPSC) |<--poll---| Worker 1 (Subinterp+GIL)| - * [PN]--enqueue--+ +---------+ +-------------------------+ - * ``` - * - * @section benefits Key Benefits - * - * - No GIL acquire/release per request (workers hold GIL) - * - Module/callable cached per worker (no reimport) - * - True parallelism with subinterpreters (each has OWN_GIL) - * - * @section modes Python Mode Support - * - * | Mode | Python Version | Strategy | - * |------|----------------|----------| - * | FREE_THREADED | 3.13+ (no-GIL) | N workers, no GIL needed | - * | SUBINTERP | 3.12+ | N subinterpreters, each OWN_GIL | - * | FALLBACK | <3.12 | N workers share GIL, batching reduces overhead | - */ - -#ifndef PY_WORKER_POOL_H -#define PY_WORKER_POOL_H - -#include "py_nif.h" - -/* ============================================================================ - * Configuration - * ============================================================================ */ - -/** - * @def POOL_MAX_WORKERS - * @brief Maximum number of workers in the pool - */ -#define POOL_MAX_WORKERS 32 - -/** - * @def POOL_QUEUE_SIZE - * @brief Size of the request queue (power of 2 for efficient modulo) - */ -#define POOL_QUEUE_SIZE 4096 - -/** - * @def POOL_DEFAULT_WORKERS - * @brief Default number of workers (0 = use CPU count) - */ -#define POOL_DEFAULT_WORKERS 0 - -/* ============================================================================ - * Request Types - * ============================================================================ */ - -/** - * @enum py_pool_request_type_t - * @brief Types of requests that can be submitted to the worker pool - */ -typedef enum { - PY_POOL_REQ_CALL, /**< py:call(Module, Func, Args) */ - PY_POOL_REQ_APPLY, /**< py:apply(Module, Func, Args, Kwargs) */ - PY_POOL_REQ_EVAL, /**< py:eval(Code) */ - PY_POOL_REQ_EXEC, /**< py:exec(Code) */ - PY_POOL_REQ_SHUTDOWN /**< Shutdown signal */ -} py_pool_request_type_t; - -/* ============================================================================ - * Request Structure - * ============================================================================ */ - -/** - * @struct py_pool_request_t - * @brief Request submitted to the worker pool - * - * Contains all information needed to process a Python operation. - * The result is sent back to the caller via enif_send(). - */ -typedef struct py_pool_request { - /** @brief Unique request ID for correlation */ - uint64_t request_id; - - /** @brief Type of operation to perform */ - py_pool_request_type_t type; - - /** @brief PID of the calling Erlang process */ - ErlNifPid caller_pid; - - /** @brief Environment for building result terms (thread-safe copy) */ - ErlNifEnv *msg_env; - - /* ========== CALL/APPLY parameters ========== */ - - /** @brief Module name (heap-allocated, NULL-terminated) */ - char *module_name; - - /** @brief Function name (heap-allocated, NULL-terminated) */ - char *func_name; - - /** @brief Arguments list term (copied to msg_env) */ - ERL_NIF_TERM args_term; - - /** @brief Keyword arguments map term (copied to msg_env, optional) */ - ERL_NIF_TERM kwargs_term; - - /* ========== EVAL/EXEC parameters ========== */ - - /** @brief Python code to evaluate/execute (heap-allocated, NULL-terminated) */ - char *code; - - /** @brief Local variables for eval (copied to msg_env) */ - ERL_NIF_TERM locals_term; - - /* ========== Timeout ========== */ - - /** @brief Timeout in milliseconds (0 = no timeout) */ - unsigned long timeout_ms; - - /* ========== Queue linkage ========== */ - - /** @brief Next request in queue (for linked list) */ - struct py_pool_request *next; -} py_pool_request_t; - -/* ============================================================================ - * Worker Structure - * ============================================================================ */ - -/** - * @struct py_pool_worker_t - * @brief Single worker thread in the pool - * - * Each worker runs in its own thread and optionally has its own - * subinterpreter (Python 3.12+) for true parallelism. - */ -typedef struct { - /** @brief Worker thread handle */ - pthread_t thread; - - /** @brief Worker ID (0 to num_workers-1) */ - int worker_id; - - /** @brief Flag: worker is running */ - volatile bool running; - - /** @brief Flag: worker should shut down */ - volatile bool shutdown; - -#ifdef HAVE_SUBINTERPRETERS - /** @brief Python interpreter for this worker */ - PyInterpreterState *interp; - - /** @brief Thread state in this interpreter */ - PyThreadState *tstate; -#endif - - /* ========== Cached state per worker ========== */ - - /** @brief Module cache (Dict: module_name -> PyModule) */ - PyObject *module_cache; - - /** @brief Global namespace for eval/exec */ - PyObject *globals; - - /** @brief Local namespace for eval/exec */ - PyObject *locals; - - /* ========== Statistics ========== */ - - /** @brief Total requests processed by this worker */ - _Atomic uint64_t requests_processed; - - /** @brief Total processing time in nanoseconds */ - _Atomic uint64_t total_processing_ns; -} py_pool_worker_t; - -/* ============================================================================ - * Request Queue Structure - * ============================================================================ */ - -/** - * @struct py_pool_queue_t - * @brief MPSC (Multi-Producer Single-Consumer) queue for requests - * - * Uses a simple linked list with mutex protection. Workers dequeue - * using condition variable waits. - */ -typedef struct { - /** @brief Queue head (oldest request) */ - py_pool_request_t *head; - - /** @brief Queue tail (newest request) */ - py_pool_request_t *tail; - - /** @brief Number of pending requests */ - _Atomic uint64_t pending_count; - - /** @brief Total requests enqueued */ - _Atomic uint64_t total_enqueued; - - /** @brief Mutex protecting the queue */ - pthread_mutex_t mutex; - - /** @brief Condition variable for worker notification */ - pthread_cond_t cond; -} py_pool_queue_t; - -/* ============================================================================ - * Worker Pool Structure - * ============================================================================ */ - -/** - * @struct py_worker_pool_t - * @brief The main worker pool structure - */ -typedef struct { - /** @brief Array of workers */ - py_pool_worker_t workers[POOL_MAX_WORKERS]; - - /** @brief Number of active workers */ - int num_workers; - - /** @brief Request queue */ - py_pool_queue_t queue; - - /** @brief Flag: pool is initialized */ - volatile bool initialized; - - /** @brief Flag: pool is shutting down */ - volatile bool shutting_down; - - /** @brief Request ID counter */ - _Atomic uint64_t request_id_counter; - - /** @brief Mode: use subinterpreters */ - bool use_subinterpreters; - - /** @brief Mode: free-threaded Python */ - bool free_threaded; -} py_worker_pool_t; - -/* ============================================================================ - * Global Pool Instance - * ============================================================================ */ - -/** @brief Global worker pool instance */ -extern py_worker_pool_t g_pool; - -/* ============================================================================ - * Pool Lifecycle Functions - * ============================================================================ */ - -/** - * @brief Initialize the worker pool - * - * Creates and starts num_workers worker threads. If num_workers is 0, - * uses the number of CPU cores. - * - * @param num_workers Number of workers (0 = auto-detect CPU count) - * @return 0 on success, -1 on failure - */ -static int py_pool_init(int num_workers); - -/** - * @brief Shut down the worker pool - * - * Signals all workers to stop and waits for them to terminate. - * Processes any remaining requests with error responses. - */ -static void py_pool_shutdown(void); - -/* ============================================================================ - * Request Submission Functions - * ============================================================================ */ - -/** - * @brief Submit a request to the pool - * - * Thread-safe enqueue operation. The request is processed by an - * available worker and the result is sent to caller_pid. - * - * @param req Request to submit (ownership transferred to pool) - * @return 0 on success, -1 if pool not initialized - */ -static int py_pool_enqueue(py_pool_request_t *req); - -/** - * @brief Create a new pool request - * - * Allocates and initializes a request structure. - * - * @param type Request type - * @param caller_pid Calling process PID - * @return New request, or NULL on allocation failure - */ -static py_pool_request_t *py_pool_request_new(py_pool_request_type_t type, - ErlNifPid caller_pid); - -/** - * @brief Free a pool request - * - * Releases all resources associated with the request. - * - * @param req Request to free - */ -static void py_pool_request_free(py_pool_request_t *req); - -/* ============================================================================ - * Worker Functions - * ============================================================================ */ - -/** - * @brief Worker thread main function - * - * Entry point for worker threads. Processes requests until shutdown. - * - * @param arg Pointer to py_pool_worker_t - * @return NULL - */ -static void *py_pool_worker_thread(void *arg); - -/** - * @brief Process a single request - * - * Dispatches based on request type and sends result to caller. - * - * @param worker Worker processing the request - * @param req Request to process - */ -static void py_pool_process_request(py_pool_worker_t *worker, - py_pool_request_t *req); - -/** - * @brief Send response to caller - * - * Uses enif_send() to send result back to calling process. - * - * @param req Request with caller info - * @param result Result term to send - */ -static void py_pool_send_response(py_pool_request_t *req, ERL_NIF_TERM result); - -/* ============================================================================ - * Request Processing Functions - * ============================================================================ */ - -/** - * @brief Process CALL request - * - * @param worker Worker processing request - * @param req Request with module, func, args - * @return Result term - */ -static ERL_NIF_TERM py_pool_process_call(py_pool_worker_t *worker, - py_pool_request_t *req); - -/** - * @brief Process APPLY request - * - * @param worker Worker processing request - * @param req Request with module, func, args, kwargs - * @return Result term - */ -static ERL_NIF_TERM py_pool_process_apply(py_pool_worker_t *worker, - py_pool_request_t *req); - -/** - * @brief Process EVAL request - * - * @param worker Worker processing request - * @param req Request with code - * @return Result term - */ -static ERL_NIF_TERM py_pool_process_eval(py_pool_worker_t *worker, - py_pool_request_t *req); - -/** - * @brief Process EXEC request - * - * @param worker Worker processing request - * @param req Request with code - * @return Result term - */ -static ERL_NIF_TERM py_pool_process_exec(py_pool_worker_t *worker, - py_pool_request_t *req); - -/* ============================================================================ - * Module Caching - * ============================================================================ */ - -/** - * @brief Get or import a Python module - * - * Checks the worker's module cache first, imports if not cached. - * - * @param worker Worker with module cache - * @param module_name Module name to get - * @return Borrowed reference to module, or NULL on error - */ -static PyObject *py_pool_get_module(py_pool_worker_t *worker, - const char *module_name); - -/* ============================================================================ - * Statistics - * ============================================================================ */ - -/** - * @brief Pool statistics structure - */ -typedef struct { - int num_workers; - bool initialized; - bool use_subinterpreters; - bool free_threaded; - uint64_t pending_count; - uint64_t total_enqueued; - struct { - uint64_t requests_processed; - uint64_t total_processing_ns; - } worker_stats[POOL_MAX_WORKERS]; -} py_pool_stats_t; - -/** - * @brief Get pool statistics - * - * @param stats Output structure for statistics - */ -static void py_pool_get_stats(py_pool_stats_t *stats); - -/* ============================================================================ - * NIF Functions - * ============================================================================ */ - -/** - * @brief NIF: Start the worker pool - * - * py_nif:pool_start(NumWorkers) -> ok | {error, Reason} - */ -static ERL_NIF_TERM nif_pool_start(ErlNifEnv *env, int argc, - const ERL_NIF_TERM argv[]); - -/** - * @brief NIF: Stop the worker pool - * - * py_nif:pool_stop() -> ok - */ -static ERL_NIF_TERM nif_pool_stop(ErlNifEnv *env, int argc, - const ERL_NIF_TERM argv[]); - -/** - * @brief NIF: Submit a request to the pool - * - * py_nif:pool_submit(Type, Arg1, Arg2, Arg3, Arg4) -> {ok, RequestId} | {error, Reason} - */ -static ERL_NIF_TERM nif_pool_submit(ErlNifEnv *env, int argc, - const ERL_NIF_TERM argv[]); - -/** - * @brief NIF: Get pool statistics - * - * py_nif:pool_stats() -> StatsMap - */ -static ERL_NIF_TERM nif_pool_stats(ErlNifEnv *env, int argc, - const ERL_NIF_TERM argv[]); - -#endif /* PY_WORKER_POOL_H */ diff --git a/docs/architecture.md b/docs/architecture.md index 53e6426e..841a2a74 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -79,10 +79,9 @@ one Python execution environment and serves calls in order. Pools `{py_result, Ref, Result}` to the `py_context` process, which replies `From ! {MRef, Result}`. -The older paths in `nif_context_call` (a blocking variant with an inline -"legacy" executor) are kept for the fallback -`{error, async_requires_worker_thread}` and are not taken by contexts -created today; see [code map](code-map.md) for the list of legacy code. +`nif_context_call` and friends also have a blocking variant used as a +fallback when a context has no thread (`{error, async_requires_worker_thread}`), +which never happens for contexts created today. ### isolated @@ -118,9 +117,7 @@ this order (the comment above it is the authoritative version): a request on a pipe and blocks; the `py_context` process has a dedicated handler (`callback_handler_loop/1`) that runs the fun and writes the response frame back with `context_write_callback_response`. -3. **Legacy worker handler** (`worker_*` NIFs): only used by - `examples/gen_test.erl`. -4. **Thread worker** (`c_src/py_thread_worker.c`): any Python thread that is +3. **Thread worker** (`c_src/py_thread_worker.c`): any Python thread that is not a context thread (`threading.Thread`, executors) asks the `py_thread_handler` coordinator for a handler process and talks to it over a pipe. There is also an async variant (`erlang.async_call`) using a @@ -190,9 +187,8 @@ loop and [asyncio](asyncio.md) for the API. ## What is live and what is not -Kept for now, not used by current contexts: the `worker_*` NIF API and its -single executor thread (`c_src/py_exec.c`), the `async_worker_*` NIFs (they -return `deprecated`), `c_src/py_worker_pool.c` (no caller), the inline -"legacy" executor branches in `nif_context_*`, and the test-only fd NIFs in -`c_src/py_event_loop.c`. They are listed in the [code map](code-map.md) so -nobody debugs them by mistake; removing them is planned. +Every code path in `src/` and `c_src/` is on the path of a context created +today, with one exception: the test-only fd/TCP/UDP NIFs in +`c_src/py_event_loop.c` ("Test Helper Functions"), which the suites use. +The legacy worker API, its executor thread, the deprecated async worker +NIFs and the unused worker pool were removed in 5.0.0. diff --git a/docs/code-map.md b/docs/code-map.md index 31398715..5b237d8e 100644 --- a/docs/code-map.md +++ b/docs/code-map.md @@ -1,9 +1,8 @@ # Code map Every source file, what it owns, and where to look for its behaviour. Status -is `live` (on the path of a context created today), `legacy` (kept for -compatibility, no current caller in `src/`), or `test` (only exercised by -suites). Guides are in `docs/`, suites in `test/`. Start with +is `live` (on the path of a context created today) or `test` (only +exercised by suites). Guides are in `docs/`, suites in `test/`. Start with [architecture](architecture.md). ## Erlang (`src/`) @@ -42,9 +41,9 @@ files. Editing `py_convert.c` alone does not compile it alone; build with | File | Owns | Status | |---|---|---| | `py_nif.h` | Every shared type: `py_context_t`, request types, runtime state machine, atoms, globals | live | -| `py_nif.c` | Runtime init, context creation and destruction, the request queue and the two context thread mains, the process-per-context NIFs (`nif_context_*`), process-local envs, `py_ref`, the NIF table | live, with legacy branches | +| `py_nif.c` | Runtime init, context creation and destruction, the request queue and the two context thread mains, the process-per-context NIFs (`nif_context_*`), process-local envs, `py_ref`, the NIF table | live | | `py_convert.c` | `py_to_term` / `term_to_py`, the type mapping, tagged tuples (`{bytes, B}`, shared handles) | live | -| `py_exec.c` | Execution with suspension support; the legacy single executor thread | live (suspension), legacy (executor) | +| `py_exec.c` | Execution mode detection and GIL helpers | live | | `py_callback.c` | The `erlang` Python module: `call`, `send`, `whereis`, `Atom`/`Pid`/`Ref` types, schedule markers, callback pipes, channel and shared dict methods | live | | `py_thread_worker.c` | Python threads calling Erlang through `py_thread_handler` | live | | `py_subinterp_thread.c` | Sub-interpreter thread pool used by owngil contexts and loop pools | live | @@ -52,14 +51,10 @@ files. Editing `py_convert.c` alone does not compile it alone; build with | `py_channel.c`, `py_buffer.c`, `py_reactor_buffer.c`, `py_shared_dict.c` | The corresponding resources and their Python-facing methods | live | | `py_logging.c` | Logging and tracing NIFs | live | | `py_mem_limit.c` | Per-interpreter memory caps (owngil) | live | -| `py_worker_pool.c/.h` | An older worker pool | legacy, no caller | | `py_util.c/.h` | Macros and helpers | live | -Inside `py_nif.c`, these are legacy: the `worker_*` NIFs and the "Worker -management" section, the `async_worker_*` NIFs (return `deprecated`), the -inline executor branches marked "Legacy mode" in `nif_context_call`, -`nif_context_eval`, `nif_context_exec`, and the `cancel_reader/writer` -aliases. +The only code not on a live path is the "Test Helper Functions" section of +`py_event_loop.c` (fd, pipe, TCP and UDP helpers the suites use). ## Python (`priv/`) diff --git a/docs/glossary.md b/docs/glossary.md index cf5864c5..439622eb 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -40,8 +40,6 @@ The most overloaded word. Meanings, by file: |---|---|---| | `py_context:new(#{mode => worker})` | the context mode above | worker mode | | `worker_context_thread_main`, `uses_worker_thread` (`py_nif.c`) | the pthread that serves a context's queue | context thread | -| `worker_new/call/eval/exec` NIFs, `py_worker_t` | the legacy per-worker API, before contexts | legacy worker API | -| `py_worker_pool.c`, `py_pool_worker_t` | an older pool, no caller | legacy pool | | `thread_worker`, `thread_worker_call` (`py_thread_worker.c`), `py_thread_handler` | the channel a Python thread uses to call Erlang | thread callback bridge | | `py_event_worker` | the Erlang process that drives one asyncio loop (readiness, timers) | loop driver | | `docs/workers.md`, "worker loop" | a long-running asyncio loop on a context thread, gunicorn-style | worker loop | @@ -51,7 +49,7 @@ The most overloaded word. Meanings, by file: `py_context_router` pools (`py:call(Pool, M, F, A)`): named sets of contexts routed by scheduler. `py_event_loop_pool`: main-interpreter asyncio loops with process affinity. `g_thread_pool` in `py_subinterp_thread.c`: the -threads behind owngil contexts. `g_pool` in `py_worker_pool.c`: legacy. +threads behind owngil contexts. ## Callback diff --git a/docs/scalability.md b/docs/scalability.md index a725af70..4968bc76 100644 --- a/docs/scalability.md +++ b/docs/scalability.md @@ -495,7 +495,7 @@ def process(x): │ 2. Erlang executes the registered callback │ │ └──► May call py:call() to run Python (on different worker) │ │ │ -│ 3. Erlang calls resume_callback with result │ +│ 3. Erlang calls context_resume with result │ │ └──► Schedules dirty NIF to return result to Python │ │ │ │ 4. Python continues with the callback result │ diff --git a/examples/gen_test.erl b/examples/gen_test.erl index d3c75af4..27b5ad9f 100644 --- a/examples/gen_test.erl +++ b/examples/gen_test.erl @@ -1,58 +1,37 @@ -#!/usr/bin/env escript -%%% Generator iteration test --mode(compile). - -main(_) -> - code:add_patha("_build/default/lib/erlang_python/ebin"), - +%% Iterating Python generators from Erlang. +%% +%% A generator object cannot cross into Erlang, so the values are streamed: +%% py:stream/3 collects everything a generator yields, py:stream_start/3 +%% delivers them one message at a time. Run with: +%% +%% rebar3 shell +%% > c("examples/gen_test.erl"), gen_test:run(). +-module(gen_test). +-export([run/0]). + +run() -> {ok, _} = application:ensure_all_started(erlang_python), - - io:format("=== Generator Iteration Test ===~n~n"), - - %% Get a worker directly - ok = py_nif:init(), - {ok, Worker} = py_nif:worker_new(), - - %% Create a generator via eval - io:format("Creating generator (x**2 for x in range(5))...~n"), - {ok, {generator, Gen}} = py_nif:worker_eval(Worker, <<"(x**2 for x in range(5))">>, #{}), - io:format("Got generator ref~n~n"), - - %% Iterate manually - io:format("Iterating: "), - iterate(Worker, Gen), - io:format("~n~n"), - - %% Test with range - io:format("Range(10): "), - {ok, {generator, Gen2}} = py_nif:worker_eval(Worker, <<"iter(range(10))">>, #{}), - iterate(Worker, Gen2), - io:format("~n~n"), - - %% Test Fibonacci generator defined inline - io:format("Fibonacci via exec + call:~n"), - ok = py_nif:worker_exec(Worker, <<" -def fib(n): - a, b = 0, 1 - for _ in range(n): - yield a - a, b = b, a + b -">>), - {ok, {generator, Gen3}} = py_nif:worker_call(Worker, <<"__main__">>, <<"fib">>, [10], #{}), - io:format(" fib(10) = "), - iterate(Worker, Gen3), - io:format("~n~n"), - - io:format("=== Done ===~n"), - ok = application:stop(erlang_python). - -iterate(Worker, Gen) -> - case py_nif:worker_next(Worker, Gen) of - {ok, Value} -> - io:format("~p ", [Value]), - iterate(Worker, Gen); - {error, stop_iteration} -> - ok; - {error, Error} -> - io:format("Error: ~p", [Error]) + %% A generator expression, collected in one go + {ok, Squares} = py:stream_eval(<<"(x**2 for x in range(5))">>), + io:format("squares: ~p~n", [Squares]), + + %% Any iterable a module function returns + {ok, Range} = py:stream(builtins, range, [5]), + io:format("range: ~p~n", [Range]), + + %% One value per message, as the generator yields them + {ok, Ref} = py:stream_start(builtins, iter, [[1, 2, 3]]), + receive_all(Ref). + +receive_all(Ref) -> + receive + {py_stream, Ref, {data, V}} -> + io:format("got ~p~n", [V]), + receive_all(Ref); + {py_stream, Ref, done} -> + io:format("done~n"); + {py_stream, Ref, {error, Reason}} -> + io:format("error: ~p~n", [Reason]) + after 5000 -> + timeout end. diff --git a/src/erlang_python.app.src b/src/erlang_python.app.src index 70a95fad..c478e298 100644 --- a/src/erlang_python.app.src +++ b/src/erlang_python.app.src @@ -1,6 +1,6 @@ {application, erlang_python, [ {description, "Execute Python applications from Erlang using dirty NIFs"}, - {vsn, "4.2.0"}, + {vsn, "5.0.0"}, {registered, []}, {mod, {erlang_python_app, []}}, {applications, [ diff --git a/src/py_nif.erl b/src/py_nif.erl index e868dc87..f4dddc39 100644 --- a/src/py_nif.erl +++ b/src/py_nif.erl @@ -24,17 +24,6 @@ init/0, init/1, finalize/0, - worker_new/0, - worker_new/1, - worker_destroy/1, - worker_call/5, - worker_call/6, - worker_eval/3, - worker_eval/4, - worker_exec/2, - worker_next/2, - import_module/2, - get_attr/3, version/0, memory_stats/0, get_debug_counters/0, @@ -43,15 +32,7 @@ tracemalloc_start/0, tracemalloc_start/1, tracemalloc_stop/0, - set_callback_handler/2, - send_callback_response/2, - resume_callback/2, %% Async workers - async_worker_new/0, - async_worker_destroy/1, - async_call/6, - async_gather/3, - async_stream/6, %% Subinterpreter capability probes (Python 3.12+ / 3.14+) subinterp_supported/0, owngil_supported/0, @@ -126,8 +107,6 @@ start_reader/1, stop_writer/1, start_writer/1, - cancel_reader/2, %% Legacy alias for stop_reader - cancel_writer/2, %% Legacy alias for stop_writer close_fd/1, %% File descriptor utilities dup_fd/1, @@ -151,10 +130,6 @@ set_isolation_mode/1, set_shared_worker/1, %% Worker pool - pool_start/1, - pool_stop/0, - pool_submit/5, - pool_stats/0, %% Process-per-context API (no mutex) context_create/1, context_destroy/1, @@ -286,82 +261,10 @@ init(_Opts) -> finalize() -> ?NIF_STUB. -%%% ============================================================================ -%%% Worker Management -%%% ============================================================================ - -%% @doc Create a new Python worker context. -%% Returns an opaque reference to be used with other worker functions. --spec worker_new() -> {ok, reference()} | {error, term()}. -worker_new() -> - worker_new(#{}). - -%% @doc Create a worker with options. -%% Options: -%% use_subinterpreter => boolean() - Use a separate sub-interpreter (Python 3.12+) --spec worker_new(map()) -> {ok, reference()} | {error, term()}. -worker_new(_Opts) -> - ?NIF_STUB. - -%% @doc Destroy a worker context. --spec worker_destroy(reference()) -> ok. -worker_destroy(_WorkerRef) -> - ?NIF_STUB. - -%% @doc Call a Python function from a worker. -%% This is a dirty NIF that acquires the GIL. -%% May return {suspended, ...} if Python calls erlang.call() (reentrant callback). --spec worker_call(reference(), binary(), binary(), list(), map()) -> - {ok, term()} | {error, term()} | {suspended, term(), reference(), {binary(), term()}}. -worker_call(_WorkerRef, _Module, _Func, _Args, _Kwargs) -> - ?NIF_STUB. - -%% @doc Call a Python function from a worker with timeout. -%% May return {suspended, ...} if Python calls erlang.call() (reentrant callback). --spec worker_call(reference(), binary(), binary(), list(), map(), non_neg_integer()) -> - {ok, term()} | {error, term()} | {suspended, term(), reference(), {binary(), term()}}. -worker_call(_WorkerRef, _Module, _Func, _Args, _Kwargs, _TimeoutMs) -> - ?NIF_STUB. - -%% @doc Evaluate a Python expression in a worker. -%% May return {suspended, ...} if Python calls erlang.call() (reentrant callback). --spec worker_eval(reference(), binary(), map()) -> - {ok, term()} | {error, term()} | {suspended, term(), reference(), {binary(), term()}}. -worker_eval(_WorkerRef, _Code, _Locals) -> - ?NIF_STUB. - -%% @doc Evaluate a Python expression in a worker with timeout. -%% May return {suspended, ...} if Python calls erlang.call() (reentrant callback). --spec worker_eval(reference(), binary(), map(), non_neg_integer()) -> - {ok, term()} | {error, term()} | {suspended, term(), reference(), {binary(), term()}}. -worker_eval(_WorkerRef, _Code, _Locals, _TimeoutMs) -> - ?NIF_STUB. - -%% @doc Execute Python statements in a worker. --spec worker_exec(reference(), binary()) -> ok | {error, term()}. -worker_exec(_WorkerRef, _Code) -> - ?NIF_STUB. - -%% @doc Get next item from a generator/iterator. -%% Returns {ok, Value} | {error, stop_iteration} | {error, Error} --spec worker_next(reference(), reference()) -> {ok, term()} | {error, term()}. -worker_next(_WorkerRef, _GeneratorRef) -> - ?NIF_STUB. - %%% ============================================================================ %%% Module Operations %%% ============================================================================ -%% @doc Import a Python module in a worker context. --spec import_module(reference(), binary()) -> {ok, reference()} | {error, term()}. -import_module(_WorkerRef, _ModuleName) -> - ?NIF_STUB. - -%% @doc Get an attribute from a Python object. --spec get_attr(reference(), reference(), binary()) -> {ok, term()} | {error, term()}. -get_attr(_WorkerRef, _ObjRef, _AttrName) -> - ?NIF_STUB. - %%% ============================================================================ %%% Info %%% ============================================================================ @@ -420,65 +323,10 @@ tracemalloc_stop() -> %%% Callback Support %%% ============================================================================ -%% @doc Set callback handler process for a worker. -%% Returns {ok, Fd} where Fd is the file descriptor for sending responses. --spec set_callback_handler(reference(), pid()) -> {ok, integer()} | {error, term()}. -set_callback_handler(_WorkerRef, _HandlerPid) -> - ?NIF_STUB. - -%% @doc Send a callback response to a worker via file descriptor. --spec send_callback_response(integer(), binary()) -> ok | {error, term()}. -send_callback_response(_Fd, _Response) -> - ?NIF_STUB. - -%% @doc Resume a suspended Python callback with the result. -%% StateRef is the reference returned in the {suspended, ...} tuple. -%% Result is the callback result as a binary (status byte + data). -%% Returns {ok, FinalResult}, {error, Reason}, or another {suspended, ...} for nested callbacks. --spec resume_callback(reference(), binary()) -> - {ok, term()} | {error, term()} | {suspended, term(), reference(), {binary(), term()}}. -resume_callback(_StateRef, _Result) -> - ?NIF_STUB. - %%% ============================================================================ %%% Async Worker Support %%% ============================================================================ -%% @doc Create a new async worker with background event loop. -%% Returns an opaque reference to be used with async functions. --spec async_worker_new() -> {ok, reference()} | {error, term()}. -async_worker_new() -> - ?NIF_STUB. - -%% @doc Destroy an async worker. --spec async_worker_destroy(reference()) -> ok. -async_worker_destroy(_WorkerRef) -> - ?NIF_STUB. - -%% @doc Submit an async call to the event loop. -%% Args: AsyncWorkerRef, Module, Func, Args, Kwargs, CallerPid -%% Returns: {ok, AsyncId} | {ok, {immediate, Result}} | {error, term()} --spec async_call(reference(), binary(), binary(), list(), map(), pid()) -> - {ok, non_neg_integer() | {immediate, term()}} | {error, term()}. -async_call(_WorkerRef, _Module, _Func, _Args, _Kwargs, _CallerPid) -> - ?NIF_STUB. - -%% @doc Execute multiple async calls concurrently using asyncio.gather. -%% Args: AsyncWorkerRef, CallsList (list of {Module, Func, Args}), CallerPid -%% Returns: {ok, AsyncId} | {ok, {immediate, Results}} | {error, term()} --spec async_gather(reference(), [{binary(), binary(), list()}], pid()) -> - {ok, non_neg_integer() | {immediate, list()}} | {error, term()}. -async_gather(_WorkerRef, _Calls, _CallerPid) -> - ?NIF_STUB. - -%% @doc Stream from an async generator. -%% Args: AsyncWorkerRef, Module, Func, Args, Kwargs, CallerPid -%% Returns: {ok, AsyncId} | {error, term()} --spec async_stream(reference(), binary(), binary(), list(), map(), pid()) -> - {ok, non_neg_integer()} | {error, term()}. -async_stream(_WorkerRef, _Module, _Func, _Args, _Kwargs, _CallerPid) -> - ?NIF_STUB. - %%% ============================================================================ %%% Sub-interpreter Support (Python 3.12+) %%% ============================================================================ @@ -958,18 +806,6 @@ stop_writer(_FdRef) -> start_writer(_FdRef) -> ?NIF_STUB. -%% @doc Cancel read monitoring (legacy alias for stop_reader). -%% Kept for backward compatibility. --spec cancel_reader(reference(), reference()) -> ok | {error, term()}. -cancel_reader(_LoopRef, _FdRef) -> - ?NIF_STUB. - -%% @doc Cancel write monitoring (legacy alias for stop_writer). -%% Kept for backward compatibility. --spec cancel_writer(reference(), reference()) -> ok | {error, term()}. -cancel_writer(_LoopRef, _FdRef) -> - ?NIF_STUB. - %% @doc Explicitly close an FD with proper lifecycle cleanup. %% Transfers ownership and triggers proper cleanup via ERL_NIF_SELECT_STOP. %% Safe to call multiple times (idempotent). @@ -1092,73 +928,6 @@ set_shared_worker(_WorkerPid) -> %%% Worker Pool %%% ============================================================================ -%% @doc Start the worker pool with the specified number of workers. -%% -%% Creates a pool of worker threads that process Python operations. -%% Each worker may have its own subinterpreter (Python 3.12+) for true -%% parallelism, or share the GIL with optimized batching. -%% -%% If NumWorkers is 0, the pool will use the number of CPU cores. -%% -%% @param NumWorkers Number of worker threads (0 = auto-detect) -%% @returns ok on success, or {error, Reason} --spec pool_start(non_neg_integer()) -> ok | {error, term()}. -pool_start(_NumWorkers) -> - ?NIF_STUB. - -%% @doc Stop the worker pool. -%% -%% Signals all workers to shut down and waits for them to terminate. -%% Any pending requests will receive {error, pool_shutdown}. -%% -%% @returns ok --spec pool_stop() -> ok. -pool_stop() -> - ?NIF_STUB. - -%% @doc Submit a request to the worker pool. -%% -%% Submits an asynchronous request to the pool. The caller will receive -%% a {py_response, RequestId, Result} message when the request completes. -%% -%% Request types and arguments: -%%
    -%%
  • `call' - Module, Func, Args, undefined (or Timeout)
  • -%%
  • `apply' - Module, Func, Args, Kwargs
  • -%%
  • `eval' - Code, Locals, undefined, undefined
  • -%%
  • `exec' - Code, undefined, undefined, undefined
  • -%%
  • `asgi' - Runner, Module, Callable, {Scope, Body}
  • -%%
  • `wsgi' - Module, Callable, Environ, undefined
  • -%%
-%% -%% @param Type Request type atom -%% @param Arg1 First argument (varies by type) -%% @param Arg2 Second argument (varies by type) -%% @param Arg3 Third argument (varies by type) -%% @param Arg4 Fourth argument (varies by type) -%% @returns {ok, RequestId} on success, or {error, Reason} --spec pool_submit(atom(), term(), term(), term(), term()) -> - {ok, non_neg_integer()} | {error, term()}. -pool_submit(_Type, _Arg1, _Arg2, _Arg3, _Arg4) -> - ?NIF_STUB. - -%% @doc Get worker pool statistics. -%% -%% Returns a map with the following keys: -%%
    -%%
  • `num_workers' - Number of worker threads
  • -%%
  • `initialized' - Whether the pool is started
  • -%%
  • `use_subinterpreters' - Whether using subinterpreters (Python 3.12+)
  • -%%
  • `free_threaded' - Whether using free-threaded Python (3.13+)
  • -%%
  • `pending_count' - Number of pending requests in queue
  • -%%
  • `total_enqueued' - Total requests submitted
  • -%%
-%% -%% @returns Stats map --spec pool_stats() -> map(). -pool_stats() -> - ?NIF_STUB. - %%% ============================================================================ %%% Process-per-context API (no mutex) %%%