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

@@ -15,33 +15,15 @@
15
15
  """Run ServerApp."""
16
16
 
17
17
 
18
- import argparse
19
18
  import sys
20
- from logging import DEBUG, INFO, WARN
21
- from pathlib import Path
19
+ from logging import DEBUG, ERROR
22
20
  from typing import Optional
23
21
 
24
- from flwr.cli.config_utils import get_fab_metadata
25
- from flwr.cli.install import install_from_fab
26
- from flwr.common import Context, EventType, RecordSet, event
27
- from flwr.common.config import (
28
- get_flwr_dir,
29
- get_fused_config_from_dir,
30
- get_metadata_from_config,
31
- get_project_config,
32
- get_project_dir,
33
- )
34
- from flwr.common.constant import SERVERAPPIO_API_DEFAULT_ADDRESS
35
- from flwr.common.logger import log, update_console_handler, warn_deprecated_feature
22
+ from flwr.common import Context
23
+ from flwr.common.logger import log, warn_unsupported_feature
36
24
  from flwr.common.object_ref import load_app
37
- from flwr.proto.fab_pb2 import GetFabRequest, GetFabResponse # pylint: disable=E0611
38
- from flwr.proto.run_pb2 import ( # pylint: disable=E0611
39
- CreateRunRequest,
40
- CreateRunResponse,
41
- )
42
25
 
43
26
  from .driver import Driver
44
- from .driver.grpc_driver import GrpcDriver
45
27
  from .server_app import LoadServerAppError, ServerApp
46
28
 
47
29
 
