Skip to content

Commit 9cf64c0

Browse files
committed
Synchronize module cache; report error when deleting and reimporting
1 parent dafc3f6 commit 9cf64c0

7 files changed

Lines changed: 183 additions & 9 deletions

File tree

mypyc/codegen/emitmodule.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1290,6 +1290,8 @@ def emit_module_exec_func(
12901290
emitter.context.declarations[exec_name] = HeaderDeclaration(declaration + ";")
12911291
impl_name = f"{exec_name}__impl"
12921292
module_static = self.module_internal_static_name(module_name, emitter)
1293+
state = self.import_state_name(module_name)
1294+
module_cache = emitter.static_name(module_name, None, prefix=MODULE_PREFIX)
12931295
emitter.emit_lines(f"static int {impl_name}(PyObject *module)", "{")
12941296
if not self.use_shared_lib:
12951297
emitter.emit_lines("if (intern_strings() < 0)", " return -1;")
@@ -1385,9 +1387,11 @@ def emit_module_exec_func(
13851387
emitter.emit_line("return -1;")
13861388
emitter.emit_line("}")
13871389

1388-
state = self.import_state_name(module_name)
13891390
emitter.emit_lines(
1390-
declaration, "{", f"return CPyImport_Exec(module, {impl_name}, &{state});", "}"
1391+
declaration,
1392+
"{",
1393+
f"return CPyImport_Exec(module, {impl_name}, &{state}, &{module_cache});",
1394+
"}",
13911395
)
13921396

13931397
def emit_init_only_func(self, emitter: Emitter, module_name: str, module_prefix: str) -> None:

mypyc/irbuild/builder.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -629,7 +629,9 @@ def check_if_module_loaded(
629629
check_initialized = BasicBlock()
630630
self.add_bool_branch(comparison, check_initialized, needs_import)
631631
self.activate_block(check_initialized)
632-
initialized = self.call_c(native_import_is_initialized_op, [import_state], line)
632+
initialized = self.call_c(
633+
native_import_is_initialized_op, [import_state, first_load, module_cache], line
634+
)
633635
self.add_bool_branch(initialized, out, needs_import)
634636

635637
def get_module(self, module: str, line: int) -> Value:

mypyc/lib-rt/CPy.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,12 @@ int CPyImport_AcquireLock(CPyModuleLockAPI *api, PyObject *module_name,
4242
PyObject **module_lock);
4343
int CPyImport_ReleaseLock(PyObject *module_lock);
4444
bool CPyImport_IsInitialized(const CPyImportState *state);
45+
bool CPyImport_IsInitializedForModule(const CPyImportState *state, PyObject *module,
46+
CPyModule **module_cache);
4547
void CPyImport_SetInitialized(CPyImportState *state, bool initialized);
4648
PyObject *CPyImport_GetModuleCache(CPyModule **cache);
4749
void CPyImport_SetModuleCache(CPyModule **cache, PyObject *module);
50+
void CPyImport_ReplaceModuleCache(CPyModule **cache, PyObject *module);
4851

4952

5053
// Naming conventions:
@@ -1071,7 +1074,8 @@ PyObject *CPyImport_ImportNative(PyObject *module_name,
10711074
CPyImportState *state, CPyModuleLockAPI *lock_api,
10721075
PyObject *shared_lib_file, PyObject *ext_suffix,
10731076
Py_ssize_t is_package);
1074-
int CPyImport_Exec(PyObject *module, int (*exec_fn)(PyObject *), CPyImportState *state);
1077+
int CPyImport_Exec(PyObject *module, int (*exec_fn)(PyObject *), CPyImportState *state,
1078+
CPyModule **module_cache);
10751079
PyObject *CPyImport_BeginInitializing(PyObject *module);
10761080
int CPyImport_EndInitializing(PyObject *spec);
10771081
int CPyImport_SetDunderAttrs(PyObject *module, PyObject *module_name, PyObject *shared_lib_file,

mypyc/lib-rt/locks.c

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,14 @@ bool CPyImport_IsInitialized(const CPyImportState *state) {
8989
#endif
9090
}
9191

92+
bool CPyImport_IsInitializedForModule(const CPyImportState *state, PyObject *module,
93+
CPyModule **module_cache) {
94+
// Read initialized before re-reading the cache. If a retry completed after
95+
// the caller's first cache load, this load rejects its stale pointer.
96+
return CPyImport_IsInitialized(state)
97+
&& CPyImport_GetModuleCache(module_cache) == module;
98+
}
99+
92100
void CPyImport_SetInitialized(CPyImportState *state, bool initialized) {
93101
#ifdef _WIN32
94102
InterlockedExchange((volatile LONG *)&state->initialized, initialized);
@@ -97,6 +105,19 @@ void CPyImport_SetInitialized(CPyImportState *state, bool initialized) {
97105
#endif
98106
}
99107

108+
static void CPyImport_DecRefOld(PyObject *previous) {
109+
if (previous == NULL) {
110+
return;
111+
}
112+
#ifdef Py_GIL_DISABLED
113+
// Atomic loads return borrowed references, so defer releasing the old
114+
// reference until concurrent readers have passed a quiescent point.
115+
CPy_DecRefAttrOld(previous);
116+
#else
117+
Py_DECREF(previous);
118+
#endif
119+
}
120+
100121
PyObject *CPyImport_GetModuleCache(CPyModule **cache) {
101122
#ifdef _WIN32
102123
return InterlockedCompareExchangePointer((PVOID volatile *)cache, NULL, NULL);
@@ -121,3 +142,17 @@ void CPyImport_SetModuleCache(CPyModule **cache, PyObject *module) {
121142
}
122143
#endif
123144
}
145+
146+
void CPyImport_ReplaceModuleCache(CPyModule **cache, PyObject *module) {
147+
Py_INCREF(module);
148+
CPyModule *previous;
149+
#ifdef _WIN32
150+
previous = InterlockedExchangePointer((PVOID volatile *)cache, module);
151+
#else
152+
previous = __atomic_exchange_n(cache, (CPyModule *)module, __ATOMIC_ACQ_REL);
153+
#endif
154+
if (previous == NULL || previous == (CPyModule *)Py_None) {
155+
return;
156+
}
157+
CPyImport_DecRefOld((PyObject *)previous);
158+
}

mypyc/lib-rt/misc_ops.c

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1585,13 +1585,28 @@ static int CPyImport_ReleaseLockPreservingException(PyObject *module_lock) {
15851585
}
15861586

15871587
// Execute a module once; caller holds the module lock.
1588-
int CPyImport_Exec(PyObject *module, int (*exec_fn)(PyObject *), CPyImportState *state) {
1588+
int CPyImport_Exec(PyObject *module, int (*exec_fn)(PyObject *), CPyImportState *state,
1589+
CPyModule **module_cache) {
15891590
if (CPyImport_IsInitialized(state)) {
1590-
return 0;
1591+
const char *module_name = PyModule_GetName(module);
1592+
if (module_name != NULL) {
1593+
PyErr_Format(PyExc_ImportError,
1594+
"native module '%s' does not support reinitialization",
1595+
module_name);
1596+
}
1597+
return -1;
15911598
}
15921599

15931600
int result = exec_fn(module);
15941601
if (result == 0) {
1602+
// Keep the cache lazy. A normal shim import should not populate it, since
1603+
// the first compiled native import must still validate sys.modules. If a
1604+
// compiled import already populated the cache (including with a partial
1605+
// module during a circular import), refresh it to this instance.
1606+
PyObject *cached_module = CPyImport_GetModuleCache(module_cache);
1607+
if (cached_module != NULL && cached_module != Py_None) {
1608+
CPyImport_ReplaceModuleCache(module_cache, module);
1609+
}
15951610
CPyImport_SetInitialized(state, true);
15961611
}
15971612
return result;
@@ -1737,8 +1752,8 @@ PyObject *CPyImport_ImportNative(PyObject *module_name,
17371752
PyObject_DelItem(module_dict, module_name);
17381753
PyErr_Clear();
17391754
PyErr_Restore(exc_type, exc_val, exc_tb);
1740-
Py_CLEAR(*module_static);
17411755
CPyImport_SetInitialized(state, false);
1756+
Py_CLEAR(*module_static);
17421757
CPyImport_ReleaseLockPreservingException(module_lock);
17431758
return NULL;
17441759
}

mypyc/primitives/misc_ops.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,9 @@
160160
)
161161

162162
native_import_is_initialized_op = custom_op(
163-
arg_types=[c_pointer_rprimitive],
163+
arg_types=[c_pointer_rprimitive, object_rprimitive, object_pointer_rprimitive],
164164
return_type=bit_rprimitive,
165-
c_function_name="CPyImport_IsInitialized",
165+
c_function_name="CPyImport_IsInitializedForModule",
166166
error_kind=ERR_NEVER,
167167
)
168168

mypyc/test-data/run-multimodule.test

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2415,6 +2415,93 @@ assert errors == ["first initialization failed"]
24152415
assert failure_state.attempts == 2
24162416
assert other_importer.load() == 42
24172417

2418+
[case testFailedConcurrentCircularNativeImportRefreshesCache]
2419+
# separate: [(["native.py", "other_a.py", "other_b.py"], "testgroup")]
2420+
pass
2421+
2422+
[file import_sync.py]
2423+
from threading import Event
2424+
2425+
a_started = Event()
2426+
b_started = Event()
2427+
a_importing_b = Event()
2428+
b_waiting_for_driver = Event()
2429+
b_may_import_a = Event()
2430+
2431+
[file failure_state.py]
2432+
attempts = 0
2433+
2434+
[file other_a.py]
2435+
import failure_state
2436+
2437+
failure_state.attempts += 1
2438+
if failure_state.attempts == 1:
2439+
import import_sync
2440+
2441+
import_sync.a_started.set()
2442+
assert import_sync.b_started.wait(timeout=5)
2443+
import_sync.a_importing_b.set()
2444+
import other_b
2445+
raise RuntimeError("first initialization failed")
2446+
2447+
value = 42
2448+
2449+
[file other_b.py]
2450+
import import_sync
2451+
2452+
import_sync.b_started.set()
2453+
assert import_sync.a_started.wait(timeout=5)
2454+
assert import_sync.a_importing_b.wait(timeout=5)
2455+
import_sync.b_waiting_for_driver.set()
2456+
assert import_sync.b_may_import_a.wait(timeout=5)
2457+
import other_a
2458+
2459+
saw_partial_a = not hasattr(other_a, "value")
2460+
2461+
def get_a_value() -> int:
2462+
return other_a.value
2463+
2464+
[file driver.py]
2465+
from concurrent.futures import ThreadPoolExecutor
2466+
import importlib
2467+
from time import monotonic, sleep
2468+
2469+
from testutil import assertRaises
2470+
2471+
import failure_state
2472+
import import_sync
2473+
import native # Preload the compilation group's shared library.
2474+
2475+
with ThreadPoolExecutor(max_workers=2) as executor:
2476+
future_a = executor.submit(importlib.import_module, "other_a")
2477+
assert import_sync.a_started.wait(timeout=5)
2478+
future_b = executor.submit(importlib.import_module, "other_b")
2479+
assert import_sync.a_importing_b.wait(timeout=5)
2480+
assert import_sync.b_waiting_for_driver.wait(timeout=5)
2481+
2482+
# Wait until other_a is blocked on other_b's module lock. Letting other_b
2483+
# import other_a then deterministically closes the lock cycle and returns
2484+
# other_a's partial module.
2485+
bootstrap = importlib.import_module("importlib._bootstrap")
2486+
lock = bootstrap._get_module_lock("other_b")
2487+
deadline = monotonic() + 5
2488+
while not lock.waiters and monotonic() < deadline:
2489+
sleep(0.001)
2490+
has_waiter = bool(lock.waiters)
2491+
import_sync.b_may_import_a.set()
2492+
assert has_waiter
2493+
2494+
with assertRaises(RuntimeError, "first initialization failed"):
2495+
future_a.result(timeout=10)
2496+
other_b = future_b.result(timeout=10)
2497+
2498+
assert failure_state.attempts == 1
2499+
assert other_b.saw_partial_a
2500+
other_a = importlib.import_module("other_a")
2501+
assert failure_state.attempts == 2
2502+
assert other_a.value == 42
2503+
assert other_b.get_a_value() == 42
2504+
24182505
[case testConcurrentCircularNativeImports]
24192506
# separate: [(["other_a.py", "other_b.py"], "testgroup")]
24202507
pass
@@ -2457,6 +2544,33 @@ with ThreadPoolExecutor(max_workers=2) as executor:
24572544
assert other_a.other_value == "b"
24582545
assert other_b.other_value == "a"
24592546

2547+
[case testNativeModuleReimportBehavior]
2548+
value = 42
2549+
2550+
def get_value() -> int:
2551+
return value
2552+
2553+
[file driver.py]
2554+
import importlib
2555+
import sys
2556+
2557+
from testutil import assertRaises
2558+
2559+
import native
2560+
2561+
first = native
2562+
assert first.get_value() == 42
2563+
del sys.modules["native"]
2564+
if hasattr(sys, "_is_gil_enabled") and not sys._is_gil_enabled():
2565+
with assertRaises(ImportError, "native module 'native' does not support reinitialization"):
2566+
importlib.import_module("native")
2567+
assert "native" not in sys.modules
2568+
else:
2569+
# Legacy single-phase initialization returns the original module object.
2570+
second = importlib.import_module("native")
2571+
assert second is first
2572+
assert first.get_value() == 42
2573+
24602574
[case testTopLevelThreadImportsNativeModuleFromSameGroup]
24612575
# separate: [(["native.py", "other_target.py"], "testgroup")]
24622576
from threading import Thread

0 commit comments

Comments
 (0)