290 lines
9.6 KiB
Python
290 lines
9.6 KiB
Python
"""
|
|
Shared resources supporting priorities and preemption.
|
|
|
|
These resources can be used to limit the number of processes using them
|
|
concurrently. A process needs to *request* the usage right to a resource. Once
|
|
the usage right is not needed anymore it has to be *released*. A gas station
|
|
can be modelled as a resource with a limited amount of fuel-pumps. Vehicles
|
|
arrive at the gas station and request to use a fuel-pump. If all fuel-pumps are
|
|
in use, the vehicle needs to wait until one of the users has finished refueling
|
|
and releases its fuel-pump.
|
|
|
|
These resources can be used by a limited number of processes at a time.
|
|
Processes *request* these resources to become a user and have to *release* them
|
|
once they are done. For example, a gas station with a limited number of fuel
|
|
pumps can be modeled with a `Resource`. Arriving vehicles request a fuel-pump.
|
|
Once one is available they refuel. When they are done, the release the
|
|
fuel-pump and leave the gas station.
|
|
|
|
Requesting a resource is modelled as "putting a process' token into the
|
|
resources" and releasing a resources correspondingly as "getting a process'
|
|
token out of the resource". Thus, calling ``request()``/``release()`` is
|
|
equivalent to calling ``put()``/``get()``. Note, that releasing a resource will
|
|
always succeed immediately, no matter if a process is actually using a resource
|
|
or not.
|
|
|
|
Besides :class:`Resource`, there is a :class:`PriorityResource`, where
|
|
processes can define a request priority, and a :class:`PreemptiveResource`
|
|
whose resource users can be preempted by requests with a higher priority.
|
|
|
|
"""
|
|
from types import TracebackType
|
|
from typing import TYPE_CHECKING, Any, List, Optional, Type
|
|
|
|
from simpy.core import BoundClass, Environment, SimTime
|
|
from simpy.events import Process
|
|
from simpy.resources import base
|
|
|
|
|
|
class Preempted:
|
|
"""Cause of an preemption :class:`~simpy.exceptions.Interrupt` containing
|
|
information about the preemption.
|
|
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
by: Optional[Process],
|
|
usage_since: Optional[SimTime],
|
|
resource: 'Resource',
|
|
):
|
|
self.by = by
|
|
"""The preempting :class:`simpy.events.Process`."""
|
|
self.usage_since = usage_since
|
|
"""The simulation time at which the preempted process started to use
|
|
the resource."""
|
|
self.resource = resource
|
|
"""The resource which was lost, i.e., caused the preemption."""
|
|
|
|
|
|
class Request(base.Put):
|
|
"""Request usage of the *resource*. The event is triggered once access is
|
|
granted. Subclass of :class:`simpy.resources.base.Put`.
|
|
|
|
If the maximum capacity of users has not yet been reached, the request is
|
|
triggered immediately. If the maximum capacity has been
|
|
reached, the request is triggered once an earlier usage request on the
|
|
resource is released.
|
|
|
|
The request is automatically released when the request was created within
|
|
a :keyword:`with` statement.
|
|
|
|
"""
|
|
|
|
resource: 'Resource'
|
|
|
|
#: The time at which the request succeeded.
|
|
usage_since: Optional[SimTime] = None
|
|
|
|
def __exit__(
|
|
self,
|
|
exc_type: Optional[Type[BaseException]],
|
|
exc_value: Optional[BaseException],
|
|
traceback: Optional[TracebackType],
|
|
) -> Optional[bool]:
|
|
super().__exit__(exc_type, exc_value, traceback)
|
|
# Don't release the resource on generator cleanups. This seems to
|
|
# create unclaimable circular references otherwise.
|
|
if exc_type is not GeneratorExit:
|
|
self.resource.release(self)
|
|
return None
|
|
|
|
|
|
class Release(base.Get):
|
|
"""Releases the usage of *resource* granted by *request*. This event is
|
|
triggered immediately. Subclass of :class:`simpy.resources.base.Get`.
|
|
|
|
"""
|
|
|
|
def __init__(self, resource: 'Resource', request: Request):
|
|
self.request = request
|
|
"""The request (:class:`Request`) that is to be released."""
|
|
super().__init__(resource)
|
|
|
|
|
|
class PriorityRequest(Request):
|
|
"""Request the usage of *resource* with a given *priority*. If the
|
|
*resource* supports preemption and *preempt* is ``True`` other usage
|
|
requests of the *resource* may be preempted (see
|
|
:class:`PreemptiveResource` for details).
|
|
|
|
This event type inherits :class:`Request` and adds some additional
|
|
attributes needed by :class:`PriorityResource` and
|
|
:class:`PreemptiveResource`
|
|
|
|
"""
|
|
|
|
def __init__(
|
|
self, resource: 'Resource', priority: int = 0, preempt: bool = True
|
|
):
|
|
self.priority = priority
|
|
"""The priority of this request. A smaller number means higher
|
|
priority."""
|
|
|
|
self.preempt = preempt
|
|
"""Indicates whether the request should preempt a resource user or not
|
|
(:class:`PriorityResource` ignores this flag)."""
|
|
|
|
self.time = resource._env.now
|
|
"""The time at which the request was made."""
|
|
|
|
self.key = (self.priority, self.time, not self.preempt)
|
|
"""Key for sorting events. Consists of the priority (lower value is
|
|
more important), the time at which the request was made (earlier
|
|
requests are more important) and finally the preemption flag (preempt
|
|
requests are more important)."""
|
|
|
|
super().__init__(resource)
|
|
|
|
|
|
class SortedQueue(list):
|
|
"""Queue for sorting events by their :attr:`~PriorityRequest.key`
|
|
attribute.
|
|
|
|
"""
|
|
|
|
def __init__(self, maxlen: Optional[int] = None):
|
|
super().__init__()
|
|
self.maxlen = maxlen
|
|
"""Maximum length of the queue."""
|
|
|
|
def append(self, item: Any) -> None:
|
|
"""Sort *item* into the queue.
|
|
|
|
Raise a :exc:`RuntimeError` if the queue is full.
|
|
|
|
"""
|
|
if self.maxlen is not None and len(self) >= self.maxlen:
|
|
raise RuntimeError('Cannot append event. Queue is full.')
|
|
|
|
super().append(item)
|
|
super().sort(key=lambda e: e.key)
|
|
|
|
|
|
class Resource(base.BaseResource):
|
|
"""Resource with *capacity* of usage slots that can be requested by
|
|
processes.
|
|
|
|
If all slots are taken, requests are enqueued. Once a usage request is
|
|
released, a pending request will be triggered.
|
|
|
|
The *env* parameter is the :class:`~simpy.core.Environment` instance the
|
|
resource is bound to.
|
|
|
|
"""
|
|
|
|
def __init__(self, env: Environment, capacity: int = 1):
|
|
if capacity <= 0:
|
|
raise ValueError('"capacity" must be > 0.')
|
|
|
|
super().__init__(env, capacity)
|
|
|
|
self.users: List[Request] = []
|
|
"""List of :class:`Request` events for the processes that are currently
|
|
using the resource."""
|
|
self.queue = self.put_queue
|
|
"""Queue of pending :class:`Request` events. Alias of
|
|
:attr:`~simpy.resources.base.BaseResource.put_queue`.
|
|
"""
|
|
|
|
@property
|
|
def count(self) -> int:
|
|
"""Number of users currently using the resource."""
|
|
return len(self.users)
|
|
|
|
if TYPE_CHECKING:
|
|
|
|
def request(self) -> Request:
|
|
"""Request a usage slot."""
|
|
return Request(self)
|
|
|
|
def release(self, request: Request) -> Release:
|
|
"""Release a usage slot."""
|
|
return Release(self, request)
|
|
|
|
else:
|
|
request = BoundClass(Request)
|
|
release = BoundClass(Release)
|
|
|
|
def _do_put(self, event: Request) -> None:
|
|
if len(self.users) < self.capacity:
|
|
self.users.append(event)
|
|
event.usage_since = self._env.now
|
|
event.succeed()
|
|
|
|
def _do_get(self, event: Release) -> None:
|
|
try:
|
|
self.users.remove(event.request) # type: ignore
|
|
except ValueError:
|
|
pass
|
|
event.succeed()
|
|
|
|
|
|
class PriorityResource(Resource):
|
|
"""A :class:`~simpy.resources.resource.Resource` supporting prioritized
|
|
requests.
|
|
|
|
Pending requests in the :attr:`~Resource.queue` are sorted in ascending
|
|
order by their *priority* (that means lower values are more important).
|
|
|
|
"""
|
|
|
|
PutQueue = SortedQueue
|
|
"""Type of the put queue. See
|
|
:attr:`~simpy.resources.base.BaseResource.put_queue` for details."""
|
|
|
|
GetQueue = list
|
|
"""Type of the get queue. See
|
|
:attr:`~simpy.resources.base.BaseResource.get_queue` for details."""
|
|
|
|
def __init__(self, env: Environment, capacity: int = 1):
|
|
super().__init__(env, capacity)
|
|
|
|
if TYPE_CHECKING:
|
|
|
|
def request(
|
|
self, priority: int = 0, preempt: bool = True
|
|
) -> PriorityRequest:
|
|
"""Request a usage slot with the given *priority*."""
|
|
return PriorityRequest(self, priority, preempt)
|
|
|
|
def release( # type: ignore[override] # noqa: F821
|
|
self, request: PriorityRequest
|
|
) -> Release:
|
|
"""Release a usage slot."""
|
|
return Release(self, request)
|
|
|
|
else:
|
|
request = BoundClass(PriorityRequest)
|
|
release = BoundClass(Release)
|
|
|
|
|
|
class PreemptiveResource(PriorityResource):
|
|
"""A :class:`~simpy.resources.resource.PriorityResource` with preemption.
|
|
|
|
If a request is preempted, the process of that request will receive an
|
|
:class:`~simpy.exceptions.Interrupt` with a :class:`Preempted` instance as
|
|
cause.
|
|
|
|
"""
|
|
|
|
users: List[PriorityRequest] # type: ignore
|
|
|
|
def _do_put( # type: ignore[override] # noqa: F821
|
|
self, event: PriorityRequest
|
|
) -> None:
|
|
if len(self.users) >= self.capacity and event.preempt:
|
|
# Check if we can preempt another process
|
|
preempt = sorted(self.users, key=lambda e: e.key)[-1]
|
|
if preempt.key > event.key:
|
|
self.users.remove(preempt)
|
|
preempt.proc.interrupt( # type: ignore
|
|
Preempted(
|
|
by=event.proc,
|
|
usage_since=preempt.usage_since,
|
|
resource=self,
|
|
)
|
|
)
|
|
|
|
return super()._do_put(event)
|