-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat(framework) Add flwr-app-scheduler command
#5627
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 18 commits
03ca07e
e4f513d
2d4e3de
98d0065
c268714
b8682a2
9c5e49e
925ed5e
fee6a3e
b43543f
5e247a0
a0d92ba
df09bf1
b229fb8
8225da3
86deb8d
614de70
5cf52a0
9c1531a
fece66a
31943a4
f231d50
4caf671
fc204a7
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 |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| # Copyright 2025 Flower Labs GmbH. All Rights Reserved. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
| # ============================================================================== | ||
| """Flower command line interface for shared infrastructure components.""" | ||
|
|
||
|
|
||
| from .flwr_app_scheduler import flwr_app_scheduler | ||
|
|
||
| __all__ = [ | ||
| "flwr_app_scheduler", | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| # Copyright 2025 Flower Labs GmbH. All Rights Reserved. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
| # ============================================================================== | ||
| """`flwr-app-scheduler` command.""" | ||
|
|
||
|
|
||
| import argparse | ||
| from logging import INFO | ||
|
|
||
| from flwr.common import EventType, event | ||
| from flwr.common.constant import SchedulerPluginType | ||
| from flwr.common.logger import log | ||
| from flwr.supercore.scheduler import run_app_scheduler | ||
| from flwr.supernode.scheduler import SimpleClientAppSchedulerPlugin | ||
|
|
||
|
|
||
| def flwr_app_scheduler() -> None: | ||
| """Run `flwr-app-scheduler` command.""" | ||
| args = _parse_args().parse_args() | ||
|
|
||
| # Log the first message after parsing arguments in case of `--help` | ||
| log(INFO, "Starting Flower App Scheduler") | ||
|
|
||
| # Trigger telemetry event | ||
| event(EventType.FLWR_APP_SCHEDULER_RUN_ENTER) | ||
panh99 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| run_app_scheduler( | ||
| plugin_class=_get_plugin_class(args.plugin_type), | ||
| appio_api_address=args.appio_api_address, | ||
| flwr_dir=args.flwr_dir, | ||
| ) | ||
|
|
||
|
|
||
| def _parse_args() -> argparse.ArgumentParser: | ||
| """Parse `flwr-app-scheduler` command line arguments.""" | ||
| parser = argparse.ArgumentParser( | ||
| description="Run a Flower App Scheduler", | ||
| ) | ||
| parser.add_argument( | ||
| "--appio-api-address", type=str, required=True, help="Address of the AppIO API" | ||
| ) | ||
| parser.add_argument( | ||
| "--plugin-type", | ||
| type=str, | ||
| choices=SchedulerPluginType.all(), | ||
|
Member
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. nice! |
||
| required=True, | ||
| help="The type of plugin to use.", | ||
| ) | ||
| parser.add_argument( | ||
| "--insecure", | ||
| action="store_true", | ||
| help="Connect to the AppIO API without TLS. " | ||
| "Data transmitted between the client and server is not encrypted. " | ||
| "Use this flag only if you understand the risks.", | ||
| ) | ||
| parser.add_argument( | ||
| "--flwr-dir", | ||
| default=None, | ||
| help="""The path containing installed Flower Apps. | ||
| By default, this value is equal to: | ||
|
|
||
| - `$FLWR_HOME/` if `$FLWR_HOME` is defined | ||
| - `$XDG_DATA_HOME/.flwr/` if `$XDG_DATA_HOME` is defined | ||
| - `$HOME/.flwr/` in all other cases | ||
| """, | ||
| ) | ||
|
Comment on lines
+60
to
+77
Member
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. Should we inject these aregs via |
||
| return parser | ||
|
|
||
|
|
||
| def _get_plugin_class(plugin_type: str) -> type[SimpleClientAppSchedulerPlugin]: | ||
| """Get the plugin class based on the plugin type.""" | ||
| if plugin_type == SchedulerPluginType.CLIENT_APP: | ||
| return SimpleClientAppSchedulerPlugin | ||
| raise ValueError(f"Unknown plugin type: {plugin_type}") | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| # Copyright 2025 Flower Labs GmbH. All Rights Reserved. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
| # ============================================================================== | ||
| """Flower app scheduler process.""" | ||
|
|
||
|
|
||
| import time | ||
| from typing import Optional | ||
|
|
||
| from flwr.common.exit_handlers import register_exit_handlers | ||
| from flwr.common.grpc import create_channel, on_channel_state_change | ||
| from flwr.common.serde import run_from_proto | ||
| from flwr.common.telemetry import EventType | ||
| from flwr.common.typing import Run | ||
| from flwr.proto.clientappio_pb2 import ( # pylint: disable=E0611 | ||
| GetRunIdsWithPendingMessagesRequest, | ||
| RequestTokenRequest, | ||
| ) | ||
| from flwr.proto.clientappio_pb2_grpc import ClientAppIoStub | ||
| from flwr.proto.run_pb2 import GetRunRequest # pylint: disable=E0611 | ||
|
|
||
| from .plugin import SchedulerPlugin | ||
|
|
||
|
|
||
| def run_app_scheduler( | ||
| plugin_class: type[SchedulerPlugin], | ||
| appio_api_address: str, | ||
| flwr_dir: Optional[str] = None, | ||
| ) -> None: | ||
| """Run the Flower app scheduler. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| plugin_class : type[SchedulerPlugin] | ||
| The class of the scheduler plugin to use. | ||
| appio_api_address : str | ||
| The address of the AppIO API. | ||
| flwr_dir : Optional[str] (default: None) | ||
| The Flower directory. | ||
| """ | ||
| # Create the channel to the AppIO API | ||
| # No TLS support for now, so insecure connection | ||
| channel = create_channel( | ||
| server_address=appio_api_address, | ||
| insecure=True, | ||
| root_certificates=None, | ||
| ) | ||
| channel.subscribe(on_channel_state_change) | ||
|
|
||
| # Register exit handlers to close the channel on exit | ||
| register_exit_handlers( | ||
| event_type=EventType.FLWR_APP_SCHEDULER_RUN_LEAVE, | ||
| exit_message="Flower app scheduler terminated gracefully.", | ||
| exit_handlers=[lambda: channel.close()], | ||
| ) | ||
|
|
||
| # Create the gRPC stub for the AppIO API | ||
| # We shall merge the ClientAppIo and ServerAppIo in the future | ||
| # so we can use the same stub for both. | ||
| # For now, we use ClientAppIoStub. | ||
| stub = ClientAppIoStub(channel) | ||
|
|
||
| def get_run(run_id: int) -> Run: | ||
| _req = GetRunRequest(run_id=run_id) | ||
| _res = stub.GetRun(_req) | ||
| return run_from_proto(_res.run) | ||
|
|
||
| # Create the scheduler plugin instance | ||
| plugin = plugin_class( | ||
| appio_api_address=appio_api_address, | ||
| flwr_dir=flwr_dir, | ||
panh99 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| get_run=get_run, | ||
| ) | ||
|
|
||
| # Start the scheduler loop | ||
| try: | ||
| while True: | ||
| # Fetch suitable run IDs | ||
| get_runs_req = GetRunIdsWithPendingMessagesRequest() | ||
| get_runs_res = stub.GetRunIdsWithPendingMessages(get_runs_req) | ||
|
|
||
| # Allow the plugin to select a run ID | ||
| run_id = None | ||
| if get_runs_res.run_ids: | ||
| run_id = plugin.select_run_id(candidate_run_ids=get_runs_res.run_ids) | ||
|
|
||
| # Apply for a token if a run ID was selected | ||
| if run_id is not None: | ||
| tk_req = RequestTokenRequest(run_id=run_id) | ||
| tk_res = stub.RequestToken(tk_req) | ||
|
|
||
| # Launch the app if a token was granted; do nothing if not | ||
| if tk_res.token: | ||
| plugin.launch_app(token=tk_res.token, run_id=run_id) | ||
|
|
||
| # Sleep for a while before checking again | ||
| time.sleep(1) | ||
| finally: | ||
| channel.close() | ||
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.
Should we add logs similar to those in the
flwr-***commands mentioning aboutTLSand the address used? For example this is how it's done withflwr-clientappatm:https://github.com/adap/flower/blob/ce17e4e2573bb53a94a6a879e871f38a4394ffb2/framework/py/flwr/supernode/cli/flwr_clientapp.py#L32-L45