Current section

Files

Jump to
erlang_python c_src py_subinterp_pool.c
Raw

c_src/py_subinterp_pool.c

/*
* 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_subinterp_pool.c
* @brief Subinterpreter pool implementation
* @author Benoit Chesneau
*
* Implements a pool of pre-created subinterpreters with shared GIL.
* The pool is initialized at Python startup and provides fast allocation
* of isolated Python namespaces for contexts.
*/
#include "py_subinterp_pool.h"
#include "py_reactor_buffer.h"
#include <string.h>
#ifdef HAVE_SUBINTERPRETERS
/* ============================================================================
* Pool State
* ============================================================================ */
/** @brief Pool of subinterpreters */
static subinterp_slot_t g_subinterp_pool[MAX_SUBINTERPRETERS];
/** @brief Bitmap of allocated slots (1 = in-use, 0 = free) */
static _Atomic uint64_t g_pool_allocation = 0;
/** @brief Current pool size */
static int g_pool_size = 0;
/** @brief Pool initialized flag */
static _Atomic bool g_pool_initialized = false;
/* ============================================================================
* Forward declarations for functions defined in other modules
* ============================================================================ */
/* Defined in py_callback.c, included from py_nif.c */
extern int create_erlang_module(void);
/* Defined in py_event_loop.c, included from py_nif.c */
extern int init_subinterpreter_event_loop(ErlNifEnv *env);
/* ============================================================================
* Pool Implementation
* ============================================================================ */
int subinterp_pool_init(int size) {
if (atomic_load(&g_pool_initialized)) {
return 0; /* Already initialized */
}
/* Cap size to maximum */
if (size <= 0) {
size = DEFAULT_POOL_SIZE;
}
if (size > MAX_SUBINTERPRETERS) {
size = MAX_SUBINTERPRETERS;
}
/* Clear pool state */
memset(g_subinterp_pool, 0, sizeof(g_subinterp_pool));
atomic_store(&g_pool_allocation, 0);
/* Create subinterpreters.
* We need to be careful here: Py_NewInterpreterFromConfig changes
* the current thread state. We save the main state and restore
* after creating each subinterpreter. */
PyThreadState *main_tstate = PyThreadState_Get();
for (int i = 0; i < size; i++) {
subinterp_slot_t *slot = &g_subinterp_pool[i];
/* Configure subinterpreter with SHARED GIL (default).
* PyInterpreterConfig_SHARED_GIL (0) shares the GIL with the main interpreter.
* This is the key difference from OWN_GIL which gives each interpreter its own GIL. */
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_SHARED_GIL, /* SHARED GIL for pool model */
};
PyThreadState *tstate = NULL;
PyStatus status = Py_NewInterpreterFromConfig(&tstate, &config);
if (PyStatus_Exception(status) || tstate == NULL) {
/* Failed to create subinterpreter - clean up and fail */
fprintf(stderr, "subinterp_pool_init: failed to create subinterp %d\n", i);
/* Restore main thread state before cleanup */
PyThreadState_Swap(main_tstate);
/* Clean up already created interpreters */
for (int j = 0; j < i; j++) {
if (g_subinterp_pool[j].initialized) {
PyThreadState_Swap(g_subinterp_pool[j].tstate);
Py_XDECREF(g_subinterp_pool[j].module_cache);
Py_XDECREF(g_subinterp_pool[j].globals);
Py_XDECREF(g_subinterp_pool[j].locals);
Py_EndInterpreter(g_subinterp_pool[j].tstate);
}
}
PyThreadState_Swap(main_tstate);
return -1;
}
/* tstate is now the current thread state for the new interpreter */
slot->interp = PyThreadState_GetInterpreter(tstate);
slot->tstate = tstate;
/* Initialize globals/locals */
slot->globals = PyDict_New();
slot->locals = PyDict_New();
slot->module_cache = PyDict_New();
if (slot->globals == NULL || slot->locals == NULL || slot->module_cache == NULL) {
/* Cleanup on failure */
Py_XDECREF(slot->module_cache);
Py_XDECREF(slot->globals);
Py_XDECREF(slot->locals);
Py_EndInterpreter(tstate);
PyThreadState_Swap(main_tstate);
/* Clean up already created interpreters */
for (int j = 0; j < i; j++) {
if (g_subinterp_pool[j].initialized) {
PyThreadState_Swap(g_subinterp_pool[j].tstate);
Py_XDECREF(g_subinterp_pool[j].module_cache);
Py_XDECREF(g_subinterp_pool[j].globals);
Py_XDECREF(g_subinterp_pool[j].locals);
Py_EndInterpreter(g_subinterp_pool[j].tstate);
}
}
PyThreadState_Swap(main_tstate);
return -1;
}
/* Import __builtins__ into globals */
PyObject *builtins = PyEval_GetBuiltins();
PyDict_SetItemString(slot->globals, "__builtins__", builtins);
/* Create erlang module in this subinterpreter */
if (create_erlang_module() < 0) {
fprintf(stderr, "subinterp_pool_init: failed to create erlang module in subinterp %d\n", i);
PyErr_Clear();
/* Non-fatal - continue without erlang module */
} else {
/* Register ReactorBuffer with erlang module in this subinterpreter */
if (ReactorBuffer_register_with_reactor() < 0) {
PyErr_Clear();
/* Non-fatal - ReactorBuffer just won't be available */
}
/* Import erlang module into globals */
PyObject *erlang_module = PyImport_ImportModule("erlang");
if (erlang_module != NULL) {
PyDict_SetItemString(slot->globals, "erlang", erlang_module);
Py_DECREF(erlang_module);
} else {
PyErr_Clear();
}
}
/* Initialize event loop for this subinterpreter.
* This enables asyncio support (sleep, timers, etc.) */
if (init_subinterpreter_event_loop(NULL) < 0) {
fprintf(stderr, "subinterp_pool_init: failed to init event loop in subinterp %d\n", i);
PyErr_Clear();
/* Non-fatal - async features just won't work */
}
slot->initialized = true;
/* Swap back to main thread state before creating next subinterpreter */
PyThreadState_Swap(main_tstate);
}
g_pool_size = size;
atomic_store(&g_pool_initialized, true);
#ifdef DEBUG
fprintf(stderr, "subinterp_pool_init: created %d subinterpreters with shared GIL\n", size);
#endif
return 0;
}
int subinterp_pool_alloc(void) {
if (!atomic_load(&g_pool_initialized)) {
return -1;
}
/* Try to find and allocate a free slot using CAS */
while (1) {
uint64_t current = atomic_load(&g_pool_allocation);
/* Find first free bit (0 bit) */
int slot = -1;
for (int i = 0; i < g_pool_size; i++) {
if ((current & (1ULL << i)) == 0) {
slot = i;
break;
}
}
if (slot < 0) {
/* Pool exhausted */
return -1;
}
/* Try to set the bit atomically */
uint64_t new_val = current | (1ULL << slot);
if (atomic_compare_exchange_weak(&g_pool_allocation, &current, new_val)) {
return slot;
}
/* CAS failed, retry */
}
}
void subinterp_pool_free(int slot) {
if (slot < 0 || slot >= g_pool_size) {
return;
}
/* Clear the allocation bit atomically */
uint64_t mask = ~(1ULL << slot);
atomic_fetch_and(&g_pool_allocation, mask);
}
subinterp_slot_t *subinterp_pool_get(int slot) {
if (slot < 0 || slot >= g_pool_size) {
return NULL;
}
if (!g_subinterp_pool[slot].initialized) {
return NULL;
}
return &g_subinterp_pool[slot];
}
bool subinterp_pool_is_allocated(int slot) {
if (slot < 0 || slot >= g_pool_size) {
return false;
}
uint64_t current = atomic_load(&g_pool_allocation);
return (current & (1ULL << slot)) != 0;
}
int subinterp_pool_size(void) {
return g_pool_size;
}
int subinterp_pool_allocated_count(void) {
uint64_t current = atomic_load(&g_pool_allocation);
int count = 0;
/* Count set bits (popcount) */
while (current) {
count += current & 1;
current >>= 1;
}
return count;
}
void subinterp_pool_shutdown(void) {
if (!atomic_load(&g_pool_initialized)) {
return;
}
/* Mark as not initialized first to prevent new allocations */
atomic_store(&g_pool_initialized, false);
/* Get current thread state (should be main interpreter) */
PyThreadState *main_tstate = PyThreadState_Get();
/* Clean up each subinterpreter */
for (int i = 0; i < g_pool_size; i++) {
subinterp_slot_t *slot = &g_subinterp_pool[i];
if (!slot->initialized) {
continue;
}
/* Swap to the subinterpreter's thread state */
PyThreadState_Swap(slot->tstate);
/* Clean up Python objects */
Py_XDECREF(slot->module_cache);
Py_XDECREF(slot->globals);
Py_XDECREF(slot->locals);
/* End the interpreter */
Py_EndInterpreter(slot->tstate);
slot->interp = NULL;
slot->tstate = NULL;
slot->globals = NULL;
slot->locals = NULL;
slot->module_cache = NULL;
slot->initialized = false;
}
/* Restore main thread state */
PyThreadState_Swap(main_tstate);
/* Reset pool state */
g_pool_size = 0;
atomic_store(&g_pool_allocation, 0);
#ifdef DEBUG
fprintf(stderr, "subinterp_pool_shutdown: cleaned up all subinterpreters\n");
#endif
}
bool subinterp_pool_is_initialized(void) {
return atomic_load(&g_pool_initialized);
}
#endif /* HAVE_SUBINTERPRETERS */