flwr-nightly 1.14.0.dev20241214__py3-none-any.whl → 1.15.0.dev20250107__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 (46) hide show
  1. flwr/cli/log.py +9 -7
  2. flwr/cli/login/login.py +1 -3
  3. flwr/cli/ls.py +25 -22
  4. flwr/cli/new/templates/app/.gitignore.tpl +3 -0
  5. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +1 -1
  6. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +1 -1
  7. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +1 -1
  8. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
  9. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +1 -1
  10. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
  11. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +3 -3
  12. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
  13. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +1 -1
  14. flwr/cli/run/run.py +11 -18
  15. flwr/cli/stop.py +71 -32
  16. flwr/cli/utils.py +81 -25
  17. flwr/client/app.py +11 -1
  18. flwr/client/client.py +0 -32
  19. flwr/client/clientapp/app.py +3 -1
  20. flwr/client/grpc_rere_client/connection.py +10 -4
  21. flwr/client/message_handler/message_handler.py +0 -2
  22. flwr/client/numpy_client.py +0 -44
  23. flwr/client/supernode/app.py +1 -2
  24. flwr/common/logger.py +16 -1
  25. flwr/common/record/recordset.py +1 -1
  26. flwr/common/retry_invoker.py +3 -1
  27. flwr/common/secure_aggregation/crypto/symmetric_encryption.py +45 -0
  28. flwr/common/telemetry.py +13 -3
  29. flwr/server/app.py +8 -8
  30. flwr/server/run_serverapp.py +8 -9
  31. flwr/server/serverapp/app.py +17 -2
  32. flwr/server/superlink/driver/serverappio_servicer.py +9 -0
  33. flwr/server/superlink/fleet/message_handler/message_handler.py +1 -3
  34. flwr/server/superlink/fleet/vce/vce_api.py +2 -2
  35. flwr/server/superlink/linkstate/in_memory_linkstate.py +10 -2
  36. flwr/server/superlink/linkstate/linkstate.py +4 -0
  37. flwr/server/superlink/linkstate/sqlite_linkstate.py +6 -2
  38. flwr/server/superlink/simulation/simulationio_servicer.py +13 -0
  39. flwr/simulation/app.py +15 -4
  40. flwr/simulation/run_simulation.py +35 -22
  41. flwr/simulation/simulationio_connection.py +3 -0
  42. {flwr_nightly-1.14.0.dev20241214.dist-info → flwr_nightly-1.15.0.dev20250107.dist-info}/METADATA +2 -2
  43. {flwr_nightly-1.14.0.dev20241214.dist-info → flwr_nightly-1.15.0.dev20250107.dist-info}/RECORD +46 -46
  44. {flwr_nightly-1.14.0.dev20241214.dist-info → flwr_nightly-1.15.0.dev20250107.dist-info}/LICENSE +0 -0
  45. {flwr_nightly-1.14.0.dev20241214.dist-info → flwr_nightly-1.15.0.dev20250107.dist-info}/WHEEL +0 -0
  46. {flwr_nightly-1.14.0.dev20241214.dist-info → flwr_nightly-1.15.0.dev20250107.dist-info}/entry_points.txt +0 -0
flwr/client/client.py CHANGED
@@ -22,7 +22,6 @@ from abc import ABC
22
22
 
