flwr-nightly 1.13.0.dev20241021__py3-none-any.whl → 1.13.0.dev20241111__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.

Potentially problematic release.


This version of flwr-nightly might be problematic. Click here for more details.

Files changed (92) hide show
  1. flwr/cli/build.py +2 -2
  2. flwr/cli/config_utils.py +97 -0
  3. flwr/cli/log.py +63 -97
  4. flwr/cli/new/templates/app/code/flwr_tune/dataset.py.tpl +1 -1
  5. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +1 -0
  6. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +1 -1
  7. flwr/cli/run/run.py +34 -88
  8. flwr/client/app.py +23 -20
  9. flwr/client/clientapp/app.py +22 -18
  10. flwr/client/nodestate/__init__.py +25 -0
  11. flwr/client/nodestate/in_memory_nodestate.py +38 -0
  12. flwr/client/nodestate/nodestate.py +30 -0
  13. flwr/client/nodestate/nodestate_factory.py +37 -0
  14. flwr/client/{node_state.py → run_info_store.py} +4 -3
  15. flwr/client/supernode/app.py +6 -8
  16. flwr/common/args.py +83 -0
  17. flwr/common/config.py +10 -0
  18. flwr/common/constant.py +39 -5
  19. flwr/common/context.py +9 -4
  20. flwr/common/date.py +3 -3
  21. flwr/common/logger.py +108 -1
  22. flwr/common/object_ref.py +47 -16
  23. flwr/common/serde.py +24 -0
  24. flwr/common/telemetry.py +0 -6
  25. flwr/common/typing.py +10 -1
  26. flwr/proto/exec_pb2.py +14 -17
  27. flwr/proto/exec_pb2.pyi +14 -22
  28. flwr/proto/log_pb2.py +29 -0
  29. flwr/proto/log_pb2.pyi +39 -0
  30. flwr/proto/log_pb2_grpc.py +4 -0
  31. flwr/proto/log_pb2_grpc.pyi +4 -0
  32. flwr/proto/message_pb2.py +8 -8
  33. flwr/proto/message_pb2.pyi +4 -1
  34. flwr/proto/run_pb2.py +32 -27
  35. flwr/proto/run_pb2.pyi +26 -0
  36. flwr/proto/serverappio_pb2.py +52 -0
  37. flwr/proto/{driver_pb2.pyi → serverappio_pb2.pyi} +54 -0
  38. flwr/proto/serverappio_pb2_grpc.py +376 -0
  39. flwr/proto/serverappio_pb2_grpc.pyi +147 -0
  40. flwr/proto/simulationio_pb2.py +38 -0
  41. flwr/proto/simulationio_pb2.pyi +65 -0
  42. flwr/proto/simulationio_pb2_grpc.py +205 -0
  43. flwr/proto/simulationio_pb2_grpc.pyi +81 -0
  44. flwr/server/app.py +272 -105
  45. flwr/server/driver/driver.py +15 -1
  46. flwr/server/driver/grpc_driver.py +25 -36
  47. flwr/server/driver/inmemory_driver.py +6 -16
  48. flwr/server/run_serverapp.py +29 -23
  49. flwr/server/{superlink/state → serverapp}/__init__.py +3 -9
  50. flwr/server/serverapp/app.py +214 -0
  51. flwr/server/strategy/aggregate.py +4 -4
  52. flwr/server/strategy/fedadam.py +11 -1
  53. flwr/server/superlink/driver/__init__.py +1 -1
  54. flwr/server/superlink/driver/{driver_grpc.py → serverappio_grpc.py} +19 -16
  55. flwr/server/superlink/driver/{driver_servicer.py → serverappio_servicer.py} +125 -39
  56. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +4 -2
  57. flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +2 -2
  58. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +4 -2
  59. flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +2 -2
  60. flwr/server/superlink/fleet/message_handler/message_handler.py +7 -7
  61. flwr/server/superlink/fleet/rest_rere/rest_api.py +7 -7
  62. flwr/server/superlink/fleet/vce/vce_api.py +23 -23
  63. flwr/server/superlink/linkstate/__init__.py +28 -0
  64. flwr/server/superlink/{state/in_memory_state.py → linkstate/in_memory_linkstate.py} +184 -36
  65. flwr/server/superlink/{state/state.py → linkstate/linkstate.py} +149 -19
  66. flwr/server/superlink/{state/state_factory.py → linkstate/linkstate_factory.py} +9 -9
  67. flwr/server/superlink/{state/sqlite_state.py → linkstate/sqlite_linkstate.py} +306 -65
  68. flwr/server/superlink/{state → linkstate}/utils.py +81 -30
  69. flwr/server/superlink/simulation/__init__.py +15 -0
  70. flwr/server/superlink/simulation/simulationio_grpc.py +65 -0
  71. flwr/server/superlink/simulation/simulationio_servicer.py +153 -0
  72. flwr/simulation/__init__.py +5 -1
  73. flwr/simulation/app.py +273 -345
  74. flwr/simulation/legacy_app.py +382 -0
  75. flwr/simulation/ray_transport/ray_client_proxy.py +2 -2
  76. flwr/simulation/run_simulation.py +57 -131
  77. flwr/simulation/simulationio_connection.py +86 -0
  78. flwr/superexec/app.py +6 -134
  79. flwr/superexec/deployment.py +61 -66
  80. flwr/superexec/exec_grpc.py +15 -8
  81. flwr/superexec/exec_servicer.py +36 -65
  82. flwr/superexec/executor.py +26 -7
  83. flwr/superexec/simulation.py +54 -107
  84. {flwr_nightly-1.13.0.dev20241021.dist-info → flwr_nightly-1.13.0.dev20241111.dist-info}/METADATA +5 -4
  85. {flwr_nightly-1.13.0.dev20241021.dist-info → flwr_nightly-1.13.0.dev20241111.dist-info}/RECORD +88 -69
  86. {flwr_nightly-1.13.0.dev20241021.dist-info → flwr_nightly-1.13.0.dev20241111.dist-info}/entry_points.txt +2 -0
  87. flwr/client/node_state_tests.py +0 -66
  88. flwr/proto/driver_pb2.py +0 -42
  89. flwr/proto/driver_pb2_grpc.py +0 -239
  90. flwr/proto/driver_pb2_grpc.pyi +0 -94
  91. {flwr_nightly-1.13.0.dev20241021.dist-info → flwr_nightly-1.13.0.dev20241111.dist-info}/LICENSE +0 -0
  92. {flwr_nightly-1.13.0.dev20241021.dist-info → flwr_nightly-1.13.0.dev20241111.dist-info}/WHEEL +0 -0
