Skip to content

Commit 70843dd

Browse files
committed
fix: Debug cleanup
1 parent 61c194c commit 70843dd

4 files changed

Lines changed: 29 additions & 9 deletions

File tree

src/c2pa/c2pa.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2001,6 +2001,7 @@ def close(self):
20012001
return
20022002
if is_foreign_process(self):
20032003
self._closed = True
2004+
self._initialized = False
20042005
return
20052006

20062007
try:

src/c2pa/lib.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -314,10 +314,12 @@ def is_foreign_process(obj):
314314
absent in the child -> any lock() call deadlocks. Callers must skip native frees
315315
when this returns True.
316316
317-
Skipping the free does not cause a cumulative memory leak: the child either calls exec()
318-
(replacing the address space entirely) or exits shortly after fork. In both cases
319-
the OS should reclaim all process memory. The skipped allocation lives only until child
320-
process termination.
317+
Skipping the free does not cause a cumulative leak. If the child calls exec() the
318+
address space is replaced entirely; if it exits, the OS reclaims all process memory.
319+
Even a long-lived fork child (e.g. a multiprocessing fork-start worker) leaks at most
320+
the objects inherited at fork time — a one-time, bounded amount reclaimed when the
321+
child terminates. Objects the child creates itself carry the child's PID and are
322+
freed normally.
321323
322324
Defensive default: if _owner_pid was never set, returns False (no regression)."""
323325
owner = getattr(obj, '_owner_pid', None)

tests/perf/scenarios.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -541,9 +541,16 @@ def scenario_fork_contended_mutex(iterations: int = 100) -> None:
541541
"""Fork safety benchmark scenario:
542542
8 threads create/close Readers in a tight loop while the main thread
543543
forks 5× per iteration (500 total forks). Maximises the probability that
544-
the registry Mutex is held at the instant of fork(). If pthread_atfork
545-
didn't reinit the Rust mutex the first cimpl_free in the child would
546-
deadlock; the Python guard is also exercised by child GC.
544+
the registry Mutex is held at the instant of fork(). Each fork inherits
545+
a Reader created by the main thread; the child explicitly closes it
546+
(then runs GC), so the PID guard is exercised on every fork — without
547+
the guard the close would call into the native library and could
548+
deadlock on a mutex left locked by a vanished worker thread. The parent
549+
closes the same Reader after the child exits (its own PID: real free).
550+
551+
Note: the workers' Readers are pinned by frozen thread frames in the
552+
child, so child gc.collect() alone would free nothing — hence the
553+
explicit close of an inherited object.
547554
"""
548555
if not hasattr(os, "fork"):
549556
return
@@ -562,7 +569,15 @@ def _worker():
562569
try:
563570
for _ in _iterate(iterations):
564571
for _ in range(5):
565-
_fork_wait(lambda: gc.collect())
572+
with open(SIGNED_JPEG, "rb") as f:
573+
reader = Reader("image/jpeg", f)
574+
575+
def _child(r=reader):
576+
r.close()
577+
gc.collect()
578+
579+
_fork_wait(_child)
580+
reader.close()
566581
finally:
567582
stop.set()
568583
for t in threads:

tests/test_unit_tests_threaded.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,11 +163,13 @@ def test_already_closed_is_noop_via_close(self):
163163
mock_lib.c2pa_release_stream.assert_not_called()
164164

165165
def test_foreign_pid_close_marks_closed(self):
166-
"""close() in forked child must set _closed=True to prevent re-entry."""
166+
"""close() in forked child must set _closed=True to prevent re-entry,
167+
and _initialized=False so the public properties report a closed stream."""
167168
obj = _make_stream(pid_offset=1)
168169
with patch('c2pa.c2pa._lib'):
169170
obj.close()
170171
self.assertTrue(obj._closed)
172+
self.assertFalse(obj._initialized)
171173

172174

173175
class TestHelpers(unittest.TestCase):

0 commit comments

Comments
 (0)