-
Notifications
You must be signed in to change notification settings - Fork 3
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
Dispatch Managing Actor #54
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
cd5bf23
Send STOPPED message when duration is reached
Marenz 134ddab
Fix wrong running behaviors with duration=None
Marenz 4dd7b2b
Rewrite internal architecture to be more flexible
Marenz 0b671ed
Add dispatch mananging actor
Marenz 432b3cb
DispatchManagingActor: Support starting/stopping of multiple actors
Marenz bcd5c0f
Fix wrong Dispatch.running() behavior with duration=None
Marenz 611a4eb
DispatchActor: Update timer correctly after schedule modifications
Marenz 14d4510
Set dependency to latest dispatch-client
Marenz 0287a09
Update event_loop to explicitly set scope session
Marenz 820dcc3
Wrap scheduler to tolerate somehow invalid dispatches
Marenz 039d22e
Add test that notifications are sent at startup
Marenz 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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
# License: All rights reserved | ||
# Copyright © 2024 Frequenz Energy-as-a-Service GmbH | ||
|
||
"""Helper class to manage actors based on dispatches.""" | ||
|
||
import logging | ||
from dataclasses import dataclass | ||
from typing import Any, Set | ||
|
||
from frequenz.channels import Receiver, Sender | ||
from frequenz.client.dispatch.types import ComponentSelector | ||
from frequenz.sdk.actor import Actor | ||
|
||
from ._dispatch import Dispatch, RunningState | ||
|
||
_logger = logging.getLogger(__name__) | ||
|
||
|
||
@dataclass(frozen=True, kw_only=True) | ||
class DispatchUpdate: | ||
"""Event emitted when the dispatch changes.""" | ||
|
||
components: ComponentSelector | ||
"""Components to be used.""" | ||
|
||
dry_run: bool | ||
"""Whether this is a dry run.""" | ||
|
||
options: dict[str, Any] | ||
"""Additional options.""" | ||
|
||
|
||
class DispatchManagingActor(Actor): | ||
"""Helper class to manage actors based on dispatches. | ||
|
||
Example usage: | ||
|
||
```python | ||
import os | ||
import asyncio | ||
from frequenz.dispatch import Dispatcher, DispatchManagingActor, DispatchUpdate | ||
from frequenz.client.dispatch.types import ComponentSelector | ||
from frequenz.client.common.microgrid.components import ComponentCategory | ||
|
||
from frequenz.channels import Receiver, Broadcast | ||
|
||
class MyActor(Actor): | ||
def __init__(self, updates_channel: Receiver[DispatchUpdate]): | ||
super().__init__() | ||
self._updates_channel = updates_channel | ||
self._dry_run: bool | ||
self._options : dict[str, Any] | ||
|
||
async def _run(self) -> None: | ||
while True: | ||
update = await self._updates_channel.receive() | ||
print("Received update:", update) | ||
|
||
self.set_components(update.components) | ||
self._dry_run = update.dry_run | ||
self._options = update.options | ||
|
||
def set_components(self, components: ComponentSelector) -> None: | ||
match components: | ||
case [int(), *_] as component_ids: | ||
print("Dispatch: Setting components to %s", components) | ||
case [ComponentCategory.BATTERY, *_]: | ||
print("Dispatch: Using all battery components") | ||
case unsupported: | ||
print( | ||
"Dispatch: Requested an unsupported selector %r, " | ||
"but only component IDs or category BATTERY are supported.", | ||
unsupported, | ||
) | ||
|
||
async def run(): | ||
url = os.getenv("DISPATCH_API_URL", "grpc://fz-0004.frequenz.io:50051") | ||
key = os.getenv("DISPATCH_API_KEY", "some-key") | ||
|
||
microgrid_id = 1 | ||
|
||
dispatcher = Dispatcher( | ||
microgrid_id=microgrid_id, | ||
server_url=url, | ||
key=key | ||
) | ||
|
||
# Create update channel to receive dispatch update events pre-start and mid-run | ||
dispatch_updates_channel = Broadcast[DispatchUpdate](name="dispatch_updates_channel") | ||
|
||
# Start actor and give it an dispatch updates channel receiver | ||
my_actor = MyActor(dispatch_updates_channel.new_receiver()) | ||
|
||
status_receiver = dispatcher.running_status_change.new_receiver() | ||
|
||
managing_actor = DispatchManagingActor( | ||
actor=my_actor, | ||
dispatch_type="EXAMPLE", | ||
running_status_receiver=status_receiver, | ||
updates_sender=dispatch_updates_channel.new_sender(), | ||
) | ||
|
||
await asyncio.gather(dispatcher.start(), managing_actor.start()) | ||
``` | ||
""" | ||
|
||
def __init__( | ||
self, | ||
actor: Actor | Set[Actor], | ||
dispatch_type: str, | ||
running_status_receiver: Receiver[Dispatch], | ||
updates_sender: Sender[DispatchUpdate] | None = None, | ||
) -> None: | ||
"""Initialize the dispatch handler. | ||
|
||
Args: | ||
actor: A set of actors or a single actor to manage. | ||
dispatch_type: The type of dispatches to handle. | ||
running_status_receiver: The receiver for dispatch running status changes. | ||
updates_sender: The sender for dispatch events | ||
""" | ||
super().__init__() | ||
self._dispatch_rx = running_status_receiver | ||
self._actors = frozenset([actor] if isinstance(actor, Actor) else actor) | ||
self._dispatch_type = dispatch_type | ||
self._updates_sender = updates_sender | ||
|
||
def _start_actors(self) -> None: | ||
"""Start all actors.""" | ||
for actor in self._actors: | ||
if actor.is_running: | ||
_logger.warning("Actor %s is already running", actor.name) | ||
else: | ||
actor.start() | ||
|
||
async def _stop_actors(self, msg: str) -> None: | ||
"""Stop all actors. | ||
|
||
Args: | ||
msg: The message to be passed to the actors being stopped. | ||
""" | ||
for actor in self._actors: | ||
if actor.is_running: | ||
await actor.stop(msg) | ||
else: | ||
_logger.warning("Actor %s is not running", actor.name) | ||
|
||
async def _run(self) -> None: | ||
"""Wait for dispatches and handle them.""" | ||
async for dispatch in self._dispatch_rx: | ||
await self._handle_dispatch(dispatch=dispatch) | ||
|
||
async def _handle_dispatch(self, dispatch: Dispatch) -> None: | ||
"""Handle a dispatch. | ||
|
||
Args: | ||
dispatch: The dispatch to handle. | ||
""" | ||
running = dispatch.running(self._dispatch_type) | ||
match running: | ||
case RunningState.STOPPED: | ||
_logger.info("Stopped by dispatch %s", dispatch.id) | ||
await self._stop_actors("Dispatch stopped") | ||
case RunningState.RUNNING: | ||
if self._updates_sender is not None: | ||
_logger.info("Updated by dispatch %s", dispatch.id) | ||
await self._updates_sender.send( | ||
DispatchUpdate( | ||
components=dispatch.selector, | ||
dry_run=dispatch.dry_run, | ||
options=dispatch.payload, | ||
) | ||
) | ||
|
||
_logger.info("Started by dispatch %s", dispatch.id) | ||
self._start_actors() | ||
case RunningState.DIFFERENT_TYPE: | ||
_logger.debug( | ||
"Unknown dispatch! Ignoring dispatch of type %s", dispatch.type | ||
) |
Oops, something went wrong.
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.
Maybe we should remove this and keep the default as it is?
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.
Then we get a depreciation warning saying the behavior will change and one should set a default
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.
Oh, OK, I'm confused now. So this has nothing to do with trying to make tests pass and is just a new deprecation message in a new version of
pytest-asyncio
? If so it would be probably nice to put it in a separate commit with a corresponding commit message.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.
I def. tried this to make the test pass, bit it also fixes a deprecation warning, it just didn't help though.
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.
OK, this seems to be related to an update to https://github.com/pytest-dev/pytest-asyncio/releases/tag/v0.24.0. Issue: pytest-dev/pytest-asyncio#924