flwr-nightly 1.8.0.dev20240314__py3-none-any.whl → 1.15.0.dev20250114__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- flwr/cli/app.py +16 -2
- flwr/cli/build.py +181 -0
- flwr/cli/cli_user_auth_interceptor.py +90 -0
- flwr/cli/config_utils.py +343 -0
- flwr/cli/example.py +4 -1
- flwr/cli/install.py +253 -0
- flwr/cli/log.py +182 -0
- flwr/{server/superlink/state → cli/login}/__init__.py +4 -10
- flwr/cli/login/login.py +88 -0
- flwr/cli/ls.py +327 -0
- flwr/cli/new/__init__.py +1 -0
- flwr/cli/new/new.py +210 -66
- flwr/cli/new/templates/app/.gitignore.tpl +163 -0
- flwr/cli/new/templates/app/LICENSE.tpl +202 -0
- flwr/cli/new/templates/app/README.baseline.md.tpl +127 -0
- flwr/cli/new/templates/app/README.flowertune.md.tpl +66 -0
- flwr/cli/new/templates/app/README.md.tpl +16 -32
- flwr/cli/new/templates/app/code/__init__.baseline.py.tpl +1 -0
- flwr/cli/new/templates/app/code/__init__.py.tpl +1 -1
- flwr/cli/new/templates/app/code/client.baseline.py.tpl +58 -0
- flwr/cli/new/templates/app/code/client.huggingface.py.tpl +55 -0
- flwr/cli/new/templates/app/code/client.jax.py.tpl +50 -0
- flwr/cli/new/templates/app/code/client.mlx.py.tpl +73 -0
- flwr/cli/new/templates/app/code/client.numpy.py.tpl +7 -7
- flwr/cli/new/templates/app/code/client.pytorch.py.tpl +30 -21
- flwr/cli/new/templates/app/code/client.sklearn.py.tpl +63 -0
- flwr/cli/new/templates/app/code/client.tensorflow.py.tpl +57 -1
- flwr/cli/new/templates/app/code/dataset.baseline.py.tpl +36 -0
- flwr/cli/new/templates/app/code/flwr_tune/__init__.py +15 -0
- flwr/cli/new/templates/app/code/flwr_tune/client_app.py.tpl +126 -0
- flwr/cli/new/templates/app/code/flwr_tune/dataset.py.tpl +87 -0
- flwr/cli/new/templates/app/code/flwr_tune/models.py.tpl +78 -0
- flwr/cli/new/templates/app/code/flwr_tune/server_app.py.tpl +94 -0
- flwr/cli/new/templates/app/code/flwr_tune/strategy.py.tpl +83 -0
- flwr/cli/new/templates/app/code/model.baseline.py.tpl +80 -0
- flwr/cli/new/templates/app/code/server.baseline.py.tpl +46 -0
- flwr/cli/new/templates/app/code/server.huggingface.py.tpl +38 -0
- flwr/cli/new/templates/app/code/server.jax.py.tpl +26 -0
- flwr/cli/new/templates/app/code/server.mlx.py.tpl +31 -0
- flwr/cli/new/templates/app/code/server.numpy.py.tpl +22 -9
- flwr/cli/new/templates/app/code/server.pytorch.py.tpl +21 -18
- flwr/cli/new/templates/app/code/server.sklearn.py.tpl +36 -0
- flwr/cli/new/templates/app/code/server.tensorflow.py.tpl +29 -1
- flwr/cli/new/templates/app/code/strategy.baseline.py.tpl +1 -0
- flwr/cli/new/templates/app/code/task.huggingface.py.tpl +102 -0
- flwr/cli/new/templates/app/code/task.jax.py.tpl +57 -0
- flwr/cli/new/templates/app/code/task.mlx.py.tpl +102 -0
- flwr/cli/new/templates/app/code/task.numpy.py.tpl +7 -0
- flwr/cli/new/templates/app/code/task.pytorch.py.tpl +29 -24
- flwr/cli/new/templates/app/code/task.sklearn.py.tpl +67 -0
- flwr/cli/new/templates/app/code/task.tensorflow.py.tpl +53 -0
- flwr/cli/new/templates/app/code/utils.baseline.py.tpl +1 -0
- flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +138 -0
- flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +68 -0
- flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +46 -0
- flwr/cli/new/templates/app/pyproject.jax.toml.tpl +35 -0
- flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +39 -0
- flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +25 -12
- flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +29 -14
- flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +35 -0
- flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +29 -14
- flwr/cli/run/__init__.py +1 -0
- flwr/cli/run/run.py +212 -34
- flwr/cli/stop.py +130 -0
- flwr/cli/utils.py +240 -5
- flwr/client/__init__.py +3 -2
- flwr/client/app.py +432 -255
- flwr/client/client.py +1 -11
- flwr/client/client_app.py +74 -13
- flwr/client/clientapp/__init__.py +22 -0
- flwr/client/clientapp/app.py +259 -0
- flwr/client/clientapp/clientappio_servicer.py +244 -0
- flwr/client/clientapp/utils.py +115 -0
- flwr/client/dpfedavg_numpy_client.py +7 -8
- flwr/client/grpc_adapter_client/__init__.py +15 -0
- flwr/client/grpc_adapter_client/connection.py +98 -0
- flwr/client/grpc_client/connection.py +21 -7
- flwr/client/grpc_rere_client/__init__.py +1 -1
- flwr/client/grpc_rere_client/client_interceptor.py +176 -0
- flwr/client/grpc_rere_client/connection.py +163 -56
- flwr/client/grpc_rere_client/grpc_adapter.py +167 -0
- flwr/client/heartbeat.py +74 -0
- flwr/client/message_handler/__init__.py +1 -1
- flwr/client/message_handler/message_handler.py +10 -11
- flwr/client/mod/__init__.py +5 -5
- flwr/client/mod/centraldp_mods.py +4 -2
- flwr/client/mod/comms_mods.py +5 -4
- flwr/client/mod/localdp_mod.py +10 -5
- flwr/client/mod/secure_aggregation/__init__.py +1 -1
- flwr/client/mod/secure_aggregation/secaggplus_mod.py +26 -26
- flwr/client/mod/utils.py +2 -4
- flwr/client/nodestate/__init__.py +26 -0
- flwr/client/nodestate/in_memory_nodestate.py +38 -0
- flwr/client/nodestate/nodestate.py +31 -0
- flwr/client/nodestate/nodestate_factory.py +38 -0
- flwr/client/numpy_client.py +8 -31
- flwr/client/rest_client/__init__.py +1 -1
- flwr/client/rest_client/connection.py +199 -176
- flwr/client/run_info_store.py +112 -0
- flwr/client/supernode/__init__.py +24 -0
- flwr/client/supernode/app.py +321 -0
- flwr/client/typing.py +1 -0
- flwr/common/__init__.py +17 -11
- flwr/common/address.py +47 -3
- flwr/common/args.py +153 -0
- flwr/common/auth_plugin/__init__.py +24 -0
- flwr/common/auth_plugin/auth_plugin.py +121 -0
- flwr/common/config.py +243 -0
- flwr/common/constant.py +132 -1
- flwr/common/context.py +32 -2
- flwr/common/date.py +22 -4
- flwr/common/differential_privacy.py +2 -2
- flwr/common/dp.py +2 -4
- flwr/common/exit_handlers.py +3 -3
- flwr/common/grpc.py +164 -5
- flwr/common/logger.py +230 -12
- flwr/common/message.py +191 -106
- flwr/common/object_ref.py +179 -44
- flwr/common/pyproject.py +1 -0
- flwr/common/record/__init__.py +2 -1
- flwr/common/record/configsrecord.py +58 -18
- flwr/common/record/metricsrecord.py +57 -17
- flwr/common/record/parametersrecord.py +88 -20
- flwr/common/record/recordset.py +153 -30
- flwr/common/record/typeddict.py +30 -55
- flwr/common/recordset_compat.py +31 -12
- flwr/common/retry_invoker.py +123 -30
- flwr/common/secure_aggregation/__init__.py +1 -1
- flwr/common/secure_aggregation/crypto/__init__.py +1 -1
- flwr/common/secure_aggregation/crypto/shamir.py +11 -11
- flwr/common/secure_aggregation/crypto/symmetric_encryption.py +68 -4
- flwr/common/secure_aggregation/ndarrays_arithmetic.py +17 -17
- flwr/common/secure_aggregation/quantization.py +8 -8
- flwr/common/secure_aggregation/secaggplus_constants.py +1 -1
- flwr/common/secure_aggregation/secaggplus_utils.py +10 -12
- flwr/common/serde.py +298 -19
- flwr/common/telemetry.py +65 -29
- flwr/common/typing.py +120 -19
- flwr/common/version.py +17 -3
- flwr/proto/clientappio_pb2.py +45 -0
- flwr/proto/clientappio_pb2.pyi +132 -0
- flwr/proto/clientappio_pb2_grpc.py +135 -0
- flwr/proto/clientappio_pb2_grpc.pyi +53 -0
- flwr/proto/exec_pb2.py +62 -0
- flwr/proto/exec_pb2.pyi +212 -0
- flwr/proto/exec_pb2_grpc.py +237 -0
- flwr/proto/exec_pb2_grpc.pyi +93 -0
- flwr/proto/fab_pb2.py +31 -0
- flwr/proto/fab_pb2.pyi +65 -0
- flwr/proto/fab_pb2_grpc.py +4 -0
- flwr/proto/fab_pb2_grpc.pyi +4 -0
- flwr/proto/fleet_pb2.py +42 -23
- flwr/proto/fleet_pb2.pyi +123 -1
- flwr/proto/fleet_pb2_grpc.py +170 -0
- flwr/proto/fleet_pb2_grpc.pyi +61 -0
- flwr/proto/grpcadapter_pb2.py +32 -0
- flwr/proto/grpcadapter_pb2.pyi +43 -0
- flwr/proto/grpcadapter_pb2_grpc.py +66 -0
- flwr/proto/grpcadapter_pb2_grpc.pyi +24 -0
- flwr/proto/log_pb2.py +29 -0
- flwr/proto/log_pb2.pyi +39 -0
- flwr/proto/log_pb2_grpc.py +4 -0
- flwr/proto/log_pb2_grpc.pyi +4 -0
- flwr/proto/message_pb2.py +41 -0
- flwr/proto/message_pb2.pyi +128 -0
- flwr/proto/message_pb2_grpc.py +4 -0
- flwr/proto/message_pb2_grpc.pyi +4 -0
- flwr/proto/node_pb2.py +1 -1
- flwr/proto/recordset_pb2.py +35 -33
- flwr/proto/recordset_pb2.pyi +40 -14
- flwr/proto/run_pb2.py +64 -0
- flwr/proto/run_pb2.pyi +268 -0
- flwr/proto/run_pb2_grpc.py +4 -0
- flwr/proto/run_pb2_grpc.pyi +4 -0
- flwr/proto/serverappio_pb2.py +52 -0
- flwr/proto/{driver_pb2.pyi → serverappio_pb2.pyi} +62 -20
- flwr/proto/serverappio_pb2_grpc.py +410 -0
- flwr/proto/serverappio_pb2_grpc.pyi +160 -0
- flwr/proto/simulationio_pb2.py +38 -0
- flwr/proto/simulationio_pb2.pyi +65 -0
- flwr/proto/simulationio_pb2_grpc.py +239 -0
- flwr/proto/simulationio_pb2_grpc.pyi +94 -0
- flwr/proto/task_pb2.py +7 -8
- flwr/proto/task_pb2.pyi +8 -5
- flwr/proto/transport_pb2.py +8 -8
- flwr/proto/transport_pb2.pyi +9 -6
- flwr/server/__init__.py +2 -10
- flwr/server/app.py +579 -402
- flwr/server/client_manager.py +8 -6
- flwr/server/compat/app.py +6 -62
- flwr/server/compat/app_utils.py +14 -8
- flwr/server/compat/driver_client_proxy.py +25 -58
- flwr/server/compat/legacy_context.py +5 -4
- flwr/server/driver/__init__.py +2 -0
- flwr/server/driver/driver.py +36 -131
- flwr/server/driver/grpc_driver.py +217 -81
- flwr/server/driver/inmemory_driver.py +182 -0
- flwr/server/history.py +28 -29
- flwr/server/run_serverapp.py +15 -126
- flwr/server/server.py +50 -44
- flwr/server/server_app.py +59 -10
- flwr/server/serverapp/__init__.py +22 -0
- flwr/server/serverapp/app.py +256 -0
- flwr/server/serverapp_components.py +52 -0
- flwr/server/strategy/__init__.py +2 -2
- flwr/server/strategy/aggregate.py +37 -23
- flwr/server/strategy/bulyan.py +9 -9
- flwr/server/strategy/dp_adaptive_clipping.py +25 -25
- flwr/server/strategy/dp_fixed_clipping.py +23 -22
- flwr/server/strategy/dpfedavg_adaptive.py +8 -8
- flwr/server/strategy/dpfedavg_fixed.py +13 -12
- flwr/server/strategy/fault_tolerant_fedavg.py +11 -11
- flwr/server/strategy/fedadagrad.py +9 -9
- flwr/server/strategy/fedadam.py +20 -10
- flwr/server/strategy/fedavg.py +16 -16
- flwr/server/strategy/fedavg_android.py +17 -17
- flwr/server/strategy/fedavgm.py +9 -9
- flwr/server/strategy/fedmedian.py +5 -5
- flwr/server/strategy/fedopt.py +6 -6
- flwr/server/strategy/fedprox.py +7 -7
- flwr/server/strategy/fedtrimmedavg.py +8 -8
- flwr/server/strategy/fedxgb_bagging.py +12 -12
- flwr/server/strategy/fedxgb_cyclic.py +10 -10
- flwr/server/strategy/fedxgb_nn_avg.py +6 -6
- flwr/server/strategy/fedyogi.py +9 -9
- flwr/server/strategy/krum.py +9 -9
- flwr/server/strategy/qfedavg.py +16 -16
- flwr/server/strategy/strategy.py +10 -10
- flwr/server/superlink/driver/__init__.py +2 -2
- flwr/server/superlink/driver/serverappio_grpc.py +61 -0
- flwr/server/superlink/driver/serverappio_servicer.py +363 -0
- flwr/server/superlink/ffs/__init__.py +24 -0
- flwr/server/superlink/ffs/disk_ffs.py +108 -0
- flwr/server/superlink/ffs/ffs.py +79 -0
- flwr/server/superlink/ffs/ffs_factory.py +47 -0
- flwr/server/superlink/fleet/__init__.py +1 -1
- flwr/server/superlink/fleet/grpc_adapter/__init__.py +15 -0
- flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +162 -0
- flwr/server/superlink/fleet/grpc_bidi/__init__.py +1 -1
- flwr/server/superlink/fleet/grpc_bidi/flower_service_servicer.py +4 -2
- flwr/server/superlink/fleet/grpc_bidi/grpc_bridge.py +3 -2
- flwr/server/superlink/fleet/grpc_bidi/grpc_client_proxy.py +1 -1
- flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +5 -154
- flwr/server/superlink/fleet/grpc_rere/__init__.py +1 -1
- flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +120 -13
- flwr/server/superlink/fleet/grpc_rere/server_interceptor.py +228 -0
- flwr/server/superlink/fleet/message_handler/__init__.py +1 -1
- flwr/server/superlink/fleet/message_handler/message_handler.py +153 -9
- flwr/server/superlink/fleet/rest_rere/__init__.py +1 -1
- flwr/server/superlink/fleet/rest_rere/rest_api.py +119 -81
- flwr/server/superlink/fleet/vce/__init__.py +1 -0
- flwr/server/superlink/fleet/vce/backend/__init__.py +4 -4
- flwr/server/superlink/fleet/vce/backend/backend.py +8 -9
- flwr/server/superlink/fleet/vce/backend/raybackend.py +87 -68
- flwr/server/superlink/fleet/vce/vce_api.py +208 -146
- flwr/server/superlink/linkstate/__init__.py +28 -0
- flwr/server/superlink/linkstate/in_memory_linkstate.py +581 -0
- flwr/server/superlink/linkstate/linkstate.py +389 -0
- flwr/server/superlink/{state/state_factory.py → linkstate/linkstate_factory.py} +19 -10
- flwr/server/superlink/linkstate/sqlite_linkstate.py +1236 -0
- flwr/server/superlink/linkstate/utils.py +389 -0
- flwr/server/superlink/simulation/__init__.py +15 -0
- flwr/server/superlink/simulation/simulationio_grpc.py +65 -0
- flwr/server/superlink/simulation/simulationio_servicer.py +186 -0
- flwr/server/superlink/utils.py +65 -0
- flwr/server/typing.py +2 -0
- flwr/server/utils/__init__.py +1 -1
- flwr/server/utils/tensorboard.py +5 -5
- flwr/server/utils/validator.py +31 -11
- flwr/server/workflow/default_workflows.py +70 -26
- flwr/server/workflow/secure_aggregation/secagg_workflow.py +1 -0
- flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +40 -27
- flwr/simulation/__init__.py +12 -5
- flwr/simulation/app.py +247 -315
- flwr/simulation/legacy_app.py +402 -0
- flwr/simulation/ray_transport/__init__.py +1 -1
- flwr/simulation/ray_transport/ray_actor.py +42 -67
- flwr/simulation/ray_transport/ray_client_proxy.py +37 -17
- flwr/simulation/ray_transport/utils.py +1 -0
- flwr/simulation/run_simulation.py +306 -163
- flwr/simulation/simulationio_connection.py +89 -0
- flwr/superexec/__init__.py +15 -0
- flwr/superexec/app.py +59 -0
- flwr/superexec/deployment.py +188 -0
- flwr/superexec/exec_grpc.py +80 -0
- flwr/superexec/exec_servicer.py +231 -0
- flwr/superexec/exec_user_auth_interceptor.py +101 -0
- flwr/superexec/executor.py +96 -0
- flwr/superexec/simulation.py +124 -0
- {flwr_nightly-1.8.0.dev20240314.dist-info → flwr_nightly-1.15.0.dev20250114.dist-info}/METADATA +33 -26
- flwr_nightly-1.15.0.dev20250114.dist-info/RECORD +328 -0
- flwr_nightly-1.15.0.dev20250114.dist-info/entry_points.txt +12 -0
- flwr/cli/flower_toml.py +0 -140
- flwr/cli/new/templates/app/flower.toml.tpl +0 -13
- flwr/cli/new/templates/app/requirements.numpy.txt.tpl +0 -2
- flwr/cli/new/templates/app/requirements.pytorch.txt.tpl +0 -4
- flwr/cli/new/templates/app/requirements.tensorflow.txt.tpl +0 -4
- flwr/client/node_state.py +0 -48
- flwr/client/node_state_tests.py +0 -65
- flwr/proto/driver_pb2.py +0 -44
- flwr/proto/driver_pb2_grpc.py +0 -169
- flwr/proto/driver_pb2_grpc.pyi +0 -66
- flwr/server/superlink/driver/driver_grpc.py +0 -54
- flwr/server/superlink/driver/driver_servicer.py +0 -129
- flwr/server/superlink/state/in_memory_state.py +0 -230
- flwr/server/superlink/state/sqlite_state.py +0 -630
- flwr/server/superlink/state/state.py +0 -154
- flwr_nightly-1.8.0.dev20240314.dist-info/RECORD +0 -211
- flwr_nightly-1.8.0.dev20240314.dist-info/entry_points.txt +0 -9
- {flwr_nightly-1.8.0.dev20240314.dist-info → flwr_nightly-1.15.0.dev20250114.dist-info}/LICENSE +0 -0
- {flwr_nightly-1.8.0.dev20240314.dist-info → flwr_nightly-1.15.0.dev20250114.dist-info}/WHEEL +0 -0
flwr/client/app.py
CHANGED
@@ -15,177 +15,64 @@
|
|
15
15
|
"""Flower client app."""
|
16
16
|
|
17
17
|
|
18
|
-
import
|
18
|
+
import multiprocessing
|
19
|
+
import signal
|
19
20
|
import sys
|
20
21
|
import time
|
21
|
-
from
|
22
|
+
from contextlib import AbstractContextManager
|
23
|
+
from dataclasses import dataclass
|
24
|
+
from logging import ERROR, INFO, WARN
|
25
|
+
from os import urandom
|
22
26
|
from pathlib import Path
|
23
|
-
from typing import Callable,
|
27
|
+
from typing import Callable, Optional, Union, cast
|
24
28
|
|
29
|
+
import grpc
|
30
|
+
from cryptography.hazmat.primitives.asymmetric import ec
|
25
31
|
from grpc import RpcError
|
26
32
|
|
33
|
+
from flwr.cli.config_utils import get_fab_metadata
|
34
|
+
from flwr.cli.install import install_from_fab
|
27
35
|
from flwr.client.client import Client
|
28
36
|
from flwr.client.client_app import ClientApp, LoadClientAppError
|
29
|
-
from flwr.client.
|
30
|
-
from flwr.
|
37
|
+
from flwr.client.clientapp.app import flwr_clientapp
|
38
|
+
from flwr.client.nodestate.nodestate_factory import NodeStateFactory
|
39
|
+
from flwr.client.typing import ClientFnExt
|
40
|
+
from flwr.common import GRPC_MAX_MESSAGE_LENGTH, Context, EventType, Message, event
|
31
41
|
from flwr.common.address import parse_address
|
32
42
|
from flwr.common.constant import (
|
43
|
+
CLIENT_OCTET,
|
44
|
+
CLIENTAPPIO_API_DEFAULT_SERVER_ADDRESS,
|
45
|
+
ISOLATION_MODE_PROCESS,
|
46
|
+
ISOLATION_MODE_SUBPROCESS,
|
47
|
+
MAX_RETRY_DELAY,
|
33
48
|
MISSING_EXTRA_REST,
|
49
|
+
RUN_ID_NUM_BYTES,
|
50
|
+
SERVER_OCTET,
|
51
|
+
TRANSPORT_TYPE_GRPC_ADAPTER,
|
34
52
|
TRANSPORT_TYPE_GRPC_BIDI,
|
35
53
|
TRANSPORT_TYPE_GRPC_RERE,
|
36
54
|
TRANSPORT_TYPE_REST,
|
37
55
|
TRANSPORT_TYPES,
|
56
|
+
ErrorCode,
|
38
57
|
)
|
39
|
-
from flwr.common.
|
40
|
-
from flwr.common.logger import log, warn_deprecated_feature
|
41
|
-
from flwr.common.
|
42
|
-
from flwr.common.retry_invoker import RetryInvoker, exponential
|
43
|
-
|
58
|
+
from flwr.common.grpc import generic_create_grpc_server
|
59
|
+
from flwr.common.logger import log, warn_deprecated_feature
|
60
|
+
from flwr.common.message import Error
|
61
|
+
from flwr.common.retry_invoker import RetryInvoker, RetryState, exponential
|
62
|
+
from flwr.common.typing import Fab, Run, RunNotRunningException, UserConfig
|
63
|
+
from flwr.proto.clientappio_pb2_grpc import add_ClientAppIoServicer_to_server
|
64
|
+
|
65
|
+
from .clientapp.clientappio_servicer import ClientAppInputs, ClientAppIoServicer
|
66
|
+
from .grpc_adapter_client.connection import grpc_adapter
|
44
67
|
from .grpc_client.connection import grpc_connection
|
45
68
|
from .grpc_rere_client.connection import grpc_request_response
|
46
69
|
from .message_handler.message_handler import handle_control_message
|
47
|
-
from .node_state import NodeState
|
48
70
|
from .numpy_client import NumPyClient
|
49
|
-
|
50
|
-
|
51
|
-
def run_client_app() -> None:
|
52
|
-
"""Run Flower client app."""
|
53
|
-
event(EventType.RUN_CLIENT_APP_ENTER)
|
54
|
-
|
55
|
-
log(INFO, "Long-running Flower client starting")
|
56
|
-
|
57
|
-
args = _parse_args_run_client_app().parse_args()
|
58
|
-
|
59
|
-
# Obtain 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. "
|
71
|
-
"Starting insecure HTTP client connected to %s.",
|
72
|
-
args.server,
|
73
|
-
)
|
74
|
-
root_certificates = None
|
75
|
-
else:
|
76
|
-
# Load the certificates if provided, or load the system certificates
|
77
|
-
cert_path = args.root_certificates
|
78
|
-
if cert_path is None:
|
79
|
-
root_certificates = None
|
80
|
-
else:
|
81
|
-
root_certificates = Path(cert_path).read_bytes()
|
82
|
-
log(
|
83
|
-
DEBUG,
|
84
|
-
"Starting secure HTTPS client connected to %s "
|
85
|
-
"with the following certificates: %s.",
|
86
|
-
args.server,
|
87
|
-
cert_path,
|
88
|
-
)
|
89
|
-
|
90
|
-
log(
|
91
|
-
DEBUG,
|
92
|
-
"Flower will load ClientApp `%s`",
|
93
|
-
getattr(args, "client-app"),
|
94
|
-
)
|
95
|
-
|
96
|
-
client_app_dir = args.dir
|
97
|
-
if client_app_dir is not None:
|
98
|
-
sys.path.insert(0, client_app_dir)
|
99
|
-
|
100
|
-
app_ref: str = getattr(args, "client-app")
|
101
|
-
valid, error_msg = validate(app_ref)
|
102
|
-
if not valid and error_msg:
|
103
|
-
raise LoadClientAppError(error_msg) from None
|
104
|
-
|
105
|
-
def _load() -> ClientApp:
|
106
|
-
client_app = load_app(app_ref, LoadClientAppError)
|
107
|
-
|
108
|
-
if not isinstance(client_app, ClientApp):
|
109
|
-
raise LoadClientAppError(
|
110
|
-
f"Attribute {app_ref} is not of type {ClientApp}",
|
111
|
-
) from None
|
112
|
-
|
113
|
-
return client_app
|
114
|
-
|
115
|
-
_start_client_internal(
|
116
|
-
server_address=args.server,
|
117
|
-
load_client_app_fn=_load,
|
118
|
-
transport="rest" if args.rest else "grpc-rere",
|
119
|
-
root_certificates=root_certificates,
|
120
|
-
insecure=args.insecure,
|
121
|
-
max_retries=args.max_retries,
|
122
|
-
max_wait_time=args.max_wait_time,
|
123
|
-
)
|
124
|
-
register_exit_handlers(event_type=EventType.RUN_CLIENT_APP_LEAVE)
|
125
|
-
|
126
|
-
|
127
|
-
def _parse_args_run_client_app() -> argparse.ArgumentParser:
|
128
|
-
"""Parse flower-client-app command line arguments."""
|
129
|
-
parser = argparse.ArgumentParser(
|
130
|
-
description="Start a Flower client app",
|
131
|
-
)
|
132
|
-
|
133
|
-
parser.add_argument(
|
134
|
-
"client-app",
|
135
|
-
help="For example: `client:app` or `project.package.module:wrapper.app`",
|
136
|
-
)
|
137
|
-
parser.add_argument(
|
138
|
-
"--insecure",
|
139
|
-
action="store_true",
|
140
|
-
help="Run the client without HTTPS. By default, the client runs with "
|
141
|
-
"HTTPS enabled. Use this flag only if you understand the risks.",
|
142
|
-
)
|
143
|
-
parser.add_argument(
|
144
|
-
"--rest",
|
145
|
-
action="store_true",
|
146
|
-
help="Use REST as a transport layer for the client.",
|
147
|
-
)
|
148
|
-
parser.add_argument(
|
149
|
-
"--root-certificates",
|
150
|
-
metavar="ROOT_CERT",
|
151
|
-
type=str,
|
152
|
-
help="Specifies the path to the PEM-encoded root certificate file for "
|
153
|
-
"establishing secure HTTPS connections.",
|
154
|
-
)
|
155
|
-
parser.add_argument(
|
156
|
-
"--server",
|
157
|
-
default="0.0.0.0:9092",
|
158
|
-
help="Server address",
|
159
|
-
)
|
160
|
-
parser.add_argument(
|
161
|
-
"--max-retries",
|
162
|
-
type=int,
|
163
|
-
default=None,
|
164
|
-
help="The maximum number of times the client will try to connect to the"
|
165
|
-
"server before giving up in case of a connection error. By default,"
|
166
|
-
"it is set to None, meaning there is no limit to the number of tries.",
|
167
|
-
)
|
168
|
-
parser.add_argument(
|
169
|
-
"--max-wait-time",
|
170
|
-
type=float,
|
171
|
-
default=None,
|
172
|
-
help="The maximum duration before the client stops trying to"
|
173
|
-
"connect to the server in case of connection error. By default, it"
|
174
|
-
"is set to None, meaning there is no limit to the total time.",
|
175
|
-
)
|
176
|
-
parser.add_argument(
|
177
|
-
"--dir",
|
178
|
-
default="",
|
179
|
-
help="Add specified directory to the PYTHONPATH and load Flower "
|
180
|
-
"app from there."
|
181
|
-
" Default: current working directory.",
|
182
|
-
)
|
183
|
-
|
184
|
-
return parser
|
71
|
+
from .run_info_store import DeprecatedRunInfoStore
|
185
72
|
|
186
73
|
|
187
74
|
def _check_actionable_client(
|
188
|
-
client: Optional[Client], client_fn: Optional[
|
75
|
+
client: Optional[Client], client_fn: Optional[ClientFnExt]
|
189
76
|
) -> None:
|
190
77
|
if client_fn is None and client is None:
|
191
78
|
raise ValueError(
|
@@ -206,24 +93,32 @@ def _check_actionable_client(
|
|
206
93
|
def start_client(
|
207
94
|
*,
|
208
95
|
server_address: str,
|
209
|
-
client_fn: Optional[
|
96
|
+
client_fn: Optional[ClientFnExt] = None,
|
210
97
|
client: Optional[Client] = None,
|
211
98
|
grpc_max_message_length: int = GRPC_MAX_MESSAGE_LENGTH,
|
212
99
|
root_certificates: Optional[Union[bytes, str]] = None,
|
213
100
|
insecure: Optional[bool] = None,
|
214
101
|
transport: Optional[str] = None,
|
102
|
+
authentication_keys: Optional[
|
103
|
+
tuple[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey]
|
104
|
+
] = None,
|
215
105
|
max_retries: Optional[int] = None,
|
216
106
|
max_wait_time: Optional[float] = None,
|
217
107
|
) -> None:
|
218
108
|
"""Start a Flower client node which connects to a Flower server.
|
219
109
|
|
110
|
+
Warning
|
111
|
+
-------
|
112
|
+
This function is deprecated since 1.13.0. Use :code:`flower-supernode` command
|
113
|
+
instead to start a SuperNode.
|
114
|
+
|
220
115
|
Parameters
|
221
116
|
----------
|
222
117
|
server_address : str
|
223
118
|
The IPv4 or IPv6 address of the server. If the Flower
|
224
119
|
server runs on the same machine on port 8080, then `server_address`
|
225
120
|
would be `"[::]:8080"`.
|
226
|
-
client_fn : Optional[
|
121
|
+
client_fn : Optional[ClientFnExt]
|
227
122
|
A callable that instantiates a Client. (default: None)
|
228
123
|
client : Optional[flwr.client.Client]
|
229
124
|
An implementation of the abstract base
|
@@ -247,6 +142,11 @@ def start_client(
|
|
247
142
|
- 'grpc-bidi': gRPC, bidirectional streaming
|
248
143
|
- 'grpc-rere': gRPC, request-response (experimental)
|
249
144
|
- 'rest': HTTP (experimental)
|
145
|
+
authentication_keys : Optional[Tuple[PrivateKey, PublicKey]] (default: None)
|
146
|
+
Tuple containing the elliptic curve private key and public key for
|
147
|
+
authentication from the cryptography library.
|
148
|
+
Source: https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ec/
|
149
|
+
Used to establish an authenticated connection with the server.
|
250
150
|
max_retries: Optional[int] (default: None)
|
251
151
|
The maximum number of times the client will try to connect to the
|
252
152
|
server before giving up in case of a connection error. If set to None,
|
@@ -267,8 +167,8 @@ def start_client(
|
|
267
167
|
|
268
168
|
Starting an SSL-enabled gRPC client using system certificates:
|
269
169
|
|
270
|
-
>>> def client_fn(
|
271
|
-
>>> return FlowerClient()
|
170
|
+
>>> def client_fn(context: Context):
|
171
|
+
>>> return FlowerClient().to_client()
|
272
172
|
>>>
|
273
173
|
>>> start_client(
|
274
174
|
>>> server_address=localhost:8080,
|
@@ -286,9 +186,21 @@ def start_client(
|
|
286
186
|
>>> root_certificates=Path("/crts/root.pem").read_bytes(),
|
287
187
|
>>> )
|
288
188
|
"""
|
189
|
+
msg = (
|
190
|
+
"flwr.client.start_client() is deprecated."
|
191
|
+
"\n\tInstead, use the `flower-supernode` CLI command to start a SuperNode "
|
192
|
+
"as shown below:"
|
193
|
+
"\n\n\t\t$ flower-supernode --insecure --superlink='<IP>:<PORT>'"
|
194
|
+
"\n\n\tTo view all available options, run:"
|
195
|
+
"\n\n\t\t$ flower-supernode --help"
|
196
|
+
"\n\n\tUsing `start_client()` is deprecated."
|
197
|
+
)
|
198
|
+
warn_deprecated_feature(name=msg)
|
199
|
+
|
289
200
|
event(EventType.START_CLIENT_ENTER)
|
290
|
-
|
201
|
+
start_client_internal(
|
291
202
|
server_address=server_address,
|
203
|
+
node_config={},
|
292
204
|
load_client_app_fn=None,
|
293
205
|
client_fn=client_fn,
|
294
206
|
client=client,
|
@@ -296,6 +208,7 @@ def start_client(
|
|
296
208
|
root_certificates=root_certificates,
|
297
209
|
insecure=insecure,
|
298
210
|
transport=transport,
|
211
|
+
authentication_keys=authentication_keys,
|
299
212
|
max_retries=max_retries,
|
300
213
|
max_wait_time=max_wait_time,
|
301
214
|
)
|
@@ -306,18 +219,25 @@ def start_client(
|
|
306
219
|
# pylint: disable=too-many-branches
|
307
220
|
# pylint: disable=too-many-locals
|
308
221
|
# pylint: disable=too-many-statements
|
309
|
-
def
|
222
|
+
def start_client_internal(
|
310
223
|
*,
|
311
224
|
server_address: str,
|
312
|
-
|
313
|
-
|
225
|
+
node_config: UserConfig,
|
226
|
+
load_client_app_fn: Optional[Callable[[str, str, str], ClientApp]] = None,
|
227
|
+
client_fn: Optional[ClientFnExt] = None,
|
314
228
|
client: Optional[Client] = None,
|
315
229
|
grpc_max_message_length: int = GRPC_MAX_MESSAGE_LENGTH,
|
316
230
|
root_certificates: Optional[Union[bytes, str]] = None,
|
317
231
|
insecure: Optional[bool] = None,
|
318
232
|
transport: Optional[str] = None,
|
233
|
+
authentication_keys: Optional[
|
234
|
+
tuple[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey]
|
235
|
+
] = None,
|
319
236
|
max_retries: Optional[int] = None,
|
320
237
|
max_wait_time: Optional[float] = None,
|
238
|
+
flwr_path: Optional[Path] = None,
|
239
|
+
isolation: Optional[str] = None,
|
240
|
+
clientappio_api_address: Optional[str] = CLIENTAPPIO_API_DEFAULT_SERVER_ADDRESS,
|
321
241
|
) -> None:
|
322
242
|
"""Start a Flower client node which connects to a Flower server.
|
323
243
|
|
@@ -327,9 +247,11 @@ def _start_client_internal(
|
|
327
247
|
The IPv4 or IPv6 address of the server. If the Flower
|
328
248
|
server runs on the same machine on port 8080, then `server_address`
|
329
249
|
would be `"[::]:8080"`.
|
250
|
+
node_config: UserConfig
|
251
|
+
The configuration of the node.
|
330
252
|
load_client_app_fn : Optional[Callable[[], ClientApp]] (default: None)
|
331
253
|
A function that can be used to load a `ClientApp` instance.
|
332
|
-
client_fn : Optional[
|
254
|
+
client_fn : Optional[ClientFnExt]
|
333
255
|
A callable that instantiates a Client. (default: None)
|
334
256
|
client : Optional[flwr.client.Client]
|
335
257
|
An implementation of the abstract base
|
@@ -353,6 +275,11 @@ def _start_client_internal(
|
|
353
275
|
- 'grpc-bidi': gRPC, bidirectional streaming
|
354
276
|
- 'grpc-rere': gRPC, request-response (experimental)
|
355
277
|
- 'rest': HTTP (experimental)
|
278
|
+
authentication_keys : Optional[Tuple[PrivateKey, PublicKey]] (default: None)
|
279
|
+
Tuple containing the elliptic curve private key and public key for
|
280
|
+
authentication from the cryptography library.
|
281
|
+
Source: https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ec/
|
282
|
+
Used to establish an authenticated connection with the server.
|
356
283
|
max_retries: Optional[int] (default: None)
|
357
284
|
The maximum number of times the client will try to connect to the
|
358
285
|
server before giving up in case of a connection error. If set to None,
|
@@ -361,6 +288,19 @@ def _start_client_internal(
|
|
361
288
|
The maximum duration before the client stops trying to
|
362
289
|
connect to the server in case of connection error.
|
363
290
|
If set to None, there is no limit to the total time.
|
291
|
+
flwr_path: Optional[Path] (default: None)
|
292
|
+
The fully resolved path containing installed Flower Apps.
|
293
|
+
isolation : Optional[str] (default: None)
|
294
|
+
Isolation mode for `ClientApp`. Possible values are `subprocess` and
|
295
|
+
`process`. Defaults to `None`, which runs the `ClientApp` in the same process
|
296
|
+
as the SuperNode. If `subprocess`, the `ClientApp` runs in a subprocess started
|
297
|
+
by the SueprNode and communicates using gRPC at the address
|
298
|
+
`clientappio_api_address`. If `process`, the `ClientApp` runs in a separate
|
299
|
+
isolated process and communicates using gRPC at the address
|
300
|
+
`clientappio_api_address`.
|
301
|
+
clientappio_api_address : Optional[str]
|
302
|
+
(default: `CLIENTAPPIO_API_DEFAULT_SERVER_ADDRESS`)
|
303
|
+
The SuperNode gRPC server address.
|
364
304
|
"""
|
365
305
|
if insecure is None:
|
366
306
|
insecure = root_certificates is None
|
@@ -371,7 +311,7 @@ def _start_client_internal(
|
|
371
311
|
if client_fn is None:
|
372
312
|
# Wrap `Client` instance in `client_fn`
|
373
313
|
def single_client_factory(
|
374
|
-
|
314
|
+
context: Context, # pylint: disable=unused-argument
|
375
315
|
) -> Client:
|
376
316
|
if client is None: # Added this to keep mypy happy
|
377
317
|
raise ValueError(
|
@@ -381,12 +321,22 @@ def _start_client_internal(
|
|
381
321
|
|
382
322
|
client_fn = single_client_factory
|
383
323
|
|
384
|
-
def _load_client_app() -> ClientApp:
|
324
|
+
def _load_client_app(_1: str, _2: str, _3: str) -> ClientApp:
|
385
325
|
return ClientApp(client_fn=client_fn)
|
386
326
|
|
387
327
|
load_client_app_fn = _load_client_app
|
388
|
-
|
389
|
-
|
328
|
+
|
329
|
+
if isolation:
|
330
|
+
if clientappio_api_address is None:
|
331
|
+
raise ValueError(
|
332
|
+
f"`clientappio_api_address` required when `isolation` is "
|
333
|
+
f"{ISOLATION_MODE_SUBPROCESS} or {ISOLATION_MODE_PROCESS}",
|
334
|
+
)
|
335
|
+
_clientappio_grpc_server, clientappio_servicer = run_clientappio_api_grpc(
|
336
|
+
address=clientappio_api_address,
|
337
|
+
certificates=None,
|
338
|
+
)
|
339
|
+
clientappio_api_address = cast(str, clientappio_api_address)
|
390
340
|
|
391
341
|
# At this point, only `load_client_app_fn` should be used
|
392
342
|
# Both `client` and `client_fn` must not be used directly
|
@@ -396,10 +346,33 @@ def _start_client_internal(
|
|
396
346
|
transport, server_address
|
397
347
|
)
|
398
348
|
|
349
|
+
app_state_tracker = _AppStateTracker()
|
350
|
+
|
351
|
+
def _on_sucess(retry_state: RetryState) -> None:
|
352
|
+
app_state_tracker.is_connected = True
|
353
|
+
if retry_state.tries > 1:
|
354
|
+
log(
|
355
|
+
INFO,
|
356
|
+
"Connection successful after %.2f seconds and %s tries.",
|
357
|
+
retry_state.elapsed_time,
|
358
|
+
retry_state.tries,
|
359
|
+
)
|
360
|
+
|
361
|
+
def _on_backoff(retry_state: RetryState) -> None:
|
362
|
+
app_state_tracker.is_connected = False
|
363
|
+
if retry_state.tries == 1:
|
364
|
+
log(WARN, "Connection attempt failed, retrying...")
|
365
|
+
else:
|
366
|
+
log(
|
367
|
+
WARN,
|
368
|
+
"Connection attempt failed, retrying in %.2f seconds",
|
369
|
+
retry_state.actual_wait,
|
370
|
+
)
|
371
|
+
|
399
372
|
retry_invoker = RetryInvoker(
|
400
|
-
|
373
|
+
wait_gen_factory=lambda: exponential(max_delay=MAX_RETRY_DELAY),
|
401
374
|
recoverable_exceptions=connection_error_type,
|
402
|
-
max_tries=max_retries,
|
375
|
+
max_tries=max_retries + 1 if max_retries is not None else None,
|
403
376
|
max_time=max_wait_time,
|
404
377
|
on_giveup=lambda retry_state: (
|
405
378
|
log(
|
@@ -411,30 +384,19 @@ def _start_client_internal(
|
|
411
384
|
if retry_state.tries > 1
|
412
385
|
else None
|
413
386
|
),
|
414
|
-
on_success=
|
415
|
-
|
416
|
-
INFO,
|
417
|
-
"Connection successful after %.2f seconds and %s tries.",
|
418
|
-
retry_state.elapsed_time,
|
419
|
-
retry_state.tries,
|
420
|
-
)
|
421
|
-
if retry_state.tries > 1
|
422
|
-
else None
|
423
|
-
),
|
424
|
-
on_backoff=lambda retry_state: (
|
425
|
-
log(WARN, "Connection attempt failed, retrying...")
|
426
|
-
if retry_state.tries == 1
|
427
|
-
else log(
|
428
|
-
DEBUG,
|
429
|
-
"Connection attempt failed, retrying in %.2f seconds",
|
430
|
-
retry_state.actual_wait,
|
431
|
-
)
|
432
|
-
),
|
387
|
+
on_success=_on_sucess,
|
388
|
+
on_backoff=_on_backoff,
|
433
389
|
)
|
434
390
|
|
435
|
-
|
391
|
+
# DeprecatedRunInfoStore gets initialized when the first connection is established
|
392
|
+
run_info_store: Optional[DeprecatedRunInfoStore] = None
|
393
|
+
state_factory = NodeStateFactory()
|
394
|
+
state = state_factory.state()
|
395
|
+
mp_spawn_context = multiprocessing.get_context("spawn")
|
396
|
+
|
397
|
+
runs: dict[int, Run] = {}
|
436
398
|
|
437
|
-
while
|
399
|
+
while not app_state_tracker.interrupt:
|
438
400
|
sleep_duration: int = 0
|
439
401
|
with connection(
|
440
402
|
address,
|
@@ -442,80 +404,241 @@ def _start_client_internal(
|
|
442
404
|
retry_invoker,
|
443
405
|
grpc_max_message_length,
|
444
406
|
root_certificates,
|
407
|
+
authentication_keys,
|
445
408
|
) as conn:
|
446
|
-
receive, send, create_node, delete_node = conn
|
447
|
-
|
448
|
-
# Register node
|
449
|
-
if
|
450
|
-
create_node
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
409
|
+
receive, send, create_node, delete_node, get_run, get_fab = conn
|
410
|
+
|
411
|
+
# Register node when connecting the first time
|
412
|
+
if run_info_store is None:
|
413
|
+
if create_node is None:
|
414
|
+
if transport not in ["grpc-bidi", None]:
|
415
|
+
raise NotImplementedError(
|
416
|
+
"All transports except `grpc-bidi` require "
|
417
|
+
"an implementation for `create_node()`.'"
|
418
|
+
)
|
419
|
+
# gRPC-bidi doesn't have the concept of node_id,
|
420
|
+
# so we set it to -1
|
421
|
+
run_info_store = DeprecatedRunInfoStore(
|
422
|
+
node_id=-1,
|
423
|
+
node_config={},
|
424
|
+
)
|
425
|
+
else:
|
426
|
+
# Call create_node fn to register node
|
427
|
+
# and store node_id in state
|
428
|
+
if (node_id := create_node()) is None:
|
429
|
+
raise ValueError(
|
430
|
+
"Failed to register SuperNode with the SuperLink"
|
431
|
+
)
|
432
|
+
state.set_node_id(node_id)
|
433
|
+
run_info_store = DeprecatedRunInfoStore(
|
434
|
+
node_id=state.get_node_id(),
|
435
|
+
node_config=node_config,
|
436
|
+
)
|
437
|
+
|
438
|
+
app_state_tracker.register_signal_handler()
|
439
|
+
# pylint: disable=too-many-nested-blocks
|
440
|
+
while not app_state_tracker.interrupt:
|
441
|
+
try:
|
442
|
+
# Receive
|
443
|
+
message = receive()
|
444
|
+
if message is None:
|
445
|
+
time.sleep(3) # Wait for 3s before asking again
|
446
|
+
continue
|
447
|
+
|
448
|
+
log(INFO, "")
|
449
|
+
if len(message.metadata.group_id) > 0:
|
450
|
+
log(
|
451
|
+
INFO,
|
452
|
+
"[RUN %s, ROUND %s]",
|
453
|
+
message.metadata.run_id,
|
454
|
+
message.metadata.group_id,
|
455
|
+
)
|
456
|
+
log(
|
457
|
+
INFO,
|
458
|
+
"Received: %s message %s",
|
459
|
+
message.metadata.message_type,
|
460
|
+
message.metadata.message_id,
|
461
|
+
)
|
478
462
|
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
463
|
+
# Handle control message
|
464
|
+
out_message, sleep_duration = handle_control_message(message)
|
465
|
+
if out_message:
|
466
|
+
send(out_message)
|
467
|
+
break
|
468
|
+
|
469
|
+
# Get run info
|
470
|
+
run_id = message.metadata.run_id
|
471
|
+
if run_id not in runs:
|
472
|
+
if get_run is not None:
|
473
|
+
runs[run_id] = get_run(run_id)
|
474
|
+
# If get_run is None, i.e., in grpc-bidi mode
|
475
|
+
else:
|
476
|
+
runs[run_id] = Run.create_empty(run_id=run_id)
|
477
|
+
|
478
|
+
run: Run = runs[run_id]
|
479
|
+
if get_fab is not None and run.fab_hash:
|
480
|
+
fab = get_fab(run.fab_hash, run_id)
|
481
|
+
if not isolation:
|
482
|
+
# If `ClientApp` runs in the same process, install the FAB
|
483
|
+
install_from_fab(fab.content, flwr_path, True)
|
484
|
+
fab_id, fab_version = get_fab_metadata(fab.content)
|
485
|
+
else:
|
486
|
+
fab = None
|
487
|
+
fab_id, fab_version = run.fab_id, run.fab_version
|
488
|
+
|
489
|
+
run.fab_id, run.fab_version = fab_id, fab_version
|
490
|
+
|
491
|
+
# Register context for this run
|
492
|
+
run_info_store.register_context(
|
493
|
+
run_id=run_id,
|
494
|
+
run=run,
|
495
|
+
flwr_path=flwr_path,
|
496
|
+
fab=fab,
|
497
|
+
)
|
498
|
+
|
499
|
+
# Retrieve context for this run
|
500
|
+
context = run_info_store.retrieve_context(run_id=run_id)
|
501
|
+
# Create an error reply message that will never be used to prevent
|
502
|
+
# the used-before-assignment linting error
|
503
|
+
reply_message = message.create_error_reply(
|
504
|
+
error=Error(code=ErrorCode.UNKNOWN, reason="Unknown")
|
505
|
+
)
|
506
|
+
|
507
|
+
# Handle app loading and task message
|
508
|
+
try:
|
509
|
+
if isolation:
|
510
|
+
# Two isolation modes:
|
511
|
+
# 1. `subprocess`: SuperNode is starting the ClientApp
|
512
|
+
# process as a subprocess.
|
513
|
+
# 2. `process`: ClientApp process gets started separately
|
514
|
+
# (via `flwr-clientapp`), for example, in a separate
|
515
|
+
# Docker container.
|
516
|
+
|
517
|
+
# Generate SuperNode token
|
518
|
+
token = int.from_bytes(urandom(RUN_ID_NUM_BYTES), "little")
|
519
|
+
|
520
|
+
# Mode 1: SuperNode starts ClientApp as subprocess
|
521
|
+
start_subprocess = isolation == ISOLATION_MODE_SUBPROCESS
|
522
|
+
|
523
|
+
# Share Message and Context with servicer
|
524
|
+
clientappio_servicer.set_inputs(
|
525
|
+
clientapp_input=ClientAppInputs(
|
526
|
+
message=message,
|
527
|
+
context=context,
|
528
|
+
run=run,
|
529
|
+
fab=fab,
|
530
|
+
token=token,
|
531
|
+
),
|
532
|
+
token_returned=start_subprocess,
|
533
|
+
)
|
534
|
+
|
535
|
+
if start_subprocess:
|
536
|
+
_octet, _colon, _port = (
|
537
|
+
clientappio_api_address.rpartition(":")
|
538
|
+
)
|
539
|
+
io_address = (
|
540
|
+
f"{CLIENT_OCTET}:{_port}"
|
541
|
+
if _octet == SERVER_OCTET
|
542
|
+
else clientappio_api_address
|
543
|
+
)
|
544
|
+
# Start ClientApp subprocess
|
545
|
+
command = [
|
546
|
+
"flwr-clientapp",
|
547
|
+
"--clientappio-api-address",
|
548
|
+
io_address,
|
549
|
+
"--token",
|
550
|
+
str(token),
|
551
|
+
]
|
552
|
+
command.append("--insecure")
|
553
|
+
|
554
|
+
proc = mp_spawn_context.Process(
|
555
|
+
target=_run_flwr_clientapp,
|
556
|
+
args=(command,),
|
557
|
+
daemon=True,
|
558
|
+
)
|
559
|
+
proc.start()
|
560
|
+
proc.join()
|
561
|
+
else:
|
562
|
+
# Wait for output to become available
|
563
|
+
while not clientappio_servicer.has_outputs():
|
564
|
+
time.sleep(0.1)
|
565
|
+
|
566
|
+
outputs = clientappio_servicer.get_outputs()
|
567
|
+
reply_message, context = outputs.message, outputs.context
|
568
|
+
else:
|
569
|
+
# Load ClientApp instance
|
570
|
+
client_app: ClientApp = load_client_app_fn(
|
571
|
+
fab_id, fab_version, run.fab_hash
|
572
|
+
)
|
573
|
+
|
574
|
+
# Execute ClientApp
|
575
|
+
reply_message = client_app(message=message, context=context)
|
576
|
+
except Exception as ex: # pylint: disable=broad-exception-caught
|
577
|
+
|
578
|
+
# Legacy grpc-bidi
|
579
|
+
if transport in ["grpc-bidi", None]:
|
580
|
+
log(ERROR, "Client raised an exception.", exc_info=ex)
|
581
|
+
# Raise exception, crash process
|
582
|
+
raise ex
|
583
|
+
|
584
|
+
# Don't update/change DeprecatedRunInfoStore
|
585
|
+
|
586
|
+
e_code = ErrorCode.CLIENT_APP_RAISED_EXCEPTION
|
587
|
+
# Ex fmt: "<class 'ZeroDivisionError'>:<'division by zero'>"
|
588
|
+
reason = str(type(ex)) + ":<'" + str(ex) + "'>"
|
589
|
+
exc_entity = "ClientApp"
|
590
|
+
if isinstance(ex, LoadClientAppError):
|
591
|
+
reason = (
|
592
|
+
"An exception was raised when attempting to load "
|
593
|
+
"`ClientApp`"
|
594
|
+
)
|
595
|
+
e_code = ErrorCode.LOAD_CLIENT_APP_EXCEPTION
|
596
|
+
exc_entity = "SuperNode"
|
597
|
+
|
598
|
+
if not app_state_tracker.interrupt:
|
599
|
+
log(
|
600
|
+
ERROR, "%s raised an exception", exc_entity, exc_info=ex
|
601
|
+
)
|
602
|
+
|
603
|
+
# Create error message
|
604
|
+
reply_message = message.create_error_reply(
|
605
|
+
error=Error(code=e_code, reason=reason)
|
606
|
+
)
|
607
|
+
else:
|
608
|
+
# No exception, update node state
|
609
|
+
run_info_store.update_context(
|
610
|
+
run_id=run_id,
|
611
|
+
context=context,
|
612
|
+
)
|
613
|
+
|
614
|
+
# Send
|
615
|
+
send(reply_message)
|
616
|
+
log(INFO, "Sent reply")
|
617
|
+
|
618
|
+
except RunNotRunningException:
|
619
|
+
log(INFO, "")
|
620
|
+
log(
|
621
|
+
INFO,
|
622
|
+
"SuperNode aborted sending the reply message. "
|
623
|
+
"Run ID %s is not in `RUNNING` status.",
|
624
|
+
run_id,
|
625
|
+
)
|
626
|
+
log(INFO, "")
|
627
|
+
|
628
|
+
except StopIteration:
|
629
|
+
sleep_duration = 0
|
630
|
+
break
|
631
|
+
# pylint: enable=too-many-nested-blocks
|
511
632
|
|
512
633
|
# Unregister node
|
513
|
-
if delete_node is not None:
|
634
|
+
if delete_node is not None and app_state_tracker.is_connected:
|
514
635
|
delete_node() # pylint: disable=not-callable
|
515
636
|
|
516
637
|
if sleep_duration == 0:
|
517
638
|
log(INFO, "Disconnect and shut down")
|
639
|
+
del app_state_tracker
|
518
640
|
break
|
641
|
+
|
519
642
|
# Sleep and reconnect afterwards
|
520
643
|
log(
|
521
644
|
INFO,
|
@@ -626,20 +749,29 @@ def start_numpy_client(
|
|
626
749
|
)
|
627
750
|
|
628
751
|
|
629
|
-
def _init_connection(transport: Optional[str], server_address: str) ->
|
752
|
+
def _init_connection(transport: Optional[str], server_address: str) -> tuple[
|
630
753
|
Callable[
|
631
|
-
[
|
632
|
-
|
633
|
-
|
754
|
+
[
|
755
|
+
str,
|
756
|
+
bool,
|
757
|
+
RetryInvoker,
|
758
|
+
int,
|
759
|
+
Union[bytes, str, None],
|
760
|
+
Optional[tuple[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey]],
|
761
|
+
],
|
762
|
+
AbstractContextManager[
|
763
|
+
tuple[
|
634
764
|
Callable[[], Optional[Message]],
|
635
765
|
Callable[[Message], None],
|
766
|
+
Optional[Callable[[], Optional[int]]],
|
636
767
|
Optional[Callable[[], None]],
|
637
|
-
Optional[Callable[[],
|
768
|
+
Optional[Callable[[int], Run]],
|
769
|
+
Optional[Callable[[str, int], Fab]],
|
638
770
|
]
|
639
771
|
],
|
640
772
|
],
|
641
773
|
str,
|
642
|
-
|
774
|
+
type[Exception],
|
643
775
|
]:
|
644
776
|
# Parse IP address
|
645
777
|
parsed_address = parse_address(server_address)
|
@@ -668,6 +800,8 @@ def _init_connection(transport: Optional[str], server_address: str) -> Tuple[
|
|
668
800
|
connection, error_type = http_request_response, RequestsConnectionError
|
669
801
|
elif transport == TRANSPORT_TYPE_GRPC_RERE:
|
670
802
|
connection, error_type = grpc_request_response, RpcError
|
803
|
+
elif transport == TRANSPORT_TYPE_GRPC_ADAPTER:
|
804
|
+
connection, error_type = grpc_adapter, RpcError
|
671
805
|
elif transport == TRANSPORT_TYPE_GRPC_BIDI:
|
672
806
|
connection, error_type = grpc_connection, RpcError
|
673
807
|
else:
|
@@ -676,3 +810,46 @@ def _init_connection(transport: Optional[str], server_address: str) -> Tuple[
|
|
676
810
|
)
|
677
811
|
|
678
812
|
return connection, address, error_type
|
813
|
+
|
814
|
+
|
815
|
+
@dataclass
|
816
|
+
class _AppStateTracker:
|
817
|
+
interrupt: bool = False
|
818
|
+
is_connected: bool = False
|
819
|
+
|
820
|
+
def register_signal_handler(self) -> None:
|
821
|
+
"""Register handlers for exit signals."""
|
822
|
+
|
823
|
+
def signal_handler(sig, frame): # type: ignore
|
824
|
+
# pylint: disable=unused-argument
|
825
|
+
self.interrupt = True
|
826
|
+
raise StopIteration from None
|
827
|
+
|
828
|
+
signal.signal(signal.SIGINT, signal_handler)
|
829
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
830
|
+
|
831
|
+
|
832
|
+
def _run_flwr_clientapp(args: list[str]) -> None:
|
833
|
+
sys.argv = args
|
834
|
+
flwr_clientapp()
|
835
|
+
|
836
|
+
|
837
|
+
def run_clientappio_api_grpc(
|
838
|
+
address: str,
|
839
|
+
certificates: Optional[tuple[bytes, bytes, bytes]],
|
840
|
+
) -> tuple[grpc.Server, ClientAppIoServicer]:
|
841
|
+
"""Run ClientAppIo API gRPC server."""
|
842
|
+
clientappio_servicer: grpc.Server = ClientAppIoServicer()
|
843
|
+
clientappio_add_servicer_to_server_fn = add_ClientAppIoServicer_to_server
|
844
|
+
clientappio_grpc_server = generic_create_grpc_server(
|
845
|
+
servicer_and_add_fn=(
|
846
|
+
clientappio_servicer,
|
847
|
+
clientappio_add_servicer_to_server_fn,
|
848
|
+
),
|
849
|
+
server_address=address,
|
850
|
+
max_message_length=GRPC_MAX_MESSAGE_LENGTH,
|
851
|
+
certificates=certificates,
|
852
|
+
)
|
853
|
+
log(INFO, "Starting Flower ClientAppIo gRPC server on %s", address)
|
854
|
+
clientappio_grpc_server.start()
|
855
|
+
return clientappio_grpc_server, clientappio_servicer
|