@@ -24,6 +24,8 @@ import grpc
24
24
  from flwr.cli.install import install_from_fab
25
25
  from flwr.client.client_app import ClientApp, LoadClientAppError
26
26
  from flwr.common import Context, Message
27
+ from flwr.common.args import add_args_flwr_app_common
28
+ from flwr.common.config import get_flwr_dir
27
29
  from flwr.common.constant import ErrorCode
28
30
  from flwr.common.grpc import create_channel
29
31
  from flwr.common.logger import log
@@ -54,15 +56,13 @@ from .utils import get_load_client_app_fn
54
56
 
55
57
  def flwr_clientapp() -> None:
56
58
  """Run process-isolated Flower ClientApp."""
57
- log(INFO, "Starting Flower ClientApp")
58
-
59
59
  parser = argparse.ArgumentParser(
60
60
  description="Run a Flower ClientApp",
61
61
  )
62
62
  parser.add_argument(
63
63
  "--supernode",
64
64
  type=str,
65
- help="Address of SuperNode ClientAppIo gRPC servicer",
65
+ help="Address of SuperNode's ClientAppIo API",
66
66
  )
67
67
  parser.add_argument(
68
68
  "--token",
@@ -70,16 +70,24 @@ def flwr_clientapp() -> None:
70
70
  required=False,
71
71
  help="Unique token generated by SuperNode for each ClientApp execution",
72
72
  )
