Skip to content

Commit ea500fb

Browse files
committed
[WIP] tests
1 parent e47fa72 commit ea500fb

File tree

4 files changed

+134
-93
lines changed

4 files changed

+134
-93
lines changed

src/extrainterpreters/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def raw_list_all():
6969
from .base_interpreter import BaseInterpreter
7070
from .queue import SingleQueue, Queue
7171
from .simple_interpreter import SimpleInterpreter as Interpreter
72+
from .lock import Lock, RLock
7273

7374

7475
def list_all():

src/extrainterpreters/lock.py

Lines changed: 49 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,26 @@
1-
import os
2-
import pickle
3-
import threading
41
import time
52
import sys
6-
from functools import wraps
73

8-
from collections.abc import MutableSequence
94

10-
from . import interpreters, running_interpreters, get_current, raw_list_all
11-
from . import _memoryboard
5+
from . import running_interpreters
6+
127
from .utils import (
8+
_atomic_byte_lock,
9+
_remote_memory,
10+
_address_and_size,
1311
guard_internal_use,
1412
Field,
15-
DoubleField,
1613
StructBase,
17-
_InstMode,
1814
ResourceBusyError,
1915
)
2016

21-
from ._memoryboard import _remote_memory, _address_and_size, _atomic_byte_lock
22-
23-
_remote_memory = guard_internal_use(_remote_memory)
24-
_address_and_size = guard_internal_use(_address_and_size)
25-
_atomic_byte_lock = guard_internal_use(_atomic_byte_lock)
26-
27-
28-
class RemoteState:
29-
building = 0
30-
ready = 1
31-
serialized = 2
32-
received = 2
33-
garbage = 3
34-
35-
36-
class RemoteHeader(StructBase):
17+
class _LockBuffer(StructBase):
3718
lock = Field(1)
38-
state = Field(1)
39-
enter_count = Field(3)
40-
exit_count = Field(3)
41-
42-
43-
class RemoteDataState:
44-
not_ready = 0
45-
read_only = 1 # not used for now.
46-
read_write = 2
47-
4819

4920
TIME_RESOLUTION = sys.getswitchinterval()
5021
DEFAULT_TIMEOUT = 50 * TIME_RESOLUTION
5122
DEFAULT_TTL = 3600
52-
REMOTE_HEADER_SIZE = RemoteHeader._size
23+
LOCK_BUFFER_SIZE = _LockBuffer._size
5324

5425

5526
class _CrossInterpreterStructLock:
@@ -73,6 +44,8 @@ def timeout(self, timeout: None | float):
7344
return self
7445

7546
def __enter__(self):
47+
# Remember: all attributes are "interpreter-local"
48+
# just the bytes in the passed in struct are shared.
7649
if self._entered:
7750
self._entered += 1
7851
return self
@@ -108,3 +81,43 @@ def __getstate__(self):
10881
state["_entered"] = False
10982
return state
11083

84+
85+
class IntRLock:
86+
"""Cross Interpreter re-entrant lock
87+
88+
This will allow re-entrant acquires in the same
89+
interpreter, _even_ if it is being acquired
90+
in another thread in the same interpreter.
91+
92+
It should not be very useful - but
93+
the implementation path code leads here. Prefer the public
94+
"RLock" and "Lock" classes to avoid surprises.
95+
"""
96+
97+
def __init__(self):
98+
self._lock = _LockBuffer(lock=0)
99+
100+
def acquire(self, blocking=True, timeout=-1):
101+
pass
102+
103+
def release(self):
104+
pass
105+
106+
def locked(self):
107+
return False
108+
109+
110+
class RLock(IntRLock):
111+
"""Cross interpreter re-entrant lock, analogous to
112+
threading.RLock
113+
https://docs.python.org/3/library/threading.html#rlock-objects
114+
115+
More specifically: it will allow re-entrancy in
116+
_the same thread_ and _same interpreter_ -
117+
a different thread in the same interpreter will still
118+
be blocked out.
119+
"""
120+
121+
122+
class Lock(IntRLock):
123+
pass

src/extrainterpreters/memoryboard.py

Lines changed: 0 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -50,63 +50,6 @@ class RemoteDataState:
5050
REMOTE_HEADER_SIZE = RemoteHeader._size
5151

5252

