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.
Files changed (94) hide show
  1. flwr/cli/build.py +15 -5
  2. flwr/cli/new/new.py +12 -4
  3. flwr/cli/new/templates/app/README.flowertune.md.tpl +2 -0
  4. flwr/cli/new/templates/app/README.md.tpl +5 -0
  5. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +14 -3
  6. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +13 -1
  7. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +21 -2
  8. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +18 -1
  9. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +19 -2
  10. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +18 -1
  11. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +20 -3
  12. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +18 -1
  13. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +18 -1
  14. flwr/cli/run/run.py +45 -38
  15. flwr/cli/utils.py +12 -5
  16. flwr/client/grpc_adapter_client/connection.py +11 -4
  17. flwr/client/grpc_rere_client/connection.py +92 -117
  18. flwr/client/rest_client/connection.py +131 -164
  19. flwr/common/constant.py +3 -1
  20. flwr/common/exit/exit_code.py +16 -1
  21. flwr/common/grpc.py +12 -1
  22. flwr/common/{inflatable_grpc_utils.py → inflatable_protobuf_utils.py} +52 -10
  23. flwr/common/inflatable_utils.py +191 -24
  24. flwr/common/record/array.py +101 -22
  25. flwr/common/record/arraychunk.py +59 -0
  26. flwr/common/serde.py +0 -28
  27. flwr/compat/client/app.py +14 -31
  28. flwr/proto/appio_pb2.py +43 -0
  29. flwr/proto/appio_pb2.pyi +151 -0
  30. flwr/proto/appio_pb2_grpc.py +4 -0
  31. flwr/proto/appio_pb2_grpc.pyi +4 -0
  32. flwr/proto/clientappio_pb2.py +12 -19
  33. flwr/proto/clientappio_pb2.pyi +23 -101
  34. flwr/proto/clientappio_pb2_grpc.py +269 -28
  35. flwr/proto/clientappio_pb2_grpc.pyi +114 -20
  36. flwr/proto/fleet_pb2.py +12 -20
  37. flwr/proto/fleet_pb2.pyi +6 -36
  38. flwr/proto/serverappio_pb2.py +8 -31
  39. flwr/proto/serverappio_pb2.pyi +0 -152
  40. flwr/proto/serverappio_pb2_grpc.py +39 -38
  41. flwr/proto/serverappio_pb2_grpc.pyi +21 -20
  42. flwr/server/app.py +1 -1
  43. flwr/server/fleet_event_log_interceptor.py +4 -0
  44. flwr/server/grid/grpc_grid.py +91 -54
  45. flwr/server/serverapp/app.py +27 -17
  46. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +8 -0
  47. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +1 -1
  48. flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +2 -5
  49. flwr/server/superlink/fleet/message_handler/message_handler.py +10 -16
  50. flwr/server/superlink/fleet/rest_rere/rest_api.py +1 -2
  51. flwr/server/superlink/serverappio/serverappio_grpc.py +1 -1
  52. flwr/server/superlink/serverappio/serverappio_servicer.py +35 -43
  53. flwr/server/superlink/simulation/simulationio_grpc.py +1 -1
  54. flwr/server/superlink/simulation/simulationio_servicer.py +1 -1
  55. flwr/server/superlink/utils.py +0 -35
  56. flwr/simulation/app.py +8 -0
  57. flwr/simulation/run_simulation.py +17 -0
  58. flwr/{server/superlink → supercore}/ffs/disk_ffs.py +1 -1
  59. flwr/supercore/grpc_health/__init__.py +22 -0
  60. flwr/supercore/grpc_health/simple_health_servicer.py +38 -0
  61. flwr/supercore/license_plugin/__init__.py +22 -0
  62. flwr/supercore/license_plugin/license_plugin.py +26 -0
  63. flwr/supercore/object_store/in_memory_object_store.py +31 -31
  64. flwr/supercore/object_store/object_store.py +20 -42
  65. flwr/supercore/object_store/utils.py +43 -0
  66. flwr/supercore/scheduler/__init__.py +22 -0
  67. flwr/supercore/scheduler/plugin.py +71 -0
  68. flwr/supercore/utils.py +32 -0
  69. flwr/superexec/deployment.py +1 -2
  70. flwr/superexec/exec_event_log_interceptor.py +4 -0
  71. flwr/superexec/exec_grpc.py +18 -2
  72. flwr/superexec/exec_license_interceptor.py +82 -0
  73. flwr/superexec/exec_servicer.py +10 -1
  74. flwr/superexec/exec_user_auth_interceptor.py +10 -2
  75. flwr/superexec/executor.py +1 -1
  76. flwr/superexec/simulation.py +1 -2
  77. flwr/supernode/cli/flower_supernode.py +0 -7
  78. flwr/supernode/cli/flwr_clientapp.py +10 -3
  79. flwr/supernode/nodestate/in_memory_nodestate.py +11 -2
  80. flwr/supernode/nodestate/nodestate.py +15 -0
  81. flwr/supernode/runtime/run_clientapp.py +110 -33
  82. flwr/supernode/scheduler/__init__.py +22 -0
  83. flwr/supernode/scheduler/simple_clientapp_scheduler_plugin.py +49 -0
  84. flwr/supernode/servicer/clientappio/__init__.py +1 -3
  85. flwr/supernode/servicer/clientappio/clientappio_servicer.py +223 -164
  86. flwr/supernode/start_client_internal.py +202 -104
  87. {flwr-1.19.0.dist-info → flwr-1.20.0.dist-info}/METADATA +2 -1
  88. {flwr-1.19.0.dist-info → flwr-1.20.0.dist-info}/RECORD +93 -78
  89. flwr/common/inflatable_rest_utils.py +0 -99
  90. /flwr/{server/superlink → supercore}/ffs/__init__.py +0 -0
  91. /flwr/{server/superlink → supercore}/ffs/ffs.py +0 -0
  92. /flwr/{server/superlink → supercore}/ffs/ffs_factory.py +0 -0
  93. {flwr-1.19.0.dist-info → flwr-1.20.0.dist-info}/WHEEL +0 -0
  94. {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 copy import copy
20
- from logging import DEBUG, ERROR, INFO, WARN
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.inflatable import (
34
- get_all_nested_objects,
35
- get_object_tree,
36
- no_object_id_recompute,
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], None],
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
- log(ERROR, "Node instance missing")
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
- """Receive next Message from server."""
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
- log(ERROR, "Node instance missing")
304
- return None
329
+ raise RuntimeError("Node instance missing")
305
330
 
