flwr 1.19.0__py3-none-any.whl → 1.20.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flwr/cli/build.py +15 -5
- flwr/cli/new/new.py +12 -4
- flwr/cli/new/templates/app/README.flowertune.md.tpl +2 -0
- flwr/cli/new/templates/app/README.md.tpl +5 -0
- flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +14 -3
- flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +13 -1
- flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +21 -2
- flwr/cli/new/templates/app/pyproject.jax.toml.tpl +18 -1
- flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +19 -2
- flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +18 -1
- flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +20 -3
- flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +18 -1
- flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +18 -1
- flwr/cli/run/run.py +45 -38
- flwr/cli/utils.py +12 -5
- flwr/client/grpc_adapter_client/connection.py +11 -4
- flwr/client/grpc_rere_client/connection.py +92 -117
- flwr/client/rest_client/connection.py +131 -164
- flwr/common/constant.py +3 -1
- flwr/common/exit/exit_code.py +16 -1
- flwr/common/grpc.py +12 -1
- flwr/common/{inflatable_grpc_utils.py → inflatable_protobuf_utils.py} +52 -10
- flwr/common/inflatable_utils.py +191 -24
- flwr/common/record/array.py +101 -22
- flwr/common/record/arraychunk.py +59 -0
- flwr/common/serde.py +0 -28
- flwr/compat/client/app.py +14 -31
- flwr/proto/appio_pb2.py +43 -0
- flwr/proto/appio_pb2.pyi +151 -0
- flwr/proto/appio_pb2_grpc.py +4 -0
- flwr/proto/appio_pb2_grpc.pyi +4 -0
- flwr/proto/clientappio_pb2.py +12 -19
- flwr/proto/clientappio_pb2.pyi +23 -101
- flwr/proto/clientappio_pb2_grpc.py +269 -28
- flwr/proto/clientappio_pb2_grpc.pyi +114 -20
- flwr/proto/fleet_pb2.py +12 -20
- flwr/proto/fleet_pb2.pyi +6 -36
- flwr/proto/serverappio_pb2.py +8 -31
- flwr/proto/serverappio_pb2.pyi +0 -152
- flwr/proto/serverappio_pb2_grpc.py +39 -38
- flwr/proto/serverappio_pb2_grpc.pyi +21 -20
- flwr/server/app.py +1 -1
- flwr/server/fleet_event_log_interceptor.py +4 -0
- flwr/server/grid/grpc_grid.py +91 -54
- flwr/server/serverapp/app.py +27 -17
- flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +8 -0
- flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +1 -1
- flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +2 -5
- flwr/server/superlink/fleet/message_handler/message_handler.py +10 -16
- flwr/server/superlink/fleet/rest_rere/rest_api.py +1 -2
- flwr/server/superlink/serverappio/serverappio_grpc.py +1 -1
- flwr/server/superlink/serverappio/serverappio_servicer.py +35 -43
- flwr/server/superlink/simulation/simulationio_grpc.py +1 -1
- flwr/server/superlink/simulation/simulationio_servicer.py +1 -1
- flwr/server/superlink/utils.py +0 -35
- flwr/simulation/app.py +8 -0
- flwr/simulation/run_simulation.py +17 -0
- flwr/{server/superlink → supercore}/ffs/disk_ffs.py +1 -1
- flwr/supercore/grpc_health/__init__.py +22 -0
- flwr/supercore/grpc_health/simple_health_servicer.py +38 -0
- flwr/supercore/license_plugin/__init__.py +22 -0
- flwr/supercore/license_plugin/license_plugin.py +26 -0
- flwr/supercore/object_store/in_memory_object_store.py +31 -31
- flwr/supercore/object_store/object_store.py +20 -42
- flwr/supercore/object_store/utils.py +43 -0
- flwr/supercore/scheduler/__init__.py +22 -0
- flwr/supercore/scheduler/plugin.py +71 -0
- flwr/supercore/utils.py +32 -0
- flwr/superexec/deployment.py +1 -2
- flwr/superexec/exec_event_log_interceptor.py +4 -0
- flwr/superexec/exec_grpc.py +18 -2
- flwr/superexec/exec_license_interceptor.py +82 -0
- flwr/superexec/exec_servicer.py +10 -1
- flwr/superexec/exec_user_auth_interceptor.py +10 -2
- flwr/superexec/executor.py +1 -1
- flwr/superexec/simulation.py +1 -2
- flwr/supernode/cli/flower_supernode.py +0 -7
- flwr/supernode/cli/flwr_clientapp.py +10 -3
- flwr/supernode/nodestate/in_memory_nodestate.py +11 -2
- flwr/supernode/nodestate/nodestate.py +15 -0
- flwr/supernode/runtime/run_clientapp.py +110 -33
- flwr/supernode/scheduler/__init__.py +22 -0
- flwr/supernode/scheduler/simple_clientapp_scheduler_plugin.py +49 -0
- flwr/supernode/servicer/clientappio/__init__.py +1 -3
- flwr/supernode/servicer/clientappio/clientappio_servicer.py +223 -164
- flwr/supernode/start_client_internal.py +202 -104
- {flwr-1.19.0.dist-info → flwr-1.20.0.dist-info}/METADATA +2 -1
- {flwr-1.19.0.dist-info → flwr-1.20.0.dist-info}/RECORD +93 -78
- flwr/common/inflatable_rest_utils.py +0 -99
- /flwr/{server/superlink → supercore}/ffs/__init__.py +0 -0
- /flwr/{server/superlink → supercore}/ffs/ffs.py +0 -0
- /flwr/{server/superlink → supercore}/ffs/ffs_factory.py +0 -0
- {flwr-1.19.0.dist-info → flwr-1.20.0.dist-info}/WHEEL +0 -0
- {flwr-1.19.0.dist-info → flwr-1.20.0.dist-info}/entry_points.txt +0 -0
flwr/server/superlink/utils.py
CHANGED
|
@@ -20,11 +20,7 @@ from typing import Optional, Union
|
|
|
20
20
|
import grpc
|
|
21
21
|
|
|
22
22
|
from flwr.common.constant import Status, SubStatus
|
|
23
|
-
from flwr.common.inflatable import iterate_object_tree
|
|
24
23
|
from flwr.common.typing import RunStatus
|
|
25
|
-
from flwr.proto.fleet_pb2 import PushMessagesRequest # pylint: disable=E0611
|
|
26
|
-
from flwr.proto.message_pb2 import ObjectIDs # pylint: disable=E0611
|
|
27
|
-
from flwr.proto.serverappio_pb2 import PushInsMessagesRequest # pylint: disable=E0611
|
|
28
24
|
from flwr.server.superlink.linkstate import LinkState
|
|
29
25
|
from flwr.supercore.object_store import ObjectStore
|
|
30
26
|
|
|
@@ -74,34 +70,3 @@ def abort_if(
|
|
|
74
70
|
"""Abort context if status of the provided `run_id` is in `abort_status_list`."""
|
|
75
71
|
msg = check_abort(run_id, abort_status_list, state, store)
|
|
76
72
|
abort_grpc_context(msg, context)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def store_mapping_and_register_objects(
|
|
80
|
-
store: ObjectStore, request: Union[PushInsMessagesRequest, PushMessagesRequest]
|
|
81
|
-
) -> dict[str, ObjectIDs]:
|
|
82
|
-
"""Store Message object to descendants mapping and preregister objects."""
|
|
83
|
-
if not request.messages_list:
|
|
84
|
-
return {}
|
|
85
|
-
|
|
86
|
-
objects_to_push: dict[str, ObjectIDs] = {}
|
|
87
|
-
|
|
88
|
-
# Get run_id from the first message in the list
|
|
89
|
-
# All messages of a request should in the same run
|
|
90
|
-
run_id = request.messages_list[0].metadata.run_id
|
|
91
|
-
|
|
92
|
-
for object_tree in request.message_object_trees:
|
|
93
|
-
all_object_ids = [obj.object_id for obj in iterate_object_tree(object_tree)]
|
|
94
|
-
msg_object_id, descendant_ids = all_object_ids[-1], all_object_ids[:-1]
|
|
95
|
-
# Store mapping
|
|
96
|
-
store.set_message_descendant_ids(
|
|
97
|
-
msg_object_id=msg_object_id, descendant_ids=descendant_ids
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
# Preregister
|
|
101
|
-
object_ids_just_registered = store.preregister(run_id, object_tree)
|
|
102
|
-
# Keep track of objects that need to be pushed
|
|
103
|
-
objects_to_push[msg_object_id] = ObjectIDs(
|
|
104
|
-
object_ids=object_ids_just_registered
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
return objects_to_push
|
flwr/simulation/app.py
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
import argparse
|
|
19
|
+
import gc
|
|
19
20
|
from logging import DEBUG, ERROR, INFO
|
|
20
21
|
from queue import Queue
|
|
21
22
|
from time import sleep
|
|
@@ -271,6 +272,13 @@ def run_simulation_process( # pylint: disable=R0914, disable=W0212, disable=R09
|
|
|
271
272
|
)
|
|
272
273
|
)
|
|
273
274
|
|
|
275
|
+
# Clean up the Context if it exists
|
|
276
|
+
try:
|
|
277
|
+
del updated_context
|
|
278
|
+
except NameError:
|
|
279
|
+
pass
|
|
280
|
+
gc.collect()
|
|
281
|
+
|
|
274
282
|
# Stop the loop if `flwr-simulation` is expected to process a single run
|
|
275
283
|
if run_once:
|
|
276
284
|
break
|
|
@@ -19,6 +19,7 @@ import argparse
|
|
|
19
19
|
import asyncio
|
|
20
20
|
import json
|
|
21
21
|
import logging
|
|
22
|
+
import platform
|
|
22
23
|
import sys
|
|
23
24
|
import threading
|
|
24
25
|
import traceback
|
|
@@ -63,6 +64,18 @@ def _replace_keys(d: Any, match: str, target: str) -> Any:
|
|
|
63
64
|
return d
|
|
64
65
|
|
|
65
66
|
|
|
67
|
+
def _check_ray_support(backend_name: str) -> None:
|
|
68
|
+
if backend_name.lower() == "ray":
|
|
69
|
+
if platform.system() == "Windows":
|
|
70
|
+
log(
|
|
71
|
+
WARNING,
|
|
72
|
+
"Ray support on Windows is experimental "
|
|
73
|
+
"and may not work as expected. "
|
|
74
|
+
"On Windows, Flower Simulations run best in WSL2: "
|
|
75
|
+
"https://learn.microsoft.com/en-us/windows/wsl/about",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
66
79
|
# Entry point from CLI
|
|
67
80
|
# pylint: disable=too-many-locals
|
|
68
81
|
def run_simulation_from_cli() -> None:
|
|
@@ -82,6 +95,8 @@ def run_simulation_from_cli() -> None:
|
|
|
82
95
|
code_example='TF_FORCE_GPU_ALLOW_GROWTH="true" flower-simulation <...>',
|
|
83
96
|
)
|
|
84
97
|
|
|
98
|
+
_check_ray_support(args.backend)
|
|
99
|
+
|
|
85
100
|
# Load JSON config
|
|
86
101
|
backend_config_dict = json.loads(args.backend_config)
|
|
87
102
|
|
|
@@ -208,6 +223,8 @@ def run_simulation(
|
|
|
208
223
|
"\n\tflwr.simulation.run_simulationt(...)",
|
|
209
224
|
)
|
|
210
225
|
|
|
226
|
+
_check_ray_support(backend_name)
|
|
227
|
+
|
|
211
228
|
_ = _run_simulation(
|
|
212
229
|
num_supernodes=num_supernodes,
|
|
213
230
|
client_app=client_app,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Copyright 2025 Flower Labs GmbH. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
# ==============================================================================
|
|
15
|
+
"""GRPC health servicers."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from .simple_health_servicer import SimpleHealthServicer
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"SimpleHealthServicer",
|
|
22
|
+
]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Copyright 2025 Flower Labs GmbH. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
# ==============================================================================
|
|
15
|
+
"""Simple gRPC health servicers."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
import grpc
|
|
19
|
+
|
|
20
|
+
# pylint: disable=E0611
|
|
21
|
+
from grpc_health.v1.health_pb2 import HealthCheckRequest, HealthCheckResponse
|
|
22
|
+
from grpc_health.v1.health_pb2_grpc import HealthServicer
|
|
23
|
+
|
|
24
|
+
# pylint: enable=E0611
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SimpleHealthServicer(HealthServicer): # type: ignore
|
|
28
|
+
"""A simple gRPC health servicer that always returns SERVING."""
|
|
29
|
+
|
|
30
|
+
def Check(
|
|
31
|
+
self, request: HealthCheckRequest, context: grpc.RpcContext
|
|
32
|
+
) -> HealthCheckResponse:
|
|
33
|
+
"""Return a HealthCheckResponse with SERVING status."""
|
|
34
|
+
return HealthCheckResponse(status=HealthCheckResponse.SERVING)
|
|
35
|
+
|
|
36
|
+
def Watch(self, request: HealthCheckRequest, context: grpc.RpcContext) -> None:
|
|
37
|
+
"""Watch the health status (not implemented)."""
|
|
38
|
+
context.abort(grpc.StatusCode.UNIMPLEMENTED, "Watch is not implemented")
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Copyright 2025 Flower Labs GmbH. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
# ==============================================================================
|
|
15
|
+
"""Flower license plugin components."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from .license_plugin import LicensePlugin as LicensePlugin
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"LicensePlugin",
|
|
22
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Copyright 2025 Flower Labs GmbH. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
# ==============================================================================
|
|
15
|
+
"""Abstract class for Flower License Plugin."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from abc import ABC, abstractmethod
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class LicensePlugin(ABC):
|
|
22
|
+
"""Abstract Flower License Plugin class."""
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def check_license(self) -> bool:
|
|
26
|
+
"""Check if the license is valid."""
|
|
@@ -20,7 +20,6 @@ from dataclasses import dataclass
|
|
|
20
20
|
from typing import Optional
|
|
21
21
|
|
|
22
22
|
from flwr.common.inflatable import (
|
|
23
|
-
get_object_children_ids_from_object_content,
|
|
24
23
|
get_object_id,
|
|
25
24
|
is_valid_sha256_hash,
|
|
26
25
|
iterate_object_tree,
|
|
@@ -37,6 +36,7 @@ class ObjectEntry:
|
|
|
37
36
|
|
|
38
37
|
content: bytes
|
|
39
38
|
is_available: bool
|
|
39
|
+
child_object_ids: list[str] # List of child object IDs
|
|
40
40
|
ref_count: int # Number of references (direct parents) to this object
|
|
41
41
|
runs: set[int] # Set of run IDs that used this object
|
|
42
42
|
|
|
@@ -70,6 +70,9 @@ class InMemoryObjectStore(ObjectStore):
|
|
|
70
70
|
self.store[obj_id] = ObjectEntry(
|
|
71
71
|
content=b"", # Initially empty content
|
|
72
72
|
is_available=False, # Initially not available
|
|
73
|
+
child_object_ids=[ # List of child object IDs
|
|
74
|
+
child.object_id for child in tree_node.children
|
|
75
|
+
],
|
|
73
76
|
ref_count=0, # Reference count starts at 0
|
|
74
77
|
runs={run_id}, # Start with the current run ID
|
|
75
78
|
)
|
|
@@ -102,6 +105,32 @@ class InMemoryObjectStore(ObjectStore):
|
|
|
102
105
|
|
|
103
106
|
return new_objects
|
|
104
107
|
|
|
108
|
+
def get_object_tree(self, object_id: str) -> ObjectTree:
|
|
109
|
+
"""Get the object tree for a given object ID."""
|
|
110
|
+
with self.lock_store:
|
|
111
|
+
# Raise an exception if there's no object with the given ID
|
|
112
|
+
if not (object_entry := self.store.get(object_id)):
|
|
113
|
+
raise NoObjectInStoreError(
|
|
114
|
+
f"Object with ID '{object_id}' was not pre-registered."
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Build the object trees of all children
|
|
118
|
+
try:
|
|
119
|
+
child_trees = [
|
|
120
|
+
self.get_object_tree(child_id)
|
|
121
|
+
for child_id in object_entry.child_object_ids
|
|
122
|
+
]
|
|
123
|
+
except NoObjectInStoreError as e:
|
|
124
|
+
# Raise an error if any child object is missing
|
|
125
|
+
# This indicates an integrity issue
|
|
126
|
+
raise NoObjectInStoreError(
|
|
127
|
+
f"Object tree for object ID '{object_id}' contains missing "
|
|
128
|
+
"children. This may indicate a corrupted object store."
|
|
129
|
+
) from e
|
|
130
|
+
|
|
131
|
+
# Create and return the ObjectTree for the current object
|
|
132
|
+
return ObjectTree(object_id=object_id, children=child_trees)
|
|
133
|
+
|
|
105
134
|
def put(self, object_id: str, object_content: bytes) -> None:
|
|
106
135
|
"""Put an object into the store."""
|
|
107
136
|
if self.verify:
|
|
@@ -128,29 +157,6 @@ class InMemoryObjectStore(ObjectStore):
|
|
|
128
157
|
self.store[object_id].content = object_content
|
|
129
158
|
self.store[object_id].is_available = True
|
|
130
159
|
|
|
131
|
-
def set_message_descendant_ids(
|
|
132
|
-
self, msg_object_id: str, descendant_ids: list[str]
|
|
133
|
-
) -> None:
|
|
134
|
-
"""Store the mapping from a ``Message`` object ID to the object IDs of its
|
|
135
|
-
descendants."""
|
|
136
|
-
with self.lock_msg_mapping:
|
|
137
|
-
self.msg_descendant_objects_mapping[msg_object_id] = descendant_ids
|
|
138
|
-
|
|
139
|
-
def get_message_descendant_ids(self, msg_object_id: str) -> list[str]:
|
|
140
|
-
"""Retrieve the object IDs of all descendants of a given Message."""
|
|
141
|
-
with self.lock_msg_mapping:
|
|
142
|
-
if msg_object_id not in self.msg_descendant_objects_mapping:
|
|
143
|
-
raise NoObjectInStoreError(
|
|
144
|
-
f"No message registered in Object Store with ID '{msg_object_id}'. "
|
|
145
|
-
"Mapping to descendants could not be found."
|
|
146
|
-
)
|
|
147
|
-
return self.msg_descendant_objects_mapping[msg_object_id]
|
|
148
|
-
|
|
149
|
-
def delete_message_descendant_ids(self, msg_object_id: str) -> None:
|
|
150
|
-
"""Delete the mapping from a ``Message`` object ID to its descendants."""
|
|
151
|
-
with self.lock_msg_mapping:
|
|
152
|
-
self.msg_descendant_objects_mapping.pop(msg_object_id, None)
|
|
153
|
-
|
|
154
160
|
def get(self, object_id: str) -> Optional[bytes]:
|
|
155
161
|
"""Get an object from the store."""
|
|
156
162
|
with self.lock_store:
|
|
@@ -177,10 +183,7 @@ class InMemoryObjectStore(ObjectStore):
|
|
|
177
183
|
self.run_objects_mapping[run_id].discard(object_id)
|
|
178
184
|
|
|
179
185
|
# Decrease the reference count of its children
|
|
180
|
-
|
|
181
|
-
object_entry.content
|
|
182
|
-
)
|
|
183
|
-
for child_id in children_ids:
|
|
186
|
+
for child_id in object_entry.child_object_ids:
|
|
184
187
|
self.store[child_id].ref_count -= 1
|
|
185
188
|
|
|
186
189
|
# Recursively try to delete the child object
|
|
@@ -205,9 +208,6 @@ class InMemoryObjectStore(ObjectStore):
|
|
|
205
208
|
# Delete the message object and its unreferenced descendants
|
|
206
209
|
self.delete(object_id)
|
|
207
210
|
|
|
208
|
-
# Delete the message's descendants mapping
|
|
209
|
-
self.delete_message_descendant_ids(object_id)
|
|
210
|
-
|
|
211
211
|
# Remove the run from the mapping
|
|
212
212
|
del self.run_objects_mapping[run_id]
|
|
213
213
|
|
|
@@ -60,6 +60,22 @@ class ObjectStore(abc.ABC):
|
|
|
60
60
|
in the `ObjectStore`, or were preregistered but are not yet available.
|
|
61
61
|
"""
|
|
62
62
|
|
|
63
|
+
@abc.abstractmethod
|
|
64
|
+
def get_object_tree(self, object_id: str) -> ObjectTree:
|
|
65
|
+
"""Get the object tree for a given object ID.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
object_id : str
|
|
70
|
+
The ID of the object for which to retrieve the object tree.
|
|
71
|
+
|
|
72
|
+
Returns
|
|
73
|
+
-------
|
|
74
|
+
ObjectTree
|
|
75
|
+
An ObjectTree representing the hierarchical structure of the object with
|
|
76
|
+
the given ID and its descendants.
|
|
77
|
+
"""
|
|
78
|
+
|
|
63
79
|
@abc.abstractmethod
|
|
64
80
|
def put(self, object_id: str, object_content: bytes) -> None:
|
|
65
81
|
"""Put an object into the store.
|
|
@@ -83,8 +99,10 @@ class ObjectStore(abc.ABC):
|
|
|
83
99
|
|
|
84
100
|
Returns
|
|
85
101
|
-------
|
|
86
|
-
bytes
|
|
87
|
-
The object stored under the given object_id.
|
|
102
|
+
Optional[bytes]
|
|
103
|
+
The object stored under the given object_id if it exists, else None.
|
|
104
|
+
The returned bytes will be b"" if the object is not yet available,
|
|
105
|
+
but has been preregistered.
|
|
88
106
|
"""
|
|
89
107
|
|
|
90
108
|
@abc.abstractmethod
|
|
@@ -126,46 +144,6 @@ class ObjectStore(abc.ABC):
|
|
|
126
144
|
This method should remove all objects from the store.
|
|
127
145
|
"""
|
|
128
146
|
|
|
129
|
-
@abc.abstractmethod
|
|
130
|
-
def set_message_descendant_ids(
|
|
131
|
-
self, msg_object_id: str, descendant_ids: list[str]
|
|
132
|
-
) -> None:
|
|
133
|
-
"""Store the mapping from a ``Message`` object ID to the object IDs of its
|
|
134
|
-
descendants.
|
|
135
|
-
|
|
136
|
-
Parameters
|
|
137
|
-
----------
|
|
138
|
-
msg_object_id : str
|
|
139
|
-
The object ID of the ``Message``.
|
|
140
|
-
descendant_ids : list[str]
|
|
141
|
-
A list of object IDs representing all descendant objects of the ``Message``.
|
|
142
|
-
"""
|
|
143
|
-
|
|
144
|
-
@abc.abstractmethod
|
|
145
|
-
def get_message_descendant_ids(self, msg_object_id: str) -> list[str]:
|
|
146
|
-
"""Retrieve the object IDs of all descendants of a given ``Message``.
|
|
147
|
-
|
|
148
|
-
Parameters
|
|
149
|
-
----------
|
|
150
|
-
msg_object_id : str
|
|
151
|
-
The object ID of the ``Message``.
|
|
152
|
-
|
|
153
|
-
Returns
|
|
154
|
-
-------
|
|
155
|
-
list[str]
|
|
156
|
-
A list of object IDs of all descendant objects of the ``Message``.
|
|
157
|
-
"""
|
|
158
|
-
|
|
159
|
-
@abc.abstractmethod
|
|
160
|
-
def delete_message_descendant_ids(self, msg_object_id: str) -> None:
|
|
161
|
-
"""Delete the mapping from a ``Message`` object ID to its descendants.
|
|
162
|
-
|
|
163
|
-
Parameters
|
|
164
|
-
----------
|
|
165
|
-
msg_object_id : str
|
|
166
|
-
The object ID of the ``Message``.
|
|
167
|
-
"""
|
|
168
|
-
|
|
169
147
|
@abc.abstractmethod
|
|
170
148
|
def __contains__(self, object_id: str) -> bool:
|
|
171
149
|
"""Check if an object_id is in the store.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Copyright 2025 Flower Labs GmbH. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
# ==============================================================================
|
|
15
|
+
"""Utils for ObjectStore."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from typing import Union
|
|
19
|
+
|
|
20
|
+
from flwr.proto.appio_pb2 import PushAppMessagesRequest # pylint: disable=E0611
|
|
21
|
+
from flwr.proto.fleet_pb2 import PushMessagesRequest # pylint: disable=E0611
|
|
22
|
+
|
|
23
|
+
from . import ObjectStore
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def store_mapping_and_register_objects(
|
|
27
|
+
store: ObjectStore, request: Union[PushAppMessagesRequest, PushMessagesRequest]
|
|
28
|
+
) -> set[str]:
|
|
29
|
+
"""Store Message object to descendants mapping and preregister objects."""
|
|
30
|
+
if not request.messages_list:
|
|
31
|
+
return set()
|
|
32
|
+
objects_to_push: set[str] = set()
|
|
33
|
+
# Get run_id from the first message in the list
|
|
34
|
+
# All messages of a request should in the same run
|
|
35
|
+
run_id = request.messages_list[0].metadata.run_id
|
|
36
|
+
|
|
37
|
+
for object_tree in request.message_object_trees:
|
|
38
|
+
# Preregister
|
|
39
|
+
unavailable_obj_ids = store.preregister(run_id, object_tree)
|
|
40
|
+
# Keep track of objects that need to be pushed
|
|
41
|
+
objects_to_push |= set(unavailable_obj_ids)
|
|
42
|
+
|
|
43
|
+
return objects_to_push
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Copyright 2025 Flower Labs GmbH. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
# ==============================================================================
|
|
15
|
+
"""Flower App Scheduler."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from .plugin import SchedulerPlugin
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"SchedulerPlugin",
|
|
22
|
+
]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Copyright 2025 Flower Labs GmbH. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
# ==============================================================================
|
|
15
|
+
"""Abstract base class SchedulerPlugin."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from abc import ABC, abstractmethod
|
|
19
|
+
from collections.abc import Sequence
|
|
20
|
+
from typing import Callable, Optional
|
|
21
|
+
|
|
22
|
+
from flwr.common.typing import Run
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SchedulerPlugin(ABC):
|
|
26
|
+
"""Abstract base class for Scheduler plugins."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
appio_api_address: str,
|
|
31
|
+
flwr_dir: str,
|
|
32
|
+
get_run: Callable[[int], Run],
|
|
33
|
+
) -> None:
|
|
34
|
+
self.appio_api_address = appio_api_address
|
|
35
|
+
self.flwr_dir = flwr_dir
|
|
36
|
+
self.get_run = get_run
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def select_run_id(self, candidate_run_ids: Sequence[int]) -> Optional[int]:
|
|
40
|
+
"""Select a run ID to execute from a sequence of candidates.
|
|
41
|
+
|
|
42
|
+
A candidate run ID is one that has at least one pending message and is
|
|
43
|
+
not currently in progress (i.e., not associated with a token).
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
candidate_run_ids : Sequence[int]
|
|
48
|
+
A sequence of candidate run IDs to choose from.
|
|
49
|
+
|
|
50
|
+
Returns
|
|
51
|
+
-------
|
|
52
|
+
Optional[int]
|
|
53
|
+
The selected run ID, or None if no suitable candidate is found.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def launch_app(self, token: str, run_id: int) -> None:
|
|
58
|
+
"""Launch the application associated with a given run ID and token.
|
|
59
|
+
|
|
60
|
+
This method starts the application process using the given `token`.
|
|
61
|
+
The `run_id` is used solely for bookkeeping purposes, allowing any
|
|
62
|
+
scheduler implementation to associate this launch with a specific run.
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
token : str
|
|
67
|
+
The token required to run the application.
|
|
68
|
+
run_id : int
|
|
69
|
+
The ID of the run associated with the token, used for tracking or
|
|
70
|
+
logging purposes.
|
|
71
|
+
"""
|
flwr/supercore/utils.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Copyright 2025 Flower Labs GmbH. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
# ==============================================================================
|
|
15
|
+
"""Utility functions for the infrastructure."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def mask_string(value: str, head: int = 4, tail: int = 4) -> str:
|
|
19
|
+
"""Mask a string by preserving only the head and tail characters.
|
|
20
|
+
|
|
21
|
+
Mask a string for safe display by preserving the head and tail characters,
|
|
22
|
+
and replacing the middle with '...'. Useful for logging tokens, secrets,
|
|
23
|
+
or IDs without exposing sensitive data.
|
|
24
|
+
|
|
25
|
+
Notes
|
|
26
|
+
-----
|
|
27
|
+
If the string is shorter than the combined length of `head` and `tail`,
|
|
28
|
+
the original string is returned unchanged.
|
|
29
|
+
"""
|
|
30
|
+
if len(value) <= head + tail:
|
|
31
|
+
return value
|
|
32
|
+
return f"{value[:head]}...{value[-tail:]}"
|
flwr/superexec/deployment.py
CHANGED
|
@@ -31,9 +31,8 @@ from flwr.common.constant import (
|
|
|
31
31
|
)
|
|
32
32
|
from flwr.common.logger import log
|
|
33
33
|
from flwr.common.typing import Fab, RunStatus, UserConfig
|
|
34
|
-
from flwr.server.superlink.ffs import Ffs
|
|
35
|
-
from flwr.server.superlink.ffs.ffs_factory import FfsFactory
|
|
36
34
|
from flwr.server.superlink.linkstate import LinkState, LinkStateFactory
|
|
35
|
+
from flwr.supercore.ffs import Ffs, FfsFactory
|
|
37
36
|
|
|
38
37
|
from .executor import Executor
|
|
39
38
|
|
|
@@ -44,6 +44,10 @@ class ExecEventLogInterceptor(grpc.ServerInterceptor): # type: ignore
|
|
|
44
44
|
Continue RPC call if event logger is enabled on the SuperLink, else, terminate
|
|
45
45
|
RPC call by setting context to abort.
|
|
46
46
|
"""
|
|
47
|
+
# Only apply to Exec service
|
|
48
|
+
if not handler_call_details.method.startswith("/flwr.proto.Exec/"):
|
|
49
|
+
return continuation(handler_call_details)
|
|
50
|
+
|
|
47
51
|
# One of the method handlers in
|
|
48
52
|
# `flwr.superexec.exec_servicer.ExecServicer`
|
|
49
53
|
method_handler: grpc.RpcMethodHandler = continuation(handler_call_details)
|
flwr/superexec/exec_grpc.py
CHANGED
|
@@ -23,21 +23,31 @@ import grpc
|
|
|
23
23
|
from flwr.common import GRPC_MAX_MESSAGE_LENGTH
|
|
24
24
|
from flwr.common.auth_plugin import ExecAuthPlugin, ExecAuthzPlugin
|
|
25
25
|
from flwr.common.event_log_plugin import EventLogWriterPlugin
|
|
26
|
+
from flwr.common.exit import ExitCode, flwr_exit
|
|
26
27
|
from flwr.common.grpc import generic_create_grpc_server
|
|
27
28
|
from flwr.common.logger import log
|
|
28
29
|
from flwr.common.typing import UserConfig
|
|
29
30
|
from flwr.proto.exec_pb2_grpc import add_ExecServicer_to_server
|
|
30
|
-
from flwr.server.superlink.ffs.ffs_factory import FfsFactory
|
|
31
31
|
from flwr.server.superlink.linkstate import LinkStateFactory
|
|
32
|
+
from flwr.supercore.ffs import FfsFactory
|
|
33
|
+
from flwr.supercore.license_plugin import LicensePlugin
|
|
32
34
|
from flwr.supercore.object_store import ObjectStoreFactory
|
|
33
35
|
from flwr.superexec.exec_event_log_interceptor import ExecEventLogInterceptor
|
|
36
|
+
from flwr.superexec.exec_license_interceptor import ExecLicenseInterceptor
|
|
34
37
|
from flwr.superexec.exec_user_auth_interceptor import ExecUserAuthInterceptor
|
|
35
38
|
|
|
36
39
|
from .exec_servicer import ExecServicer
|
|
37
40
|
from .executor import Executor
|
|
38
41
|
|
|
42
|
+
try:
|
|
43
|
+
from flwr.ee import get_license_plugin
|
|
44
|
+
except ImportError:
|
|
39
45
|
|
|
40
|
-
|
|
46
|
+
def get_license_plugin() -> Optional[LicensePlugin]:
|
|
47
|
+
"""Return the license plugin."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# pylint: disable-next=too-many-arguments,too-many-positional-arguments,too-many-locals
|
|
41
51
|
def run_exec_api_grpc(
|
|
42
52
|
address: str,
|
|
43
53
|
executor: Executor,
|
|
@@ -53,6 +63,10 @@ def run_exec_api_grpc(
|
|
|
53
63
|
"""Run Exec API (gRPC, request-response)."""
|
|
54
64
|
executor.set_config(config)
|
|
55
65
|
|
|
66
|
+
license_plugin: Optional[LicensePlugin] = get_license_plugin()
|
|
67
|
+
if license_plugin and not license_plugin.check_license():
|
|
68
|
+
flwr_exit(ExitCode.SUPERLINK_LICENSE_INVALID)
|
|
69
|
+
|
|
56
70
|
exec_servicer: grpc.Server = ExecServicer(
|
|
57
71
|
linkstate_factory=state_factory,
|
|
58
72
|
ffs_factory=ffs_factory,
|
|
@@ -61,6 +75,8 @@ def run_exec_api_grpc(
|
|
|
61
75
|
auth_plugin=auth_plugin,
|
|
62
76
|
)
|
|
63
77
|
interceptors: list[grpc.ServerInterceptor] = []
|
|
78
|
+
if license_plugin is not None:
|
|
79
|
+
interceptors.append(ExecLicenseInterceptor(license_plugin))
|
|
64
80
|
if auth_plugin is not None and authz_plugin is not None:
|
|
65
81
|
interceptors.append(ExecUserAuthInterceptor(auth_plugin, authz_plugin))
|
|
66
82
|
# Event log interceptor must be added after user auth interceptor
|