53-
#class _CrossInterpreterStructLock:
54-
#def __init__(self, struct, timeout=DEFAULT_TIMEOUT):
55-
#buffer_ptr, size = _address_and_size(struct._data) # , struct._offset)
56-
## struct_ptr = buffer_ptr + struct._offset
57-
#lock_offset = struct._offset + struct._get_offset_for_field("lock")
58-
#if lock_offset >= size:
59-
#raise ValueError("Lock address out of bounds for struct buffer")
60-
#self._lock_address = buffer_ptr + lock_offset
61-
#self._original_timeout = self._timeout = timeout
62-
#self._entered = 0
63-
64-
#def timeout(self, timeout: None | float):
65-
#"""One use only timeout, for the same lock
66-
67-
#with lock.timeout(0.5):
68-
#...
69-
#"""
70-
#self._timeout = timeout
71-
#return self
72-
73-
#def __enter__(self):
74-
#if self._entered:
75-
#self._entered += 1
76-
#return self
77-
#if self._timeout is None:
78-
#if not _atomic_byte_lock(self._lock_address):
79-
#self._timeout = self._original_timeout
80-
#raise ResourceBusyError("Couldn't acquire lock")
81-
#else:
82-
#threshold = time.time() + self._timeout
83-
#while time.time() <= threshold:
84-
#if _atomic_byte_lock(self._lock_address):
85-
#break
86-
#else:
87-
#self._timeout = self._original_timeout
88-
#raise TimeoutError("Timeout trying to acquire lock")
89-
#self._entered += 1
90-
#return self
91-
92-
#def __exit__(self, *args):
93-
#if not self._entered:
94-
#return
95-
#self._entered -= 1
96-
#if self._entered:
97-
#return
98-
#buffer = _remote_memory(self._lock_address, 1)
99-
#buffer[0] = 0
100-
#del buffer
101-
#self._entered = False
102-
#self._timeout = self._original_timeout
103-
104-
#def __getstate__(self):
105-
#state = self.__dict__.copy()
106-
#state["_entered"] = False
107-
#return state
108-
109-
11053
# when a RemoteArray can't be destroyed in parent,
11154
# it comes to "sleep" here, where a callback in the
11255
# GC will periodically try to remove it:

tests/test_lock.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import pickle
2+
from textwrap import dedent as D
3+
4+
5+
import pytest
6+
7+
import extrainterpreters as ei
8+
9+
10+
from extrainterpreters import Lock, RLock
11+
from extrainterpreters.lock import IntRLock
12+
13+
@pytest.mark.parametrize("LockCls", [Lock, RLock, IntRLock])
14+
def test_locks_are_acquireable(LockCls):
15+
lock = LockCls()
16+
assert not lock.locked()
17+
lock.acquire()
18+
assert lock.locked()
19+
lock.release()
20+
assert not lock.locked()
21+
22+
23+
@pytest.mark.parametrize("LockCls", [Lock, RLock, IntRLock])
24+
def test_locks_work_as_context_manager(LockCls):
25+
lock = LockCls()
26+
assert not lock.locked()
27+
with lock:
28+
assert lock.locked()
29+
assert not lock.locked()
30+
31+
32+
33+
def test_lock_cant_be_reacquired():
34+
lock = Lock()
35+
36+
lock.acquire()
37+
38+
with pytest.raises(TimeoutError):
39+
lock.acquire(timeout=0)
40+
41+
42+
@pytest.mark.parametrize("LockCls", [Lock, RLock, IntRLock])
43+
def test_locks_cant_be_passed_to_other_interpreter(LockCls):
44+
lock = LockCls()
45+
interp = ei.Interpreter().start()
46+
interp.run_string(
47+
D(
48+
f"""
49+
import extrainterpreters; extrainterpreters.DEBUG=True
50+
lock = pickle.loads({pickle.dumps(lock)})
51+
assert lock._lock._data == 0
52+
"""
53+
)
54+
)
55+
lock._lock._data[0] = 2
56+
interp.run_string(
57+
D(
58+
"""
59+
assert lock._lock._data[0] == 2
60+
lock._lock._data[0] = 5
61+
"""
62+
)
63+
)
64+
assert lock._lock._data[0] == 5
65+
interp.close()
66+
67+
68+
#@pytest.mark.parametrize("LockCls", [Lock, RLock, IntRLock])
69+
#def test_locks_cant_be_acquired_in_other_interpreter(LockCls):
70+
#lock = LockCls()
71+
#interp = ei.Interpreter().start()
72+
#board.new_item((1, 2))
73+
#interp.run_string(
74+
#D(
75+
#f"""
76+
#import extrainterpreters; extrainterpreters.DEBUG=True
77+
#lock = pickle.loads({pickle.dumps(lock)})
78+
79+
#index, item = board.fetch_item()
80+
#assert item == (1,2)
81+
#board.new_item((3,4))
82+
#"""
83+
#)
84+
#)

0 commit comments

Comments
 (0)