23
23
  from flwr.common import (
24
24
  Code,
25
- Context,
26
25
  EvaluateIns,
27
26
  EvaluateRes,
28
27
  FitIns,
@@ -34,14 +33,11 @@ from flwr.common import (
34
33
  Parameters,
35
34
  Status,
36
35
  )
37
- from flwr.common.logger import warn_deprecated_feature_with_example
38
36
 
39
37
 
40
38
  class Client(ABC):
41
39
  """Abstract base class for Flower clients."""
42
40
 
43
- _context: Context
44
-
45
41
  def get_properties(self, ins: GetPropertiesIns) -> GetPropertiesRes:
46
42
  """Return set of client's properties.
47
43
 
@@ -143,34 +139,6 @@ class Client(ABC):
143
139
  metrics={},
144
140
  )
145
141
 
146
- @property
147
- def context(self) -> Context:
148
- """Getter for `Context` client attribute."""
149
- warn_deprecated_feature_with_example(
150
- "Accessing the context via the client's attribute is deprecated.",
151
- example_message="Instead, pass it to the client's "
152
- "constructor in your `client_fn()` which already "
153
- "receives a context object.",
154
- code_example="def client_fn(context: Context) -> Client:\n\n"
155
- "\t\t# Your existing client_fn\n\n"
156
- "\t\t# Pass `context` to the constructor\n"
157
- "\t\treturn FlowerClient(context).to_client()",
158
- )
159
- return self._context
160
-
161
- @context.setter
162
- def context(self, context: Context) -> None:
163
- """Setter for `Context` client attribute."""
164
- self._context = context
165
-
166
- def get_context(self) -> Context:
167
- """Get the run context from this client."""
168
- return self.context
169
-
170
- def set_context(self, context: Context) -> None:
171
- """Apply a run context to this client."""
172
- self.context = context
173
-
174
142
  def to_client(self) -> Client:
175
143
  """Return client (itself)."""
176
144
  return self
@@ -32,6 +32,7 @@ from flwr.common.constant import CLIENTAPPIO_API_DEFAULT_CLIENT_ADDRESS, ErrorCo
32
32
  from flwr.common.grpc import create_channel
33
33
  from flwr.common.logger import log
34
34
  from flwr.common.message import Error
35
+ from flwr.common.retry_invoker import _make_simple_grpc_retry_invoker, _wrap_stub
35
36
  from flwr.common.serde import (
36
37
  context_from_proto,
37
38
  context_to_proto,
@@ -106,9 +107,9 @@ def run_clientapp( # pylint: disable=R0914
106
107
 
107
108
  # Resolve directory where FABs are installed
108
109
  flwr_dir_ = get_flwr_dir(flwr_dir)
109
-
110
110
  try:
111
111
  stub = ClientAppIoStub(channel)
112
+ _wrap_stub(stub, _make_simple_grpc_retry_invoker())
112
113
 
113
114
  while True:
114
115
  # If token is not set, loop until token is received from SuperNode
@@ -139,6 +140,7 @@ def run_clientapp( # pylint: disable=R0914
139
140
 
140
141
  # Execute ClientApp
141
142
  reply_message = client_app(message=message, context=context)
143
+
142
144
  except Exception as ex: # pylint: disable=broad-exception-caught
143
145
  # Don't update/change NodeState
144
146
 
@@ -42,7 +42,7 @@ from flwr.common.logger import log
42
42
  from flwr.common.message import Message, Metadata
43
43
  from flwr.common.retry_invoker import RetryInvoker
44
44
  from flwr.common.serde import message_from_taskins, message_to_taskres, run_from_proto
45
- from flwr.common.typing import Fab, Run
45
+ from flwr.common.typing import Fab, Run, RunNotRunningException
46
46
  from flwr.proto.fab_pb2 import GetFabRequest, GetFabResponse # pylint: disable=E0611
47
47
  from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
48
48
  CreateNodeRequest,
@@ -155,10 +155,16 @@ def grpc_request_response( # pylint: disable=R0913,R0914,R0915,R0917
155
155
  ping_thread: Optional[threading.Thread] = None
156
156
  ping_stop_event = threading.Event()
157
157
 
158
+ def _should_giveup_fn(e: Exception) -> bool:
159
+ if e.code() == grpc.StatusCode.PERMISSION_DENIED: # type: ignore
160
+ raise RunNotRunningException
161
+ if e.code() == grpc.StatusCode.UNAVAILABLE: # type: ignore
162
+ return False
163
+ return True
164
+
158
165
  # Restrict retries to cases where the status code is UNAVAILABLE
159
- retry_invoker.should_giveup = (
160
- lambda e: e.code() != grpc.StatusCode.UNAVAILABLE # type: ignore
161
- )
166
+ # If the status code is PERMISSION_DENIED, additionally raise RunNotRunningException
167
+ retry_invoker.should_giveup = _should_giveup_fn
162
168
 
163
169
  ###########################################################################
164
170
  # ping/create_node/delete_node/receive/send/get_run functions
@@ -105,8 +105,6 @@ def handle_legacy_message_from_msgtype(
105
105
  "Please use `NumPyClient.to_client()` method to convert it to `Client`.",
106
106
  )
107
107
 
108
- client.set_context(context)
109
-
110
108
  message_type = message.metadata.message_type
111
109
 
112
110
  # Handle GetPropertiesIns
@@ -21,13 +21,11 @@ from typing import Callable
21
21
  from flwr.client.client import Client
22
22
  from flwr.common import (
23
23
  Config,
24
- Context,
25
24
  NDArrays,
26
25
  Scalar,
27
26
  ndarrays_to_parameters,
28
27
  parameters_to_ndarrays,
29
28
  )
30
- from flwr.common.logger import warn_deprecated_feature_with_example
31
29
  from flwr.common.typing import (
32
30
  Code,
33
31
  EvaluateIns,
@@ -71,8 +69,6 @@ Example
71
69
  class NumPyClient(ABC):
72
70
  """Abstract base class for Flower clients using NumPy."""
73
71
 
74
- _context: Context
75
-
76
72
  def get_properties(self, config: Config) -> dict[str, Scalar]:
77
73
  """Return a client's set of properties.
78
74
 
@@ -175,34 +171,6 @@ class NumPyClient(ABC):
175
171
  _ = (self, parameters, config)
176
172
  return 0.0, 0, {}
177
173
 
178
- @property
179
- def context(self) -> Context:
180
- """Getter for `Context` client attribute."""
181
- warn_deprecated_feature_with_example(
182
- "Accessing the context via the client's attribute is deprecated.",
183
- example_message="Instead, pass it to the client's "
184
- "constructor in your `client_fn()` which already "
185
- "receives a context object.",
186
- code_example="def client_fn(context: Context) -> Client:\n\n"
187
- "\t\t# Your existing client_fn\n\n"
188
- "\t\t# Pass `context` to the constructor\n"
189
- "\t\treturn FlowerClient(context).to_client()",
190
- )
191
- return self._context
192
-
193
- @context.setter
194
- def context(self, context: Context) -> None:
195
- """Setter for `Context` client attribute."""
196
- self._context = context
197
-
198
- def get_context(self) -> Context:
199
- """Get the run context from this client."""
200
- return self.context
201
-
202
- def set_context(self, context: Context) -> None:
203
- """Apply a run context to this client."""
204
- self.context = context
205
-
206
174
  def to_client(self) -> Client:
207
175
  """Convert to object to Client type and return it."""
208
176
  return _wrap_numpy_client(client=self)
@@ -299,21 +267,9 @@ def _evaluate(self: Client, ins: EvaluateIns) -> EvaluateRes:
299
267
  )
