Skip to content

Commit

Permalink
Make Lock classes safe to pass around
Browse files Browse the repository at this point in the history
  • Loading branch information
jsbueno committed Oct 1, 2024
1 parent b2a1d01 commit 8d95040
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 20 deletions.
36 changes: 29 additions & 7 deletions src/extrainterpreters/lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ class _CrossInterpreterStructLock:
"""

def __init__(self, struct, timeout=DEFAULT_TIMEOUT):
buffer_ptr, size = _address_and_size(struct._data) # , struct._offset)
if isinstance(struct._data, RemoteArray):
buffer_ptr, size = struct._data._data_for_remote()
else: # bytes, bytearray
buffer_ptr, size = _address_and_size(struct._data) # , struct._offset)
# struct_ptr = buffer_ptr + struct._offset
lock_offset = struct._offset + struct._get_offset_for_field("lock")
if lock_offset >= size:
Expand Down Expand Up @@ -105,7 +108,7 @@ def release(self):

def __getstate__(self):
state = self.__dict__.copy()
state["_entered"] = False
state["_entered"] = 0
return state


Expand All @@ -122,9 +125,21 @@ class IntRLock:
"""

def __init__(self):
self._buffer = bytearray(1)
# prevents buffer from being moved around by Python allocators
self._anchor = memoryview(self._buffer)

# RemoteArray is a somewhat high-level data structure,
# which includes another byte for a lock - just
# to take account of the buffer life-cycle
# across interpreters.

# unfortunatelly, I got no simpler mechanism than that
# to resolve the problem of the Lock object, along
# with the buffer being deleted in its owner interpreter
# while alive in a scondary one.
# (Remotearrays will go to a parking area, waiting until they
# are dereferenced remotely before freeing the memory)

self._buffer = RemoteArray(size=1)
self._buffer._enter_parent()

lock_str = _LockBuffer._from_data(self._buffer)
self._lock = _CrossInterpreterStructLock(lock_str)
Expand Down Expand Up @@ -156,10 +171,17 @@ def __exit__(self, *args):
#self._lock.__exit__()

def locked(self):
return bool(self._lock._entered)
if self._lock._entered:
return True
try:
self._lock.acquire(0)
except ResourceBusyError:
return True
self._lock.release()
return False

def __getstate__(self):
return {"_lock": self._lock}
return {"_lock": self._lock, "_buffer": self._buffer}



Expand Down
47 changes: 34 additions & 13 deletions tests/test_lock.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pickle
from functools import partial
from textwrap import dedent as D


Expand All @@ -10,6 +11,21 @@
from extrainterpreters import Lock, RLock
from extrainterpreters.lock import IntRLock


@pytest.fixture
def interpreter(lowlevel):
interp = ei.Interpreter().start()
interp.run_string(
D(
f"""
import extrainterpreters as ei; ei.DEBUG=True
"""
),
raise_=True
)
yield interp
interp.close()

@pytest.mark.parametrize("LockCls", [Lock, RLock, IntRLock])
def test_locks_are_acquireable(LockCls):
lock = LockCls()
Expand Down Expand Up @@ -37,25 +53,31 @@ def test_lock_cant_be_reacquired_same_interpreter():

assert not lock.acquire(blocking=False)

@pytest.mark.skip("to be implemented")
def test_lock_cant_be_reacquired_other_interpreter():
lock = Lock()

lock.acquire()

with pytest.raises(TimeoutError):
lock.acquire(timeout=0)
def test_lock_cant_be_reacquired_other_interpreter(interpreter):
lock = Lock()
# some assertion lasagna -
# just checks basic toggling - no race conditions tested here:
run = partial(interpreter.run_string, raise_=True)
run(f"lock = pickle.loads({pickle.dumps(lock)})")
run (f"assert lock.acquire(blocking=False)")
assert not lock.acquire(blocking=False)
run (f"assert not lock.acquire(blocking=False)")
run (f"lock.release()")
assert lock.acquire(blocking=False)
run (f"assert not lock.acquire(blocking=False)")
lock.release()
run (f"assert lock.acquire(blocking=False)")
run (f"lock.release()")


@pytest.mark.parametrize("LockCls", [Lock, RLock, IntRLock])
def test_locks_can_be_passed_to_other_interpreter(LockCls, lowlevel):
def test_locks_can_be_passed_to_other_interpreter(LockCls, interpreter):
lock = LockCls()
lock_data = ei.utils._remote_memory(lock._lock._lock_address, 1)
interp = ei.Interpreter().start()
interp.run_string(
interpreter.run_string(
D(
f"""
import extrainterpreters as ei; ei.DEBUG=True
lock = pickle.loads({pickle.dumps(lock)})
lock_data = ei.utils._remote_memory(lock._lock._lock_address, 1)
assert lock_data[0] == 0
Expand All @@ -64,7 +86,7 @@ def test_locks_can_be_passed_to_other_interpreter(LockCls, lowlevel):
raise_=True
)
lock_data[0] = 2
interp.run_string(
interpreter.run_string(
D(
"""
assert lock_data[0] == 2
Expand All @@ -74,7 +96,6 @@ def test_locks_can_be_passed_to_other_interpreter(LockCls, lowlevel):
raise_=True
)
assert lock_data[0] == 5
interp.close()


#@pytest.mark.parametrize("LockCls", [Lock, RLock, IntRLock])
Expand Down

0 comments on commit 8d95040

Please sign in to comment.