""" 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