diff --git a/CHANGELOG.md b/CHANGELOG.md index a143464..437a85c 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 a8f240e..8d1dc06 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 ba630a2..b560673 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 198b048..91b37ca 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 2485e5b..e9e9413 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 8672664..7430c0d 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 83f83df..14c3b62 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 45020cb..7dd3b18 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 7452c19..0000000 --- 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 2ca6147..0000000 --- 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 53e6426..841a2a7 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 3139871..5b237d8 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 cf5864c..439622e 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 a725af7..4968bc7 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 d3c75af..27b5ad9 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 70a95fa..c478e29 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 e868dc8..f4dddc3 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) %%%