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
|
@@ -14,40 +14,29 @@
|
|
|
14
14
|
# ==============================================================================
|
|
15
15
|
"""Contextmanager for a REST request-response channel to the Flower server."""
|
|
16
16
|
|
|
17
|
+
|
|
17
18
|
from collections.abc import Iterator
|
|
18
19
|
from contextlib import contextmanager
|
|
19
|
-
from
|
|
20
|
-
from
|
|
21
|
-
from typing import Callable, Optional, TypeVar, Union, cast
|
|
20
|
+
from logging import ERROR, WARN
|
|
21
|
+
from typing import Callable, Optional, TypeVar, Union
|
|
22
22
|
|
|
23
23
|
from cryptography.hazmat.primitives.asymmetric import ec
|
|
24
24
|
from google.protobuf.message import Message as GrpcMessage
|
|
25
25
|
from requests.exceptions import ConnectionError as RequestsConnectionError
|
|
26
26
|
|
|
27
|
-
from flwr.app.metadata import Metadata
|
|
28
|
-
from flwr.client.message_handler.message_handler import validate_out_message
|
|
29
27
|
from flwr.common import GRPC_MAX_MESSAGE_LENGTH
|
|
30
28
|
from flwr.common.constant import HEARTBEAT_DEFAULT_INTERVAL
|
|
31
29
|
from flwr.common.exit import ExitCode, flwr_exit
|
|
32
30
|
from flwr.common.heartbeat import HeartbeatSender
|
|
33
|
-
from flwr.common.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
)
|
|
38
|
-
from flwr.common.inflatable_rest_utils import (
|
|
39
|
-
make_pull_object_fn_rest,
|
|
40
|
-
make_push_object_fn_rest,
|
|
41
|
-
)
|
|
42
|
-
from flwr.common.inflatable_utils import (
|
|
43
|
-
inflate_object_from_contents,
|
|
44
|
-
pull_objects,
|
|
45
|
-
push_objects,
|
|
31
|
+
from flwr.common.inflatable_protobuf_utils import (
|
|
32
|
+
make_confirm_message_received_fn_protobuf,
|
|
33
|
+
make_pull_object_fn_protobuf,
|
|
34
|
+
make_push_object_fn_protobuf,
|
|
46
35
|
)
|
|
47
36
|
from flwr.common.logger import log
|
|
48
37
|
from flwr.common.message import Message, remove_content_from_message
|
|
49
38
|
from flwr.common.retry_invoker import RetryInvoker
|
|
50
|
-
from flwr.common.serde import message_to_proto, run_from_proto
|
|
39
|
+
from flwr.common.serde import message_from_proto, message_to_proto, run_from_proto
|
|
51
40
|
from flwr.common.typing import Fab, Run
|
|
52
41
|
from flwr.proto.fab_pb2 import GetFabRequest, GetFabResponse # pylint: disable=E0611
|
|
53
42
|
from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
|
|
@@ -67,6 +56,7 @@ from flwr.proto.heartbeat_pb2 import ( # pylint: disable=E0611
|
|
|
67
56
|
from flwr.proto.message_pb2 import ( # pylint: disable=E0611
|
|
68
57
|
ConfirmMessageReceivedRequest,
|
|
69
58
|
ConfirmMessageReceivedResponse,
|
|
59
|
+
ObjectTree,
|
|
70
60
|
PullObjectRequest,
|
|
71
61
|
PullObjectResponse,
|
|
72
62
|
PushObjectRequest,
|
|
@@ -109,12 +99,15 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
|
109
99
|
] = None,
|
|
110
100
|
) -> Iterator[
|
|
111
101
|
tuple[
|
|
112
|
-
Callable[[], Optional[Message]],
|
|
113
|
-
Callable[[Message],
|
|
102
|
+
Callable[[], Optional[tuple[Message, ObjectTree]]],
|
|
103
|
+
Callable[[Message, ObjectTree], set[str]],
|
|
114
104
|
Callable[[], Optional[int]],
|
|
115
105
|
Callable[[], None],
|
|
116
106
|
Callable[[int], Run],
|
|
117
107
|
Callable[[str, int], Fab],
|
|
108
|
+
Callable[[int, str], bytes],
|
|
109
|
+
Callable[[int, str, bytes], None],
|
|
110
|
+
Callable[[int, str], None],
|
|
118
111
|
]
|
|
119
112
|
]:
|
|
120
113
|
"""Primitives for request/response-based interaction with a server.
|
|
@@ -150,6 +143,9 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
|
150
143
|
create_node : Optional[Callable]
|
|
151
144
|
delete_node : Optional[Callable]
|
|
152
145
|
get_run : Optional[Callable]
|
|
146
|
+
pull_object : Callable[[str], bytes]
|
|
147
|
+
push_object : Callable[[str, bytes], None]
|
|
148
|
+
confirm_message_received : Callable[[str], None]
|
|
153
149
|
"""
|
|
154
150
|
log(
|
|
155
151
|
WARN,
|
|
@@ -178,7 +174,6 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
|
178
174
|
log(ERROR, "Client authentication is not supported for this transport type.")
|
|
179
175
|
|
|
180
176
|
# Shared variables for inner functions
|
|
181
|
-
metadata: Optional[Metadata] = None
|
|
182
177
|
node: Optional[Node] = None
|
|
183
178
|
|
|
184
179
|
###########################################################################
|
|
@@ -232,6 +227,38 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
|
232
227
|
grpc_res.ParseFromString(res.content)
|
|
233
228
|
return grpc_res
|
|
234
229
|
|
|
230
|
+
def _pull_object_protobuf(request: PullObjectRequest) -> PullObjectResponse:
|
|
231
|
+
res = _request(
|
|
232
|
+
req=request,
|
|
233
|
+
res_type=PullObjectResponse,
|
|
234
|
+
api_path=PATH_PULL_OBJECT,
|
|
235
|
+
)
|
|
236
|
+
if res is None:
|
|
237
|
+
raise ValueError(f"{PullObjectResponse.__name__} is None.")
|
|
238
|
+
return res
|
|
239
|
+
|
|
240
|
+
def _push_object_protobuf(request: PushObjectRequest) -> PushObjectResponse:
|
|
241
|
+
res = _request(
|
|
242
|
+
req=request,
|
|
243
|
+
res_type=PushObjectResponse,
|
|
244
|
+
api_path=PATH_PUSH_OBJECT,
|
|
245
|
+
)
|
|
246
|
+
if res is None:
|
|
247
|
+
raise ValueError(f"{PushObjectResponse.__name__} is None.")
|
|
248
|
+
return res
|
|
249
|
+
|
|
250
|
+
def _confirm_message_received_protobuf(
|
|
251
|
+
request: ConfirmMessageReceivedRequest,
|
|
252
|
+
) -> ConfirmMessageReceivedResponse:
|
|
253
|
+
res = _request(
|
|
254
|
+
req=request,
|
|
255
|
+
res_type=ConfirmMessageReceivedResponse,
|
|
256
|
+
api_path=PATH_CONFIRM_MESSAGE_RECEIVED,
|
|
257
|
+
)
|
|
258
|
+
if res is None:
|
|
259
|
+
raise ValueError(f"{ConfirmMessageReceivedResponse.__name__} is None.")
|
|
260
|
+
return res
|
|
261
|
+
|
|
235
262
|
def send_node_heartbeat() -> bool:
|
|
236
263
|
# Get Node
|
|
237
264
|
if node is None:
|
|
@@ -279,8 +306,7 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
|
279
306
|
"""Set delete_node."""
|
|
280
307
|
nonlocal node
|
|
281
308
|
if node is None:
|
|
282
|
-
|
|
283
|
-
return
|
|
309
|
+
raise RuntimeError("Node instance missing")
|
|
284
310
|
|
|
285
311
|
# Stop the heartbeat sender
|
|
286
312
|
heartbeat_sender.stop()
|
|
@@ -296,162 +322,54 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
|
296
322
|
# Cleanup
|
|
297
323
|
node = None
|
|
298
324
|
|
|
299
|
-
def receive() -> Optional[Message]:
|
|
300
|
-
"""
|
|
325
|
+
def receive() -> Optional[tuple[Message, ObjectTree]]:
|
|
326
|
+
"""Pull a message with its ObjectTree from SuperLink."""
|
|
301
327
|
# Get Node
|
|
302
328
|
if node is None:
|
|
303
|
-
|
|
304
|
-
return None
|
|
329
|
+
raise RuntimeError("Node instance missing")
|
|
305
330
|
|
|
306
|
-
#
|
|
331
|
+
# Try to pull a message with its object tree from SuperLink
|
|
307
332
|
req = PullMessagesRequest(node=node)
|
|
308
|
-
|
|
309
|
-
# Send the request
|
|
310
333
|
res = _request(req, PullMessagesResponse, PATH_PULL_MESSAGES)
|
|
311
334
|
if res is None:
|
|
312
|
-
|
|
335
|
+
raise ValueError("PushMessagesResponse is None.")
|
|
313
336
|
|
|
314
|
-
#
|
|
315
|
-
|
|
337
|
+
# If no messages are available, return None
|
|
338
|
+
if len(res.messages_list) == 0:
|
|
339
|
+
return None
|
|
316
340
|
|
|
317
|
-
#
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
):
|
|
321
|
-
message_proto = None
|
|
341
|
+
# Get the current Message and its object tree
|
|
342
|
+
message_proto = res.messages_list[0]
|
|
343
|
+
object_tree = res.message_object_trees[0]
|
|
322
344
|
|
|
323
345
|
# Construct the Message
|
|
324
|
-
in_message
|
|
325
|
-
|
|
326
|
-
if message_proto:
|
|
327
|
-
log(INFO, "[Node] POST /%s: success", PATH_PULL_MESSAGES)
|
|
328
|
-
msg_id = message_proto.metadata.message_id
|
|
329
|
-
run_id = message_proto.metadata.run_id
|
|
330
|
-
|
|
331
|
-
def fn(request: PullObjectRequest) -> PullObjectResponse:
|
|
332
|
-
res = _request(
|
|
333
|
-
req=request, res_type=PullObjectResponse, api_path=PATH_PULL_OBJECT
|
|
334
|
-
)
|
|
335
|
-
if res is None:
|
|
336
|
-
raise ValueError("PushObjectResponse is None.")
|
|
337
|
-
return res
|
|
338
|
-
|
|
339
|
-
try:
|
|
340
|
-
all_object_contents = pull_objects(
|
|
341
|
-
list(res.objects_to_pull[msg_id].object_ids) + [msg_id],
|
|
342
|
-
pull_object_fn=make_pull_object_fn_rest(
|
|
343
|
-
pull_object_rest=fn,
|
|
344
|
-
node=node,
|
|
345
|
-
run_id=run_id,
|
|
346
|
-
),
|
|
347
|
-
)
|
|
348
|
-
|
|
349
|
-
# Confirm that the message has been received
|
|
350
|
-
_request(
|
|
351
|
-
req=ConfirmMessageReceivedRequest(
|
|
352
|
-
node=node, run_id=run_id, message_object_id=msg_id
|
|
353
|
-
),
|
|
354
|
-
res_type=ConfirmMessageReceivedResponse,
|
|
355
|
-
api_path=PATH_CONFIRM_MESSAGE_RECEIVED,
|
|
356
|
-
)
|
|
357
|
-
except ValueError as e:
|
|
358
|
-
log(
|
|
359
|
-
ERROR,
|
|
360
|
-
"Pulling objects failed. Potential irrecoverable error: %s",
|
|
361
|
-
str(e),
|
|
362
|
-
)
|
|
363
|
-
in_message = cast(
|
|
364
|
-
Message, inflate_object_from_contents(msg_id, all_object_contents)
|
|
365
|
-
)
|
|
366
|
-
# The deflated message doesn't contain the message_id (its own object_id)
|
|
367
|
-
# Inject
|
|
368
|
-
in_message.metadata.__dict__["_message_id"] = msg_id
|
|
369
|
-
|
|
370
|
-
# Remember `metadata` of the in message
|
|
371
|
-
nonlocal metadata
|
|
372
|
-
metadata = copy(in_message.metadata) if in_message else None
|
|
346
|
+
in_message = message_from_proto(message_proto)
|
|
373
347
|
|
|
374
|
-
|
|
348
|
+
# Return the Message and its object tree
|
|
349
|
+
return in_message, object_tree
|
|
375
350
|
|
|
376
|
-
def send(message: Message) ->
|
|
377
|
-
"""Send
|
|
351
|
+
def send(message: Message, object_tree: ObjectTree) -> set[str]:
|
|
352
|
+
"""Send the message with its ObjectTree to SuperLink."""
|
|
378
353
|
# Get Node
|
|
379
354
|
if node is None:
|
|
380
|
-
|
|
381
|
-
return
|
|
355
|
+
raise RuntimeError("Node instance missing")
|
|
382
356
|
|
|
383
|
-
#
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
log(ERROR, "No current message")
|
|
387
|
-
return
|
|
357
|
+
# Remove the content from the message if it has
|
|
358
|
+
if message.has_content():
|
|
359
|
+
message = remove_content_from_message(message)
|
|
388
360
|
|
|
389
|
-
#
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
all_objects = get_all_nested_objects(message)
|
|
399
|
-
object_tree = get_object_tree(message)
|
|
400
|
-
|
|
401
|
-
# Serialize Message
|
|
402
|
-
message_proto = message_to_proto(
|
|
403
|
-
message=remove_content_from_message(message)
|
|
404
|
-
)
|
|
405
|
-
req = PushMessagesRequest(
|
|
406
|
-
node=node,
|
|
407
|
-
messages_list=[message_proto],
|
|
408
|
-
message_object_trees=[object_tree],
|
|
409
|
-
)
|
|
410
|
-
|
|
411
|
-
# Send the request
|
|
412
|
-
res = _request(req, PushMessagesResponse, PATH_PUSH_MESSAGES)
|
|
413
|
-
if res:
|
|
414
|
-
log(
|
|
415
|
-
INFO,
|
|
416
|
-
"[Node] POST /%s: success, created result %s",
|
|
417
|
-
PATH_PUSH_MESSAGES,
|
|
418
|
-
res.results, # pylint: disable=no-member
|
|
419
|
-
)
|
|
420
|
-
|
|
421
|
-
if res and res.objects_to_push:
|
|
422
|
-
objs_to_push = set(res.objects_to_push[message.object_id].object_ids)
|
|
423
|
-
|
|
424
|
-
def fn(request: PushObjectRequest) -> PushObjectResponse:
|
|
425
|
-
res = _request(
|
|
426
|
-
req=request,
|
|
427
|
-
res_type=PushObjectResponse,
|
|
428
|
-
api_path=PATH_PUSH_OBJECT,
|
|
429
|
-
)
|
|
430
|
-
if res is None:
|
|
431
|
-
raise ValueError("PushObjectResponse is None.")
|
|
432
|
-
return res
|
|
433
|
-
|
|
434
|
-
try:
|
|
435
|
-
push_objects(
|
|
436
|
-
all_objects,
|
|
437
|
-
push_object_fn=make_push_object_fn_rest(
|
|
438
|
-
push_object_rest=fn,
|
|
439
|
-
node=node,
|
|
440
|
-
run_id=message_proto.metadata.run_id,
|
|
441
|
-
),
|
|
442
|
-
object_ids_to_push=objs_to_push,
|
|
443
|
-
)
|
|
444
|
-
log(DEBUG, "Pushed %s objects to servicer.", len(objs_to_push))
|
|
445
|
-
except ValueError as e:
|
|
446
|
-
log(
|
|
447
|
-
ERROR,
|
|
448
|
-
"Pushing objects failed. Potential irrecoverable error: %s",
|
|
449
|
-
str(e),
|
|
450
|
-
)
|
|
451
|
-
log(ERROR, str(e))
|
|
361
|
+
# Send the message with its ObjectTree to SuperLink
|
|
362
|
+
req = PushMessagesRequest(
|
|
363
|
+
node=node,
|
|
364
|
+
messages_list=[message_to_proto(message)],
|
|
365
|
+
message_object_trees=[object_tree],
|
|
366
|
+
)
|
|
367
|
+
res = _request(req, PushMessagesResponse, PATH_PUSH_MESSAGES)
|
|
368
|
+
if res is None:
|
|
369
|
+
raise ValueError("PushMessagesResponse is None.")
|
|
452
370
|
|
|
453
|
-
#
|
|
454
|
-
|
|
371
|
+
# Get and return the object IDs to push
|
|
372
|
+
return set(res.objects_to_push)
|
|
455
373
|
|
|
456
374
|
def get_run(run_id: int) -> Run:
|
|
457
375
|
# Construct the request
|
|
@@ -478,9 +396,58 @@ def http_request_response( # pylint: disable=R0913,R0914,R0915,R0917
|
|
|
478
396
|
res.fab.content,
|
|
479
397
|
)
|
|
480
398
|
|
|
399
|
+
def pull_object(run_id: int, object_id: str) -> bytes:
|
|
400
|
+
"""Pull the object from the SuperLink."""
|
|
401
|
+
# Check Node
|
|
402
|
+
if node is None:
|
|
403
|
+
raise RuntimeError("Node instance missing")
|
|
404
|
+
|
|
405
|
+
fn = make_pull_object_fn_protobuf(
|
|
406
|
+
pull_object_protobuf=_pull_object_protobuf,
|
|
407
|
+
node=node,
|
|
408
|
+
run_id=run_id,
|
|
409
|
+
)
|
|
410
|
+
return fn(object_id)
|
|
411
|
+
|
|
412
|
+
def push_object(run_id: int, object_id: str, contents: bytes) -> None:
|
|
413
|
+
"""Push the object to the SuperLink."""
|
|
414
|
+
# Check Node
|
|
415
|
+
if node is None:
|
|
416
|
+
raise RuntimeError("Node instance missing")
|
|
417
|
+
|
|
418
|
+
fn = make_push_object_fn_protobuf(
|
|
419
|
+
push_object_protobuf=_push_object_protobuf,
|
|
420
|
+
node=node,
|
|
421
|
+
run_id=run_id,
|
|
422
|
+
)
|
|
423
|
+
fn(object_id, contents)
|
|
424
|
+
|
|
425
|
+
def confirm_message_received(run_id: int, object_id: str) -> None:
|
|
426
|
+
"""Confirm that the message has been received."""
|
|
427
|
+
# Check Node
|
|
428
|
+
if node is None:
|
|
429
|
+
raise RuntimeError("Node instance missing")
|
|
430
|
+
|
|
431
|
+
fn = make_confirm_message_received_fn_protobuf(
|
|
432
|
+
confirm_message_received_protobuf=_confirm_message_received_protobuf,
|
|
433
|
+
node=node,
|
|
434
|
+
run_id=run_id,
|
|
435
|
+
)
|
|
436
|
+
fn(object_id)
|
|
437
|
+
|
|
481
438
|
try:
|
|
482
439
|
# Yield methods
|
|
483
|
-
yield (
|
|
440
|
+
yield (
|
|
441
|
+
receive,
|
|
442
|
+
send,
|
|
443
|
+
create_node,
|
|
444
|
+
delete_node,
|
|
445
|
+
get_run,
|
|
446
|
+
get_fab,
|
|
447
|
+
pull_object,
|
|
448
|
+
push_object,
|
|
449
|
+
confirm_message_received,
|
|
450
|
+
)
|
|
484
451
|
except Exception as exc: # pylint: disable=broad-except
|
|
485
452
|
log(ERROR, exc)
|
|
486
453
|
# Cleanup
|
flwr/common/constant.py
CHANGED
|
@@ -74,6 +74,7 @@ FAB_ALLOWED_EXTENSIONS = {".py", ".toml", ".md"}
|
|
|
74
74
|
FAB_CONFIG_FILE = "pyproject.toml"
|
|
75
75
|
FAB_DATE = (2024, 10, 1, 0, 0, 0)
|
|
76
76
|
FAB_HASH_TRUNCATION = 8
|
|
77
|
+
FAB_MAX_SIZE = 10 * 1024 * 1024 # 10 MB
|
|
77
78
|
FLWR_DIR = ".flwr" # The default Flower directory: ~/.flwr/
|
|
78
79
|
FLWR_HOME = "FLWR_HOME" # If set, override the default Flower directory
|
|
79
80
|
|
|
@@ -122,7 +123,7 @@ AUTHZ_TYPE_YAML_KEY = "authz_type" # For key name in YAML file
|
|
|
122
123
|
PUBLIC_KEY_HEADER = "flwr-public-key-bin" # Must end with "-bin" for binary data
|
|
123
124
|
SIGNATURE_HEADER = "flwr-signature-bin" # Must end with "-bin" for binary data
|
|
124
125
|
TIMESTAMP_HEADER = "flwr-timestamp"
|
|
125
|
-
TIMESTAMP_TOLERANCE =
|
|
126
|
+
TIMESTAMP_TOLERANCE = 300 # General tolerance for timestamp verification
|
|
126
127
|
SYSTEM_TIME_TOLERANCE = 5 # Allowance for system time drift
|
|
127
128
|
|
|
128
129
|
# Constants for grpc retry
|
|
@@ -134,6 +135,7 @@ GC_THRESHOLD = 200_000_000 # 200 MB
|
|
|
134
135
|
# Constants for Inflatable
|
|
135
136
|
HEAD_BODY_DIVIDER = b"\x00"
|
|
136
137
|
HEAD_VALUE_DIVIDER = " "
|
|
138
|
+
MAX_ARRAY_CHUNK_SIZE = 20_971_520 # 20 MB
|
|
137
139
|
|
|
138
140
|
# Constants for serialization
|
|
139
141
|
INT64_MAX_VALUE = 9223372036854775807 # (1 << 63) - 1
|
flwr/common/exit/exit_code.py
CHANGED
|
@@ -29,6 +29,9 @@ class ExitCode:
|
|
|
29
29
|
|
|
30
30
|
# SuperLink-specific exit codes (100-199)
|
|
31
31
|
SUPERLINK_THREAD_CRASH = 100
|
|
32
|
+
SUPERLINK_LICENSE_INVALID = 101
|
|
33
|
+
SUPERLINK_LICENSE_MISSING = 102
|
|
34
|
+
SUPERLINK_LICENSE_URL_INVALID = 103
|
|
32
35
|
|
|
33
36
|
# ServerApp-specific exit codes (200-299)
|
|
34
37
|
|
|
@@ -60,6 +63,18 @@ EXIT_CODE_HELP = {
|
|
|
60
63
|
ExitCode.GRACEFUL_EXIT_SIGTERM: "",
|
|
61
64
|
# SuperLink-specific exit codes (100-199)
|
|
62
65
|
ExitCode.SUPERLINK_THREAD_CRASH: "An important background thread has crashed.",
|
|
66
|
+
ExitCode.SUPERLINK_LICENSE_INVALID: (
|
|
67
|
+
"The license is invalid or has expired. "
|
|
68
|
+
"Please contact `hello@flower.ai` for assistance."
|
|
69
|
+
),
|
|
70
|
+
ExitCode.SUPERLINK_LICENSE_MISSING: (
|
|
71
|
+
"The license is missing. Please specify the license key by setting the "
|
|
72
|
+
"environment variable `FLWR_LICENSE_KEY`."
|
|
73
|
+
),
|
|
74
|
+
ExitCode.SUPERLINK_LICENSE_URL_INVALID: (
|
|
75
|
+
"The license URL is invalid. Please ensure that the `FLWR_LICENSE_URL` "
|
|
76
|
+
"environment variable is set to a valid URL."
|
|
77
|
+
),
|
|
63
78
|
# ServerApp-specific exit codes (200-299)
|
|
64
79
|
# SuperNode-specific exit codes (300-399)
|
|
65
80
|
ExitCode.SUPERNODE_REST_ADDRESS_INVALID: (
|
|
@@ -72,7 +87,7 @@ EXIT_CODE_HELP = {
|
|
|
72
87
|
"to be provided (providing only one of them is not sufficient)."
|
|
73
88
|
),
|
|
74
89
|
ExitCode.SUPERNODE_NODE_AUTH_KEYS_INVALID: (
|
|
75
|
-
"Node
|
|
90
|
+
"Node authentication requires elliptic curve private and public key pair. "
|
|
76
91
|
"Please ensure that the file path points to a valid private/public key "
|
|
77
92
|
"file and try again."
|
|
78
93
|
),
|
flwr/common/grpc.py
CHANGED
|
@@ -23,6 +23,9 @@ from logging import DEBUG, ERROR
|
|
|
23
23
|
from typing import Any, Callable, Optional
|
|
24
24
|
|
|
25
25
|
import grpc
|
|
26
|
+
from grpc_health.v1.health_pb2_grpc import add_HealthServicer_to_server
|
|
27
|
+
|
|
28
|
+
from flwr.supercore.grpc_health import SimpleHealthServicer
|
|
26
29
|
|
|
27
30
|
from .address import is_port_in_use
|
|
28
31
|
from .logger import log
|
|
@@ -98,7 +101,7 @@ def valid_certificates(certificates: tuple[bytes, bytes, bytes]) -> bool:
|
|
|
98
101
|
return is_valid
|
|
99
102
|
|
|
100
103
|
|
|
101
|
-
def generic_create_grpc_server( # pylint: disable=too-many-arguments,R0917
|
|
104
|
+
def generic_create_grpc_server( # pylint: disable=too-many-arguments, R0914, R0917
|
|
102
105
|
servicer_and_add_fn: tuple[Any, AddServicerToServerFn],
|
|
103
106
|
server_address: str,
|
|
104
107
|
max_concurrent_workers: int = 1000,
|
|
@@ -106,6 +109,7 @@ def generic_create_grpc_server( # pylint: disable=too-many-arguments,R0917
|
|
|
106
109
|
keepalive_time_ms: int = 210000,
|
|
107
110
|
certificates: Optional[tuple[bytes, bytes, bytes]] = None,
|
|
108
111
|
interceptors: Optional[Sequence[grpc.ServerInterceptor]] = None,
|
|
112
|
+
health_servicer: Optional[Any] = None,
|
|
109
113
|
) -> grpc.Server:
|
|
110
114
|
"""Create a gRPC server with a single servicer.
|
|
111
115
|
|
|
@@ -153,6 +157,10 @@ def generic_create_grpc_server( # pylint: disable=too-many-arguments,R0917
|
|
|
153
157
|
* server private key.
|
|
154
158
|
interceptors : Optional[Sequence[grpc.ServerInterceptor]] (default: None)
|
|
155
159
|
A list of gRPC interceptors.
|
|
160
|
+
health_servicer : Optional[Any] (default: None)
|
|
161
|
+
An optional health servicer to add to the server. If provided, it should be an
|
|
162
|
+
instance of a class that inherits the `HealthServicer` class.
|
|
163
|
+
If None is provided, `SimpleHealthServicer` will be used by default.
|
|
156
164
|
|
|
157
165
|
Returns
|
|
158
166
|
-------
|
|
@@ -203,6 +211,9 @@ def generic_create_grpc_server( # pylint: disable=too-many-arguments,R0917
|
|
|
203
211
|
)
|
|
204
212
|
add_servicer_to_server_fn(servicer, server)
|
|
205
213
|
|
|
214
|
+
# Enable health service
|
|
215
|
+
add_HealthServicer_to_server(health_servicer or SimpleHealthServicer(), server)
|
|
216
|
+
|
|
206
217
|
if certificates is not None:
|
|
207
218
|
if not valid_certificates(certificates):
|
|
208
219
|
sys.exit(1)
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
from typing import Callable
|
|
19
19
|
|
|
20
20
|
from flwr.proto.message_pb2 import ( # pylint: disable=E0611
|
|
21
|
+
ConfirmMessageReceivedRequest,
|
|
22
|
+
ConfirmMessageReceivedResponse,
|
|
21
23
|
PullObjectRequest,
|
|
22
24
|
PullObjectResponse,
|
|
23
25
|
PushObjectRequest,
|
|
@@ -27,9 +29,13 @@ from flwr.proto.node_pb2 import Node # pylint: disable=E0611
|
|
|
27
29
|
|
|
28
30
|
from .inflatable_utils import ObjectIdNotPreregisteredError, ObjectUnavailableError
|
|
29
31
|
|
|
32
|
+
ConfirmMessageReceivedProtobuf = Callable[
|
|
33
|
+
[ConfirmMessageReceivedRequest], ConfirmMessageReceivedResponse
|
|
34
|
+
]
|
|
30
35
|
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
|
|
37
|
+
def make_pull_object_fn_protobuf(
|
|
38
|
+
pull_object_protobuf: Callable[[PullObjectRequest], PullObjectResponse],
|
|
33
39
|
node: Node,
|
|
34
40
|
run_id: int,
|
|
35
41
|
) -> Callable[[str], bytes]:
|
|
@@ -37,8 +43,9 @@ def make_pull_object_fn_grpc(
|
|
|
37
43
|
|
|
38
44
|
Parameters
|
|
39
45
|
----------
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
pull_object_protobuf : Callable[[PullObjectRequest], PullObjectResponse]
|
|
47
|
+
A callable that takes a `PullObjectRequest` and returns a `PullObjectResponse`.
|
|
48
|
+
This function is typically backed by a gRPC client stub.
|
|
42
49
|
node : Node
|
|
43
50
|
The node making the request.
|
|
44
51
|
run_id : int
|
|
@@ -54,7 +61,7 @@ def make_pull_object_fn_grpc(
|
|
|
54
61
|
|
|
55
62
|
def pull_object_fn(object_id: str) -> bytes:
|
|
56
63
|
request = PullObjectRequest(node=node, run_id=run_id, object_id=object_id)
|
|
57
|
-
response: PullObjectResponse =
|
|
64
|
+
response: PullObjectResponse = pull_object_protobuf(request)
|
|
58
65
|
if not response.object_found:
|
|
59
66
|
raise ObjectIdNotPreregisteredError(object_id)
|
|
60
67
|
if not response.object_available:
|
|
@@ -64,8 +71,8 @@ def make_pull_object_fn_grpc(
|
|
|
64
71
|
return pull_object_fn
|
|
65
72
|
|
|
66
73
|
|
|
67
|
-
def
|
|
68
|
-
|
|
74
|
+
def make_push_object_fn_protobuf(
|
|
75
|
+
push_object_protobuf: Callable[[PushObjectRequest], PushObjectResponse],
|
|
69
76
|
node: Node,
|
|
70
77
|
run_id: int,
|
|
71
78
|
) -> Callable[[str, bytes], None]:
|
|
@@ -73,8 +80,9 @@ def make_push_object_fn_grpc(
|
|
|
73
80
|
|
|
74
81
|
Parameters
|
|
75
82
|
----------
|
|
76
|
-
|
|
77
|
-
|
|
83
|
+
push_object_protobuf : Callable[[PushObjectRequest], PushObjectResponse]
|
|
84
|
+
A callable that takes a `PushObjectRequest` and returns a `PushObjectResponse`.
|
|
85
|
+
This function is typically backed by a gRPC client stub.
|
|
78
86
|
node : Node
|
|
79
87
|
The node making the request.
|
|
80
88
|
run_id : int
|
|
@@ -92,8 +100,42 @@ def make_push_object_fn_grpc(
|
|
|
92
100
|
request = PushObjectRequest(
|
|
93
101
|
node=node, run_id=run_id, object_id=object_id, object_content=object_content
|
|
94
102
|
)
|
|
95
|
-
response: PushObjectResponse =
|
|
103
|
+
response: PushObjectResponse = push_object_protobuf(request)
|
|
96
104
|
if not response.stored:
|
|
97
105
|
raise ObjectIdNotPreregisteredError(object_id)
|
|
98
106
|
|
|
99
107
|
return push_object_fn
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def make_confirm_message_received_fn_protobuf(
|
|
111
|
+
confirm_message_received_protobuf: ConfirmMessageReceivedProtobuf,
|
|
112
|
+
node: Node,
|
|
113
|
+
run_id: int,
|
|
114
|
+
) -> Callable[[str], None]:
|
|
115
|
+
"""Create a confirm message received function that uses protobuf.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
confirm_message_received_protobuf : ConfirmMessageReceivedProtobuf
|
|
120
|
+
A callable that takes a `ConfirmMessageReceivedRequest` and returns a
|
|
121
|
+
`ConfirmMessageReceivedResponse`, confirming message receipt.
|
|
122
|
+
This function is typically backed by a gRPC client stub.
|
|
123
|
+
node : Node
|
|
124
|
+
The node making the request.
|
|
125
|
+
run_id : int
|
|
126
|
+
The run ID for the current message.
|
|
127
|
+
|
|
128
|
+
Returns
|
|
129
|
+
-------
|
|
130
|
+
Callable[[str], None]
|
|
131
|
+
A wrapper function that takes an object ID and confirms that
|
|
132
|
+
the message has been received.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
def confirm_message_received_fn(object_id: str) -> None:
|
|
136
|
+
request = ConfirmMessageReceivedRequest(
|
|
137
|
+
node=node, run_id=run_id, message_object_id=object_id
|
|
138
|
+
)
|
|
139
|
+
confirm_message_received_protobuf(request)
|
|
140
|
+
|
|
141
|
+
return confirm_message_received_fn
|