-
Notifications
You must be signed in to change notification settings - Fork 12
Let a run end on an interrupt without naming another cause #691
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
842d703
6b3220f
a2baca0
b2fb037
d5e4fa2
004a5a8
851da7a
df49ba8
f09172d
ee97552
89c3831
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,7 @@ | |
| import traceback | ||
| from collections import Counter, defaultdict, deque | ||
| from collections.abc import Callable, Iterator, Mapping | ||
| from contextlib import contextmanager | ||
| from enum import IntEnum | ||
| from multiprocessing import resource_tracker | ||
| from multiprocessing.managers import ValueProxy | ||
|
|
@@ -42,6 +43,7 @@ | |
| logger = logging.getLogger(__name__) | ||
|
|
||
| T = TypeVar('T') | ||
| U = TypeVar('U') | ||
| Req = TypeVar('Req') | ||
| Res = TypeVar('Res') | ||
|
|
||
|
|
@@ -75,6 +77,28 @@ def emit(self, data: T, ts: int = -1): | |
| pass | ||
|
|
||
|
|
||
| # Set in a process that has taken an interrupt. An interrupt can land inside a call to the manager, and | ||
| # that connection then holds half a message: the next call over it returns what another one asked for, so | ||
| # a reader takes a value from a channel it never subscribed to. Nothing may be sent or read after it. | ||
| _interrupted = False | ||
|
|
||
|
|
||
| @contextmanager | ||
| def _noting_interrupt() -> Iterator[None]: | ||
| """Record an interrupt taken inside the block, and let it go on. | ||
|
|
||
| A connection is torn by an interrupt that lands in the middle of a call over it, so every process that | ||
| reaches a transport records its own -- there is nowhere else the tearing can happen, and no process has | ||
| to have had a handler installed for it. | ||
| """ | ||
| global _interrupted | ||
| try: | ||
| yield | ||
| except KeyboardInterrupt: | ||
| _interrupted = True | ||
| raise | ||
|
|
||
|
|
||
| class MultiprocessEmitter(SignalEmitter[T]): | ||
| """Signal emitter that transparently bridges processes. | ||
|
|
||
|
|
@@ -118,7 +142,8 @@ def __init__( | |
| @property | ||
| def transport_mode(self) -> TransportMode: | ||
| if self._mode is TransportMode.UNDECIDED: | ||
| self._mode = TransportMode(self._mode_value.value) | ||
| with _noting_interrupt(): # reading the manager is where a connection is torn | ||
| self._mode = TransportMode(self._mode_value.value) | ||
| return self._mode | ||
|
|
||
| @property | ||
|
|
@@ -188,9 +213,12 @@ def _emit_shared_memory(self, data: SMCompliant, ts: int) -> bool: | |
|
|
||
| return True | ||
|
|
||
| @_noting_interrupt() | ||
| def emit(self, data: T, ts: int = -1): | ||
| if _interrupted: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Rule hidden-dependency violated: AGENTS.md reference: AGENTS.md:L7-L8 Useful? React with 👍 / 👎. |
||
| return | ||
| ts = ts if ts >= 0 else self._clock.now_ns() | ||
| mode = self._ensure_mode(data) | ||
| mode = self._ensure_mode(data) # itself a call to the manager, so it sits inside the guard | ||
|
|
||
| if mode is TransportMode.SHARED_MEMORY: | ||
| if not isinstance(data, SMCompliant): | ||
|
|
@@ -262,7 +290,8 @@ def __init__( | |
| @property | ||
| def transport_mode(self) -> TransportMode: | ||
| if self._mode is TransportMode.UNDECIDED: | ||
| self._mode = TransportMode(self._mode_value.value) | ||
| with _noting_interrupt(): # reading the manager is where a connection is torn | ||
| self._mode = TransportMode(self._mode_value.value) | ||
| return self._mode | ||
|
|
||
| @property | ||
|
|
@@ -275,6 +304,10 @@ def _read_queue(self) -> Message[T] | None: | |
| except Empty: | ||
| message = None | ||
| else: | ||
| if not isinstance(message, Message): | ||
| # An interrupt that lands inside a manager call leaves that connection holding half a | ||
| # message, and every read after it comes back as whatever another call asked for. | ||
| raise ConnectionError(f'the queue was read after an interrupt tore its connection: {message!r}') | ||
| self._last_queue_message = Message(message.data, message.ts, True) | ||
| if self._mode is TransportMode.UNDECIDED: | ||
| self._mode = TransportMode.QUEUE | ||
|
|
@@ -332,8 +365,11 @@ def _read_shared_memory(self) -> Message[T] | None: | |
| self._up_value.value = False | ||
| return Message(data=self._out_value, ts=self._ts_value.value, updated=updated) # instead of True | ||
|
|
||
| @_noting_interrupt() | ||
| def read(self) -> Message[T] | None: | ||
| mode = self.transport_mode | ||
| if _interrupted: | ||
| return None | ||
| mode = self.transport_mode # itself a call to the manager, so it sits inside the guard | ||
|
|
||
| if mode is TransportMode.SHARED_MEMORY: | ||
| return self._read_shared_memory() | ||
|
|
@@ -642,13 +678,35 @@ def interleave(self, *loops: ControlLoop) -> Iterator[Command]: | |
| self._advance_to(target_ns) | ||
| yield Sleep(wait_ns / 1e9) if wait_ns else Yield() | ||
|
|
||
| @overload | ||
| def connect(self, source: ControlSystemCaller[Req, Res], target: ControlSystemHandler[Req, Res]) -> None: ... | ||
|
|
||
| @overload | ||
| def connect( | ||
| self, | ||
| source: ControlSystemEmitter[T], | ||
| target: ControlSystemReceiver[T], | ||
| *, | ||
| emitter_wrapper: Callable[[SignalEmitter[T]], SignalEmitter[T]] = ..., | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Rule overspecific violated: AGENTS.md reference: AGENTS.md:L7-L8 Useful? React with 👍 / 👎.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not this change's to make. The overloads added here restate the annotation |
||
| ) -> None: ... | ||
|
|
||
| @overload | ||
| def connect( | ||
| self, | ||
| source: ControlSystemEmitter[T], | ||
| target: ControlSystemReceiver[U], | ||
| *, | ||
| emitter_wrapper: Callable[[SignalEmitter[T]], SignalEmitter[T]] = ..., | ||
| receiver_wrapper: Callable[[SignalReceiver[T]], SignalReceiver[U]], | ||
| ) -> None: ... | ||
|
|
||
| def connect( | ||
| self, | ||
| source: ControlSystemEmitter[T] | ControlSystemCaller[Req, Res], | ||
| target: ControlSystemReceiver[T] | ControlSystemHandler[Req, Res], | ||
| target: ControlSystemReceiver[U] | ControlSystemHandler[Req, Res], | ||
| *, | ||
| emitter_wrapper: Callable[[SignalEmitter[T]], SignalEmitter[T]] = identity, | ||
| receiver_wrapper: Callable[[SignalReceiver[T]], SignalReceiver[T]] = identity, | ||
| receiver_wrapper: Callable[[SignalReceiver[T]], SignalReceiver[U]] = identity, | ||
|
DarksaCY marked this conversation as resolved.
|
||
| ) -> None: | ||
| """Declare a logical connection: an Emitter feeding a Receiver, or a Caller invoking a Handler. | ||
|
|
||
|
|
@@ -663,7 +721,8 @@ def connect( | |
| emitter_wrapper: Optional function to wrap the underlying SignalEmitter | ||
| before binding. Defaults to identity function. | ||
| receiver_wrapper: Optional function to wrap the underlying SignalReceiver | ||
| before binding. Defaults to identity function. | ||
| before binding. Defaults to identity function. It is the only way the | ||
| receiver may carry a type other than the emitter's. | ||
|
|
||
| The wrapper functions allow for transformation or decoration of the | ||
| underlying signal transport mechanisms, such as adding logging, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.