73
+ add_args_flwr_app_common(parser=parser)
73
74
  args = parser.parse_args()
74
75
 
76
+ log(INFO, "Starting Flower ClientApp")
77
+
75
78
  log(
76
79
  DEBUG,
77
- "Staring isolated `ClientApp` connected to SuperNode ClientAppIo at %s "
80
+ "Starting isolated `ClientApp` connected to SuperNode's ClientAppIo API at %s "
78
81
  "with token %s",
79
82
  args.supernode,
80
83
  args.token,
81
84
  )
82
- run_clientapp(supernode=args.supernode, token=args.token)
85
+ run_clientapp(
86
+ supernode=args.supernode,
87
+ run_once=(args.token is not None),
88
+ token=args.token,
89
+ flwr_dir=args.flwr_dir,
90
+ )
83
91
 
84
92
 
85
93
  def on_channel_state_change(channel_connectivity: str) -> None:
@@ -89,27 +97,23 @@ def on_channel_state_change(channel_connectivity: str) -> None:
89
97
 
90
98
  def run_clientapp( # pylint: disable=R0914
91
99
  supernode: str,
100
+ run_once: bool,
92
101
  token: Optional[int] = None,
102
+ flwr_dir: Optional[str] = None,
93
103
  ) -> None:
94
- """Run Flower ClientApp process.
95
-
96
- Parameters
97
- ----------
98
- supernode : str
99
- Address of SuperNode
100
- token : Optional[int] (default: None)
101
- Unique SuperNode token for ClientApp-SuperNode authentication
102
- """
104
+ """Run Flower ClientApp process."""
103
105
  channel = create_channel(
104
106
  server_address=supernode,
105
107
  insecure=True,
106
108
  )
107
109
  channel.subscribe(on_channel_state_change)
108
110
 
111
+ # Resolve directory where FABs are installed
112
+ flwr_dir_ = get_flwr_dir(flwr_dir)
113
+
109
114
  try:
110
115
  stub = ClientAppIoStub(channel)
111
116
 
112
- only_once = token is not None
113
117
  while True:
114
118
  # If token is not set, loop until token is received from SuperNode
115
119
  while token is None:
