flwr 1.19.0__py3-none-any.whl → 1.21.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 (167) hide show
  1. flwr/__init__.py +4 -1
  2. flwr/app/__init__.py +28 -0
  3. flwr/app/exception.py +31 -0
  4. flwr/cli/auth_plugin/oidc_cli_plugin.py +4 -4
  5. flwr/cli/build.py +15 -5
  6. flwr/cli/cli_user_auth_interceptor.py +1 -1
  7. flwr/cli/config_utils.py +3 -3
  8. flwr/cli/constant.py +25 -8
  9. flwr/cli/log.py +9 -9
  10. flwr/cli/login/login.py +3 -3
  11. flwr/cli/ls.py +5 -5
  12. flwr/cli/new/new.py +23 -4
  13. flwr/cli/new/templates/app/README.flowertune.md.tpl +2 -0
  14. flwr/cli/new/templates/app/README.md.tpl +5 -0
  15. flwr/cli/new/templates/app/code/__init__.pytorch_msg_api.py.tpl +1 -0
  16. flwr/cli/new/templates/app/code/client.pytorch_msg_api.py.tpl +80 -0
  17. flwr/cli/new/templates/app/code/server.pytorch_msg_api.py.tpl +41 -0
  18. flwr/cli/new/templates/app/code/task.pytorch_msg_api.py.tpl +98 -0
  19. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +14 -3
  20. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +13 -1
  21. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +21 -2
  22. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +18 -1
  23. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +19 -2
  24. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +18 -1
  25. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +20 -3
  26. flwr/cli/new/templates/app/pyproject.pytorch_msg_api.toml.tpl +53 -0
  27. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +18 -1
  28. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +18 -1
  29. flwr/cli/run/run.py +53 -50
  30. flwr/cli/stop.py +7 -4
  31. flwr/cli/utils.py +29 -11
  32. flwr/client/grpc_adapter_client/connection.py +11 -4
  33. flwr/client/grpc_rere_client/connection.py +93 -129
  34. flwr/client/rest_client/connection.py +134 -164
  35. flwr/clientapp/__init__.py +10 -0
  36. flwr/clientapp/mod/__init__.py +26 -0
  37. flwr/clientapp/mod/centraldp_mods.py +132 -0
  38. flwr/common/args.py +20 -6
  39. flwr/common/auth_plugin/__init__.py +4 -4
  40. flwr/common/auth_plugin/auth_plugin.py +7 -7
  41. flwr/common/constant.py +26 -5
  42. flwr/common/event_log_plugin/event_log_plugin.py +1 -1
  43. flwr/common/exit/__init__.py +4 -0
  44. flwr/common/exit/exit.py +8 -1
  45. flwr/common/exit/exit_code.py +42 -8
  46. flwr/common/exit/exit_handler.py +62 -0
  47. flwr/common/{exit_handlers.py → exit/signal_handler.py} +20 -37
  48. flwr/common/grpc.py +1 -1
  49. flwr/common/{inflatable_grpc_utils.py → inflatable_protobuf_utils.py} +52 -10
  50. flwr/common/inflatable_utils.py +191 -24
  51. flwr/common/logger.py +1 -1
  52. flwr/common/record/array.py +101 -22
  53. flwr/common/record/arraychunk.py +59 -0
  54. flwr/common/retry_invoker.py +30 -11
  55. flwr/common/serde.py +0 -28
  56. flwr/common/telemetry.py +4 -0
  57. flwr/compat/client/app.py +14 -31
  58. flwr/compat/server/app.py +2 -2
  59. flwr/proto/appio_pb2.py +51 -0
  60. flwr/proto/appio_pb2.pyi +195 -0
  61. flwr/proto/appio_pb2_grpc.py +4 -0
  62. flwr/proto/appio_pb2_grpc.pyi +4 -0
  63. flwr/proto/clientappio_pb2.py +4 -19
  64. flwr/proto/clientappio_pb2.pyi +0 -125
  65. flwr/proto/clientappio_pb2_grpc.py +269 -29
  66. flwr/proto/clientappio_pb2_grpc.pyi +114 -21
  67. flwr/proto/control_pb2.py +62 -0
  68. flwr/proto/{exec_pb2_grpc.py → control_pb2_grpc.py} +54 -54
  69. flwr/proto/{exec_pb2_grpc.pyi → control_pb2_grpc.pyi} +28 -28
  70. flwr/proto/fleet_pb2.py +12 -20
  71. flwr/proto/fleet_pb2.pyi +6 -36
  72. flwr/proto/serverappio_pb2.py +8 -31
  73. flwr/proto/serverappio_pb2.pyi +0 -152
  74. flwr/proto/serverappio_pb2_grpc.py +107 -38
  75. flwr/proto/serverappio_pb2_grpc.pyi +47 -20
  76. flwr/proto/simulationio_pb2.py +4 -11
  77. flwr/proto/simulationio_pb2.pyi +0 -58
  78. flwr/proto/simulationio_pb2_grpc.py +129 -27
  79. flwr/proto/simulationio_pb2_grpc.pyi +52 -13
  80. flwr/server/app.py +130 -153
  81. flwr/server/fleet_event_log_interceptor.py +4 -0
  82. flwr/server/grid/grpc_grid.py +94 -54
  83. flwr/server/grid/inmemory_grid.py +1 -0
  84. flwr/server/serverapp/app.py +165 -144
  85. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +8 -0
  86. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +1 -1
  87. flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +2 -5
  88. flwr/server/superlink/fleet/message_handler/message_handler.py +10 -16
  89. flwr/server/superlink/fleet/rest_rere/rest_api.py +1 -2
  90. flwr/server/superlink/fleet/vce/backend/raybackend.py +3 -1
  91. flwr/server/superlink/fleet/vce/vce_api.py +6 -6
  92. flwr/server/superlink/linkstate/in_memory_linkstate.py +34 -0
  93. flwr/server/superlink/linkstate/linkstate.py +2 -1
  94. flwr/server/superlink/linkstate/sqlite_linkstate.py +45 -0
  95. flwr/server/superlink/serverappio/serverappio_grpc.py +2 -2
  96. flwr/server/superlink/serverappio/serverappio_servicer.py +95 -48
  97. flwr/server/superlink/simulation/simulationio_grpc.py +1 -1
  98. flwr/server/superlink/simulation/simulationio_servicer.py +98 -22
  99. flwr/server/superlink/utils.py +0 -35
  100. flwr/serverapp/__init__.py +12 -0
  101. flwr/serverapp/dp_fixed_clipping.py +352 -0
  102. flwr/serverapp/exception.py +38 -0
  103. flwr/serverapp/strategy/__init__.py +38 -0
  104. flwr/serverapp/strategy/dp_fixed_clipping.py +352 -0
  105. flwr/serverapp/strategy/fedadagrad.py +162 -0
  106. flwr/serverapp/strategy/fedadam.py +181 -0
  107. flwr/serverapp/strategy/fedavg.py +295 -0
  108. flwr/serverapp/strategy/fedopt.py +218 -0
  109. flwr/serverapp/strategy/fedyogi.py +173 -0
  110. flwr/serverapp/strategy/result.py +105 -0
  111. flwr/serverapp/strategy/strategy.py +285 -0
  112. flwr/serverapp/strategy/strategy_utils.py +251 -0
  113. flwr/serverapp/strategy/strategy_utils_tests.py +304 -0
  114. flwr/simulation/app.py +159 -154
  115. flwr/simulation/run_simulation.py +17 -0
  116. flwr/supercore/app_utils.py +58 -0
  117. flwr/supercore/cli/__init__.py +22 -0
  118. flwr/supercore/cli/flower_superexec.py +141 -0
  119. flwr/supercore/corestate/__init__.py +22 -0
  120. flwr/supercore/corestate/corestate.py +81 -0
  121. flwr/{server/superlink → supercore}/ffs/disk_ffs.py +1 -1
  122. flwr/supercore/grpc_health/__init__.py +25 -0
  123. flwr/supercore/grpc_health/health_server.py +53 -0
  124. flwr/supercore/grpc_health/simple_health_servicer.py +38 -0
  125. flwr/supercore/license_plugin/__init__.py +22 -0
  126. flwr/supercore/license_plugin/license_plugin.py +26 -0
  127. flwr/supercore/object_store/in_memory_object_store.py +31 -31
  128. flwr/supercore/object_store/object_store.py +20 -42
  129. flwr/supercore/object_store/utils.py +43 -0
  130. flwr/{superexec → supercore/superexec}/__init__.py +1 -1
  131. flwr/supercore/superexec/plugin/__init__.py +28 -0
  132. flwr/supercore/superexec/plugin/base_exec_plugin.py +53 -0
  133. flwr/supercore/superexec/plugin/clientapp_exec_plugin.py +28 -0
  134. flwr/supercore/superexec/plugin/exec_plugin.py +71 -0
  135. flwr/supercore/superexec/plugin/serverapp_exec_plugin.py +28 -0
  136. flwr/supercore/superexec/plugin/simulation_exec_plugin.py +28 -0
  137. flwr/supercore/superexec/run_superexec.py +185 -0
  138. flwr/supercore/utils.py +32 -0
  139. flwr/superlink/servicer/__init__.py +15 -0
  140. flwr/superlink/servicer/control/__init__.py +22 -0
  141. flwr/{superexec/exec_event_log_interceptor.py → superlink/servicer/control/control_event_log_interceptor.py} +9 -5
  142. flwr/{superexec/exec_grpc.py → superlink/servicer/control/control_grpc.py} +39 -28
  143. flwr/superlink/servicer/control/control_license_interceptor.py +82 -0
  144. flwr/{superexec/exec_servicer.py → superlink/servicer/control/control_servicer.py} +79 -31
  145. flwr/{superexec/exec_user_auth_interceptor.py → superlink/servicer/control/control_user_auth_interceptor.py} +18 -10
  146. flwr/supernode/cli/flower_supernode.py +3 -7
  147. flwr/supernode/cli/flwr_clientapp.py +20 -16
  148. flwr/supernode/nodestate/in_memory_nodestate.py +13 -4
  149. flwr/supernode/nodestate/nodestate.py +3 -44
  150. flwr/supernode/runtime/run_clientapp.py +129 -115
  151. flwr/supernode/servicer/clientappio/__init__.py +1 -3
  152. flwr/supernode/servicer/clientappio/clientappio_servicer.py +217 -165
  153. flwr/supernode/start_client_internal.py +205 -148
  154. {flwr-1.19.0.dist-info → flwr-1.21.0.dist-info}/METADATA +5 -3
  155. {flwr-1.19.0.dist-info → flwr-1.21.0.dist-info}/RECORD +161 -117
  156. {flwr-1.19.0.dist-info → flwr-1.21.0.dist-info}/entry_points.txt +1 -0
  157. flwr/common/inflatable_rest_utils.py +0 -99
  158. flwr/proto/exec_pb2.py +0 -62
  159. flwr/superexec/app.py +0 -45
  160. flwr/superexec/deployment.py +0 -192
  161. flwr/superexec/executor.py +0 -100
  162. flwr/superexec/simulation.py +0 -130
  163. /flwr/proto/{exec_pb2.pyi → control_pb2.pyi} +0 -0
  164. /flwr/{server/superlink → supercore}/ffs/__init__.py +0 -0
  165. /flwr/{server/superlink → supercore}/ffs/ffs.py +0 -0
  166. /flwr/{server/superlink → supercore}/ffs/ffs_factory.py +0 -0
  167. {flwr-1.19.0.dist-info → flwr-1.21.0.dist-info}/WHEEL +0 -0
