flwr 1.24.0__py3-none-any.whl → 1.26.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 (204) hide show
  1. flwr/__init__.py +1 -1
  2. flwr/app/__init__.py +4 -1
  3. flwr/app/message_type.py +29 -0
  4. flwr/app/metadata.py +5 -2
  5. flwr/app/user_config.py +19 -0
  6. flwr/cli/app.py +37 -19
  7. flwr/cli/app_cmd/publish.py +25 -75
  8. flwr/cli/app_cmd/review.py +25 -66
  9. flwr/cli/auth_plugin/auth_plugin.py +5 -10
  10. flwr/cli/auth_plugin/noop_auth_plugin.py +1 -2
  11. flwr/cli/auth_plugin/oidc_cli_plugin.py +38 -38
  12. flwr/cli/build.py +15 -28
  13. flwr/cli/config/__init__.py +21 -0
  14. flwr/cli/config/ls.py +71 -0
  15. flwr/cli/config_migration.py +297 -0
  16. flwr/cli/config_utils.py +63 -156
  17. flwr/cli/constant.py +71 -0
  18. flwr/cli/federation/__init__.py +0 -2
  19. flwr/cli/federation/ls.py +256 -64
  20. flwr/cli/flower_config.py +429 -0
  21. flwr/cli/install.py +23 -62
  22. flwr/cli/log.py +23 -37
  23. flwr/cli/login/login.py +29 -63
  24. flwr/cli/ls.py +72 -61
  25. flwr/cli/new/new.py +98 -309
  26. flwr/cli/pull.py +19 -37
  27. flwr/cli/run/run.py +87 -100
  28. flwr/cli/run_utils.py +23 -5
  29. flwr/cli/stop.py +33 -74
  30. flwr/cli/supernode/ls.py +35 -62
  31. flwr/cli/supernode/register.py +31 -80
  32. flwr/cli/supernode/unregister.py +24 -70
  33. flwr/cli/typing.py +200 -0
  34. flwr/cli/utils.py +160 -412
  35. flwr/client/grpc_adapter_client/connection.py +2 -2
  36. flwr/client/grpc_rere_client/connection.py +9 -6
  37. flwr/client/grpc_rere_client/grpc_adapter.py +1 -1
  38. flwr/client/message_handler/message_handler.py +2 -1
  39. flwr/client/mod/centraldp_mods.py +1 -1
  40. flwr/client/mod/localdp_mod.py +1 -1
  41. flwr/client/mod/secure_aggregation/secaggplus_mod.py +1 -1
  42. flwr/client/rest_client/connection.py +6 -4
  43. flwr/client/run_info_store.py +2 -1
  44. flwr/clientapp/client_app.py +2 -1
  45. flwr/common/__init__.py +3 -2
  46. flwr/common/args.py +5 -5
  47. flwr/common/config.py +12 -17
  48. flwr/common/constant.py +3 -16
  49. flwr/common/context.py +2 -1
  50. flwr/common/exit/exit.py +4 -4
  51. flwr/common/exit/exit_code.py +6 -0
  52. flwr/common/grpc.py +2 -1
  53. flwr/common/logger.py +1 -1
  54. flwr/common/message.py +1 -1
  55. flwr/common/retry_invoker.py +13 -5
  56. flwr/common/secure_aggregation/ndarrays_arithmetic.py +5 -2
  57. flwr/common/serde.py +13 -5
  58. flwr/common/telemetry.py +1 -1
  59. flwr/common/typing.py +10 -3
  60. flwr/compat/client/app.py +6 -9
  61. flwr/compat/client/grpc_client/connection.py +2 -1
  62. flwr/compat/common/constant.py +29 -0
  63. flwr/compat/server/app.py +1 -1
  64. flwr/proto/clientappio_pb2.py +2 -2
  65. flwr/proto/clientappio_pb2_grpc.py +104 -88
  66. flwr/proto/clientappio_pb2_grpc.pyi +140 -80
  67. flwr/proto/federation_pb2.py +5 -3
  68. flwr/proto/federation_pb2.pyi +32 -2
  69. flwr/proto/fleet_pb2.py +10 -10
  70. flwr/proto/fleet_pb2.pyi +5 -1
  71. flwr/proto/run_pb2.py +18 -26
  72. flwr/proto/run_pb2.pyi +10 -58
  73. flwr/proto/serverappio_pb2.py +2 -2
  74. flwr/proto/serverappio_pb2_grpc.py +138 -207
  75. flwr/proto/serverappio_pb2_grpc.pyi +189 -155
  76. flwr/proto/simulationio_pb2.py +2 -2
  77. flwr/proto/simulationio_pb2_grpc.py +62 -90
  78. flwr/proto/simulationio_pb2_grpc.pyi +95 -55
  79. flwr/server/app.py +7 -13
  80. flwr/server/compat/grid_client_proxy.py +2 -1
  81. flwr/server/grid/grpc_grid.py +5 -5
  82. flwr/server/serverapp/app.py +11 -4
  83. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +1 -1
  84. flwr/server/superlink/fleet/grpc_rere/node_auth_server_interceptor.py +13 -12
  85. flwr/server/superlink/fleet/message_handler/message_handler.py +42 -2
  86. flwr/server/superlink/linkstate/__init__.py +2 -2
  87. flwr/server/superlink/linkstate/in_memory_linkstate.py +36 -10
  88. flwr/server/superlink/linkstate/linkstate.py +34 -21
  89. flwr/server/superlink/linkstate/linkstate_factory.py +16 -8
  90. flwr/server/superlink/linkstate/{sqlite_linkstate.py → sql_linkstate.py} +471 -516
  91. flwr/server/superlink/linkstate/utils.py +49 -2
  92. flwr/server/superlink/serverappio/serverappio_servicer.py +1 -33
  93. flwr/server/superlink/simulation/simulationio_servicer.py +0 -19
  94. flwr/server/utils/validator.py +1 -1
  95. flwr/server/workflow/default_workflows.py +2 -1
  96. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +1 -1
  97. flwr/serverapp/strategy/bulyan.py +7 -1
  98. flwr/serverapp/strategy/dp_fixed_clipping.py +9 -1
  99. flwr/serverapp/strategy/fedavg.py +1 -1
  100. flwr/serverapp/strategy/fedxgb_cyclic.py +1 -1
  101. flwr/simulation/ray_transport/ray_client_proxy.py +2 -6
  102. flwr/simulation/run_simulation.py +3 -12
  103. flwr/simulation/simulationio_connection.py +3 -3
  104. flwr/{common → supercore}/address.py +7 -33
  105. flwr/supercore/app_utils.py +2 -1
  106. flwr/supercore/constant.py +27 -2
  107. flwr/supercore/corestate/{sqlite_corestate.py → sql_corestate.py} +19 -23
  108. flwr/supercore/credential_store/__init__.py +33 -0
  109. flwr/supercore/credential_store/credential_store.py +34 -0
  110. flwr/supercore/credential_store/file_credential_store.py +76 -0
  111. flwr/{common → supercore}/date.py +0 -11
  112. flwr/supercore/ffs/disk_ffs.py +1 -1
  113. flwr/supercore/object_store/object_store_factory.py +14 -6
  114. flwr/supercore/object_store/{sqlite_object_store.py → sql_object_store.py} +115 -117
  115. flwr/supercore/sql_mixin.py +315 -0
  116. flwr/{cli/new/templates → supercore/state}/__init__.py +2 -2
  117. flwr/{cli/new/templates/app/code/flwr_tune → supercore/state/alembic}/__init__.py +2 -2
  118. flwr/supercore/state/alembic/env.py +103 -0
  119. flwr/supercore/state/alembic/script.py.mako +43 -0
  120. flwr/supercore/state/alembic/utils.py +239 -0
  121. flwr/{cli/new/templates/app → supercore/state/alembic/versions}/__init__.py +2 -2
  122. flwr/supercore/state/alembic/versions/rev_2026_01_28_initialize_migration_of_state_tables.py +200 -0
  123. flwr/supercore/state/schema/README.md +121 -0
  124. flwr/{cli/new/templates/app/code → supercore/state/schema}/__init__.py +2 -2
  125. flwr/supercore/state/schema/corestate_tables.py +36 -0
  126. flwr/supercore/state/schema/linkstate_tables.py +152 -0
  127. flwr/supercore/state/schema/objectstore_tables.py +90 -0
  128. flwr/supercore/superexec/run_superexec.py +2 -2
  129. flwr/supercore/utils.py +225 -0
  130. flwr/superlink/federation/federation_manager.py +2 -2
  131. flwr/superlink/federation/noop_federation_manager.py +8 -6
  132. flwr/superlink/servicer/control/control_grpc.py +2 -0
  133. flwr/superlink/servicer/control/control_servicer.py +106 -21
  134. flwr/supernode/cli/flower_supernode.py +2 -1
  135. flwr/supernode/nodestate/in_memory_nodestate.py +62 -1
  136. flwr/supernode/nodestate/nodestate.py +45 -0
  137. flwr/supernode/runtime/run_clientapp.py +14 -14
  138. flwr/supernode/servicer/clientappio/clientappio_servicer.py +13 -5
  139. flwr/supernode/start_client_internal.py +17 -10
  140. {flwr-1.24.0.dist-info → flwr-1.26.0.dist-info}/METADATA +8 -8
  141. {flwr-1.24.0.dist-info → flwr-1.26.0.dist-info}/RECORD +144 -184
  142. flwr/cli/federation/show.py +0 -317
  143. flwr/cli/new/templates/app/.gitignore.tpl +0 -163
  144. flwr/cli/new/templates/app/LICENSE.tpl +0 -202
  145. flwr/cli/new/templates/app/README.baseline.md.tpl +0 -127
  146. flwr/cli/new/templates/app/README.flowertune.md.tpl +0 -68
  147. flwr/cli/new/templates/app/README.md.tpl +0 -37
  148. flwr/cli/new/templates/app/code/__init__.baseline.py.tpl +0 -1
  149. flwr/cli/new/templates/app/code/__init__.py.tpl +0 -1
  150. flwr/cli/new/templates/app/code/__init__.pytorch_legacy_api.py.tpl +0 -1
  151. flwr/cli/new/templates/app/code/client.baseline.py.tpl +0 -75
  152. flwr/cli/new/templates/app/code/client.huggingface.py.tpl +0 -93
  153. flwr/cli/new/templates/app/code/client.jax.py.tpl +0 -71
  154. flwr/cli/new/templates/app/code/client.mlx.py.tpl +0 -102
  155. flwr/cli/new/templates/app/code/client.numpy.py.tpl +0 -46
  156. flwr/cli/new/templates/app/code/client.pytorch.py.tpl +0 -80
  157. flwr/cli/new/templates/app/code/client.pytorch_legacy_api.py.tpl +0 -55
  158. flwr/cli/new/templates/app/code/client.sklearn.py.tpl +0 -108
  159. flwr/cli/new/templates/app/code/client.tensorflow.py.tpl +0 -82
  160. flwr/cli/new/templates/app/code/client.xgboost.py.tpl +0 -110
  161. flwr/cli/new/templates/app/code/dataset.baseline.py.tpl +0 -36
  162. flwr/cli/new/templates/app/code/flwr_tune/client_app.py.tpl +0 -92
  163. flwr/cli/new/templates/app/code/flwr_tune/dataset.py.tpl +0 -87
  164. flwr/cli/new/templates/app/code/flwr_tune/models.py.tpl +0 -56
  165. flwr/cli/new/templates/app/code/flwr_tune/server_app.py.tpl +0 -73
  166. flwr/cli/new/templates/app/code/flwr_tune/strategy.py.tpl +0 -78
  167. flwr/cli/new/templates/app/code/model.baseline.py.tpl +0 -66
  168. flwr/cli/new/templates/app/code/server.baseline.py.tpl +0 -43
  169. flwr/cli/new/templates/app/code/server.huggingface.py.tpl +0 -42
  170. flwr/cli/new/templates/app/code/server.jax.py.tpl +0 -39
  171. flwr/cli/new/templates/app/code/server.mlx.py.tpl +0 -41
  172. flwr/cli/new/templates/app/code/server.numpy.py.tpl +0 -38
  173. flwr/cli/new/templates/app/code/server.pytorch.py.tpl +0 -41
  174. flwr/cli/new/templates/app/code/server.pytorch_legacy_api.py.tpl +0 -31
  175. flwr/cli/new/templates/app/code/server.sklearn.py.tpl +0 -44
  176. flwr/cli/new/templates/app/code/server.tensorflow.py.tpl +0 -38
  177. flwr/cli/new/templates/app/code/server.xgboost.py.tpl +0 -56
  178. flwr/cli/new/templates/app/code/strategy.baseline.py.tpl +0 -1
  179. flwr/cli/new/templates/app/code/task.huggingface.py.tpl +0 -98
  180. flwr/cli/new/templates/app/code/task.jax.py.tpl +0 -57
  181. flwr/cli/new/templates/app/code/task.mlx.py.tpl +0 -102
  182. flwr/cli/new/templates/app/code/task.numpy.py.tpl +0 -7
  183. flwr/cli/new/templates/app/code/task.pytorch.py.tpl +0 -99
  184. flwr/cli/new/templates/app/code/task.pytorch_legacy_api.py.tpl +0 -111
  185. flwr/cli/new/templates/app/code/task.sklearn.py.tpl +0 -67
  186. flwr/cli/new/templates/app/code/task.tensorflow.py.tpl +0 -52
  187. flwr/cli/new/templates/app/code/task.xgboost.py.tpl +0 -67
  188. flwr/cli/new/templates/app/code/utils.baseline.py.tpl +0 -1
  189. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +0 -146
  190. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +0 -80
  191. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +0 -65
  192. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +0 -52
  193. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +0 -56
  194. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +0 -49
  195. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +0 -53
  196. flwr/cli/new/templates/app/pyproject.pytorch_legacy_api.toml.tpl +0 -53
  197. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +0 -52
  198. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +0 -53
  199. flwr/cli/new/templates/app/pyproject.xgboost.toml.tpl +0 -61
  200. flwr/common/pyproject.py +0 -42
  201. flwr/supercore/sqlite_mixin.py +0 -159
  202. /flwr/{common → supercore}/version.py +0 -0
  203. {flwr-1.24.0.dist-info → flwr-1.26.0.dist-info}/WHEEL +0 -0
  204. {flwr-1.24.0.dist-info → flwr-1.26.0.dist-info}/entry_points.txt +0 -0
