1
Fork 0
mirror of https://github.com/RYGhub/royalnet.git synced 2024-11-26 21:14:19 +00:00

Implement a dispenser locking mechanism

This commit is contained in:
Steffo 2021-01-23 01:03:22 +01:00
parent 7af394d3bf
commit c2b01d768f
Signed by: steffo
GPG key ID: 6965406171929D01
3 changed files with 79 additions and 6 deletions

View file

@ -32,7 +32,7 @@ class Command(c.Conversation):
are later completed by a runner. are later completed by a runner.
""" """
def __init__(self, f: c.ConversationProtocol, *, names: t.List[str] = None, pattern: re.Pattern): def __init__(self, f: c.ConversationProtocol, *, names: t.List[str] = None, pattern: re.Pattern, lock: bool = True):
""" """
Create a new :class:`.Command` . Create a new :class:`.Command` .
""" """
@ -54,6 +54,11 @@ class Command(c.Conversation):
The pattern that should be matched by the command. The pattern that should be matched by the command.
""" """
self.lock: bool = lock
"""
If calling this command should :meth:`~royalnet.engineer.dispenser.Dispenser.lock` the dispenser.
"""
def name(self): def name(self):
""" """
:return: The main name of the Command. :return: The main name of the Command.
@ -97,8 +102,16 @@ class Command(c.Conversation):
log.debug(f"Match successful, getting capture groups of: {match!r}") log.debug(f"Match successful, getting capture groups of: {match!r}")
message_kwargs: t.Dict[str, str] = match.groupdict() message_kwargs: t.Dict[str, str] = match.groupdict()
log.debug(f"Passing args to function: {message_kwargs!r}") if self.lock:
return await super().run(_sentry=_sentry, _msg=bullet, **base_kwargs, **message_kwargs) log.debug(f"Locking the dispenser...")
with _sentry.dispenser().lock(self):
log.debug(f"Passing args to function: {message_kwargs!r}")
return await super().run(_sentry=_sentry, _msg=bullet, **base_kwargs, **message_kwargs)
else:
log.debug(f"Passing args to function: {message_kwargs!r}")
return await super().run(_sentry=_sentry, _msg=bullet, **base_kwargs, **message_kwargs)
def help(self) -> t.Optional[str]: def help(self) -> t.Optional[str]:
""" """
@ -116,7 +129,7 @@ class PartialCommand:
They can specified later using :meth:`.complete`. They can specified later using :meth:`.complete`.
""" """
def __init__(self, f: c.ConversationProtocol, syntax: str): def __init__(self, f: c.ConversationProtocol, syntax: str, lock: bool = True):
""" """
Create a new :class:`.PartialCommand` . Create a new :class:`.PartialCommand` .
@ -133,6 +146,11 @@ class PartialCommand:
Part of the pattern from where the arguments should be captured. Part of the pattern from where the arguments should be captured.
""" """
self.lock: bool = lock
"""
If calling this command should :meth:`~royalnet.engineer.dispenser.Dispenser.lock` the dispenser.
"""
@classmethod @classmethod
def new(cls, *args, **kwargs): def new(cls, *args, **kwargs):
""" """
@ -167,8 +185,10 @@ class PartialCommand:
raise ValueError(f"Name is not alphanumeric: {name!r}") raise ValueError(f"Name is not alphanumeric: {name!r}")
name_regex = f"(?:{'|'.join(names)})" name_regex = f"(?:{'|'.join(names)})"
log.debug(f"Completed pattern: {name_regex!r}")
pattern: re.Pattern = re.compile(pattern.format(name=name_regex, syntax=self.syntax), re.IGNORECASE) pattern: re.Pattern = re.compile(pattern.format(name=name_regex, syntax=self.syntax), re.IGNORECASE)
return Command(f=self.f, names=names, pattern=pattern) return Command(f=self.f, names=names, pattern=pattern, lock=self.lock)
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__qualname__} {self.f!r}>" return f"<{self.__class__.__qualname__} {self.f!r}>"

View file

@ -10,6 +10,7 @@ import contextlib
from .sentry import SentrySource from .sentry import SentrySource
from .conversation import Conversation from .conversation import Conversation
from .exc import LockedDispenserError
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -21,6 +22,13 @@ class Dispenser:
A :class:`list` of all the running sentries of this dispenser. A :class:`list` of all the running sentries of this dispenser.
""" """
self._locked_by: t.List[Conversation] = []
"""
The conversation that are currently locking this dispenser.
.. seealso:: :meth:`.lock`
"""
async def put(self, item: t.Any) -> None: async def put(self, item: t.Any) -> None:
""" """
Insert a new item in the queues of all the running sentries. Insert a new item in the queues of all the running sentries.
@ -54,7 +62,15 @@ class Dispenser:
Run the passed conversation. Run the passed conversation.
:param conv: The conversation to run. :param conv: The conversation to run.
:raises .LockedDispenserError: If the dispenser is currently :attr:`.locked_by` a :class:`.Conversation`.
""" """
log.debug(f"Trying to run: {conv!r}")
if self._locked_by is not None:
log.debug(f"Dispenser is locked by {self._locked_by!r}, refusing to run {conv!r}")
raise LockedDispenserError(self._locked_by, f"The Dispenser is currently locked by {self._locked_by!r} and "
f"cannot start new conversations.")
log.debug(f"Running: {conv}") log.debug(f"Running: {conv}")
with self.sentry() as sentry: with self.sentry() as sentry:
state = conv(_sentry=sentry, **kwargs) state = conv(_sentry=sentry, **kwargs)
@ -63,6 +79,26 @@ class Dispenser:
while state := await state: while state := await state:
log.debug(f"Switched to: {state}") log.debug(f"Switched to: {state}")
@contextlib.contextmanager
def lock(self, conv: Conversation):
"""
Lock the dispenser while this :func:`~contextlib.contextmanager` is in scope.
A locked dispenser will refuse to :meth:`.run` any new conversations, raising :exc:`.exc.LockedDispenserError`
instead.
:param conv: The conversation that requested the lock.
.. seealso:: :attr:`._locked_by`
"""
log.debug(f"Adding lock: {conv!r}")
self._locked_by.append(conv)
yield
log.debug(f"Clearing lock: {conv!r}")
self._locked_by.remove(conv)
__all__ = ( __all__ = (
"Dispenser", "Dispenser",

View file

@ -61,6 +61,21 @@ class ForbiddenError(FrontendError):
""" """
class DispenserException(EngineerException):
"""
The base class for errors in :mod:`royalnet.engineer.dispenser`.
"""
class LockedDispenserError(DispenserException):
"""
The dispenser couldn't start a new conversation as it is currently locked.
"""
def __init__(self, locked_by, *args):
super().__init__(*args)
self.locked_by = locked_by
__all__ = ( __all__ = (
"EngineerException", "EngineerException",
"WrenchException", "WrenchException",
@ -72,4 +87,6 @@ __all__ = (
"FrontendError", "FrontendError",
"NotSupportedError", "NotSupportedError",
"ForbiddenError", "ForbiddenError",
"DispenserException",
"LockedDispenserError",
) )