-
Notifications
You must be signed in to change notification settings - Fork 179
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
fix(api): New protocols print a JSON "run log" from opentrons_execute and opentrons.execute.execute() #13629
Merged
SyntaxColoring
merged 16 commits into
chore_release-7.0.1
from
stab_at_run_log_printing
Sep 28, 2023
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
9aa1b0d
Add a context manager interface to Broker.
SyntaxColoring 3ad2648
Add a read-only interface to Broker.
SyntaxColoring 32e61b8
Make LegacyContextPlugin use the read-only interface.
SyntaxColoring d1bf83d
Make LegacyContextPlugin use new context manager interface.
SyntaxColoring 3b35467
Add CommandView.get_running().
SyntaxColoring 343f323
Quick and dirty implementation of emit_runlog. Introduce protocol_eng…
SyntaxColoring 78905dc
Update test_execute.py.
SyntaxColoring ce97224
Add tests for protocol_engine.command_monitor.
SyntaxColoring f941ba7
Simplify monitor_commands() implementation, avoiding the helper class.
SyntaxColoring 43a5fbf
Cleanup in opentrons.execute.
SyntaxColoring d07ebd2
Add some comments to _adapt_command().
SyntaxColoring 91da8ab
Help out mypy.
SyntaxColoring 6c19f53
Expose the underlying broker instead of proxying it.
SyntaxColoring ec3a3fb
Update tests for ChangeNotifier.
SyntaxColoring b862401
Slightly more powerful test for non-async-ness.
SyntaxColoring 1a8c1f7
Lint.
SyntaxColoring File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
"""Monitor the execution of commands in a `ProtocolEngine`.""" | ||
|
||
|
||
from dataclasses import dataclass | ||
import typing | ||
import contextlib | ||
|
||
|
||
from opentrons.protocol_engine import Command, ProtocolEngine | ||
|
||
|
||
@dataclass | ||
class RunningEvent: | ||
"""Emitted when a command starts running.""" | ||
|
||
command: Command | ||
|
||
|
||
@dataclass | ||
class NoLongerRunningEvent: | ||
"""Emitted when a command stops running--either because it succeeded, or failed.""" | ||
|
||
command: Command | ||
|
||
|
||
Event = typing.Union[RunningEvent, NoLongerRunningEvent] | ||
Callback = typing.Callable[[Event], None] | ||
|
||
|
||
@contextlib.contextmanager | ||
def monitor_commands( | ||
protocol_engine: ProtocolEngine, | ||
callback: Callback, | ||
) -> typing.Generator[None, None, None]: | ||
"""Monitor the execution of commands in `protocol_engine`. | ||
|
||
While this context manager is open, `callback` will be called any time `protocol_engine` | ||
starts or stops a command. | ||
""" | ||
# Subscribe to all state updates in protocol_engine. | ||
# On every update, diff the new state against the last state and see if the currently | ||
# running command has changed. If it has, emit the appropriate events. | ||
|
||
last_running_id: typing.Optional[str] = None | ||
|
||
def handle_state_update(_message_from_broker: None) -> None: | ||
nonlocal last_running_id | ||
|
||
running_id = protocol_engine.state_view.commands.get_running() | ||
if running_id != last_running_id: | ||
if last_running_id is not None: | ||
callback( | ||
NoLongerRunningEvent( | ||
protocol_engine.state_view.commands.get(last_running_id) | ||
) | ||
) | ||
|
||
if running_id is not None: | ||
callback( | ||
RunningEvent(protocol_engine.state_view.commands.get(running_id)) | ||
) | ||
last_running_id = running_id | ||
|
||
with protocol_engine.state_update_broker.subscribed(handle_state_update): | ||
yield |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,8 @@ | |
EnumeratedError, | ||
) | ||
|
||
from opentrons.util.broker import ReadOnlyBroker | ||
|
||
from .errors import ProtocolCommandFailedError, ErrorOccurrence | ||
from .errors.exceptions import EStopActivatedError | ||
from . import commands, slot_standardization | ||
|
@@ -129,6 +131,36 @@ def state_view(self) -> StateView: | |
"""Get an interface to retrieve calculated state values.""" | ||
return self._state_store | ||
|
||
@property | ||
def state_update_broker(self) -> ReadOnlyBroker[None]: | ||
Comment on lines
+134
to
+135
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. Things I do not like about this:
Things I like about this:
|
||
"""Return a broker that you can use to get notified of all state updates. | ||
|
||
For example, you can use this to do something any time a new command starts running. | ||
|
||
`ProtocolEngine` will publish a message to this broker (with the placeholder value `None`) | ||
any time its state updates. Then, when you receive that message, you can get the latest | ||
state through `state_view` and inspect it to see whether something happened that you care | ||
about. | ||
|
||
Warning: | ||
Use this mechanism sparingly, because it has several footguns: | ||
|
||
* Your callbacks will run synchronously, on every state update. | ||
If they take a long time, they will harm analysis and run speed. | ||
|
||
* Your callbacks will run in the thread and asyncio event loop that own this | ||
`ProtocolEngine`. (See the concurrency notes in the `ProtocolEngine` docstring.) | ||
If your callbacks interact with things in other threads or event loops, | ||
take appropriate precautions to keep them concurrency-safe. | ||
|
||
* Currently, if your callback raises an exception, it will propagate into | ||
`ProtocolEngine` and be treated like any other internal error. This will probably | ||
stop the run. If you expect your code to raise exceptions and don't want | ||
that to happen, consider catching and logging them at the top level of your callback, | ||
before they propagate into `ProtocolEngine`. | ||
""" | ||
return self._state_store.update_broker | ||
|
||
def add_plugin(self, plugin: AbstractPlugin) -> None: | ||
"""Add a plugin to the engine to customize behavior.""" | ||
self._plugin_starter.start(plugin) | ||
|
12 changes: 12 additions & 0 deletions
12
api/src/opentrons/protocol_engine/state/change_notifier.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,31 @@ | ||
"""Simple state change notification interface.""" | ||
import asyncio | ||
|
||
from opentrons.util.broker import Broker, ReadOnlyBroker | ||
|
||
|
||
class ChangeNotifier: | ||
"""An interface tto emit or subscribe to state change notifications.""" | ||
|
||
def __init__(self) -> None: | ||
"""Initialize the ChangeNotifier with an internal Event.""" | ||
self._event = asyncio.Event() | ||
self._broker = Broker[None]() | ||
|
||
def notify(self) -> None: | ||
"""Notify all `wait`'ers that the state has changed.""" | ||
self._event.set() | ||
self._broker.publish(None) | ||
|
||
async def wait(self) -> None: | ||
"""Wait until the next state change notification.""" | ||
self._event.clear() | ||
await self._event.wait() | ||
|
||
@property | ||
def broker(self) -> ReadOnlyBroker[None]: | ||
"""Return a broker that you can use to get notified of all changes. | ||
|
||
This is an alternative interface to `wait()`. | ||
""" | ||
return self._broker |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is new to me :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah...Python. 😬