@@ -122,13 +126,13 @@ def run_clientapp( # pylint: disable=R0914
122
126
  # Install FAB, if provided
123
127
  if fab:
124
128
  log(DEBUG, "Flower ClientApp starts FAB installation.")
125
- install_from_fab(fab.content, flwr_dir=None, skip_prompt=True)
129
+ install_from_fab(fab.content, flwr_dir=flwr_dir_, skip_prompt=True)
126
130
 
127
131
  load_client_app_fn = get_load_client_app_fn(
128
132
  default_app_ref="",
129
133
  app_path=None,
130
134
  multi_app=True,
131
- flwr_dir=None,
135
+ flwr_dir=str(flwr_dir_),
132
136
  )
133
137
 
134
138
  try:
@@ -170,7 +174,7 @@ def run_clientapp( # pylint: disable=R0914
170
174
 
171
175
  # Stop the loop if `flwr-clientapp` is expected to process only a single
172
176
  # message
173
- if only_once:
177
+ if run_once:
174
178
  break
175
179
 
176
180
  except KeyboardInterrupt:
@@ -0,0 +1,25 @@
1
+ # Copyright 2024 Flower Labs GmbH. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+ """Flower NodeState."""
16
+
17
+ from .in_memory_nodestate import InMemoryNodeState as InMemoryNodeState
18
+ from .nodestate import NodeState as NodeState
19
+ from .nodestate_factory import NodeStateFactory as NodeStateFactory
20
+
21
+ __all__ = [
22
+ "InMemoryNodeState",
23
+ "NodeState",
24
+ "NodeStateFactory",
25
+ ]
@@ -0,0 +1,38 @@
1
+ # Copyright 2024 Flower Labs GmbH. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+ """In-memory NodeState implementation."""
16
+
17
+
18
+ from typing import Optional
19
+
20
+ from flwr.client.nodestate.nodestate import NodeState
21
+
22
+
23
+ class InMemoryNodeState(NodeState):
24
+ """In-memory NodeState implementation."""
25
+
26
+ def __init__(self) -> None:
27
+ # Store node_id
28
+ self.node_id: Optional[int] = None
29
+
30
+ def set_node_id(self, node_id: Optional[int]) -> None:
31
+ """Set the node ID."""
32
+ self.node_id = node_id
33
+
34
+ def get_node_id(self) -> int:
35
+ """Get the node ID."""
36
+ if self.node_id is None:
37
+ raise ValueError("Node ID not set")
38
+ return self.node_id
@@ -0,0 +1,30 @@
1
+ # Copyright 2024 Flower Labs GmbH. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+ """Abstract base class NodeState."""
16
+
17
+ import abc
18
+ from typing import Optional
19
+
20
+
21
+ class NodeState(abc.ABC):
22
+ """Abstract NodeState."""
23
+
24
+ @abc.abstractmethod
25
+ def set_node_id(self, node_id: Optional[int]) -> None:
26
+ """Set the node ID."""
27
+
28
+ @abc.abstractmethod
29
+ def get_node_id(self) -> int:
30
+ """Get the node ID."""
@@ -0,0 +1,37 @@
1
+ # Copyright 2024 Flower Labs GmbH. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+ """Factory class that creates NodeState instances."""
16
+
17
+ import threading
18
+ from typing import Optional
19
+
20
+ from .in_memory_nodestate import InMemoryNodeState
21
+ from .nodestate import NodeState
22
+
23
+
24
+ class NodeStateFactory:
25
+ """Factory class that creates NodeState instances."""
26
+
27
+ def __init__(self) -> None:
28
+ self.state_instance: Optional[NodeState] = None
29
+ self.lock = threading.RLock()
30
+
31
+ def state(self) -> NodeState:
32
+ """Return a State instance and create it, if necessary."""
33
+ # Lock access to NodeStateFactory to prevent returning different instances
34
+ with self.lock:
35
+ if self.state_instance is None:
36
+ self.state_instance = InMemoryNodeState()
37
+ return self.state_instance
@@ -1,4 +1,4 @@
1
- # Copyright 2023 Flower Labs GmbH. All Rights Reserved.
1
+ # Copyright 2024 Flower Labs GmbH. All Rights Reserved.
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -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
- """Node state."""
15
+ """Deprecated Run Info Store."""
16
16
 
17
17
 
18
18
  from dataclasses import dataclass
@@ -36,7 +36,7 @@ class RunInfo:
36
36
  initial_run_config: UserConfig
37
37
 
38
38
 
39
- class NodeState:
39
+ class DeprecatedRunInfoStore:
40
40
  """State of a node where client nodes execute runs."""
41
41
 
42
42
  def __init__(
@@ -83,6 +83,7 @@ class NodeState:
83
83
  self.run_infos[run_id] = RunInfo(
84
84
  initial_run_config=initial_run_config,
85
85
  context=Context(
86
+ run_id=run_id,
86
87
  node_id=self.node_id,
87
88
  node_config=self.node_config,
88
89
  state=RecordSet(),
@@ -31,6 +31,8 @@ from flwr.common import EventType, event
31
31
  from flwr.common.config import parse_config_args
32
32
  from flwr.common.constant import (
33
33
  FLEET_API_GRPC_RERE_DEFAULT_ADDRESS,
34
+ ISOLATION_MODE_PROCESS,
35
+ ISOLATION_MODE_SUBPROCESS,
34
36
  TRANSPORT_TYPE_GRPC_ADAPTER,
35
37
  TRANSPORT_TYPE_GRPC_RERE,
36
38
  TRANSPORT_TYPE_REST,
@@ -38,11 +40,7 @@ from flwr.common.constant import (
38
40
  from flwr.common.exit_handlers import register_exit_handlers
39
41
  from flwr.common.logger import log, warn_deprecated_feature
40
42
 
41
- from ..app import (
42
- ISOLATION_MODE_PROCESS,
43
- ISOLATION_MODE_SUBPROCESS,
44
- start_client_internal,
45
- )
43
+ from ..app import start_client_internal
46
44
  from ..clientapp.utils import get_load_client_app_fn
47
45
 
48
46
 
@@ -200,10 +198,10 @@ def _parse_args_run_supernode() -> argparse.ArgumentParser:
200
198
  ISOLATION_MODE_SUBPROCESS,
201
199
  ISOLATION_MODE_PROCESS,
202
200
  ],
203
- help="Isolation mode when running `ClientApp` (optional, possible values: "
204
- "`subprocess`, `process`). By default, `ClientApp` runs in the same process "
201
+ help="Isolation mode when running a `ClientApp` (optional, possible values: "
202
+ "`subprocess`, `process`). By default, a `ClientApp` runs in the same process "
205
203
  "that executes the SuperNode. Use `subprocess` to configure SuperNode to run "
206
- "`ClientApp` in a subprocess. Use `process` to indicate that a separate "
204
+ "a `ClientApp` in a subprocess. Use `process` to indicate that a separate "
207
205
  "independent process gets created outside of SuperNode.",
208
206
  )
209
207
  parser.add_argument(
flwr/common/args.py ADDED
@@ -0,0 +1,83 @@
1
+ # Copyright 2024 Flower Labs GmbH. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+ """Common Flower arguments."""
16
+
17
+ import argparse
18
+ import sys
19
+ from logging import DEBUG, WARN
20
+ from os.path import isfile
21
+ from pathlib import Path
22
+ from typing import Optional
23
+
24
+ from flwr.common.logger import log
25
+
26
+
27
+ def add_args_flwr_app_common(parser: argparse.ArgumentParser) -> None:
28
+ """Add common Flower arguments for flwr-*app to the provided parser."""
29
+ parser.add_argument(
30
+ "--flwr-dir",
31
+ default=None,
32
+ help="""The path containing installed Flower Apps.
33
+ By default, this value is equal to:
34
+
35
+ - `$FLWR_HOME/` if `$FLWR_HOME` is defined
36
+ - `$XDG_DATA_HOME/.flwr/` if `$XDG_DATA_HOME` is defined
37
+ - `$HOME/.flwr/` in all other cases
38
+ """,
39
+ )
40
+ parser.add_argument(
41
+ "--insecure",
42
+ action="store_true",
43
+ help="Run the server without HTTPS, regardless of whether certificate "
44
+ "paths are provided. By default, the server runs with HTTPS enabled. "
45
+ "Use this flag only if you understand the risks.",
46
+ )
47
+ parser.add_argument(
48
+ "--root-certificates",
49
+ metavar="ROOT_CERT",
50
+ type=str,
51
+ help="Specifies the path to the PEM-encoded root certificate file for "
52
+ "establishing secure HTTPS connections.",
53
+ )
54
+
55
+
56
+ def try_obtain_certificates(
57
+ args: argparse.Namespace,
58
+ ) -> Optional[bytes]:
59
+ """Validate and return the root certificates."""
60
+ if args.insecure:
61
+ if args.root_certificates is not None:
62
+ sys.exit(
63
+ "Conflicting options: The '--insecure' flag disables HTTPS, "
64
+ "but '--root-certificates' was also specified. Please remove "
65
+ "the '--root-certificates' option when running in insecure mode, "
66
+ "or omit '--insecure' to use HTTPS."
67
+ )
68
+ log(
69
+ WARN,
70
+ "Option `--insecure` was set. Starting insecure HTTP channel.",
71
+ )
72
+ root_certificates = None
73
+ else:
74
+ # Load the certificates if provided, or load the system certificates
75
+ if not isfile(args.root_certificates):
76
+ sys.exit("Path argument `--root-certificates` does not point to a file.")
77
+ root_certificates = Path(args.root_certificates).read_bytes()
78
+ log(
79
+ DEBUG,
80
+ "Starting secure HTTPS channel with the following certificates: %s.",
81
+ args.root_certificates,
82
+ )
83
+ return root_certificates
flwr/common/config.py CHANGED
@@ -22,6 +22,7 @@ from typing import Any, Optional, Union, cast, get_args
22
22
  import tomli
23
23
 
24
24
  from flwr.cli.config_utils import get_fab_config, validate_fields
25
+ from flwr.common import ConfigsRecord
25
26
  from flwr.common.constant import (
26
27
  APP_DIR,
27
28
  FAB_CONFIG_FILE,
@@ -229,3 +230,12 @@ def get_metadata_from_config(config: dict[str, Any]) -> tuple[str, str]:
229
230
  config["project"]["version"],
230
231
  f"{config['tool']['flwr']['app']['publisher']}/{config['project']['name']}",
231
232
  )
233
+
234
+
235
+ def user_config_to_configsrecord(config: UserConfig) -> ConfigsRecord:
236
+ """Construct a `ConfigsRecord` out of a `UserConfig`."""
237
+ c_record = ConfigsRecord()
238
+ for k, v in config.items():
239
+ c_record[k] = v
240
+
241
+ return c_record
flwr/common/constant.py CHANGED
@@ -40,15 +40,15 @@ TRANSPORT_TYPES = [
40
40
  # Addresses
41
41
  # SuperNode
42
42
  CLIENTAPPIO_API_DEFAULT_ADDRESS = "0.0.0.0:9094"
43
- # SuperExec
44
- EXEC_API_DEFAULT_ADDRESS = "0.0.0.0:9093"
45
43
  # SuperLink
46
- DRIVER_API_DEFAULT_ADDRESS = "0.0.0.0:9091"
44
+ SERVERAPPIO_API_DEFAULT_ADDRESS = "0.0.0.0:9091"
47
45
  FLEET_API_GRPC_RERE_DEFAULT_ADDRESS = "0.0.0.0:9092"
48
46
  FLEET_API_GRPC_BIDI_DEFAULT_ADDRESS = (
49
47
  "[::]:8080" # IPv6 to keep start_server compatible
50
48
  )
51
- FLEET_API_REST_DEFAULT_ADDRESS = "0.0.0.0:9093"
49
+ FLEET_API_REST_DEFAULT_ADDRESS = "0.0.0.0:9095"
50
+ EXEC_API_DEFAULT_ADDRESS = "0.0.0.0:9093"
51
+ SIMULATIONIO_API_DEFAULT_ADDRESS = "0.0.0.0:9096"
52
52
 
53
53
  # Constants for ping
54
54
  PING_DEFAULT_INTERVAL = 30
@@ -84,6 +84,16 @@ GRPC_ADAPTER_METADATA_MESSAGE_QUALNAME_KEY = "grpc-message-qualname"
84
84
  # Message TTL
85
85
  MESSAGE_TTL_TOLERANCE = 1e-1
86
86
 
87
+ # Isolation modes
88
+ ISOLATION_MODE_SUBPROCESS = "subprocess"
89
+ ISOLATION_MODE_PROCESS = "process"
90
+
91
+ # Log streaming configurations
92
+ CONN_REFRESH_PERIOD = 60 # Stream connection refresh period
93
+ CONN_RECONNECT_INTERVAL = 0.5 # Reconnect interval between two stream connections
94
+ LOG_STREAM_INTERVAL = 0.5 # Log stream interval for `ExecServicer.StreamLogs`
95
+ LOG_UPLOAD_INTERVAL = 0.2 # Minimum interval between two log uploads
96
+
87
97
 
88
98
  class MessageType:
89
99
  """Message type."""
@@ -124,8 +134,32 @@ class ErrorCode:
124
134
  UNKNOWN = 0
125
135
  LOAD_CLIENT_APP_EXCEPTION = 1
126
136
  CLIENT_APP_RAISED_EXCEPTION = 2
127
- NODE_UNAVAILABLE = 3
128
137
 
129
138
  def __new__(cls) -> ErrorCode:
130
139
  """Prevent instantiation."""
131
140
  raise TypeError(f"{cls.__name__} cannot be instantiated.")
141
+
142
+
143
+ class Status:
144
+ """Run status."""
145
+
146
+ PENDING = "pending"
147
+ STARTING = "starting"
148
+ RUNNING = "running"
149
+ FINISHED = "finished"
150
+
151
+ def __new__(cls) -> Status:
152
+ """Prevent instantiation."""
153
+ raise TypeError(f"{cls.__name__} cannot be instantiated.")
154
+
155
+
156
+ class SubStatus:
157
+ """Run sub-status."""
158
+
159
+ COMPLETED = "completed"
160
+ FAILED = "failed"
161
+ STOPPED = "stopped"
162
+
163
+ def __new__(cls) -> SubStatus:
164
+ """Prevent instantiation."""
165
+ raise TypeError(f"{cls.__name__} cannot be instantiated.")
flwr/common/context.py CHANGED
@@ -27,36 +27,41 @@ class Context:
27
27
 
28
28
  Parameters
29
29
  ----------
30
+ run_id : int
31
+ The ID that identifies the run.
30
32
  node_id : int
31
33
  The ID that identifies the node.
32
34
  node_config : UserConfig
33
35
  A config (key/value mapping) unique to the node and independent of the
34
36
  `run_config`. This config persists across all runs this node participates in.
35
37
  state : RecordSet
36
- Holds records added by the entity in a given run and that will stay local.
38
+ Holds records added by the entity in a given `run_id` and that will stay local.
37
39
  This means that the data it holds will never leave the system it's running from.
38
40
  This can be used as an intermediate storage or scratchpad when
39
41
  executing mods. It can also be used as a memory to access
40
42
  at different points during the lifecycle of this entity (e.g. across
41
43
  multiple rounds)
42
44
  run_config : UserConfig
43
- A config (key/value mapping) held by the entity in a given run and that will
44
- stay local. It can be used at any point during the lifecycle of this entity
45
+ A config (key/value mapping) held by the entity in a given `run_id` and that
46
+ will stay local. It can be used at any point during the lifecycle of this entity
45
47
  (e.g. across multiple rounds)
46
48
  """
47
49
 
50
+ run_id: int
48
51
  node_id: int
49
52
  node_config: UserConfig
50
53
  state: RecordSet
51
54
  run_config: UserConfig
52
55
 
53
- def __init__( # pylint: disable=too-many-arguments
56
+ def __init__( # pylint: disable=too-many-arguments, too-many-positional-arguments
54
57
  self,
58
+ run_id: int,
55
59
  node_id: int,
56
60
  node_config: UserConfig,
57
61
  state: RecordSet,
58
62
  run_config: UserConfig,
59
63
  ) -> None:
64
+ self.run_id = run_id
60
65
  self.node_id = node_id
61
66
  self.node_config = node_config
62
67
  self.state = state
flwr/common/date.py CHANGED
@@ -15,9 +15,9 @@
15
15
  """Flower date utils."""
16
16
 
17
17
 
18
- from datetime import datetime, timezone
18
+ import datetime
19
19
 
20
20
 
21
- def now() -> datetime:
21
+ def now() -> datetime.datetime:
22
22
  """Construct a datetime from time.time() with time zone set to UTC."""
23
- return datetime.now(tz=timezone.utc)
23
+ return datetime.datetime.now(tz=datetime.timezone.utc)
flwr/common/logger.py CHANGED
@@ -16,9 +16,22 @@
16
16
 
17
17
 
18
18
  import logging
19
+ import sys
20
+ import threading
21
+ import time
19
22
  from logging import WARN, LogRecord
20
23
  from logging.handlers import HTTPHandler
21
- from typing import TYPE_CHECKING, Any, Optional, TextIO
24
+ from queue import Empty, Queue
25
+ from typing import TYPE_CHECKING, Any, Optional, TextIO, Union
26
+
27
+ import grpc
28
+
29
+ from flwr.proto.log_pb2 import PushLogsRequest # pylint: disable=E0611
30
+ from flwr.proto.node_pb2 import Node # pylint: disable=E0611
31
+ from flwr.proto.serverappio_pb2_grpc import ServerAppIoStub # pylint: disable=E0611
32
+ from flwr.proto.simulationio_pb2_grpc import SimulationIoStub # pylint: disable=E0611
33
+
34
+ from .constant import LOG_UPLOAD_INTERVAL
22
35
 
23
36
  # Create logger
24
37
  LOGGER_NAME = "flwr"
@@ -259,3 +272,97 @@ def set_logger_propagation(
259
272
  if not child_logger.propagate:
260
273
  child_logger.log(logging.DEBUG, "Logger propagate set to False")
261
274
  return child_logger
275
+
276
+
277
+ def mirror_output_to_queue(log_queue: Queue[Optional[str]]) -> None:
278
+ """Mirror stdout and stderr output to the provided queue."""
279
+
280
+ def get_write_fn(stream: TextIO) -> Any:
281
+ original_write = stream.write
282
+
283
+ def fn(s: str) -> int:
284
+ ret = original_write(s)
285
+ stream.flush()
286
+ log_queue.put(s)
287
+ return ret
288
+
289
+ return fn
290
+
291
+ sys.stdout.write = get_write_fn(sys.stdout) # type: ignore[method-assign]
292
+ sys.stderr.write = get_write_fn(sys.stderr) # type: ignore[method-assign]
293
+ console_handler.stream = sys.stdout
294
+
295
+
296
+ def restore_output() -> None:
297
+ """Restore stdout and stderr.
298
+
299
+ This will stop mirroring output to queues.
300
+ """
301
+ sys.stdout = sys.__stdout__
302
+ sys.stderr = sys.__stderr__
303
+ console_handler.stream = sys.stdout
304
+
305
+
306
+ def _log_uploader(
307
+ log_queue: Queue[Optional[str]], node_id: int, run_id: int, stub: ServerAppIoStub
308
+ ) -> None:
309
+ """Upload logs to the SuperLink."""
310
+ exit_flag = False
311
+ node = Node(node_id=node_id, anonymous=False)
312
+ msgs: list[str] = []
313
+ while True:
314
+ # Fetch all messages from the queue
315
+ try:
316
+ while True:
317
+ msg = log_queue.get_nowait()
318
+ # Quit the loops if the returned message is `None`
319
+ # This is a signal that the run has finished
320
+ if msg is None:
321
+ exit_flag = True
322
+ break
323
+ msgs.append(msg)
324
+ except Empty:
325
+ pass
326
+
327
+ # Upload if any logs
328
+ if msgs:
329
+ req = PushLogsRequest(
330
+ node=node,
331
+ run_id=run_id,
332
+ logs=msgs,
333
+ )
334
+ try:
335
+ stub.PushLogs(req)
336
+ msgs.clear()
337
+ except grpc.RpcError as e:
338
+ # Ignore minor network errors
339
+ # pylint: disable-next=no-member
340
+ if e.code() != grpc.StatusCode.UNAVAILABLE:
341
+ raise e
342
+
343
+ if exit_flag:
344
+ break
345
+
346
+ time.sleep(LOG_UPLOAD_INTERVAL)
347
+
348
+
349
+ def start_log_uploader(
350
+ log_queue: Queue[Optional[str]],
351
+ node_id: int,
352
+ run_id: int,
353
+ stub: Union[ServerAppIoStub, SimulationIoStub],
354
+ ) -> threading.Thread:
355
+ """Start the log uploader thread and return it."""
356
+ thread = threading.Thread(
357
+ target=_log_uploader, args=(log_queue, node_id, run_id, stub)
358
+ )
359
+ thread.start()
360
+ return thread
361
+
362
+
363
+ def stop_log_uploader(
364
+ log_queue: Queue[Optional[str]], log_uploader: threading.Thread
365
+ ) -> None:
366
+ """Stop the log uploader thread."""
367
+ log_queue.put(None)
368
+ log_uploader.join()