276 lines
9.3 KiB
Python
Raw Normal View History

2022-06-24 17:14:37 +02:00
"""
Base classes of for SimPy's shared resource types.
:class:`BaseResource` defines the abstract base resource. It supports *get* and
*put* requests, which return :class:`Put` and :class:`Get` events respectively.
These events are triggered once the request has been completed.
"""
from types import TracebackType
from typing import (
TYPE_CHECKING,
ClassVar,
ContextManager,
Generic,
MutableSequence,
Optional,
Type,
TypeVar,
Union,
)
from simpy.core import BoundClass, Environment
from simpy.events import Event, Process
ResourceType = TypeVar('ResourceType', bound='BaseResource')
class Put(Event, ContextManager['Put'], Generic[ResourceType]):
"""Generic event for requesting to put something into the *resource*.
This event (and all of its subclasses) can act as context manager and can
be used with the :keyword:`with` statement to automatically cancel the
request if an exception (like an :class:`simpy.exceptions.Interrupt` for
example) occurs:
.. code-block:: python
with res.put(item) as request:
yield request
"""
def __init__(self, resource: ResourceType):
super().__init__(resource._env)
self.resource = resource
self.proc: Optional[Process] = self.env.active_process
resource.put_queue.append(self)
self.callbacks.append(resource._trigger_get)
resource._trigger_put(None)
def __enter__(self) -> 'Put':
return self
def __exit__(
self,
exc_type: Optional[Type[BaseException]],
exc_value: Optional[BaseException],
traceback: Optional[TracebackType],
) -> Optional[bool]:
self.cancel()
return None
def cancel(self) -> None:
"""Cancel this put request.
This method has to be called if the put request must be aborted, for
example if a process needs to handle an exception like an
:class:`~simpy.exceptions.Interrupt`.
If the put request was created in a :keyword:`with` statement, this
method is called automatically.
"""
if not self.triggered:
self.resource.put_queue.remove(self)
class Get(Event, ContextManager['Get'], Generic[ResourceType]):
"""Generic event for requesting to get something from the *resource*.
This event (and all of its subclasses) can act as context manager and can
be used with the :keyword:`with` statement to automatically cancel the
request if an exception (like an :class:`simpy.exceptions.Interrupt` for
example) occurs:
.. code-block:: python
with res.get() as request:
item = yield request
"""
def __init__(self, resource: ResourceType):
super().__init__(resource._env)
self.resource = resource
self.proc = self.env.active_process
resource.get_queue.append(self)
self.callbacks.append(resource._trigger_put)
resource._trigger_get(None)
def __enter__(self) -> 'Get':
return self
def __exit__(
self,
exc_type: Optional[Type[BaseException]],
exc_value: Optional[BaseException],
traceback: Optional[TracebackType],
) -> Optional[bool]:
self.cancel()
return None
def cancel(self) -> None:
"""Cancel this get request.
This method has to be called if the get request must be aborted, for
example if a process needs to handle an exception like an
:class:`~simpy.exceptions.Interrupt`.
If the get request was created in a :keyword:`with` statement, this
method is called automatically.
"""
if not self.triggered:
self.resource.get_queue.remove(self)
PutType = TypeVar('PutType', bound=Put)
GetType = TypeVar('GetType', bound=Get)
class BaseResource(Generic[PutType, GetType]):
"""Abstract base class for a shared resource.
You can :meth:`put()` something into the resources or :meth:`get()`
something out of it. Both methods return an event that is triggered once
the operation is completed. If a :meth:`put()` request cannot complete
immediately (for example if the resource has reached a capacity limit) it
is enqueued in the :attr:`put_queue` for later processing. Likewise for
:meth:`get()` requests.
Subclasses can customize the resource by:
- providing custom :attr:`PutQueue` and :attr:`GetQueue` types,
- providing custom :class:`Put` respectively :class:`Get` events,
- and implementing the request processing behaviour through the methods
``_do_get()`` and ``_do_put()``.
"""
PutQueue: ClassVar[Type[MutableSequence[PutType]]] = list
"""The type to be used for the :attr:`put_queue`. It is a plain
:class:`list` by default. The type must support index access (e.g.
``__getitem__()`` and ``__len__()``) as well as provide ``append()`` and
``pop()`` operations."""
GetQueue: ClassVar[Type[MutableSequence[GetType]]] = list
"""The type to be used for the :attr:`get_queue`. It is a plain
:class:`list` by default. The type must support index access (e.g.
``__getitem__()`` and ``__len__()``) as well as provide ``append()`` and
``pop()`` operations."""
def __init__(self, env: Environment, capacity: Union[float, int]):
self._env = env
self._capacity = capacity
self.put_queue = self.PutQueue()
"""Queue of pending *put* requests."""
self.get_queue = self.GetQueue()
"""Queue of pending *get* requests."""
# Bind event constructors as methods
BoundClass.bind_early(self)
@property
def capacity(self) -> Union[float, int]:
"""Maximum capacity of the resource."""
return self._capacity
if TYPE_CHECKING:
def put(self) -> Put:
"""Request to put something into the resource and return a
:class:`Put` event, which gets triggered once the request
succeeds."""
return Put(self)
def get(self) -> Get:
"""Request to get something from the resource and return a
:class:`Get` event, which gets triggered once the request
succeeds."""
return Get(self)
else:
put = BoundClass(Put)
get = BoundClass(Get)
def _do_put(self, event: PutType) -> Optional[bool]:
"""Perform the *put* operation.
This method needs to be implemented by subclasses. If the conditions
for the put *event* are met, the method must trigger the event (e.g.
call :meth:`Event.succeed()` with an apropriate value).
This method is called by :meth:`_trigger_put` for every event in the
:attr:`put_queue`, as long as the return value does not evaluate
``False``.
"""
raise NotImplementedError(self)
def _trigger_put(self, get_event: Optional[GetType]) -> None:
"""This method is called once a new put event has been created or a get
event has been processed.
The method iterates over all put events in the :attr:`put_queue` and
calls :meth:`_do_put` to check if the conditions for the event are met.
If :meth:`_do_put` returns ``False``, the iteration is stopped early.
"""
# Maintain queue invariant: All put requests must be untriggered.
# This code is not very pythonic because the queue interface should be
# simple (only append(), pop(), __getitem__() and __len__() are
# required).
idx = 0
while idx < len(self.put_queue):
put_event = self.put_queue[idx]
proceed = self._do_put(put_event)
if not put_event.triggered:
idx += 1
elif self.put_queue.pop(idx) != put_event:
raise RuntimeError('Put queue invariant violated')
if not proceed:
break
def _do_get(self, event: GetType) -> Optional[bool]:
"""Perform the *get* operation.
This method needs to be implemented by subclasses. If the conditions
for the get *event* are met, the method must trigger the event (e.g.
call :meth:`Event.succeed()` with an apropriate value).
This method is called by :meth:`_trigger_get` for every event in the
:attr:`get_queue`, as long as the return value does not evaluate
``False``.
"""
raise NotImplementedError(self)
def _trigger_get(self, put_event: Optional[PutType]) -> None:
"""Trigger get events.
This method is called once a new get event has been created or a put
event has been processed.
The method iterates over all get events in the :attr:`get_queue` and
calls :meth:`_do_get` to check if the conditions for the event are met.
If :meth:`_do_get` returns ``False``, the iteration is stopped early.
"""
# Maintain queue invariant: All get requests must be untriggered.
# This code is not very pythonic because the queue interface should be
# simple (only append(), pop(), __getitem__() and __len__() are
# required).
idx = 0
while idx < len(self.get_queue):
get_event = self.get_queue[idx]
proceed = self._do_get(get_event)
if not get_event.triggered:
idx += 1
elif self.get_queue.pop(idx) != get_event:
raise RuntimeError('Get queue invariant violated')
if not proceed:
break