@@ -12,9 +12,10 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
  # ==============================================================================
15
- """SuperExec API servicer."""
15
+ """Control API servicer."""
16
16
 
17
17
 
18
+ import hashlib
18
19
  import time
19
20
  from collections.abc import Generator
20
21
  from logging import ERROR, INFO
@@ -22,10 +23,13 @@ from typing import Any, Optional, cast
22
23
 
23
24
  import grpc
24
25
 
25
- from flwr.common import now
26
- from flwr.common.auth_plugin import ExecAuthPlugin
26
+ from flwr.cli.config_utils import get_fab_metadata
27
+ from flwr.common import Context, RecordDict, now
28
+ from flwr.common.auth_plugin import ControlAuthPlugin
27
29
  from flwr.common.constant import (
30
+ FAB_MAX_SIZE,
28
31
  LOG_STREAM_INTERVAL,
32
+ NO_USER_AUTH_MESSAGE,
29
33
  RUN_ID_NOT_FOUND_MESSAGE,
30
34
  Status,
31
35
  SubStatus,
@@ -36,9 +40,9 @@ from flwr.common.serde import (
36
40
  run_to_proto,
37
41
  user_config_from_proto,
38
42
  )
39
- from flwr.common.typing import Run, RunStatus
40
- from flwr.proto import exec_pb2_grpc # pylint: disable=E0611
41
- from flwr.proto.exec_pb2 import ( # pylint: disable=E0611
43
+ from flwr.common.typing import Fab, Run, RunStatus
44
+ from flwr.proto import control_pb2_grpc # pylint: disable=E0611
45
+ from flwr.proto.control_pb2 import ( # pylint: disable=E0611
42
46
  GetAuthTokensRequest,
43
47
  GetAuthTokensResponse,
44
48
  GetLoginDetailsRequest,
@@ -52,57 +56,101 @@ from flwr.proto.exec_pb2 import ( # pylint: disable=E0611
52
56
  StreamLogsRequest,
53
57
  StreamLogsResponse,
54
58
  )
55
- from flwr.server.superlink.ffs.ffs_factory import FfsFactory
56
59
  from flwr.server.superlink.linkstate import LinkState, LinkStateFactory
60
+ from flwr.supercore.ffs import FfsFactory
57
61
  from flwr.supercore.object_store import ObjectStore, ObjectStoreFactory
58
62
 
59
- from .exec_user_auth_interceptor import shared_account_info
60
- from .executor import Executor
63
+ from .control_user_auth_interceptor import shared_account_info
61
64
 
62
65
 
63
- class ExecServicer(exec_pb2_grpc.ExecServicer):
64
- """SuperExec API servicer."""
66
+ class ControlServicer(control_pb2_grpc.ControlServicer):
67
+ """Control API servicer."""
65
68
 
66
69
  def __init__( # pylint: disable=R0913, R0917
67
70
  self,
68
71
  linkstate_factory: LinkStateFactory,
69
72
  ffs_factory: FfsFactory,
70
73
  objectstore_factory: ObjectStoreFactory,
71
- executor: Executor,
72
- auth_plugin: Optional[ExecAuthPlugin] = None,
74
+ is_simulation: bool,
75
+ auth_plugin: Optional[ControlAuthPlugin] = None,
73
76
  ) -> None:
74
77
  self.linkstate_factory = linkstate_factory
75
78
  self.ffs_factory = ffs_factory
76
79
  self.objectstore_factory = objectstore_factory
77
- self.executor = executor
78
- self.executor.initialize(linkstate_factory, ffs_factory)
80
+ self.is_simulation = is_simulation
79
81
  self.auth_plugin = auth_plugin
80
82
 
81
83
  def StartRun(
82
84
  self, request: StartRunRequest, context: grpc.ServicerContext
83
85
  ) -> StartRunResponse:
84
86
  """Create run ID."""
85
- log(INFO, "ExecServicer.StartRun")
87
+ log(INFO, "ControlServicer.StartRun")
88
+ state = self.linkstate_factory.state()
89
+ ffs = self.ffs_factory.ffs()
90
+
91
+ if len(request.fab.content) > FAB_MAX_SIZE:
92
+ log(
93
+ ERROR,
94
+ "FAB size exceeds maximum allowed size of %d bytes.",
95
+ FAB_MAX_SIZE,
96
+ )
97
+ return StartRunResponse()
86
98
 
87
99
  flwr_aid = shared_account_info.get().flwr_aid if self.auth_plugin else None
88
- run_id = self.executor.start_run(
89
- request.fab.content,
90
- user_config_from_proto(request.override_config),
91
- config_record_from_proto(request.federation_options),
92
- flwr_aid,
93
- )
100
+ override_config = user_config_from_proto(request.override_config)
101
+ federation_options = config_record_from_proto(request.federation_options)
102
+ fab_file = request.fab.content
103
+
104
+ try:
105
+ # Check that num-supernodes is set
106
+ if self.is_simulation and "num-supernodes" not in federation_options:
107
+ raise ValueError(
108
+ "Federation options doesn't contain key `num-supernodes`."
109
+ )
110
+
111
+ # Create run
112
+ fab = Fab(hashlib.sha256(fab_file).hexdigest(), fab_file)
113
+ fab_hash = ffs.put(fab.content, {})
114
+ if fab_hash != fab.hash_str:
115
+ raise RuntimeError(
116
+ f"FAB ({fab.hash_str}) hash from request doesn't match contents"
117
+ )
118
+ fab_id, fab_version = get_fab_metadata(fab.content)
119
+
120
+ run_id = state.create_run(
121
+ fab_id,
122
+ fab_version,
123
+ fab_hash,
124
+ override_config,
125
+ federation_options,
126
+ flwr_aid,
127
+ )
128
+
129
+ # Create an empty context for the Run
130
+ context = Context(
131
+ run_id=run_id,
132
+ node_id=0,
133
+ node_config={},
134
+ state=RecordDict(),
135
+ run_config={},
136
+ )
137
+
138
+ # Register the context at the LinkState
139
+ state.set_serverapp_context(run_id=run_id, context=context)
94
140
 
95
- if run_id is None:
96
- log(ERROR, "Executor failed to start run")
141
+ # pylint: disable-next=broad-except
142
+ except Exception as e:
143
+ log(ERROR, "Could not start run: %s", str(e))
97
144
  return StartRunResponse()
98
145
 
146
+ log(INFO, "Created run %s", str(run_id))
99
147
  return StartRunResponse(run_id=run_id)
100
148
 
101
149
  def StreamLogs( # pylint: disable=C0103
102
150
  self, request: StreamLogsRequest, context: grpc.ServicerContext
103
151
  ) -> Generator[StreamLogsResponse, Any, None]:
104
152
  """Get logs."""
105
- log(INFO, "ExecServicer.StreamLogs")
153
+ log(INFO, "ControlServicer.StreamLogs")
106
154
  state = self.linkstate_factory.state()
107
155
 
108
156
  # Retrieve run ID and run
@@ -149,7 +197,7 @@ class ExecServicer(exec_pb2_grpc.ExecServicer):
149
197
  self, request: ListRunsRequest, context: grpc.ServicerContext
150
198
  ) -> ListRunsResponse:
151
199
  """Handle `flwr ls` command."""
152
- log(INFO, "ExecServicer.List")
200
+ log(INFO, "ControlServicer.List")
153
201
  state = self.linkstate_factory.state()
154
202
 
155
203
  # Build a set of run IDs for `flwr ls --runs`
@@ -195,7 +243,7 @@ class ExecServicer(exec_pb2_grpc.ExecServicer):
195
243
  self, request: StopRunRequest, context: grpc.ServicerContext
196
244
  ) -> StopRunResponse:
197
245
  """Stop a given run ID."""
198
- log(INFO, "ExecServicer.StopRun")
246
+ log(INFO, "ControlServicer.StopRun")
199
247
  state = self.linkstate_factory.state()
200
248
 
201
249
  # Retrieve run ID and run
@@ -240,11 +288,11 @@ class ExecServicer(exec_pb2_grpc.ExecServicer):
240
288
  self, request: GetLoginDetailsRequest, context: grpc.ServicerContext
241
289
  ) -> GetLoginDetailsResponse:
