Skip to content

Commit 26f11b9

Browse files
committed
Remove flush_imports - module unloading not supported
Removed flush_imports/0 and all related flush functionality: - py:flush_imports/0 - py_context:flush_imports/2 - py_nif:interp_flush_imports/2 - py_nif:subinterp_pool_flush_generation/0 - Cache generation tracking in py_context_t Import caching via py:import/1,2 remains fully functional. Module imports are cached per-interpreter and persist for the lifetime of the interpreter.
1 parent 87032fa commit 26f11b9

9 files changed

Lines changed: 9 additions & 397 deletions

File tree

c_src/py_nif.c

Lines changed: 0 additions & 198 deletions
Original file line numberDiff line numberDiff line change
@@ -3224,43 +3224,6 @@ static void owngil_execute_apply_imports(py_context_t *ctx) {
32243224
ctx->response_ok = true;
32253225
}
32263226

3227-
/**
3228-
* @brief Execute flush_imports in OWN_GIL context
3229-
*
3230-
* Clears the per-context module cache optimization layer.
3231-
* Does NOT clear sys.modules (that would break Python).
3232-
*/
3233-
static void owngil_execute_flush_imports(py_context_t *ctx) {
3234-
/* Remove specified modules from sys.modules */
3235-
PyObject *sys_modules = PyImport_GetModuleDict(); /* Borrowed ref */
3236-
if (sys_modules != NULL) {
3237-
ERL_NIF_TERM head, tail = ctx->request_data;
3238-
while (enif_get_list_cell(ctx->shared_env, tail, &head, &tail)) {
3239-
ErlNifBinary mod_bin;
3240-
if (enif_inspect_binary(ctx->shared_env, head, &mod_bin)) {
3241-
char *mod_name = enif_alloc(mod_bin.size + 1);
3242-
if (mod_name != NULL) {
3243-
memcpy(mod_name, mod_bin.data, mod_bin.size);
3244-
mod_name[mod_bin.size] = '\0';
3245-
/* Remove module from sys.modules if present */
3246-
if (PyDict_GetItemString(sys_modules, mod_name) != NULL) {
3247-
PyDict_DelItemString(sys_modules, mod_name);
3248-
}
3249-
enif_free(mod_name);
3250-
}
3251-
}
3252-
}
3253-
}
3254-
3255-
/* Clear the per-context optimization cache if it exists */
3256-
if (ctx->module_cache != NULL) {
3257-
PyDict_Clear(ctx->module_cache);
3258-
}
3259-
3260-
ctx->response_term = enif_make_atom(ctx->shared_env, "ok");
3261-
ctx->response_ok = true;
3262-
}
3263-
32643227
/**
32653228
* @brief Execute a request based on its type
32663229
*/
@@ -3299,9 +3262,6 @@ static void owngil_execute_request(py_context_t *ctx) {
32993262
case CTX_REQ_APPLY_IMPORTS:
33003263
owngil_execute_apply_imports(ctx);
33013264
break;
3302-
case CTX_REQ_FLUSH_IMPORTS:
3303-
owngil_execute_flush_imports(ctx);
3304-
break;
33053265
default:
33063266
ctx->response_term = enif_make_tuple2(ctx->shared_env,
33073267
enif_make_atom(ctx->shared_env, "error"),
@@ -3911,50 +3871,6 @@ static ERL_NIF_TERM dispatch_apply_imports_to_owngil(
39113871
return result;
39123872
}
39133873

3914-
/**
3915-
* @brief Dispatch flush_imports to OWN_GIL worker thread
3916-
*
3917-
* @param env NIF environment
3918-
* @param ctx Context resource
3919-
* @param modules_list List of module names to remove from sys.modules
3920-
* @return ok
3921-
*/
3922-
static ERL_NIF_TERM dispatch_flush_imports_to_owngil(
3923-
ErlNifEnv *env, py_context_t *ctx, ERL_NIF_TERM modules_list
3924-
) {
3925-
if (!atomic_load(&ctx->thread_running)) {
3926-
return make_error(env, "thread_not_running");
3927-
}
3928-
3929-
pthread_mutex_lock(&ctx->request_mutex);
3930-
3931-
enif_clear_env(ctx->shared_env);
3932-
ctx->request_type = CTX_REQ_FLUSH_IMPORTS;
3933-
ctx->request_data = enif_make_copy(ctx->shared_env, modules_list);
3934-
3935-
pthread_cond_signal(&ctx->request_ready);
3936-
3937-
/* Wait for response with timeout */
3938-
struct timespec deadline;
3939-
clock_gettime(CLOCK_REALTIME, &deadline);
3940-
deadline.tv_sec += OWNGIL_DISPATCH_TIMEOUT_SECS;
3941-
3942-
while (ctx->request_type != CTX_REQ_NONE) {
3943-
int rc = pthread_cond_timedwait(&ctx->response_ready, &ctx->request_mutex, &deadline);
3944-
if (rc == ETIMEDOUT) {
3945-
atomic_store(&ctx->thread_running, false);
3946-
pthread_mutex_unlock(&ctx->request_mutex);
3947-
fprintf(stderr, "OWN_GIL flush_imports dispatch timeout: worker thread unresponsive\n");
3948-
return make_error(env, "worker_timeout");
3949-
}
3950-
}
3951-
3952-
ERL_NIF_TERM result = enif_make_copy(env, ctx->response_term);
3953-
pthread_mutex_unlock(&ctx->request_mutex);
3954-
3955-
return result;
3956-
}
3957-
39583874
#endif /* HAVE_SUBINTERPRETERS */
39593875

39603876
/**
@@ -4124,7 +4040,6 @@ static ERL_NIF_TERM nif_context_create(ErlNifEnv *env, int argc, const ERL_NIF_T
41244040
ctx->globals = NULL;
41254041
ctx->locals = NULL;
41264042
ctx->module_cache = NULL;
4127-
ctx->cache_generation = 0;
41284043

41294044
/* Create callback pipe for blocking callback responses */
41304045
if (pipe(ctx->callback_pipe) < 0) {
@@ -4237,7 +4152,6 @@ static ERL_NIF_TERM nif_context_create(ErlNifEnv *env, int argc, const ERL_NIF_T
42374152
ctx->globals = PyDict_New();
42384153
ctx->locals = PyDict_New();
42394154
ctx->module_cache = PyDict_New();
4240-
ctx->cache_generation = import_cache_get_generation();
42414155

42424156
/* Import __builtins__ into globals */
42434157
PyObject *builtins = PyEval_GetBuiltins();
@@ -4370,25 +4284,6 @@ static ERL_NIF_TERM nif_context_destroy(ErlNifEnv *env, int argc, const ERL_NIF_
43704284
* Helper function - no mutex needed since context is process-owned.
43714285
*/
43724286
static PyObject *context_get_module(py_context_t *ctx, const char *module_name) {
4373-
/* Check for stale cache (main interpreter contexts only) */
4374-
if (ctx->module_cache != NULL) {
4375-
#ifdef HAVE_SUBINTERPRETERS
4376-
/* Pool-backed contexts get fresh interpreters on flush, so only check
4377-
* for non-pool contexts (main interpreter or OWN_GIL) */
4378-
bool is_main_interp = (ctx->pool_slot < 0 && !ctx->uses_own_gil);
4379-
#else
4380-
bool is_main_interp = true; /* All contexts use main interpreter */
4381-
#endif
4382-
if (is_main_interp) {
4383-
uint64_t current_gen = import_cache_get_generation();
4384-
if (ctx->cache_generation != current_gen) {
4385-
/* Cache is stale - clear it and update generation */
4386-
PyDict_Clear(ctx->module_cache);
4387-
ctx->cache_generation = current_gen;
4388-
}
4389-
}
4390-
}
4391-
43924287
/* Check cache first */
43934288
if (ctx->module_cache != NULL) {
43944289
PyObject *cached = PyDict_GetItemString(ctx->module_cache, module_name);
@@ -5040,97 +4935,6 @@ static ERL_NIF_TERM nif_interp_apply_imports(ErlNifEnv *env, int argc, const ERL
50404935
return ATOM_OK;
50414936
}
50424937

5043-
/**
5044-
* @brief Flush imports from an interpreter's sys.modules
5045-
*
5046-
* nif_interp_flush_imports(Ref, Modules) -> ok
5047-
*
5048-
* Removes specified modules from sys.modules and clears the per-context
5049-
* module_cache optimization layer.
5050-
*
5051-
* @param Ref Context reference
5052-
* @param Modules List of binary module names to remove from sys.modules
5053-
*/
5054-
static ERL_NIF_TERM nif_interp_flush_imports(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
5055-
(void)argc;
5056-
py_context_t *ctx;
5057-
5058-
if (!runtime_is_running()) {
5059-
return ATOM_OK; /* Nothing to flush if Python not running */
5060-
}
5061-
5062-
if (!enif_get_resource(env, argv[0], PY_CONTEXT_RESOURCE_TYPE, (void **)&ctx)) {
5063-
return make_error(env, "invalid_context");
5064-
}
5065-
5066-
if (ctx->destroyed) {
5067-
return ATOM_OK; /* Already destroyed */
5068-
}
5069-
5070-
#ifdef HAVE_SUBINTERPRETERS
5071-
/* OWN_GIL mode: dispatch to the dedicated thread */
5072-
if (ctx->uses_own_gil) {
5073-
return dispatch_flush_imports_to_owngil(env, ctx, argv[1]);
5074-
}
5075-
#endif
5076-
5077-
py_context_guard_t guard = py_context_acquire(ctx);
5078-
if (!guard.acquired) {
5079-
return ATOM_OK; /* Can't acquire - just return ok */
5080-
}
5081-
5082-
/* Remove specified modules from sys.modules */
5083-
PyObject *sys_modules = PyImport_GetModuleDict(); /* Borrowed ref */
5084-
if (sys_modules != NULL) {
5085-
ERL_NIF_TERM head, tail = argv[1];
5086-
while (enif_get_list_cell(env, tail, &head, &tail)) {
5087-
ErlNifBinary mod_bin;
5088-
if (enif_inspect_binary(env, head, &mod_bin)) {
5089-
char *mod_name = binary_to_string(&mod_bin);
5090-
if (mod_name != NULL) {
5091-
/* Remove module from sys.modules if present */
5092-
if (PyDict_GetItemString(sys_modules, mod_name) != NULL) {
5093-
PyDict_DelItemString(sys_modules, mod_name);
5094-
}
5095-
enif_free(mod_name);
5096-
}
5097-
}
5098-
}
5099-
}
5100-
5101-
/* Clear per-context optimization cache */
5102-
if (ctx->module_cache != NULL) {
5103-
PyDict_Clear(ctx->module_cache);
5104-
}
5105-
5106-
py_context_release(&guard);
5107-
return ATOM_OK;
5108-
}
5109-
5110-
/**
5111-
* @brief Flush import generation and mark all pool slots stale
5112-
*
5113-
* nif_subinterp_pool_flush_generation() -> {ok, NewGeneration}
5114-
*
5115-
* Increments the global import generation counter and marks all initialized
5116-
* pool slots as stale. When a stale slot's usage count drops to 0, the
5117-
* subinterpreter is automatically destroyed and the slot becomes available
5118-
* for a fresh subinterpreter.
5119-
*
5120-
* This enables flush_imports to work by replacing subinterpreters rather
5121-
* than trying to manipulate sys.modules directly (which can crash C extensions).
5122-
*/
5123-
static ERL_NIF_TERM nif_subinterp_pool_flush_generation(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
5124-
(void)argc;
5125-
(void)argv;
5126-
5127-
/* Use the unconditional version that works with or without subinterpreters.
5128-
* This increments the generation counter (used by main interpreter contexts)
5129-
* and also marks pool slots stale when subinterpreters are available. */
5130-
uint64_t new_gen = import_cache_flush_generation();
5131-
return enif_make_tuple2(env, ATOM_OK, enif_make_uint64(env, new_gen));
5132-
}
5133-
51344938
/**
51354939
* @brief Execute Python statements using a process-local environment
51364940
*
@@ -7261,8 +7065,6 @@ static ErlNifFunc nif_funcs[] = {
72617065
{"context_call", 6, nif_context_call_with_env, ERL_NIF_DIRTY_JOB_CPU_BOUND},
72627066
{"create_local_env", 1, nif_create_local_env, 0},
72637067
{"interp_apply_imports", 2, nif_interp_apply_imports, ERL_NIF_DIRTY_JOB_CPU_BOUND},
7264-
{"interp_flush_imports", 2, nif_interp_flush_imports, ERL_NIF_DIRTY_JOB_CPU_BOUND},
7265-
{"subinterp_pool_flush_generation", 0, nif_subinterp_pool_flush_generation, 0},
72667068
{"context_call_method", 4, nif_context_call_method, ERL_NIF_DIRTY_JOB_CPU_BOUND},
72677069
{"context_to_term", 1, nif_context_to_term, 0},
72687070
{"context_interp_id", 1, nif_context_interp_id, 0},

c_src/py_nif.h

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -731,8 +731,7 @@ typedef enum {
731731
CTX_REQ_EVAL_WITH_ENV, /**< Eval with process-local environment */
732732
CTX_REQ_EXEC_WITH_ENV, /**< Exec with process-local environment */
733733
CTX_REQ_CREATE_LOCAL_ENV, /**< Create process-local env dicts */
734-
CTX_REQ_APPLY_IMPORTS, /**< Apply imports to module cache */
735-
CTX_REQ_FLUSH_IMPORTS /**< Flush module cache */
734+
CTX_REQ_APPLY_IMPORTS /**< Apply imports to module cache */
736735
} ctx_request_type_t;
737736

738737
/**
@@ -888,9 +887,6 @@ typedef struct {
888887

889888
/** @brief Module cache (Dict: module_name -> PyModule) */
890889
PyObject *module_cache;
891-
892-
/** @brief Cache generation for staleness detection (main interpreter contexts) */
893-
uint64_t cache_generation;
894890
} py_context_t;
895891

896892
/* ============================================================================

c_src/py_subinterp_pool.c

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -540,30 +540,4 @@ uint64_t subinterp_pool_get_generation(void) {
540540
return atomic_load(&g_import_generation);
541541
}
542542

543-
#else /* !HAVE_SUBINTERPRETERS */
544-
545-
/* For systems without subinterpreter support, we still need the generation
546-
* counter for main interpreter context cache invalidation */
547-
static _Atomic uint64_t g_import_generation_no_subinterp = 0;
548-
549543
#endif /* HAVE_SUBINTERPRETERS */
550-
551-
/* ============================================================================
552-
* Unconditional generation functions (work with or without subinterpreters)
553-
* ============================================================================ */
554-
555-
uint64_t import_cache_get_generation(void) {
556-
#ifdef HAVE_SUBINTERPRETERS
557-
return atomic_load(&g_import_generation);
558-
#else
559-
return atomic_load(&g_import_generation_no_subinterp);
560-
#endif
561-
}
562-
563-
uint64_t import_cache_flush_generation(void) {
564-
#ifdef HAVE_SUBINTERPRETERS
565-
return subinterp_pool_flush_generation();
566-
#else
567-
return atomic_fetch_add(&g_import_generation_no_subinterp, 1) + 1;
568-
#endif
569-
}

c_src/py_subinterp_pool.h

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -211,24 +211,4 @@ uint64_t subinterp_pool_get_generation(void);
211211

212212
#endif /* HAVE_SUBINTERPRETERS */
213213

214-
/**
215-
* @brief Get the current import generation (always available)
216-
*
217-
* This function is available regardless of subinterpreter support.
218-
* It's used by main interpreter contexts to detect cache staleness.
219-
*
220-
* @return Current generation number (0 if never flushed)
221-
*/
222-
uint64_t import_cache_get_generation(void);
223-
224-
/**
225-
* @brief Increment the import generation counter (always available)
226-
*
227-
* This function is available regardless of subinterpreter support.
228-
* For systems with subinterpreters, it also marks pool slots stale.
229-
*
230-
* @return The new generation number
231-
*/
232-
uint64_t import_cache_flush_generation(void);
233-
234214
#endif /* PY_SUBINTERP_POOL_H */

src/py.erl

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@
5959
%% Module import caching
6060
import/1,
6161
import/2,
62-
flush_imports/0,
6362
import_stats/0,
6463
import_list/0,
6564
%% Import registry (global list applied to all interpreters)
@@ -462,27 +461,6 @@ clear_imports() ->
462461
end,
463462
ok.
464463

465-
%% @doc Flush import caches and trigger subinterpreter replacement.
466-
%%
467-
%% Clears the global import registry and marks all shared-GIL pool
468-
%% subinterpreters as stale. When existing contexts using those
469-
%% subinterpreters are destroyed, Python GC handles cleanup via
470-
%% Py_EndInterpreter. New contexts will get fresh subinterpreters
471-
%% with the updated import registry.
472-
%%
473-
%% OWN_GIL contexts each have their own subinterpreter which is
474-
%% destroyed when the context is destroyed.
475-
%%
476-
%% @returns ok
477-
-spec flush_imports() -> ok.
478-
flush_imports() ->
479-
%% Clear the global registry
480-
clear_imports(),
481-
%% Mark all pool slots as stale - they will be destroyed when
482-
%% usage count drops to 0 (all contexts using them are destroyed)
483-
py_nif:subinterp_pool_flush_generation(),
484-
ok.
485-
486464
%% @doc Get import registry statistics.
487465
%%
488466
%% Returns a map with the count of registered imports.

0 commit comments

Comments
 (0)