306
- # Request instructions (message) from server
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
- return None
335
+ raise ValueError("PushMessagesResponse is None.")
313
336
 
314
- # Get the current Messages
315
- message_proto = None if len(res.messages_list) == 0 else res.messages_list[0]
337
+ # If no messages are available, return None
338
+ if len(res.messages_list) == 0:
339
+ return None
316
340
 
317
- # Discard the current message if not valid
318
- if message_proto is not None and not (
319
- message_proto.metadata.dst_node_id == node.node_id
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: Optional[Message] = None
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
- return in_message
348
+ # Return the Message and its object tree
349
+ return in_message, object_tree
375
350
 
376
- def send(message: Message) -> None:
377
- """Send Message result back to server."""
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
- log(ERROR, "Node instance missing")
381
- return
355
+ raise RuntimeError("Node instance missing")
382
356
 
383
- # Get incoming message
384
- nonlocal metadata
385
- if metadata is None:
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
- # Set message_id
390
- message.metadata.__dict__["_message_id"] = message.object_id
391
- # Validate out message
392
- if not validate_out_message(message, metadata):
393
- log(ERROR, "Invalid out message")
394
- return
395
-
396
- with no_object_id_recompute():
397
- # Get all nested objects
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
- # Cleanup
454
- metadata = None
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 (receive, send, create_node, delete_node, get_run, get_fab)
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 = 10 # General tolerance for timestamp verification
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
@@ -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 uthentication requires elliptic curve private and public key pair. "
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
- def make_pull_object_fn_grpc(
32
- pull_object_grpc: Callable[[PullObjectRequest], PullObjectResponse],
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
- pull_object_grpc : Callable[[PullObjectRequest], PullObjectResponse]
41
- The gRPC function to pull objects, e.g., `FleetStub.PullObject`.
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 = pull_object_grpc(request)
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 make_push_object_fn_grpc(
68
- push_object_grpc: Callable[[PushObjectRequest], PushObjectResponse],
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
- push_object_grpc : Callable[[PushObjectRequest], PushObjectResponse]
77
- The gRPC function to push objects, e.g., `FleetStub.PushObject`.
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 = push_object_grpc(request)
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