242
290
  """Start login."""
243
- log(INFO, "ExecServicer.GetLoginDetails")
291
+ log(INFO, "ControlServicer.GetLoginDetails")
244
292
  if self.auth_plugin is None:
245
293
  context.abort(
246
294
  grpc.StatusCode.UNIMPLEMENTED,
247
- "ExecServicer initialized without user authentication",
295
+ NO_USER_AUTH_MESSAGE,
248
296
  )
249
297
  raise grpc.RpcError() # This line is unreachable
250
298
 
@@ -267,11 +315,11 @@ class ExecServicer(exec_pb2_grpc.ExecServicer):
267
315
  self, request: GetAuthTokensRequest, context: grpc.ServicerContext
268
316
  ) -> GetAuthTokensResponse:
269
317
  """Get auth token."""
270
- log(INFO, "ExecServicer.GetAuthTokens")
318
+ log(INFO, "ControlServicer.GetAuthTokens")
271
319
  if self.auth_plugin is None:
272
320
  context.abort(
273
321
  grpc.StatusCode.UNIMPLEMENTED,
274
- "ExecServicer initialized without user authentication",
322
+ NO_USER_AUTH_MESSAGE,
275
323
  )
276
324
  raise grpc.RpcError() # This line is unreachable
277
325
 
@@ -12,7 +12,7 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
  # ==============================================================================