@@ -28,7 +28,6 @@ from flwr.common.constant import (
28
28
  SERVERAPPIO_API_DEFAULT_CLIENT_ADDRESS,
29
29
  SUPERLINK_NODE_ID,
30
30
  ErrorCode,
31
- MessageType,
32
31
  )
33
32
  from flwr.common.grpc import create_channel, on_channel_state_change
34
33
  from flwr.common.inflatable import (
@@ -50,7 +49,7 @@ from flwr.common.inflatable_utils import (
50
49
  )
51
50
  from flwr.common.logger import log, warn_deprecated_feature
52
51
  from flwr.common.message import make_message, remove_content_from_message
53
- from flwr.common.retry_invoker import _make_simple_grpc_retry_invoker, _wrap_stub
52
+ from flwr.common.retry_invoker import make_simple_grpc_retry_invoker, wrap_stub
54
53
  from flwr.common.serde import message_to_proto, run_from_proto
55
54
  from flwr.common.typing import Run
56
55
  from flwr.proto.appio_pb2 import ( # pylint: disable=E0611
@@ -69,6 +68,7 @@ from flwr.proto.serverappio_pb2 import ( # pylint: disable=E0611
69
68
  GetNodesResponse,
70
69
  )
71
70
  from flwr.proto.serverappio_pb2_grpc import ServerAppIoStub # pylint: disable=E0611
71
+ from flwr.supercore.constant import SYSTEM_MESSAGE_TYPE
72
72
 
73
73
  from .grid import Grid
74
74
 
@@ -127,7 +127,7 @@ class GrpcGrid(Grid):
127
127
  self._grpc_stub: ServerAppIoStub | None = None
128
128
  self._channel: grpc.Channel | None = None
129
129
  self.node = Node(node_id=SUPERLINK_NODE_ID)
130
- self._retry_invoker = _make_simple_grpc_retry_invoker()
130
+ self._retry_invoker = make_simple_grpc_retry_invoker()
131
131
  super().__init__()
132
132
 
133
133
  @property
@@ -150,7 +150,7 @@ class GrpcGrid(Grid):
150
150
  )
151
151
  self._channel.subscribe(on_channel_state_change)
152
152
  self._grpc_stub = ServerAppIoStub(self._channel)
153
- _wrap_stub(self._grpc_stub, self._retry_invoker)
153
+ wrap_stub(self._grpc_stub, self._retry_invoker)
154
154
  log(DEBUG, "[flwr-serverapp] Connected to %s", self._addr)
155
155
 
156
156
  def _disconnect(self) -> None:
@@ -341,7 +341,7 @@ class GrpcGrid(Grid):
341
341
  message_id="",
342
342
  src_node_id=self.node.node_id,
343
343
  dst_node_id=self.node.node_id,
344
- message_type=MessageType.SYSTEM,
344
+ message_type=SYSTEM_MESSAGE_TYPE,
345
345
  group_id="",
346
346
  ttl=0,
347
347
  reply_to_message_id=msg_proto.metadata.reply_to_message_id,
@@ -144,6 +144,10 @@ def run_serverapp( # pylint: disable=R0913, R0914, R0915, R0917, W0212
144
144
  exit_code = ExitCode.SUCCESS
145
145
 
146
146
  def on_exit() -> None:
147
+ # Set Grpc max retries to 1 to avoid blocking on exit
148
+ if grid:
149
+ grid._retry_invoker.max_tries = 1
150
+
147
151
  # Stop heartbeat sender
148
152
  if heartbeat_sender and heartbeat_sender.is_running:
149
153
  heartbeat_sender.stop()
@@ -154,10 +158,13 @@ def run_serverapp( # pylint: disable=R0913, R0914, R0915, R0917, W0212
154
158
 
155
159
  # Update run status
156
160
  if run and run_status and grid:
157
- run_status_proto = run_status_to_proto(run_status)
158
- grid._stub.UpdateRunStatus(
159
- UpdateRunStatusRequest(run_id=run.run_id, run_status=run_status_proto)
160
- )
161
+ try:
162
+ req = UpdateRunStatusRequest(
163
+ run_id=run.run_id, run_status=run_status_to_proto(run_status)
164
+ )
165
+ grid._stub.UpdateRunStatus(req)
166
+ except grpc.RpcError:
167
+ pass
161
168
 
162
169
  # Close the Grpc connection
163
170
  if grid:
@@ -30,7 +30,6 @@ from flwr.common.constant import (
30
30
  GRPC_ADAPTER_METADATA_MESSAGE_QUALNAME_KEY,
31
31
  )
32
32
  from flwr.common.logger import log
33
- from flwr.common.version import package_name, package_version
34
33
  from flwr.proto import grpcadapter_pb2_grpc # pylint: disable=E0611
35
34
  from flwr.proto.fab_pb2 import GetFabRequest # pylint: disable=E0611
36
35
  from flwr.proto.fleet_pb2 import ( # pylint: disable=E0611
@@ -49,6 +48,7 @@ from flwr.proto.message_pb2 import ( # pylint: disable=E0611
49
48
  PushObjectRequest,
50
49
  )
51
50
  from flwr.proto.run_pb2 import GetRunRequest # pylint: disable=E0611
51
+ from flwr.supercore.version import package_name, package_version
52
52
 
53
53
  from ..grpc_rere.fleet_servicer import FleetServicer
54
54
 
@@ -108,7 +108,7 @@ class NodeAuthServerInterceptor(grpc.ServerInterceptor): # type: ignore
108
108
  def _wrap_method_handler(
109
109
  self,
110
110
  method_handler: grpc.RpcMethodHandler,
111
- expected_public_key: bytes,
111
+ received_public_key: bytes,
112
112
  ) -> grpc.RpcMethodHandler:
113
113
  def _generic_method_handler(
114
114
  request: GrpcMessage,
@@ -117,21 +117,22 @@ class NodeAuthServerInterceptor(grpc.ServerInterceptor): # type: ignore
117
117
  # Note: This function runs in a different thread
118
118
  # than the `intercept_service` function.
119
119
 
120
- # Retrieve the public key
121
- if isinstance(request, (RegisterNodeFleetRequest | ActivateNodeRequest)):
122
- actual_public_key = request.public_key
123
- else:
120
+ # Skip registration and activation requests
121
+ if not isinstance(request, (RegisterNodeFleetRequest, ActivateNodeRequest)):
122
+ # Retrieve the node ID from the request
124
123
  if hasattr(request, "node"):
125
- node_id = request.node.node_id
124
+ received_node_id = request.node.node_id
126
125
  else:
127
- node_id = request.node_id # type: ignore[attr-defined]
128
- actual_public_key = self.state_factory.state().get_node_public_key(
129
- node_id
126
+ received_node_id = request.node_id # type: ignore[attr-defined]
127
+
128
+ # Get the actual node ID based on the received public key
129
+ node_id = self.state_factory.state().get_node_id_by_public_key(
130
+ received_public_key
130
131
  )
131
132
 
132
- # Verify the public key
133
- if actual_public_key != expected_public_key:
134
- context.abort(grpc.StatusCode.UNAUTHENTICATED, "Invalid node ID")
133
+ # Verify that the received node ID matches the actual node ID
134
+ if received_node_id != node_id:
135
+ context.abort(grpc.StatusCode.UNAUTHENTICATED, "Invalid node ID")
135
136
 
136
137
  response: GrpcMessage = method_handler.unary_unary(request, context)
137
138
  return response
@@ -127,7 +127,7 @@ def send_node_heartbeat(
127
127
  return SendNodeHeartbeatResponse(success=res)
128
128
 
129
129
 
130
- def pull_messages(
130
+ def pull_messages( # pylint: disable=too-many-locals
131
131
  request: PullMessagesRequest,
132
132
  state: LinkState,
133
133
  store: ObjectStore,
@@ -143,6 +143,8 @@ def pull_messages(
143
143
  # Convert to Messages
144
144
  msg_proto = []
145
145
  trees = []
146
+ run_id_to_record: int | None = None
147
+
146
148
  for msg in message_list:
147
149
  try:
148
150
  # Retrieve Message object tree from ObjectStore
@@ -152,12 +154,30 @@ def pull_messages(
152
154
  # Add Message and its object tree to the response
153
155
  msg_proto.append(message_to_proto(msg))
154
156
  trees.append(obj_tree)
157
+
158
+ # Track run_id for traffic recording
159
+ run_id_to_record = msg.metadata.run_id
160
+
155
161
  except NoObjectInStoreError as e:
156
162
  log(ERROR, e.message)
157
163
  # Delete message ins from state
158
164
  state.delete_messages(message_ins_ids={msg_object_id})
159
165
 
160
- return PullMessagesResponse(messages_list=msg_proto, message_object_trees=trees)
166
+ response = PullMessagesResponse(messages_list=msg_proto, message_object_trees=trees)
167
+
168
+ # Record incoming traffic size
169
+ bytes_recv = request.ByteSize()
170
+
171
+ # Record traffic only if message was successfully processed
172
+ # All messages in this request are assumed to belong to the same run
173
+ if run_id_to_record is not None:
174
+ # Record outgoing traffic size
175
+ bytes_sent = response.ByteSize()
176
+ state.store_traffic(
177
+ run_id_to_record, bytes_sent=bytes_sent, bytes_recv=bytes_recv
178
+ )
179
+
180
+ return response
161
181
 
162
182
 
163
183
  def push_messages(
@@ -170,6 +190,9 @@ def push_messages(
170
190
  msg = message_from_proto(message_proto=request.messages_list[0])
171
191
  run_id = msg.metadata.run_id
172
192
 
193
+ # Record incoming traffic size
194
+ bytes_recv = request.ByteSize()
195
+
173
196
  # Abort if the run is not running
174
197
  abort_msg = check_abort(
175
198
  run_id,
@@ -193,6 +216,16 @@ def push_messages(
193
216
  results={str(message_id): 0},
194
217
  objects_to_push=objects_to_push,
195
218
  )
219
+
220
+ # Record outgoing traffic size
221
+ bytes_sent = response.ByteSize()
222
+
223
+ # Record traffic only if message was successfully processed
224
+ # Only one message is processed per request
225
+ state.store_traffic(run_id, bytes_sent=bytes_sent, bytes_recv=bytes_recv)
226
+ if request.clientapp_runtime_list:
227
+ state.add_clientapp_runtime(run_id, request.clientapp_runtime_list[0])
228
+
196
229
  return response
197
230
 
198
231
 
@@ -257,6 +290,10 @@ def push_object(
257
290
  try:
258
291
  store.put(request.object_id, request.object_content)
259
292
  stored = True
293
+ # Record bytes traffic pushed from SuperNode
294
+ state.store_traffic(
295
+ request.run_id, bytes_sent=0, bytes_recv=len(request.object_content)
296
+ )
260
297
  except (NoObjectInStoreError, ValueError) as e:
261
298
  log(ERROR, str(e))
262
299
  except UnexpectedObjectContentError as e:
@@ -283,6 +320,9 @@ def pull_object(
283
320
  content = store.get(request.object_id)
284
321
  if content is not None:
285
322
  object_available = content != b""
323
+ # Record bytes traffic pulled by SuperNode
324
+ if object_available:
325
+ state.store_traffic(request.run_id, bytes_sent=len(content), bytes_recv=0)
286
326
  return PullObjectResponse(
287
327
  object_found=True,
288
328
  object_available=object_available,
@@ -18,11 +18,11 @@
18
18
  from .in_memory_linkstate import InMemoryLinkState as InMemoryLinkState
19
19
  from .linkstate import LinkState as LinkState
20
20
  from .linkstate_factory import LinkStateFactory as LinkStateFactory
21
- from .sqlite_linkstate import SqliteLinkState as SqliteLinkState
21
+ from .sql_linkstate import SqlLinkState as SqlLinkState
22
22
 
23
23
  __all__ = [
24
24
  "InMemoryLinkState",
25
25
  "LinkState",
26
26
  "LinkStateFactory",
27
- "SqliteLinkState",
27
+ "SqlLinkState",
28
28
  ]
@@ -23,6 +23,7 @@ from dataclasses import dataclass, field
23
23
  from datetime import datetime, timezone
24
24
  from logging import ERROR, WARNING
25
25
 
26
+ from flwr.app.user_config import UserConfig
26
27
  from flwr.common import Context, Message, log, now
27
28
  from flwr.common.constant import (
28
29
  HEARTBEAT_PATIENCE,
@@ -35,7 +36,7 @@ from flwr.common.constant import (
35
36
  SubStatus,
36
37
  )
37
38
  from flwr.common.record import ConfigRecord
38
- from flwr.common.typing import Run, RunStatus, UserConfig
39
+ from flwr.common.typing import Run, RunStatus
39
40
  from flwr.proto.node_pb2 import NodeInfo # pylint: disable=E0611
40
41
  from flwr.server.superlink.linkstate.linkstate import LinkState
41
42
  from flwr.server.utils import validate_message
@@ -518,15 +519,6 @@ class InMemoryLinkState(LinkState, InMemoryCoreState): # pylint: disable=R0902,
518
519
  node.online_until, tz=timezone.utc
519
520
  ).isoformat()
520
521
 
521
- def get_node_public_key(self, node_id: int) -> bytes:
522
- """Get `public_key` for the specified `node_id`."""
523
- with self.lock:
524
- if (
525
- node := self.nodes.get(node_id)
526
- ) is None or node.status == NodeStatus.UNREGISTERED:
527
- raise ValueError(f"Node ID {node_id} not found")
528
- return node.public_key
529
-
530
522
  def get_node_id_by_public_key(self, public_key: bytes) -> int | None:
531
523
  """Get `node_id` for the specified `public_key` if it exists and is not
532
524
  deleted."""
@@ -576,6 +568,9 @@ class InMemoryLinkState(LinkState, InMemoryCoreState): # pylint: disable=R0902,
576
568
  ),
577
569
  flwr_aid=flwr_aid if flwr_aid else "",
578
570
  federation=federation,
571
+ bytes_sent=0,
572
+ bytes_recv=0,
573
+ clientapp_runtime=0.0,
579
574
  ),
580
575
  )
581
576
  self.run_ids[run_id] = run_record
@@ -771,3 +766,34 @@ class InMemoryLinkState(LinkState, InMemoryCoreState): # pylint: disable=R0902,
771
766
  index = bisect_right(run.logs, (after_timestamp, ""))
772
767
  latest_timestamp = run.logs[-1][0] if index < len(run.logs) else 0.0
773
768
  return "".join(log for _, log in run.logs[index:]), latest_timestamp
769
+
770
+ def store_traffic(self, run_id: int, *, bytes_sent: int, bytes_recv: int) -> None:
771
+ """Store traffic data for the specified `run_id`."""
772
+ # Validate non-negative values
773
+ if bytes_sent < 0 or bytes_recv < 0:
774
+ raise ValueError(
775
+ f"Negative traffic values for run {run_id}: "
776
+ f"bytes_sent={bytes_sent}, bytes_recv={bytes_recv}"
777
+ )
778
+
779
+ if bytes_sent == 0 and bytes_recv == 0:
780
+ raise ValueError(
781
+ f"Both bytes_sent and bytes_recv cannot be zero for run {run_id}"
782
+ )
783
+
784
+ with self.lock:
785
+ if run_id not in self.run_ids:
786
+ raise ValueError(f"Run {run_id} not found")
787
+ run_record = self.run_ids[run_id]
788
+
789
+ with run_record.lock:
790
+ run = run_record.run
791
+ run.bytes_sent += bytes_sent
792
+ run.bytes_recv += bytes_recv
793
+
794
+ def add_clientapp_runtime(self, run_id: int, runtime: float) -> None:
795
+ """Add ClientApp runtime to the cumulative total for the specified `run_id`."""
796
+ with self.lock:
797
+ if run_id not in self.run_ids:
798
+ raise ValueError(f"Run {run_id} not found")
799
+ self.run_ids[run_id].run.clientapp_runtime += runtime
@@ -18,9 +18,10 @@
18
18
  import abc
19
19
  from collections.abc import Sequence
20
20
 
21
+ from flwr.app.user_config import UserConfig
21
22
  from flwr.common import Context, Message
22
23
  from flwr.common.record import ConfigRecord
23
- from flwr.common.typing import Run, RunStatus, UserConfig
24
+ from flwr.common.typing import Run, RunStatus
24
25
  from flwr.proto.node_pb2 import NodeInfo # pylint: disable=E0611
25
26
  from flwr.supercore.corestate import CoreState
26
27
  from flwr.superlink.federation import FederationManager
@@ -245,26 +246,6 @@ class LinkState(CoreState): # pylint: disable=R0904
245
246
  the specified filters.
246
247
  """
247
248
 
248
- @abc.abstractmethod
249
- def get_node_public_key(self, node_id: int) -> bytes:
250
- """Get `public_key` for the specified `node_id`.
251
-
252
- Parameters
253
- ----------
254
- node_id : int
255
- The identifier of the node whose public key is to be retrieved.
256
-
257
- Returns
258
- -------
259
- bytes
260
- The public key associated with the specified `node_id`.
261
-
262
- Raises
263
- ------
264
- ValueError
265
- If the specified `node_id` does not exist in the link state.
266
- """
267
-
268
249
  @abc.abstractmethod
269
250
  def create_run( # pylint: disable=too-many-arguments,too-many-positional-arguments
270
251
  self,
@@ -480,3 +461,35 @@ class LinkState(CoreState): # pylint: disable=R0904
480
461
  - The timestamp of the latest log entry in the returned logs.
481
462
  Returns `0` if no logs are returned.
482
463
  """
464
+
465
+ @abc.abstractmethod
466
+ def store_traffic(self, run_id: int, *, bytes_sent: int, bytes_recv: int) -> None:
467
+ """Store traffic data for the specified `run_id`.
468
+
469
+ Parameters
470
+ ----------
471
+ run_id : int
472
+ The identifier of the run for which to store traffic data.
473
+ bytes_sent : int
474
+ The number of bytes pulled by SuperNodes from the SuperLink to add to the
475
+ run's total.
476
+ bytes_recv : int
477
+ The number of bytes received by SuperLink from SuperNodes to add to the
478
+ run's total.
479
+ """
480
+
481
+ @abc.abstractmethod
482
+ def add_clientapp_runtime(self, run_id: int, runtime: float) -> None:
483
+ """Add ClientApp runtime to the cumulative total for the specified `run_id`.
484
+
485
+ This method accumulates the runtime by adding the provided value to the
486
+ existing total runtime for the run. Multiple ClientApps can contribute
487
+ to the same run's total runtime.
488
+
489
+ Parameters
490
+ ----------
491
+ run_id : int
492
+ The identifier of the run for which to store each ClientApp's runtime.
493
+ runtime : float
494
+ The runtime in seconds to add to the `run_id`'s cumulative total.
495
+ """
@@ -24,7 +24,7 @@ from flwr.superlink.federation import FederationManager
24
24
 
25
25
  from .in_memory_linkstate import InMemoryLinkState
26
26
  from .linkstate import LinkState
27
- from .sqlite_linkstate import SqliteLinkState
27
+ from .sql_linkstate import SqlLinkState
28
28
 
29
29
 
30
30
  class LinkStateFactory:
@@ -56,20 +56,28 @@ class LinkStateFactory:
56
56
 
57
57
  def state(self) -> LinkState:
58
58
  """Return a State instance and create it, if necessary."""
59
+ # Return cached state if it exists
60
+ if self.state_instance is not None:
61
+ if self.database == FLWR_IN_MEMORY_DB_NAME:
62
+ log(DEBUG, "Using InMemoryState")
63
+ else:
64
+ log(DEBUG, "Using SqlLinkState")
65
+ return self.state_instance
66
+
59
67
  # Get the ObjectStore instance
60
68
  object_store = self.objectstore_factory.store()
61
69
 
62
70
  # InMemoryState
63
71
  if self.database == FLWR_IN_MEMORY_DB_NAME:
64
- if self.state_instance is None:
65
- self.state_instance = InMemoryLinkState(
66
- self.federation_manager, object_store
67
- )
72
+ self.state_instance = InMemoryLinkState(
73
+ self.federation_manager, object_store
74
+ )
68
75
  log(DEBUG, "Using InMemoryState")
69
76
  return self.state_instance
70
77
 
71
- # SqliteState
72
- state = SqliteLinkState(self.database, self.federation_manager, object_store)
78
+ # SqlLinkState
79
+ state = SqlLinkState(self.database, self.federation_manager, object_store)
73
80
  state.initialize()
74
- log(DEBUG, "Using SqliteState")
81
+ self.state_instance = state
82
+ log(DEBUG, "Using SqlLinkState")
75
83
  return state