@@ -87,221 +69,9 @@ def run(
87
69
  # pylint: disable-next=too-many-branches,too-many-statements,too-many-locals
88
70
  def run_server_app() -> None:
89
71
  """Run Flower server app."""
90
- event(EventType.RUN_SERVER_APP_ENTER)
91
-
92
- args = _parse_args_run_server_app().parse_args()
93
-
94
- # Check if the server app reference is passed.
95
- # Since Flower 1.11, passing a reference is not allowed.
96
- app_path: Optional[str] = args.app
97
- # If the provided app_path doesn't exist, and contains a ":",
98
- # it is likely to be a server app reference instead of a path.
99
- if app_path is not None and not Path(app_path).exists() and ":" in app_path:
100
- sys.exit(
101
- "It appears you've passed a reference like `server:app`.\n\n"
102
- "Note that since version `1.11.0`, `flower-server-app` no longer supports "
103
- "passing a reference to a `ServerApp` attribute. Instead, you need to pass "
104
- "the path to Flower app via the argument `--app`. This is the path to a "
105
- "directory containing a `pyproject.toml`. You can create a valid Flower "
106
- "app by executing `flwr new` and following the prompt."
107
- )
108
-
109
- if args.server != SERVERAPPIO_API_DEFAULT_ADDRESS:
110
- warn = "Passing flag --server is deprecated. Use --superlink instead."
111
- warn_deprecated_feature(warn)
112
-
113
- if args.superlink != SERVERAPPIO_API_DEFAULT_ADDRESS:
114
- # if `--superlink` also passed, then
115
- # warn user that this argument overrides what was passed with `--server`
116
- log(
117
- WARN,
118
- "Both `--server` and `--superlink` were passed. "
119
- "`--server` will be ignored. Connecting to the "
120
- "SuperLink ServerAppIo API at %s.",
121
- args.superlink,
122
- )
123
- else:
124
- args.superlink = args.server
125
-
126
- update_console_handler(
127
- level=DEBUG if args.verbose else INFO,
128
- timestamps=args.verbose,
129
- colored=True,
130
- )
131
-
132
- # Obtain certificates
133
- if args.insecure:
134
- if args.root_certificates is not None:
135
- sys.exit(
136
- "Conflicting options: The '--insecure' flag disables HTTPS, "
137
- "but '--root-certificates' was also specified. Please remove "
138
- "the '--root-certificates' option when running in insecure mode, "
139
- "or omit '--insecure' to use HTTPS."
140
- )
141
- log(
142
- WARN,
143
- "Option `--insecure` was set. "
144
- "Starting insecure HTTP client connected to %s.",
145
- args.superlink,
146
- )
147
- root_certificates = None
148
- else:
149
- # Load the certificates if provided, or load the system certificates
150
- cert_path = args.root_certificates
151
- if cert_path is None:
152
- root_certificates = None
153
- else:
154
- root_certificates = Path(cert_path).read_bytes()
155
- log(
156
- DEBUG,
157
- "Starting secure HTTPS client connected to %s "
158
- "with the following certificates: %s.",
159
- args.superlink,
160
- cert_path,
161
- )
162
-
163
- if not (app_path is None) ^ (args.run_id is None):
164
- raise sys.exit(
165
- "Please provide either a Flower App path or a Run ID, but not both. "
166
- "For more details, use: ``flower-server-app -h``"
167
- )
168
-
169
- # Initialize GrpcDriver
170
- if app_path is None:
171
- # User provided `--run-id`, but not `app_dir`
172
- driver = GrpcDriver(
173
- serverappio_service_address=args.superlink,
174
- root_certificates=root_certificates,
175
- )
176
- flwr_dir = get_flwr_dir(args.flwr_dir)
177
- driver.set_run(args.run_id)
178
- run_ = driver.run
179
- if not run_.fab_hash:
180
- raise ValueError("FAB hash not provided.")
181
- fab_req = GetFabRequest(hash_str=run_.fab_hash)
182
- # pylint: disable-next=W0212
183
- fab_res: GetFabResponse = driver._stub.GetFab(fab_req)
184
- if fab_res.fab.hash_str != run_.fab_hash:
185
- raise ValueError("FAB hashes don't match.")
186
- install_from_fab(fab_res.fab.content, flwr_dir, True)
187
- fab_id, fab_version = get_fab_metadata(fab_res.fab.content)
188
-
189
- app_path = str(get_project_dir(fab_id, fab_version, run_.fab_hash, flwr_dir))
190
- config = get_project_config(app_path)
191
- run_id = run_.run_id
192
- else:
193
- # User provided `app_dir`, but not `--run-id`
194
- # Create run if run_id is not provided
195
- driver = GrpcDriver(
196
- serverappio_service_address=args.superlink,
197
- root_certificates=root_certificates,
198
- )
199
- # Load config from the project directory
200
- config = get_project_config(app_path)
201
- fab_version, fab_id = get_metadata_from_config(config)
202
-
203
- # Create run
204
- req = CreateRunRequest(fab_id=fab_id, fab_version=fab_version)
205
- res: CreateRunResponse = driver._stub.CreateRun(req) # pylint: disable=W0212
206
- # Fetch full `Run` using `run_id`
207
- driver.set_run(res.run_id) # pylint: disable=W0212
208
- run_id = res.run_id
209
-
210
- # Obtain server app reference and the run config
211
- server_app_attr = config["tool"]["flwr"]["app"]["components"]["serverapp"]
212
- server_app_run_config = get_fused_config_from_dir(
213
- Path(app_path), driver.run.override_config
72
+ warn_unsupported_feature(
73
+ "The command `flower-server-app` is deprecated and no longer in use. "
74
+ "Use the `flwr-serverapp` exclusively instead."
214
75
  )
215
-
216
- log(DEBUG, "Flower will load ServerApp `%s` in %s", server_app_attr, app_path)
217
-
218
- log(
219
- DEBUG,
220
- "root_certificates: `%s`",
221
- root_certificates,
222
- )
223
-
224
- # Initialize Context
225
- context = Context(
226
- run_id=run_id,
227
- node_id=0,
228
- node_config={},
229
- state=RecordSet(),
230
- run_config=server_app_run_config,
231
- )
232
-
233
- # Run the ServerApp with the Driver
234
- run(
235
- driver=driver,
236
- context=context,
237
- server_app_dir=app_path,
238
- server_app_attr=server_app_attr,
239
- )
240
-
241
- # Clean up
242
- driver.close()
243
-
244
- event(EventType.RUN_SERVER_APP_LEAVE)
245
-
246
-
247
- def _parse_args_run_server_app() -> argparse.ArgumentParser:
248
- """Parse flower-server-app command line arguments."""
249
- parser = argparse.ArgumentParser(
250
- description="Start a Flower server app",
251
- )
252
-
253
- parser.add_argument(
254
- "app",
255
- nargs="?",
256
- default=None,
257
- help="Load and run the `ServerApp` from the specified Flower App path. "
258
- "The `pyproject.toml` file must be located in the root of this path.",
259
- )
260
- parser.add_argument(
261
- "--insecure",
262
- action="store_true",
263
- help="Run the `ServerApp` without HTTPS. By default, the app runs with "
264
- "HTTPS enabled. Use this flag only if you understand the risks.",
265
- )
266
- parser.add_argument(
267
- "--verbose",
268
- action="store_true",
269
- help="Set the logging to `DEBUG`.",
270
- )
271
- parser.add_argument(
272
- "--root-certificates",
273
- metavar="ROOT_CERT",
274
- type=str,
275
- help="Specifies the path to the PEM-encoded root certificate file for "
276
- "establishing secure HTTPS connections.",
277
- )
278
- parser.add_argument(
279
- "--server",
280
- default=SERVERAPPIO_API_DEFAULT_ADDRESS,
281
- help="Server address",
282
- )
283
- parser.add_argument(
284
- "--superlink",
285
- default=SERVERAPPIO_API_DEFAULT_ADDRESS,
286
- help="SuperLink ServerAppIo API (gRPC-rere) address "
287
- "(IPv4, IPv6, or a domain name)",
288
- )
289
- parser.add_argument(
290
- "--run-id",
291
- default=None,
292
- type=int,
293
- help="The identifier of the run.",
294
- )
295
- parser.add_argument(
296
- "--flwr-dir",
297
- default=None,
298
- help="""The path containing installed Flower Apps.
299
- By default, this value is equal to:
300
-
301
- - `$FLWR_HOME/` if `$FLWR_HOME` is defined
302
- - `$XDG_DATA_HOME/.flwr/` if `$XDG_DATA_HOME` is defined
303
- - `$HOME/.flwr/` in all other cases
304
- """,
305
- )
306
-
307
- return parser
76
+ log(ERROR, "`flower-server-app` used.")
77
+ sys.exit()
@@ -30,7 +30,11 @@ from flwr.common.config import (
30
30
  get_project_config,
31
31
  get_project_dir,
32
32
  )
33
- from flwr.common.constant import Status, SubStatus
33
+ from flwr.common.constant import (
34
+ SERVERAPPIO_API_DEFAULT_CLIENT_ADDRESS,
35
+ Status,
36
+ SubStatus,
37
+ )
34
38
  from flwr.common.logger import (
35
39
  log,
36
40
  mirror_output_to_queue,
@@ -62,33 +66,18 @@ def flwr_serverapp() -> None:
62
66
  log_queue: Queue[Optional[str]] = Queue()
63
67
  mirror_output_to_queue(log_queue)
64
68
 
65
- parser = argparse.ArgumentParser(
66
- description="Run a Flower ServerApp",
67
- )
68
- parser.add_argument(
69
- "--superlink",
70
- type=str,
71
- help="Address of SuperLink's ServerAppIo API",
72
- )
73
- parser.add_argument(
74
- "--run-once",
75
- action="store_true",
76
- help="When set, this process will start a single ServerApp for a pending Run. "
77
- "If there is no pending Run, the process will exit.",
78
- )
79
- add_args_flwr_app_common(parser=parser)
80
- args = parser.parse_args()
69
+ args = _parse_args_run_flwr_serverapp().parse_args()
81
70
 
82
71
  log(INFO, "Starting Flower ServerApp")
83
- certificates = try_obtain_root_certificates(args, args.superlink)
72
+ certificates = try_obtain_root_certificates(args, args.serverappio_api_address)
84
73
 
85
74
  log(
86
75
  DEBUG,
87
76
  "Starting isolated `ServerApp` connected to SuperLink's ServerAppIo API at %s",
88
- args.superlink,
77
+ args.serverappio_api_address,
89
78
  )
90
79
  run_serverapp(
91
- superlink=args.superlink,
80
+ serverappio_api_address=args.serverappio_api_address,
92
81
  log_queue=log_queue,
93
82
  run_once=args.run_once,
94
83
  flwr_dir=args.flwr_dir,
@@ -100,7 +89,7 @@ def flwr_serverapp() -> None:
100
89
 
101
90
 
102
91
  def run_serverapp( # pylint: disable=R0914, disable=W0212
103
- superlink: str,
92
+ serverappio_api_address: str,
104
93
  log_queue: Queue[Optional[str]],
105
94
  run_once: bool,
106
95
  flwr_dir: Optional[str] = None,
@@ -108,7 +97,7 @@ def run_serverapp( # pylint: disable=R0914, disable=W0212
108
97
  ) -> None:
109
98
  """Run Flower ServerApp process."""
110
99
  driver = GrpcDriver(
111
- serverappio_service_address=superlink,
100
+ serverappio_service_address=serverappio_api_address,
112
101
  root_certificates=certificates,
113
102
  )
114
103
 
@@ -212,3 +201,25 @@ def run_serverapp( # pylint: disable=R0914, disable=W0212
212
201
  # Stop the loop if `flwr-serverapp` is expected to process a single run
213
202
  if run_once:
214
203
  break
204
+
205
+
206
+ def _parse_args_run_flwr_serverapp() -> argparse.ArgumentParser:
207
+ """Parse flwr-serverapp command line arguments."""
208
+ parser = argparse.ArgumentParser(
209
+ description="Run a Flower ServerApp",
210
+ )
211
+ parser.add_argument(
212
+ "--serverappio-api-address",
213
+ default=SERVERAPPIO_API_DEFAULT_CLIENT_ADDRESS,
214
+ type=str,
215
+ help="Address of SuperLink's ServerAppIo API (IPv4, IPv6, or a domain name)."
216
+ f"By default, it is set to {SERVERAPPIO_API_DEFAULT_CLIENT_ADDRESS}.",
217
+ )
218
+ parser.add_argument(
219
+ "--run-once",
220
+ action="store_true",
221
+ help="When set, this process will start a single ServerApp for a pending Run. "
222
+ "If there is no pending Run, the process will exit.",
223
+ )
224
+ add_args_flwr_app_common(parser=parser)
225
+ return parser
@@ -40,6 +40,8 @@ from .utils import (
40
40
  generate_rand_int_from_bytes,
41
41
  has_valid_sub_status,
42
42
  is_valid_transition,
43
+ verify_found_taskres,
44
+ verify_taskins_ids,
43
45
  )
44
46
 
45
47
 
@@ -67,12 +69,13 @@ class InMemoryLinkState(LinkState): # pylint: disable=R0902,R0904
67
69
  self.federation_options: dict[int, ConfigsRecord] = {}
68
70
  self.task_ins_store: dict[UUID, TaskIns] = {}
69
71
  self.task_res_store: dict[UUID, TaskRes] = {}
72
+ self.task_ins_id_to_task_res_id: dict[UUID, UUID] = {}
70
73
 
71
74
  self.node_public_keys: set[bytes] = set()
72
75
  self.server_public_key: Optional[bytes] = None
73
76
  self.server_private_key: Optional[bytes] = None
74
77
 
75
- self.lock = threading.Lock()
78
+ self.lock = threading.RLock()
76
79
 
77
80
  def store_task_ins(self, task_ins: TaskIns) -> Optional[UUID]:
78
81
  """Store one TaskIns."""
@@ -222,42 +225,50 @@ class InMemoryLinkState(LinkState): # pylint: disable=R0902,R0904
222
225
  task_res.task_id = str(task_id)
223
226
  with self.lock:
224
227
  self.task_res_store[task_id] = task_res
228
+ self.task_ins_id_to_task_res_id[UUID(task_ins_id)] = task_id
225
229
 
226
230
  # Return the new task_id
227
231
  return task_id
228
232
 
229
233
  def get_task_res(self, task_ids: set[UUID]) -> list[TaskRes]:
230
- """Get all TaskRes that have not been delivered yet."""
234
+ """Get TaskRes for the given TaskIns IDs."""
235
+ ret: dict[UUID, TaskRes] = {}
236
+
231
237
  with self.lock:
232
- # Find TaskRes that were not delivered yet
233
- task_res_list: list[TaskRes] = []
234
- replied_task_ids: set[UUID] = set()
235
- for _, task_res in self.task_res_store.items():
236
- reply_to = UUID(task_res.task.ancestry[0])
237
-
238
- # Check if corresponding TaskIns exists and is not expired
239
- task_ins = self.task_ins_store.get(reply_to)
240
- if task_ins is None:
241
- log(WARNING, "TaskIns with task_id %s does not exist.", reply_to)
242
- task_ids.remove(reply_to)
243
- continue
244
-
245
- if task_ins.task.created_at + task_ins.task.ttl <= time.time():
246
- log(WARNING, "TaskIns with task_id %s is expired.", reply_to)
247
- task_ids.remove(reply_to)
248
- continue
249
-
250
- if reply_to in task_ids and task_res.task.delivered_at == "":
251
- task_res_list.append(task_res)
252
- replied_task_ids.add(reply_to)
253
-
254
- # Mark all of them as delivered
238
+ current = time.time()
239
+
240
+ # Verify TaskIns IDs
241
+ ret = verify_taskins_ids(
242
+ inquired_taskins_ids=task_ids,
243
+ found_taskins_dict=self.task_ins_store,
244
+ current_time=current,
245
+ )
246
+
247
+ # Find all TaskRes
248
+ task_res_found: list[TaskRes] = []
249
+ for task_id in task_ids:
250
+ # If TaskRes exists and is not delivered, add it to the list
251
+ if task_res_id := self.task_ins_id_to_task_res_id.get(task_id):
252
+ task_res = self.task_res_store[task_res_id]
253
+ if task_res.task.delivered_at == "":
254
+ task_res_found.append(task_res)
255
+ tmp_ret_dict = verify_found_taskres(
256
+ inquired_taskins_ids=task_ids,
257
+ found_taskins_dict=self.task_ins_store,
258
+ found_taskres_list=task_res_found,
259
+ current_time=current,
260
+ )
261
+ ret.update(tmp_ret_dict)
262
+
263
+ # Mark existing TaskRes to be returned as delivered
255
264
  delivered_at = now().isoformat()
256
- for task_res in task_res_list:
265
+ for task_res in task_res_found:
257
266
  task_res.task.delivered_at = delivered_at
258
267
 
259
- # Return TaskRes
260
- return task_res_list
268
+ # Cleanup
269
+ self._force_delete_tasks_by_ids(set(ret.keys()))
270
+
271
+ return list(ret.values())
261
272
 
262
273
  def delete_tasks(self, task_ids: set[UUID]) -> None:
263
274
  """Delete all delivered TaskIns/TaskRes pairs."""
@@ -278,9 +289,25 @@ class InMemoryLinkState(LinkState): # pylint: disable=R0902,R0904
278
289
 
279
290
  for task_id in task_ins_to_be_deleted:
280
291
  del self.task_ins_store[task_id]
292
+ del self.task_ins_id_to_task_res_id[task_id]
281
293
  for task_id in task_res_to_be_deleted:
282
294
  del self.task_res_store[task_id]
283
295
 
296
+ def _force_delete_tasks_by_ids(self, task_ids: set[UUID]) -> None:
297
+ """Delete tasks based on a set of TaskIns IDs."""
298
+ if not task_ids:
299
+ return
300
+
301
+ with self.lock:
302
+ for task_id in task_ids:
303
+ # Delete TaskIns
304
+ if task_id in self.task_ins_store:
305
+ del self.task_ins_store[task_id]
306
+ # Delete TaskRes
307
+ if task_id in self.task_ins_id_to_task_res_id:
308
+ task_res_id = self.task_ins_id_to_task_res_id.pop(task_id)
309
+ del self.task_res_store[task_res_id]
310
+
284
311
  def num_task_ins(self) -> int:
285
312
  """Calculate the number of task_ins in store.
286
313
 
@@ -101,13 +101,27 @@ class LinkState(abc.ABC): # pylint: disable=R0904
101
101
 
102
102
  @abc.abstractmethod
103
103
  def get_task_res(self, task_ids: set[UUID]) -> list[TaskRes]:
104
- """Get TaskRes for task_ids.
104
+ """Get TaskRes for the given TaskIns IDs.
105
105
 
106
- Usually, the ServerAppIo API calls this method to get results for instructions
107
- it has previously scheduled.
106
+ This method is typically called by the ServerAppIo API to obtain
107
+ results (TaskRes) for previously scheduled instructions (TaskIns).
108
+ For each task_id provided, this method returns one of the following responses:
108
109
 
109
- Retrieves all TaskRes for the given `task_ids` and returns and empty list of
110
- none could be found.
110
+ - An error TaskRes if the corresponding TaskIns does not exist or has expired.
111
+ - An error TaskRes if the corresponding TaskRes exists but has expired.
112
+ - The valid TaskRes if the TaskIns has a corresponding valid TaskRes.
113
+ - Nothing if the TaskIns is still valid and waiting for a TaskRes.
114
+
115
+ Parameters
116
+ ----------
117
+ task_ids : set[UUID]
118
+ A set of TaskIns IDs for which to retrieve results (TaskRes).
119
+
120
+ Returns
121
+ -------
122
+ list[TaskRes]
123
+ A list of TaskRes corresponding to the given task IDs. If no
124
+ TaskRes could be found for any of the task IDs, an empty list is returned.
111
125
  """
112
126
 
113
127
  @abc.abstractmethod