15
- """Flower Exec API interceptor."""
15
+ """Flower Control API interceptor."""
16
16
 
17
17
 
18
18
  import contextvars
@@ -20,9 +20,9 @@ from typing import Any, Callable, Union
20
20
 
21
21
  import grpc
22
22
 
23
- from flwr.common.auth_plugin import ExecAuthPlugin, ExecAuthzPlugin
23
+ from flwr.common.auth_plugin import ControlAuthPlugin, ControlAuthzPlugin
24
24
  from flwr.common.typing import AccountInfo
25
- from flwr.proto.exec_pb2 import ( # pylint: disable=E0611
25
+ from flwr.proto.control_pb2 import ( # pylint: disable=E0611
26
26
  GetAuthTokensRequest,
27
27
  GetAuthTokensResponse,
28
28
  GetLoginDetailsRequest,
@@ -50,13 +50,13 @@ shared_account_info: contextvars.ContextVar[AccountInfo] = contextvars.ContextVa
50
50
  )
51
51
 
52
52
 
53
- class ExecUserAuthInterceptor(grpc.ServerInterceptor): # type: ignore
54
- """Exec API interceptor for user authentication."""
53
+ class ControlUserAuthInterceptor(grpc.ServerInterceptor): # type: ignore
54
+ """Control API interceptor for user authentication."""
55
55
 
56
56
  def __init__(
57
57
  self,
58
- auth_plugin: ExecAuthPlugin,
59
- authz_plugin: ExecAuthzPlugin,
58
+ auth_plugin: ControlAuthPlugin,
59
+ authz_plugin: ControlAuthzPlugin,
60
60
  ):
61
61
  self.auth_plugin = auth_plugin
62
62
  self.authz_plugin = authz_plugin
@@ -72,8 +72,12 @@ class ExecUserAuthInterceptor(grpc.ServerInterceptor): # type: ignore
72
72
  by validating auth metadata sent by the user. Continue RPC call if user is
73
73
  authenticated, else, terminate RPC call by setting context to abort.
74
74
  """
75
+ # Only apply to Control service
76
+ if not handler_call_details.method.startswith("/flwr.proto.Control/"):
77
+ return continuation(handler_call_details)
78
+
75
79
  # One of the method handlers in
76
- # `flwr.superexec.exec_servicer.ExecServicer`
80
+ # `flwr.superlink.servicer.control.ControlServicer`
77
81
  method_handler: grpc.RpcMethodHandler = continuation(handler_call_details)
78
82
  return self._generic_auth_unary_method_handler(method_handler)
79
83
 
@@ -108,7 +112,9 @@ class ExecUserAuthInterceptor(grpc.ServerInterceptor): # type: ignore
108
112
  # Check if the user is authorized
109
113
  if not self.authz_plugin.verify_user_authorization(account_info):
110
114
  context.abort(
111
- grpc.StatusCode.PERMISSION_DENIED, "User not authorized"
115
+ grpc.StatusCode.PERMISSION_DENIED,
116
+ "❗️ User not authorized. "
117
+ "Please contact the SuperLink administrator.",
112
118
  )
113
119
  raise grpc.RpcError()
114
120
  return call(request, context) # type: ignore
@@ -127,7 +133,9 @@ class ExecUserAuthInterceptor(grpc.ServerInterceptor): # type: ignore
127
133
  # Check if the user is authorized
128
134
  if not self.authz_plugin.verify_user_authorization(account_info):
129
135
  context.abort(
130
- grpc.StatusCode.PERMISSION_DENIED, "User not authorized"
136
+ grpc.StatusCode.PERMISSION_DENIED,
137
+ "❗️ User not authorized. "
138
+ "Please contact the SuperLink administrator.",
131
139
  )
132
140
  raise grpc.RpcError()
133
141
 
@@ -40,8 +40,8 @@ from flwr.common.constant import (
40
40
  TRANSPORT_TYPE_REST,
41
41
  )
42
42
  from flwr.common.exit import ExitCode, flwr_exit
43
- from flwr.common.exit_handlers import register_exit_handlers
44
43
  from flwr.common.logger import log
44
+ from flwr.supercore.grpc_health import add_args_health
45
45
  from flwr.supernode.start_client_internal import start_client_internal
46
46
 
47
47
 
@@ -66,12 +66,6 @@ def flower_supernode() -> None:
66
66
 
67
67
  log(DEBUG, "Isolation mode: %s", args.isolation)
68
68
 
69
- # Register handlers for graceful shutdown
70
- register_exit_handlers(
71
- event_type=EventType.RUN_SUPERNODE_LEAVE,
72
- exit_message="SuperNode terminated gracefully.",
73
- )
74
-
75
69
  start_client_internal(
76
70
  server_address=args.superlink,
77
71
  transport=args.transport,
@@ -86,6 +80,7 @@ def flower_supernode() -> None:
86
80
  flwr_path=args.flwr_dir,
87
81
  isolation=args.isolation,
88
82
  clientappio_api_address=args.clientappio_api_address,
83
+ health_server_address=args.health_server_address,
89
84
  )
90
85
 
91
86
 
@@ -125,6 +120,7 @@ def _parse_args_run_supernode() -> argparse.ArgumentParser:
125
120
  help="ClientAppIo API (gRPC) server address (IPv4, IPv6, or a domain name). "
126
121
  f"By default, it is set to {CLIENTAPPIO_API_DEFAULT_SERVER_ADDRESS}.",
127
122
  )
123
+ add_args_health(parser)
128
124
 
129
125
  return parser
130
126
 
@@ -19,9 +19,13 @@ import argparse
19
19
  from logging import DEBUG, INFO
20
20
 
21
21
  from flwr.common.args import add_args_flwr_app_common
22
- from flwr.common.constant import CLIENTAPPIO_API_DEFAULT_CLIENT_ADDRESS
22
+ from flwr.common.constant import CLIENTAPPIO_API_DEFAULT_CLIENT_ADDRESS, ExecPluginType
23
23
  from flwr.common.exit import ExitCode, flwr_exit
24
24
  from flwr.common.logger import log
25
+ from flwr.proto.clientappio_pb2_grpc import ClientAppIoStub
26
+ from flwr.supercore.superexec.plugin import ClientAppExecPlugin
27
+ from flwr.supercore.superexec.run_superexec import run_with_deprecation_warning
28
+ from flwr.supercore.utils import mask_string
25
29
  from flwr.supernode.runtime.run_clientapp import run_clientapp
26
30
 
27
31
 
@@ -34,17 +38,30 @@ def flwr_clientapp() -> None:
34
38
  "flwr-clientapp does not support TLS yet.",
35
39
  )
36
40
 
41
+ # Disallow long-running `flwr-clientapp` processes
42
+ if args.token is None:
43
+ run_with_deprecation_warning(
44
+ cmd="flwr-clientapp",
45
+ plugin_type=ExecPluginType.CLIENT_APP,
46
+ plugin_class=ClientAppExecPlugin,
47
+ stub_class=ClientAppIoStub,
48
+ appio_api_address=args.clientappio_api_address,
49
+ flwr_dir=args.flwr_dir,
50
+ parent_pid=args.parent_pid,
51
+ warn_run_once=args.run_once,
52
+ )
53
+ return
54
+
37
55
  log(INFO, "Start `flwr-clientapp` process")
38
56
  log(
39
57
  DEBUG,
40
58
  "`flwr-clientapp` will attempt to connect to SuperNode's "
41
59
  "ClientAppIo API at %s with token %s",
42
60
  args.clientappio_api_address,
43
- args.token,
61
+ mask_string(args.token) if args.token else "None",
44
62
  )
45
63
  run_clientapp(
46
64
  clientappio_api_address=args.clientappio_api_address,
47
- run_once=(args.token is not None),
48
65
  token=args.token,
49
66
  flwr_dir=args.flwr_dir,
50
67
  certificates=None,
@@ -64,18 +81,5 @@ def _parse_args_run_flwr_clientapp() -> argparse.ArgumentParser:
64
81
  help="Address of SuperNode's ClientAppIo API (IPv4, IPv6, or a domain name)."
65
82
  f"By default, it is set to {CLIENTAPPIO_API_DEFAULT_CLIENT_ADDRESS}.",
66
83
  )
67
- parser.add_argument(
68
- "--token",
69
- type=int,
70
- required=False,
71
- help="Unique token generated by SuperNode for each ClientApp execution",
72
- )
73
- parser.add_argument(
74
- "--parent-pid",
75
- type=int,
76
- default=None,
77
- help="The PID of the parent process. When set, the process will terminate "
78
- "when the parent process exits.",
79
- )
80
84
  add_args_flwr_app_common(parser=parser)
81
85
  return parser
@@ -51,8 +51,9 @@ class InMemoryNodeState(NodeState): # pylint: disable=too-many-instance-attribu
51
51
  # Store run ID to Context mapping
52
52
  self.ctx_store: dict[int, Context] = {}
53
53
  self.lock_ctx_store = Lock()
54
- # Store run ID to token mapping
54
+ # Store run ID to token mapping and token to run ID mapping
55
55
  self.token_store: dict[int, str] = {}
56
+ self.token_to_run_id: dict[str, int] = {}
56
57
  self.lock_token_store = Lock()
57
58
 
58
59
  def set_node_id(self, node_id: Optional[int]) -> None:
@@ -170,13 +171,14 @@ class InMemoryNodeState(NodeState): # pylint: disable=too-many-instance-attribu
170
171
  ret -= set(self.token_store.keys())
171
172
  return list(ret)
172
173
 
173
- def create_token(self, run_id: int) -> str:
174
+ def create_token(self, run_id: int) -> Optional[str]:
174
175
  """Create a token for the given run ID."""
175
176
  token = secrets.token_hex(FLWR_APP_TOKEN_LENGTH) # Generate a random token
176
177
  with self.lock_token_store:
177
178
  if run_id in self.token_store:
178
- raise ValueError("Token already created for this run ID")
179
+ return None # Token already created for this run ID
179
180
  self.token_store[run_id] = token
181
+ self.token_to_run_id[token] = run_id
180
182
  return token
181
183
 
182
184
  def verify_token(self, run_id: int, token: str) -> bool:
@@ -187,4 +189,11 @@ class InMemoryNodeState(NodeState): # pylint: disable=too-many-instance-attribu
187
189
  def delete_token(self, run_id: int) -> None:
188
190
  """Delete the token for the given run ID."""
189
191
  with self.lock_token_store:
190
- self.token_store.pop(run_id, None)
192
+ token = self.token_store.pop(run_id, None)
193
+ if token is not None:
194
+ self.token_to_run_id.pop(token, None)
195
+
196
+ def get_run_id_by_token(self, token: str) -> Optional[int]:
197
+ """Get the run ID associated with a given token."""
198
+ with self.lock_token_store:
199
+ return self.token_to_run_id.get(token)
@@ -15,15 +15,16 @@
15
15
  """Abstract base class NodeState."""
16
16
 
17
17
 
18
- from abc import ABC, abstractmethod
18
+ from abc import abstractmethod
19
19
  from collections.abc import Sequence
20
20
  from typing import Optional
21
21
 
22
22
  from flwr.common import Context, Message
23
23
  from flwr.common.typing import Run
24
+ from flwr.supercore.corestate import CoreState
24
25
 
25
26
 
26
- class NodeState(ABC):
27
+ class NodeState(CoreState):
27
28
  """Abstract base class for node state."""
28
29
 
29
30
  @abstractmethod
@@ -168,45 +169,3 @@ class NodeState(ABC):
168
169
  Sequence[int]
169
170
  Sequence of run IDs with pending messages.
170
171
  """
171
-
172
- @abstractmethod
173
- def create_token(self, run_id: int) -> str:
174
- """Create a token for the given run ID.
175
-
176
- Parameters
177
- ----------
178
- run_id : int
179
- The ID of the run for which to create a token.
180
-
181
- Returns
182
- -------
183
- str
184
- A unique token associated with the run ID.
185
- """
186
-
187
- @abstractmethod
188
- def verify_token(self, run_id: int, token: str) -> bool:
189
- """Verify a token for the given run ID.
190
-
191
- Parameters
192
- ----------
193
- run_id : int
194
- The ID of the run for which to verify the token.
195
- token : str
196
- The token to verify.
197
-
198
- Returns
199
- -------
200
- bool
201
- True if the token is valid for the run ID, False otherwise.
202
- """
203
-
204
- @abstractmethod
205
- def delete_token(self, run_id: int) -> None:
206
- """Delete the token for the given run ID.
207
-
208
- Parameters
209
- ----------
210
- run_id : int
211
- The ID of the run for which to delete the token.
212
- """