Skip to content

Commit

Permalink
[WIP] tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jsbueno committed Sep 30, 2024
1 parent e47fa72 commit ea500fb
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 93 deletions.
1 change: 1 addition & 0 deletions src/extrainterpreters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ def raw_list_all():
from .base_interpreter import BaseInterpreter
from .queue import SingleQueue, Queue
from .simple_interpreter import SimpleInterpreter as Interpreter
from .lock import Lock, RLock


def list_all():
Expand Down
85 changes: 49 additions & 36 deletions src/extrainterpreters/lock.py
Original file line number Diff line number Diff line change
@@ -1,55 +1,26 @@
import os
import pickle
import threading
import time
import sys
from functools import wraps

from collections.abc import MutableSequence

from . import interpreters, running_interpreters, get_current, raw_list_all
from . import _memoryboard
from . import running_interpreters

from .utils import (
_atomic_byte_lock,
_remote_memory,
_address_and_size,
guard_internal_use,
Field,
DoubleField,
StructBase,
_InstMode,
ResourceBusyError,
)

from ._memoryboard import _remote_memory, _address_and_size, _atomic_byte_lock

_remote_memory = guard_internal_use(_remote_memory)
_address_and_size = guard_internal_use(_address_and_size)
_atomic_byte_lock = guard_internal_use(_atomic_byte_lock)


class RemoteState:
building = 0
ready = 1
serialized = 2
received = 2
garbage = 3


class RemoteHeader(StructBase):
class _LockBuffer(StructBase):
lock = Field(1)
state = Field(1)
enter_count = Field(3)
exit_count = Field(3)


class RemoteDataState:
not_ready = 0
read_only = 1 # not used for now.
read_write = 2


TIME_RESOLUTION = sys.getswitchinterval()
DEFAULT_TIMEOUT = 50 * TIME_RESOLUTION
DEFAULT_TTL = 3600
REMOTE_HEADER_SIZE = RemoteHeader._size
LOCK_BUFFER_SIZE = _LockBuffer._size


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

def __enter__(self):
# Remember: all attributes are "interpreter-local"
# just the bytes in the passed in struct are shared.
if self._entered:
self._entered += 1
return self
Expand Down Expand Up @@ -108,3 +81,43 @@ def __getstate__(self):
state["_entered"] = False
return state


class IntRLock:
"""Cross Interpreter re-entrant lock
This will allow re-entrant acquires in the same
interpreter, _even_ if it is being acquired
in another thread in the same interpreter.
It should not be very useful - but
the implementation path code leads here. Prefer the public
"RLock" and "Lock" classes to avoid surprises.
"""

def __init__(self):
self._lock = _LockBuffer(lock=0)

def acquire(self, blocking=True, timeout=-1):
pass

def release(self):
pass

def locked(self):
return False


class RLock(IntRLock):
"""Cross interpreter re-entrant lock, analogous to
threading.RLock
https://docs.python.org/3/library/threading.html#rlock-objects
More specifically: it will allow re-entrancy in
_the same thread_ and _same interpreter_ -
a different thread in the same interpreter will still
be blocked out.
"""


class Lock(IntRLock):
pass
57 changes: 0 additions & 57 deletions src/extrainterpreters/memoryboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,63 +50,6 @@ class RemoteDataState:
REMOTE_HEADER_SIZE = RemoteHeader._size


#class _CrossInterpreterStructLock:
#def __init__(self, struct, timeout=DEFAULT_TIMEOUT):
#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:
#raise ValueError("Lock address out of bounds for struct buffer")
#self._lock_address = buffer_ptr + lock_offset
#self._original_timeout = self._timeout = timeout
#self._entered = 0

#def timeout(self, timeout: None | float):
#"""One use only timeout, for the same lock

#with lock.timeout(0.5):
#...
#"""
#self._timeout = timeout
#return self

#def __enter__(self):
#if self._entered:
#self._entered += 1
#return self
#if self._timeout is None:
#if not _atomic_byte_lock(self._lock_address):
#self._timeout = self._original_timeout
#raise ResourceBusyError("Couldn't acquire lock")
#else:
#threshold = time.time() + self._timeout
#while time.time() <= threshold:
#if _atomic_byte_lock(self._lock_address):
#break
#else:
#self._timeout = self._original_timeout
#raise TimeoutError("Timeout trying to acquire lock")
#self._entered += 1
#return self

#def __exit__(self, *args):
#if not self._entered:
#return
#self._entered -= 1
#if self._entered:
#return
#buffer = _remote_memory(self._lock_address, 1)
#buffer[0] = 0
#del buffer
#self._entered = False
#self._timeout = self._original_timeout

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


# when a RemoteArray can't be destroyed in parent,
# it comes to "sleep" here, where a callback in the
# GC will periodically try to remove it:
Expand Down
84 changes: 84 additions & 0 deletions tests/test_lock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import pickle
from textwrap import dedent as D


import pytest

import extrainterpreters as ei


from extrainterpreters import Lock, RLock
from extrainterpreters.lock import IntRLock

@pytest.mark.parametrize("LockCls", [Lock, RLock, IntRLock])
def test_locks_are_acquireable(LockCls):
lock = LockCls()
assert not lock.locked()
lock.acquire()
assert lock.locked()
lock.release()
assert not lock.locked()


@pytest.mark.parametrize("LockCls", [Lock, RLock, IntRLock])
def test_locks_work_as_context_manager(LockCls):
lock = LockCls()
assert not lock.locked()
with lock:
assert lock.locked()
assert not lock.locked()



def test_lock_cant_be_reacquired():
lock = Lock()

lock.acquire()

with pytest.raises(TimeoutError):
lock.acquire(timeout=0)


@pytest.mark.parametrize("LockCls", [Lock, RLock, IntRLock])
def test_locks_cant_be_passed_to_other_interpreter(LockCls):
lock = LockCls()
interp = ei.Interpreter().start()
interp.run_string(
D(
f"""
import extrainterpreters; extrainterpreters.DEBUG=True
lock = pickle.loads({pickle.dumps(lock)})
assert lock._lock._data == 0
"""
)
)
lock._lock._data[0] = 2
interp.run_string(
D(
"""
assert lock._lock._data[0] == 2
lock._lock._data[0] = 5
"""
)
)
assert lock._lock._data[0] == 5
interp.close()


#@pytest.mark.parametrize("LockCls", [Lock, RLock, IntRLock])
#def test_locks_cant_be_acquired_in_other_interpreter(LockCls):
#lock = LockCls()
#interp = ei.Interpreter().start()
#board.new_item((1, 2))
#interp.run_string(
#D(
#f"""
#import extrainterpreters; extrainterpreters.DEBUG=True
#lock = pickle.loads({pickle.dumps(lock)})

#index, item = board.fetch_item()
#assert item == (1,2)
#board.new_item((3,4))
#"""
#)
#)

0 comments on commit ea500fb

Please sign in to comment.