300
268
 
301
269
 
302
- def _get_context(self: Client) -> Context:
303
- """Return context of underlying NumPyClient."""
304
- return self.numpy_client.get_context() # type: ignore
305
-
306
-
307
- def _set_context(self: Client, context: Context) -> None:
308
- """Apply context to underlying NumPyClient."""
309
- self.numpy_client.set_context(context) # type: ignore
310
-
311
-
312
270
  def _wrap_numpy_client(client: NumPyClient) -> Client:
313
271
  member_dict: dict[str, Callable] = { # type: ignore
314
272
  "__init__": _constructor,
315
- "get_context": _get_context,
316
- "set_context": _set_context,
317
273
  }
318
274
 
319
275
  # Add wrapper type methods (if overridden)
@@ -114,9 +114,8 @@ def run_client_app() -> None:
114
114
  event(EventType.RUN_CLIENT_APP_ENTER)
115
115
  log(
116
116
  ERROR,
117
- "The command `flower-client-app` has been replaced by `flower-supernode`.",
117
+ "The command `flower-client-app` has been replaced by `flwr run`.",
118
118
  )
119
- log(INFO, "Execute `flower-supernode --help` to learn how to use it.")
120
119
  register_exit_handlers(event_type=EventType.RUN_CLIENT_APP_LEAVE)
121
120
 
122
121
 
flwr/common/logger.py CHANGED
@@ -15,6 +15,7 @@
15
15
  """Flower Logger."""
16
16
 
17
17
 
18
+ import json as _json
18
19
  import logging
19
20
  import re
20
21
  import sys
@@ -27,6 +28,8 @@ from queue import Empty, Queue
27
28
  from typing import TYPE_CHECKING, Any, Optional, TextIO, Union
28
29
 
29
30
  import grpc
31
+ import typer
32
+ from rich.console import Console
30
33
 
31
34
  from flwr.proto.log_pb2 import PushLogsRequest # pylint: disable=E0611
32
35
  from flwr.proto.node_pb2 import Node # pylint: disable=E0611
@@ -377,7 +380,7 @@ def stop_log_uploader(
377
380
  log_uploader.join()
378
381
 
379
382
 
380
- def remove_emojis(text: str) -> str:
383
+ def _remove_emojis(text: str) -> str:
381
384
  """Remove emojis from the provided text."""
382
385
  emoji_pattern = re.compile(
383
386
  "["
@@ -391,3 +394,15 @@ def remove_emojis(text: str) -> str:
391
394
  flags=re.UNICODE,
392
395
  )
393
396
  return emoji_pattern.sub(r"", text)
397
+
398
+
399
+ def print_json_error(msg: str, e: Union[typer.Exit, Exception]) -> None:
400
+ """Print error message as JSON."""
401
+ Console().print_json(
402
+ _json.dumps(
403
+ {
404
+ "success": False,
405
+ "error-message": _remove_emojis(str(msg) + "\n" + str(e)),
406
+ }
407
+ )
408
+ )
@@ -151,7 +151,7 @@ class RecordSet:
151
151
  >>> p_record = ParametersRecord({"my_array": arr})
152
152
  >>>
153
153
  >>> # Adding it to the record_set would look like this
154
- >>> my_recordset.configs_records["my_config"] = c_record
154
+ >>> my_recordset.parameters_records["my_parameters"] = p_record
155
155
 
156
156
  For additional examples on how to construct each of the records types shown
157
157
  above, please refer to the documentation for :code:`ConfigsRecord`,
@@ -30,6 +30,7 @@ from flwr.common.logger import log
30
30
  from flwr.common.typing import RunNotRunningException
31
31
  from flwr.proto.clientappio_pb2_grpc import ClientAppIoStub
32
32
  from flwr.proto.serverappio_pb2_grpc import ServerAppIoStub
33
+ from flwr.proto.simulationio_pb2_grpc import SimulationIoStub
33
34
 
34
35
 
35
36
  def exponential(
@@ -365,7 +366,8 @@ def _make_simple_grpc_retry_invoker() -> RetryInvoker:
365
366
 
366
367
 
367
368
  def _wrap_stub(
368
- stub: Union[ServerAppIoStub, ClientAppIoStub], retry_invoker: RetryInvoker
369
+ stub: Union[ServerAppIoStub, ClientAppIoStub, SimulationIoStub],
370
+ retry_invoker: RetryInvoker,
369
371
  ) -> None:
370
372
  """Wrap a gRPC stub with a retry invoker."""
371
373
 
@@ -117,3 +117,48 @@ def verify_hmac(key: bytes, message: bytes, hmac_value: bytes) -> bool:
117
117
  return True
118
118
  except InvalidSignature:
119
119
  return False
120
+
121
+
122
+ def sign_message(private_key: ec.EllipticCurvePrivateKey, message: bytes) -> bytes:
123
+ """Sign a message using the provided EC private key.
124
+
125
+ Parameters
126
+ ----------
127
+ private_key : ec.EllipticCurvePrivateKey
128
+ The EC private key to sign the message with.
129
+ message : bytes
130
+ The message to be signed.
131
+
132
+ Returns
133
+ -------
134
+ bytes
135
+ The signature of the message.
136
+ """
137
+ signature = private_key.sign(message, ec.ECDSA(hashes.SHA256()))
138
+ return signature
139
+
140
+
141
+ def verify_signature(
142
+ public_key: ec.EllipticCurvePublicKey, message: bytes, signature: bytes
143
+ ) -> bool:
144
+ """Verify a signature against a message using the provided EC public key.
145
+
146
+ Parameters
147
+ ----------
148
+ public_key : ec.EllipticCurvePublicKey
149
+ The EC public key to verify the signature.
150
+ message : bytes
151
+ The original message.
152
+ signature : bytes
153
+ The signature to verify.
154
+
155
+ Returns
156
+ -------
157
+ bool
158
+ True if the signature is valid, False otherwise.
159
+ """
160
+ try:
161
+ public_key.verify(signature, message, ec.ECDSA(hashes.SHA256()))
162
+ return True
163
+ except InvalidSignature:
164
+ return False
flwr/common/telemetry.py CHANGED
@@ -151,6 +151,16 @@ class EventType(str, Enum):
151
151
 
152
152
  # Not yet implemented
153
153
 
154
+ # --- `flwr-*` commands ------------------------------------------------------------
155
+
156
+ # CLI: flwr-simulation
157
+ FLWR_SIMULATION_RUN_ENTER = auto()
158
+ FLWR_SIMULATION_RUN_LEAVE = auto()
159
+
160
+ # CLI: flwr-serverapp
161
+ FLWR_SERVERAPP_RUN_ENTER = auto()
162
+ FLWR_SERVERAPP_RUN_LEAVE = auto()
163
+
154
164
  # --- Simulation Engine ------------------------------------------------------------
155
165
 
156
166
  # CLI: flower-simulation
@@ -171,12 +181,12 @@ class EventType(str, Enum):
171
181
  RUN_SUPERNODE_ENTER = auto()
172
182
  RUN_SUPERNODE_LEAVE = auto()
173
183
 
174
- # CLI: `flower-server-app`
184
+ # --- DEPRECATED -------------------------------------------------------------------
185
+
186
+ # [DEPRECATED] CLI: `flower-server-app`
175
187
  RUN_SERVER_APP_ENTER = auto()
176
188
  RUN_SERVER_APP_LEAVE = auto()
177
189
 
178
- # --- DEPRECATED -------------------------------------------------------------------
179
-
180
190
  # [DEPRECATED] CLI: `flower-client-app`
181
191
  RUN_CLIENT_APP_ENTER = auto()
182
192
  RUN_CLIENT_APP_LEAVE = auto()
flwr/server/app.py CHANGED
@@ -93,9 +93,13 @@ BASE_DIR = get_flwr_dir() / "superlink" / "ffs"
93
93
 
94
94
 
95
95
  try:
96
- from flwr.ee import get_exec_auth_plugins
96
+ from flwr.ee import add_ee_args_superlink, get_exec_auth_plugins
97
97
  except ImportError:
98
98
 
99
+ # pylint: disable-next=unused-argument
100
+ def add_ee_args_superlink(parser: argparse.ArgumentParser) -> None:
101
+ """Add EE-specific arguments to the parser."""
102
+
99
103
  def get_exec_auth_plugins() -> dict[str, type[ExecAuthPlugin]]:
100
104
  """Return all Exec API authentication plugins."""
101
105
  raise NotImplementedError("No authentication plugins are currently supported.")
@@ -370,6 +374,7 @@ def run_superlink() -> None:
370
374
  server_public_key,
371
375
  ) = maybe_keys
372
376
  state = state_factory.state()
377
+ state.clear_supernode_auth_keys_and_credentials()
373
378
  state.store_node_public_keys(node_public_keys)
374
379
  state.store_server_private_public_key(
375
380
  private_key_to_bytes(server_private_key),
@@ -580,7 +585,7 @@ def _try_setup_node_authentication(
580
585
 
581
586
 
582
587
  def _try_obtain_user_auth_config(args: argparse.Namespace) -> Optional[dict[str, Any]]:
583
- if args.user_auth_config is not None:
588
+ if getattr(args, "user_auth_config", None) is not None:
584
589
  with open(args.user_auth_config, encoding="utf-8") as file:
585
590
  config: dict[str, Any] = yaml.safe_load(file)
586
591
  return config
@@ -703,6 +708,7 @@ def _parse_args_run_superlink() -> argparse.ArgumentParser:
703
708
  )
704
709
 
705
710
  _add_args_common(parser=parser)
711
+ add_ee_args_superlink(parser=parser)
706
712
  _add_args_serverappio_api(parser=parser)
707
713
  _add_args_fleet_api(parser=parser)
708
714
  _add_args_exec_api(parser=parser)
@@ -792,12 +798,6 @@ def _add_args_common(parser: argparse.ArgumentParser) -> None:
792
798
  type=str,
793
799
  help="The SuperLink's public key (as a path str) to enable authentication.",
794
800
  )
795
- parser.add_argument(
796
- "--user-auth-config",
797
- help="The path to the user authentication configuration YAML file.",
798
- type=str,
799
- default=None,
800
- )
801
801
 
802
802
 
803
803
  def _add_args_serverappio_api(parser: argparse.ArgumentParser) -> None:
@@ -15,12 +15,12 @@
15
15
  """Run ServerApp."""
16
16
 
17
17
 
18
- import sys
19
18
  from logging import DEBUG, ERROR
20
19
  from typing import Optional
21
20
 
22
- from flwr.common import Context
23
- from flwr.common.logger import log, warn_unsupported_feature
21
+ from flwr.common import Context, EventType, event
22
+ from flwr.common.exit_handlers import register_exit_handlers
23
+ from flwr.common.logger import log
24
24
  from flwr.common.object_ref import load_app
25
25
 
26
26
  from .driver import Driver
@@ -66,12 +66,11 @@ def run(
66
66
  return context
67
67
 
68
68
 
69
- # pylint: disable-next=too-many-branches,too-many-statements,too-many-locals
70
69
  def run_server_app() -> None:
71
70
  """Run Flower server app."""
72
- warn_unsupported_feature(
73
- "The command `flower-server-app` is deprecated and no longer in use. "
74
- "Use the `flwr-serverapp` exclusively instead."
71
+ event(EventType.RUN_SERVER_APP_ENTER)
72
+ log(
73
+ ERROR,
74
+ "The command `flower-server-app` has been replaced by `flwr run`.",
75
75
  )
76
- log(ERROR, "`flower-server-app` used.")
77
- sys.exit()
76
+ register_exit_handlers(event_type=EventType.RUN_SERVER_APP_LEAVE)
@@ -25,6 +25,7 @@ from typing import Optional
25
25
 
26
26
  from flwr.cli.config_utils import get_fab_metadata
27
27
  from flwr.cli.install import install_from_fab
28
+ from flwr.cli.utils import get_sha256_hash
28
29
  from flwr.common.args import add_args_flwr_app_common
29
30
  from flwr.common.config import (
30
31
  get_flwr_dir,
@@ -51,6 +52,7 @@ from flwr.common.serde import (
51
52
  run_from_proto,
52
53
  run_status_to_proto,
53
54
  )
55
+ from flwr.common.telemetry import EventType, event
54
56
  from flwr.common.typing import RunNotRunningException, RunStatus
55
57
  from flwr.proto.run_pb2 import UpdateRunStatusRequest # pylint: disable=E0611
56
58
  from flwr.proto.serverappio_pb2 import ( # pylint: disable=E0611
@@ -113,7 +115,8 @@ def run_serverapp( # pylint: disable=R0914, disable=W0212, disable=R0915
113
115
  # Resolve directory where FABs are installed
114
116
  flwr_dir_ = get_flwr_dir(flwr_dir)
115
117
  log_uploader = None
116
-
118
+ success = True
119
+ hash_run_id = None
117
120
  while True:
118
121
 
119
122
  try:
@@ -129,6 +132,8 @@ def run_serverapp( # pylint: disable=R0914, disable=W0212, disable=R0915
129
132
  run = run_from_proto(res.run)
130
133
  fab = fab_from_proto(res.fab)
131
134
 
135
+ hash_run_id = get_sha256_hash(run.run_id)
136
+
132
137
  driver.set_run(run.run_id)
133
138
 
134
139
  # Start log uploader for this run
@@ -171,6 +176,11 @@ def run_serverapp( # pylint: disable=R0914, disable=W0212, disable=R0915
171
176
  UpdateRunStatusRequest(run_id=run.run_id, run_status=run_status_proto)
172
177
  )
173
178
 
179
+ event(
180
+ EventType.FLWR_SERVERAPP_RUN_ENTER,
181
+ event_details={"run-id-hash": hash_run_id},
182
+ )
183
+
174
184
  # Load and run the ServerApp with the Driver
175
185
  updated_context = run_(
176
186
  driver=driver,
@@ -187,17 +197,18 @@ def run_serverapp( # pylint: disable=R0914, disable=W0212, disable=R0915
187
197
  _ = driver._stub.PushServerAppOutputs(out_req)
188
198
 
189
199
  run_status = RunStatus(Status.FINISHED, SubStatus.COMPLETED, "")
190
-
191
200
  except RunNotRunningException:
192
201
  log(INFO, "")
193
202
  log(INFO, "Run ID %s stopped.", run.run_id)
194
203
  log(INFO, "")
195
204
  run_status = None
205
+ success = False
196
206
 
197
207
  except Exception as ex: # pylint: disable=broad-exception-caught
198
208
  exc_entity = "ServerApp"
199
209
  log(ERROR, "%s raised an exception", exc_entity, exc_info=ex)
200
210
  run_status = RunStatus(Status.FINISHED, SubStatus.FAILED, str(ex))
211
+ success = False
201
212
 
202
213
  finally:
203
214
  # Stop log uploader for this run and upload final logs
@@ -213,6 +224,10 @@ def run_serverapp( # pylint: disable=R0914, disable=W0212, disable=R0915
213
224
  run_id=run.run_id, run_status=run_status_proto
214
225
  )
215
226
  )
227
+ event(
228
+ EventType.FLWR_SERVERAPP_RUN_LEAVE,
229
+ event_details={"run-id-hash": hash_run_id, "success": success},
230
+ )
216
231
 
217
232
  # Stop the loop if `flwr-serverapp` is expected to process a single run
218
233
  if run_once:
@@ -159,6 +159,9 @@ class ServerAppIoServicer(serverappio_pb2_grpc.ServerAppIoServicer):
159
159
  for task_ins in request.task_ins_list:
160
160
  validation_errors = validate_task_ins_or_res(task_ins)
161
161
  _raise_if(bool(validation_errors), ", ".join(validation_errors))
162
+ _raise_if(
163
+ request.run_id != task_ins.run_id, "`task_ins` has mismatched `run_id`"
164
+ )
162
165
 
163
166
  # Store each TaskIns
164
167
  task_ids: list[Optional[UUID]] = []
@@ -193,6 +196,12 @@ class ServerAppIoServicer(serverappio_pb2_grpc.ServerAppIoServicer):
193
196
  # Read from state
194
197
  task_res_list: list[TaskRes] = state.get_task_res(task_ids=task_ids)
195
198
 
199
+ # Validate request
200
+ for task_res in task_res_list:
201
+ _raise_if(
202
+ request.run_id != task_res.run_id, "`task_res` has mismatched `run_id`"
203
+ )
204
+
196
205
  # Delete the TaskIns/TaskRes pairs if TaskRes is found
197
206
  task_ins_ids_to_delete = {
198
207
  UUID(task_res.task.ancestry[0]) for task_res in task_res_list
@@ -123,9 +123,7 @@ def push_task_res(request: PushTaskResRequest, state: LinkState) -> PushTaskResR
123
123
  return response
124
124
 
125
125
 
126
- def get_run(
127
- request: GetRunRequest, state: LinkState # pylint: disable=W0613
128
- ) -> GetRunResponse:
126
+ def get_run(request: GetRunRequest, state: LinkState) -> GetRunResponse:
129
127
  """Get run information."""
130
128
  run = state.get_run(request.run_id)
131
129
 
@@ -182,8 +182,8 @@ def run_api(
182
182
  f_stop: threading.Event,
183
183
  ) -> None:
184
184
  """Run the VCE."""
185
- taskins_queue: "Queue[TaskIns]" = Queue()
186
- taskres_queue: "Queue[TaskRes]" = Queue()
185
+ taskins_queue: Queue[TaskIns] = Queue()
186
+ taskres_queue: Queue[TaskRes] = Queue()
187
187
 
188
188
  try:
189
189
 
@@ -430,10 +430,17 @@ class InMemoryLinkState(LinkState): # pylint: disable=R0902,R0904
430
430
  """Retrieve `server_public_key` in urlsafe bytes."""
431
431
  return self.server_public_key
432
432
 
433
+ def clear_supernode_auth_keys_and_credentials(self) -> None:
434
+ """Clear stored `node_public_keys` and credentials in the link state if any."""
435
+ with self.lock:
436
+ self.server_private_key = None
437
+ self.server_public_key = None
438
+ self.node_public_keys.clear()
439
+
433
440
  def store_node_public_keys(self, public_keys: set[bytes]) -> None:
434
441
  """Store a set of `node_public_keys` in the link state."""
435
442
  with self.lock:
436
- self.node_public_keys = public_keys
443
+ self.node_public_keys.update(public_keys)
437
444
 
438
445
  def store_node_public_key(self, public_key: bytes) -> None:
439
446
  """Store a `node_public_key` in the link state."""
@@ -442,7 +449,8 @@ class InMemoryLinkState(LinkState): # pylint: disable=R0902,R0904
442
449
 
443
450
  def get_node_public_keys(self) -> set[bytes]:
444
451
  """Retrieve all currently stored `node_public_keys` as a set."""
445
- return self.node_public_keys
452
+ with self.lock:
453
+ return self.node_public_keys.copy()
446
454
 
447
455
  def get_run_ids(self) -> set[int]:
448
456
  """Retrieve all run IDs."""
@@ -284,6 +284,10 @@ class LinkState(abc.ABC): # pylint: disable=R0904
284
284
  def get_server_public_key(self) -> Optional[bytes]:
285
285
  """Retrieve `server_public_key` in urlsafe bytes."""
286
286
 
287
+ @abc.abstractmethod
288
+ def clear_supernode_auth_keys_and_credentials(self) -> None:
289
+ """Clear stored `node_public_keys` and credentials in the link state if any."""
290
+
287
291
  @abc.abstractmethod
288
292
  def store_node_public_keys(self, public_keys: set[bytes]) -> None:
289
293
  """Store a set of `node_public_keys` in the link state."""
@@ -761,8 +761,6 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
761
761
  "federation_options, pending_at, starting_at, running_at, finished_at, "
762
762
  "sub_status, details) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"
763
763
  )
764
- if fab_hash:
765
- fab_id, fab_version = "", ""
766
764
  override_config_json = json.dumps(override_config)
767
765
  data = [
768
766
  sint64_run_id,
@@ -820,6 +818,12 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
820
818
  public_key = None
821
819
  return public_key
822
820
 
821
+ def clear_supernode_auth_keys_and_credentials(self) -> None:
822
+ """Clear stored `node_public_keys` and credentials in the link state if any."""
823
+ queries = ["DELETE FROM public_key;", "DELETE FROM credential;"]
824
+ for query in queries:
825
+ self.query(query)
826
+
823
827
  def store_node_public_keys(self, public_keys: set[bytes]) -> None:
824
828
  """Store a set of `node_public_keys` in the link state."""
825
829
  query = "INSERT INTO public_key (public_key) VALUES (?)"