flwr 1.24.0__py3-none-any.whl → 1.26.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flwr/__init__.py +1 -1
- flwr/app/__init__.py +4 -1
- flwr/app/message_type.py +29 -0
- flwr/app/metadata.py +5 -2
- flwr/app/user_config.py +19 -0
- flwr/cli/app.py +37 -19
- flwr/cli/app_cmd/publish.py +25 -75
- flwr/cli/app_cmd/review.py +25 -66
- flwr/cli/auth_plugin/auth_plugin.py +5 -10
- flwr/cli/auth_plugin/noop_auth_plugin.py +1 -2
- flwr/cli/auth_plugin/oidc_cli_plugin.py +38 -38
- flwr/cli/build.py +15 -28
- flwr/cli/config/__init__.py +21 -0
- flwr/cli/config/ls.py +71 -0
- flwr/cli/config_migration.py +297 -0
- flwr/cli/config_utils.py +63 -156
- flwr/cli/constant.py +71 -0
- flwr/cli/federation/__init__.py +0 -2
- flwr/cli/federation/ls.py +256 -64
- flwr/cli/flower_config.py +429 -0
- flwr/cli/install.py +23 -62
- flwr/cli/log.py +23 -37
- flwr/cli/login/login.py +29 -63
- flwr/cli/ls.py +72 -61
- flwr/cli/new/new.py +98 -309
- flwr/cli/pull.py +19 -37
- flwr/cli/run/run.py +87 -100
- flwr/cli/run_utils.py +23 -5
- flwr/cli/stop.py +33 -74
- flwr/cli/supernode/ls.py +35 -62
- flwr/cli/supernode/register.py +31 -80
- flwr/cli/supernode/unregister.py +24 -70
- flwr/cli/typing.py +200 -0
- flwr/cli/utils.py +160 -412
- flwr/client/grpc_adapter_client/connection.py +2 -2
- flwr/client/grpc_rere_client/connection.py +9 -6
- flwr/client/grpc_rere_client/grpc_adapter.py +1 -1
- flwr/client/message_handler/message_handler.py +2 -1
- flwr/client/mod/centraldp_mods.py +1 -1
- flwr/client/mod/localdp_mod.py +1 -1
- flwr/client/mod/secure_aggregation/secaggplus_mod.py +1 -1
- flwr/client/rest_client/connection.py +6 -4
- flwr/client/run_info_store.py +2 -1
- flwr/clientapp/client_app.py +2 -1
- flwr/common/__init__.py +3 -2
- flwr/common/args.py +5 -5
- flwr/common/config.py +12 -17
- flwr/common/constant.py +3 -16
- flwr/common/context.py +2 -1
- flwr/common/exit/exit.py +4 -4
- flwr/common/exit/exit_code.py +6 -0
- flwr/common/grpc.py +2 -1
- flwr/common/logger.py +1 -1
- flwr/common/message.py +1 -1
- flwr/common/retry_invoker.py +13 -5
- flwr/common/secure_aggregation/ndarrays_arithmetic.py +5 -2
- flwr/common/serde.py +13 -5
- flwr/common/telemetry.py +1 -1
- flwr/common/typing.py +10 -3
- flwr/compat/client/app.py +6 -9
- flwr/compat/client/grpc_client/connection.py +2 -1
- flwr/compat/common/constant.py +29 -0
- flwr/compat/server/app.py +1 -1
- flwr/proto/clientappio_pb2.py +2 -2
- flwr/proto/clientappio_pb2_grpc.py +104 -88
- flwr/proto/clientappio_pb2_grpc.pyi +140 -80
- flwr/proto/federation_pb2.py +5 -3
- flwr/proto/federation_pb2.pyi +32 -2
- flwr/proto/fleet_pb2.py +10 -10
- flwr/proto/fleet_pb2.pyi +5 -1
- flwr/proto/run_pb2.py +18 -26
- flwr/proto/run_pb2.pyi +10 -58
- flwr/proto/serverappio_pb2.py +2 -2
- flwr/proto/serverappio_pb2_grpc.py +138 -207
- flwr/proto/serverappio_pb2_grpc.pyi +189 -155
- flwr/proto/simulationio_pb2.py +2 -2
- flwr/proto/simulationio_pb2_grpc.py +62 -90
- flwr/proto/simulationio_pb2_grpc.pyi +95 -55
- flwr/server/app.py +7 -13
- flwr/server/compat/grid_client_proxy.py +2 -1
- flwr/server/grid/grpc_grid.py +5 -5
- flwr/server/serverapp/app.py +11 -4
- flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +1 -1
- flwr/server/superlink/fleet/grpc_rere/node_auth_server_interceptor.py +13 -12
- flwr/server/superlink/fleet/message_handler/message_handler.py +42 -2
- flwr/server/superlink/linkstate/__init__.py +2 -2
- flwr/server/superlink/linkstate/in_memory_linkstate.py +36 -10
- flwr/server/superlink/linkstate/linkstate.py +34 -21
- flwr/server/superlink/linkstate/linkstate_factory.py +16 -8
- flwr/server/superlink/linkstate/{sqlite_linkstate.py → sql_linkstate.py} +471 -516
- flwr/server/superlink/linkstate/utils.py +49 -2
- flwr/server/superlink/serverappio/serverappio_servicer.py +1 -33
- flwr/server/superlink/simulation/simulationio_servicer.py +0 -19
- flwr/server/utils/validator.py +1 -1
- flwr/server/workflow/default_workflows.py +2 -1
- flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +1 -1
- flwr/serverapp/strategy/bulyan.py +7 -1
- flwr/serverapp/strategy/dp_fixed_clipping.py +9 -1
- flwr/serverapp/strategy/fedavg.py +1 -1
- flwr/serverapp/strategy/fedxgb_cyclic.py +1 -1
- flwr/simulation/ray_transport/ray_client_proxy.py +2 -6
- flwr/simulation/run_simulation.py +3 -12
- flwr/simulation/simulationio_connection.py +3 -3
- flwr/{common → supercore}/address.py +7 -33
- flwr/supercore/app_utils.py +2 -1
- flwr/supercore/constant.py +27 -2
- flwr/supercore/corestate/{sqlite_corestate.py → sql_corestate.py} +19 -23
- flwr/supercore/credential_store/__init__.py +33 -0
- flwr/supercore/credential_store/credential_store.py +34 -0
- flwr/supercore/credential_store/file_credential_store.py +76 -0
- flwr/{common → supercore}/date.py +0 -11
- flwr/supercore/ffs/disk_ffs.py +1 -1
- flwr/supercore/object_store/object_store_factory.py +14 -6
- flwr/supercore/object_store/{sqlite_object_store.py → sql_object_store.py} +115 -117
- flwr/supercore/sql_mixin.py +315 -0
- flwr/{cli/new/templates → supercore/state}/__init__.py +2 -2
- flwr/{cli/new/templates/app/code/flwr_tune → supercore/state/alembic}/__init__.py +2 -2
- flwr/supercore/state/alembic/env.py +103 -0
- flwr/supercore/state/alembic/script.py.mako +43 -0
- flwr/supercore/state/alembic/utils.py +239 -0
- flwr/{cli/new/templates/app → supercore/state/alembic/versions}/__init__.py +2 -2
- flwr/supercore/state/alembic/versions/rev_2026_01_28_initialize_migration_of_state_tables.py +200 -0
- flwr/supercore/state/schema/README.md +121 -0
- flwr/{cli/new/templates/app/code → supercore/state/schema}/__init__.py +2 -2
- flwr/supercore/state/schema/corestate_tables.py +36 -0
- flwr/supercore/state/schema/linkstate_tables.py +152 -0
- flwr/supercore/state/schema/objectstore_tables.py +90 -0
- flwr/supercore/superexec/run_superexec.py +2 -2
- flwr/supercore/utils.py +225 -0
- flwr/superlink/federation/federation_manager.py +2 -2
- flwr/superlink/federation/noop_federation_manager.py +8 -6
- flwr/superlink/servicer/control/control_grpc.py +2 -0
- flwr/superlink/servicer/control/control_servicer.py +106 -21
- flwr/supernode/cli/flower_supernode.py +2 -1
- flwr/supernode/nodestate/in_memory_nodestate.py +62 -1
- flwr/supernode/nodestate/nodestate.py +45 -0
- flwr/supernode/runtime/run_clientapp.py +14 -14
- flwr/supernode/servicer/clientappio/clientappio_servicer.py +13 -5
- flwr/supernode/start_client_internal.py +17 -10
- {flwr-1.24.0.dist-info → flwr-1.26.0.dist-info}/METADATA +8 -8
- {flwr-1.24.0.dist-info → flwr-1.26.0.dist-info}/RECORD +144 -184
- flwr/cli/federation/show.py +0 -317
- flwr/cli/new/templates/app/.gitignore.tpl +0 -163
- flwr/cli/new/templates/app/LICENSE.tpl +0 -202
- flwr/cli/new/templates/app/README.baseline.md.tpl +0 -127
- flwr/cli/new/templates/app/README.flowertune.md.tpl +0 -68
- flwr/cli/new/templates/app/README.md.tpl +0 -37
- flwr/cli/new/templates/app/code/__init__.baseline.py.tpl +0 -1
- flwr/cli/new/templates/app/code/__init__.py.tpl +0 -1
- flwr/cli/new/templates/app/code/__init__.pytorch_legacy_api.py.tpl +0 -1
- flwr/cli/new/templates/app/code/client.baseline.py.tpl +0 -75
- flwr/cli/new/templates/app/code/client.huggingface.py.tpl +0 -93
- flwr/cli/new/templates/app/code/client.jax.py.tpl +0 -71
- flwr/cli/new/templates/app/code/client.mlx.py.tpl +0 -102
- flwr/cli/new/templates/app/code/client.numpy.py.tpl +0 -46
- flwr/cli/new/templates/app/code/client.pytorch.py.tpl +0 -80
- flwr/cli/new/templates/app/code/client.pytorch_legacy_api.py.tpl +0 -55
- flwr/cli/new/templates/app/code/client.sklearn.py.tpl +0 -108
- flwr/cli/new/templates/app/code/client.tensorflow.py.tpl +0 -82
- flwr/cli/new/templates/app/code/client.xgboost.py.tpl +0 -110
- flwr/cli/new/templates/app/code/dataset.baseline.py.tpl +0 -36
- flwr/cli/new/templates/app/code/flwr_tune/client_app.py.tpl +0 -92
- flwr/cli/new/templates/app/code/flwr_tune/dataset.py.tpl +0 -87
- flwr/cli/new/templates/app/code/flwr_tune/models.py.tpl +0 -56
- flwr/cli/new/templates/app/code/flwr_tune/server_app.py.tpl +0 -73
- flwr/cli/new/templates/app/code/flwr_tune/strategy.py.tpl +0 -78
- flwr/cli/new/templates/app/code/model.baseline.py.tpl +0 -66
- flwr/cli/new/templates/app/code/server.baseline.py.tpl +0 -43
- flwr/cli/new/templates/app/code/server.huggingface.py.tpl +0 -42
- flwr/cli/new/templates/app/code/server.jax.py.tpl +0 -39
- flwr/cli/new/templates/app/code/server.mlx.py.tpl +0 -41
- flwr/cli/new/templates/app/code/server.numpy.py.tpl +0 -38
- flwr/cli/new/templates/app/code/server.pytorch.py.tpl +0 -41
- flwr/cli/new/templates/app/code/server.pytorch_legacy_api.py.tpl +0 -31
- flwr/cli/new/templates/app/code/server.sklearn.py.tpl +0 -44
- flwr/cli/new/templates/app/code/server.tensorflow.py.tpl +0 -38
- flwr/cli/new/templates/app/code/server.xgboost.py.tpl +0 -56
- flwr/cli/new/templates/app/code/strategy.baseline.py.tpl +0 -1
- flwr/cli/new/templates/app/code/task.huggingface.py.tpl +0 -98
- flwr/cli/new/templates/app/code/task.jax.py.tpl +0 -57
- flwr/cli/new/templates/app/code/task.mlx.py.tpl +0 -102
- flwr/cli/new/templates/app/code/task.numpy.py.tpl +0 -7
- flwr/cli/new/templates/app/code/task.pytorch.py.tpl +0 -99
- flwr/cli/new/templates/app/code/task.pytorch_legacy_api.py.tpl +0 -111
- flwr/cli/new/templates/app/code/task.sklearn.py.tpl +0 -67
- flwr/cli/new/templates/app/code/task.tensorflow.py.tpl +0 -52
- flwr/cli/new/templates/app/code/task.xgboost.py.tpl +0 -67
- flwr/cli/new/templates/app/code/utils.baseline.py.tpl +0 -1
- flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +0 -146
- flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +0 -80
- flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +0 -65
- flwr/cli/new/templates/app/pyproject.jax.toml.tpl +0 -52
- flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +0 -56
- flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +0 -49
- flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +0 -53
- flwr/cli/new/templates/app/pyproject.pytorch_legacy_api.toml.tpl +0 -53
- flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +0 -52
- flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +0 -53
- flwr/cli/new/templates/app/pyproject.xgboost.toml.tpl +0 -61
- flwr/common/pyproject.py +0 -42
- flwr/supercore/sqlite_mixin.py +0 -159
- /flwr/{common → supercore}/version.py +0 -0
- {flwr-1.24.0.dist-info → flwr-1.26.0.dist-info}/WHEEL +0 -0
- {flwr-1.24.0.dist-info → flwr-1.26.0.dist-info}/entry_points.txt +0 -0
|
@@ -16,17 +16,21 @@
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
import hashlib
|
|
19
|
+
import json
|
|
19
20
|
import time
|
|
20
21
|
from collections.abc import Generator, Sequence
|
|
21
22
|
from logging import ERROR, INFO
|
|
22
23
|
from typing import Any, cast
|
|
23
24
|
|
|
24
25
|
import grpc
|
|
26
|
+
import requests
|
|
25
27
|
|
|
26
28
|
from flwr.cli.config_utils import get_fab_metadata
|
|
27
29
|
from flwr.common import Context, RecordDict, now
|
|
28
30
|
from flwr.common.constant import (
|
|
29
31
|
FAB_MAX_SIZE,
|
|
32
|
+
FEDERATION_NOT_FOUND_MESSAGE,
|
|
33
|
+
FEDERATION_NOT_SPECIFIED_MESSAGE,
|
|
30
34
|
HEARTBEAT_DEFAULT_INTERVAL,
|
|
31
35
|
LOG_STREAM_INTERVAL,
|
|
32
36
|
NO_ACCOUNT_AUTH_MESSAGE,
|
|
@@ -36,6 +40,8 @@ from flwr.common.constant import (
|
|
|
36
40
|
PUBLIC_KEY_NOT_VALID,
|
|
37
41
|
PULL_UNFINISHED_RUN_MESSAGE,
|
|
38
42
|
RUN_ID_NOT_FOUND_MESSAGE,
|
|
43
|
+
SUPERLINK_NODE_ID,
|
|
44
|
+
TRANSPORT_TYPE_GRPC_ADAPTER,
|
|
39
45
|
Status,
|
|
40
46
|
SubStatus,
|
|
41
47
|
)
|
|
@@ -76,9 +82,11 @@ from flwr.proto.control_pb2 import ( # pylint: disable=E0611
|
|
|
76
82
|
from flwr.proto.federation_pb2 import Federation # pylint: disable=E0611
|
|
77
83
|
from flwr.proto.node_pb2 import NodeInfo # pylint: disable=E0611
|
|
78
84
|
from flwr.server.superlink.linkstate import LinkState, LinkStateFactory
|
|
85
|
+
from flwr.supercore.constant import NOOP_FEDERATION, PLATFORM_API_URL
|
|
79
86
|
from flwr.supercore.ffs import FfsFactory
|
|
80
87
|
from flwr.supercore.object_store import ObjectStore, ObjectStoreFactory
|
|
81
88
|
from flwr.supercore.primitives.asymmetric import bytes_to_public_key, uses_nist_ec_curve
|
|
89
|
+
from flwr.supercore.utils import parse_app_spec, request_download_link
|
|
82
90
|
from flwr.superlink.artifact_provider import ArtifactProvider
|
|
83
91
|
from flwr.superlink.auth_plugin import ControlAuthnPlugin
|
|
84
92
|
|
|
@@ -96,6 +104,7 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
|
|
|
96
104
|
is_simulation: bool,
|
|
97
105
|
authn_plugin: ControlAuthnPlugin,
|
|
98
106
|
artifact_provider: ArtifactProvider | None = None,
|
|
107
|
+
fleet_api_type: str | None = None,
|
|
99
108
|
) -> None:
|
|
100
109
|
self.linkstate_factory = linkstate_factory
|
|
101
110
|
self.ffs_factory = ffs_factory
|
|
@@ -103,8 +112,9 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
|
|
|
103
112
|
self.is_simulation = is_simulation
|
|
104
113
|
self.authn_plugin = authn_plugin
|
|
105
114
|
self.artifact_provider = artifact_provider
|
|
115
|
+
self.fleet_api_type = fleet_api_type
|
|
106
116
|
|
|
107
|
-
def StartRun( # pylint: disable=too-many-locals
|
|
117
|
+
def StartRun( # pylint: disable=too-many-locals, too-many-branches, too-many-statements
|
|
108
118
|
self, request: StartRunRequest, context: grpc.ServicerContext
|
|
109
119
|
) -> StartRunResponse:
|
|
110
120
|
"""Create run ID."""
|
|
@@ -112,7 +122,15 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
|
|
|
112
122
|
state = self.linkstate_factory.state()
|
|
113
123
|
ffs = self.ffs_factory.ffs()
|
|
114
124
|
|
|
115
|
-
|
|
125
|
+
verification_dict: dict[str, str] = {}
|
|
126
|
+
if request.app_spec:
|
|
127
|
+
fab_file, verification_dict = _get_remote_fab(
|
|
128
|
+
self.fleet_api_type, request.app_spec, context
|
|
129
|
+
)
|
|
130
|
+
else:
|
|
131
|
+
fab_file = request.fab.content
|
|
132
|
+
|
|
133
|
+
if len(fab_file) > FAB_MAX_SIZE:
|
|
116
134
|
log(
|
|
117
135
|
ERROR,
|
|
118
136
|
"FAB size exceeds maximum allowed size of %d bytes.",
|
|
@@ -124,7 +142,6 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
|
|
|
124
142
|
flwr_aid = _check_flwr_aid_exists(flwr_aid, context)
|
|
125
143
|
override_config = user_config_from_proto(request.override_config)
|
|
126
144
|
federation_options = config_record_from_proto(request.federation_options)
|
|
127
|
-
fab_file = request.fab.content
|
|
128
145
|
|
|
129
146
|
try:
|
|
130
147
|
# Check that num-supernodes is set
|
|
@@ -134,10 +151,11 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
|
|
|
134
151
|
)
|
|
135
152
|
|
|
136
153
|
# Check (1) federation exists and (2) the flwr_aid is a member
|
|
137
|
-
federation = request.federation
|
|
138
|
-
|
|
154
|
+
federation = request.federation or NOOP_FEDERATION
|
|
139
155
|
if not state.federation_manager.exists(federation):
|
|
140
|
-
|
|
156
|
+
if request.federation:
|
|
157
|
+
raise ValueError(FEDERATION_NOT_FOUND_MESSAGE % federation)
|
|
158
|
+
raise ValueError(FEDERATION_NOT_SPECIFIED_MESSAGE)
|
|
141
159
|
|
|
142
160
|
if not state.federation_manager.has_member(flwr_aid, federation):
|
|
143
161
|
raise ValueError(
|
|
@@ -150,11 +168,12 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
|
|
|
150
168
|
fab = Fab(
|
|
151
169
|
hashlib.sha256(fab_file).hexdigest(),
|
|
152
170
|
fab_file,
|
|
153
|
-
|
|
171
|
+
verification_dict,
|
|
154
172
|
)
|
|
155
|
-
fab_hash = ffs.put(fab.content,
|
|
173
|
+
fab_hash = ffs.put(fab.content, fab.verifications)
|
|
174
|
+
|
|
156
175
|
if fab_hash != fab.hash_str:
|
|
157
|
-
raise
|
|
176
|
+
raise ValueError(
|
|
158
177
|
f"FAB ({fab.hash_str}) hash from request doesn't match contents"
|
|
159
178
|
)
|
|
160
179
|
fab_id, fab_version = get_fab_metadata(fab.content)
|
|
@@ -164,7 +183,7 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
|
|
|
164
183
|
fab_version,
|
|
165
184
|
fab_hash,
|
|
166
185
|
override_config,
|
|
167
|
-
|
|
186
|
+
federation,
|
|
168
187
|
federation_options,
|
|
169
188
|
flwr_aid,
|
|
170
189
|
)
|
|
@@ -180,7 +199,7 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
|
|
|
180
199
|
# Create an empty context for the Run
|
|
181
200
|
context = Context(
|
|
182
201
|
run_id=run_id,
|
|
183
|
-
node_id=
|
|
202
|
+
node_id=SUPERLINK_NODE_ID,
|
|
184
203
|
# Dict is invariant in mypy
|
|
185
204
|
node_config=node_config, # type: ignore[arg-type]
|
|
186
205
|
state=RecordDict(),
|
|
@@ -190,13 +209,9 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
|
|
|
190
209
|
# Register the context at the LinkState
|
|
191
210
|
state.set_serverapp_context(run_id=run_id, context=context)
|
|
192
211
|
|
|
193
|
-
|
|
194
|
-
except Exception as e:
|
|
212
|
+
except ValueError as e:
|
|
195
213
|
log(ERROR, "Could not start run: %s", str(e))
|
|
196
|
-
context.abort(
|
|
197
|
-
grpc.StatusCode.FAILED_PRECONDITION,
|
|
198
|
-
str(e),
|
|
199
|
-
)
|
|
214
|
+
context.abort(grpc.StatusCode.FAILED_PRECONDITION, str(e))
|
|
200
215
|
|
|
201
216
|
log(INFO, "Created run %s", str(run_id))
|
|
202
217
|
return StartRunResponse(run_id=run_id)
|
|
@@ -521,9 +536,10 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
|
|
|
521
536
|
|
|
522
537
|
# Get federations the account is a member of
|
|
523
538
|
federations = state.federation_manager.get_federations(flwr_aid=flwr_aid)
|
|
524
|
-
|
|
525
539
|
return ListFederationsResponse(
|
|
526
|
-
federations=[
|
|
540
|
+
federations=[
|
|
541
|
+
Federation(name=fed[0], description=fed[1]) for fed in federations
|
|
542
|
+
]
|
|
527
543
|
)
|
|
528
544
|
|
|
529
545
|
def ShowFederation(
|
|
@@ -543,7 +559,7 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
|
|
|
543
559
|
|
|
544
560
|
# Ensure flwr_aid is a member of the requested federation
|
|
545
561
|
federation = request.federation_name
|
|
546
|
-
if federation not in federations:
|
|
562
|
+
if federation not in [fed[0] for fed in federations]:
|
|
547
563
|
context.abort(
|
|
548
564
|
grpc.StatusCode.FAILED_PRECONDITION,
|
|
549
565
|
f"Federation '{federation}' does not exist or you are "
|
|
@@ -556,7 +572,8 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
|
|
|
556
572
|
# Build Federation proto object
|
|
557
573
|
federation_proto = Federation(
|
|
558
574
|
name=federation,
|
|
559
|
-
member_aids=details.
|
|
575
|
+
member_aids=[acc.id for acc in details.accounts], # Deprecated in v1.26.0
|
|
576
|
+
accounts=details.accounts,
|
|
560
577
|
nodes=details.nodes,
|
|
561
578
|
runs=[run_to_proto(run) for run in details.runs],
|
|
562
579
|
)
|
|
@@ -612,3 +629,71 @@ def _check_flwr_aid_in_run(
|
|
|
612
629
|
grpc.StatusCode.PERMISSION_DENIED,
|
|
613
630
|
"⛔️ Run ID does not belong to the account",
|
|
614
631
|
)
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def _format_verification(verifications: list[dict[str, str]]) -> dict[str, str]:
|
|
635
|
+
"""Format verification information for FAB."""
|
|
636
|
+
# Convert verifications to dict[str, str] type
|
|
637
|
+
verification_dict = {
|
|
638
|
+
item["public_key_id"]: json.dumps(
|
|
639
|
+
{k: v for k, v in item.items() if k != "public_key_id"}
|
|
640
|
+
)
|
|
641
|
+
for item in verifications
|
|
642
|
+
}
|
|
643
|
+
verification_dict.update({"valid_license": "Valid"})
|
|
644
|
+
|
|
645
|
+
return verification_dict
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _get_remote_fab(
|
|
649
|
+
fleet_api_type: str | None,
|
|
650
|
+
app_spec: str,
|
|
651
|
+
context: grpc.ServicerContext,
|
|
652
|
+
) -> tuple[bytes, dict[str, str]]:
|
|
653
|
+
"""Get remote FAB from Flower platform API."""
|
|
654
|
+
if fleet_api_type == TRANSPORT_TYPE_GRPC_ADAPTER:
|
|
655
|
+
context.abort(
|
|
656
|
+
grpc.StatusCode.FAILED_PRECONDITION,
|
|
657
|
+
"The selected SuperLink transport type is not "
|
|
658
|
+
"supported for connecting to Flower Platform.",
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
# Parse and validate app specification
|
|
662
|
+
try:
|
|
663
|
+
app_id, app_version = parse_app_spec(app_spec)
|
|
664
|
+
except ValueError as e:
|
|
665
|
+
context.abort(
|
|
666
|
+
grpc.StatusCode.FAILED_PRECONDITION,
|
|
667
|
+
f"{e}",
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
# Request download link and verification information
|
|
671
|
+
url = f"{PLATFORM_API_URL}/hub/fetch-fab"
|
|
672
|
+
try:
|
|
673
|
+
presigned_url, verifications = request_download_link(
|
|
674
|
+
app_id, app_version, url, "fab_url"
|
|
675
|
+
)
|
|
676
|
+
except ValueError as e:
|
|
677
|
+
context.abort(
|
|
678
|
+
grpc.StatusCode.FAILED_PRECONDITION,
|
|
679
|
+
f"{e}",
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
# Format verification information
|
|
683
|
+
verification_dict = (
|
|
684
|
+
_format_verification(verifications)
|
|
685
|
+
if verifications is not None
|
|
686
|
+
else {"valid_license": ""}
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
# Download FAB from Flower platform API
|
|
690
|
+
try:
|
|
691
|
+
r = requests.get(presigned_url, timeout=60)
|
|
692
|
+
r.raise_for_status()
|
|
693
|
+
except requests.RequestException as e:
|
|
694
|
+
context.abort(
|
|
695
|
+
grpc.StatusCode.FAILED_PRECONDITION,
|
|
696
|
+
f"FAB download failed: {str(e)}",
|
|
697
|
+
)
|
|
698
|
+
fab_file = r.content
|
|
699
|
+
return fab_file, verification_dict
|
|
@@ -233,7 +233,7 @@ def _try_setup_client_authentication(
|
|
|
233
233
|
|
|
234
234
|
try:
|
|
235
235
|
ssh_private_key = load_ssh_private_key(
|
|
236
|
-
Path(args.auth_supernode_private_key).read_bytes(),
|
|
236
|
+
Path(args.auth_supernode_private_key).expanduser().read_bytes(),
|
|
237
237
|
None,
|
|
238
238
|
)
|
|
239
239
|
if not isinstance(ssh_private_key, ec.EllipticCurvePrivateKey):
|
|
@@ -260,6 +260,7 @@ def _try_obtain_trusted_entities(
|
|
|
260
260
|
"""Validate and return the trust entities."""
|
|
261
261
|
if not trusted_entities_path:
|
|
262
262
|
return None
|
|
263
|
+
trusted_entities_path = trusted_entities_path.expanduser()
|
|
263
264
|
if not trusted_entities_path.is_file():
|
|
264
265
|
flwr_exit(
|
|
265
266
|
ExitCode.SUPERNODE_INVALID_TRUSTED_ENTITIES,
|
|
@@ -19,7 +19,7 @@ from collections.abc import Sequence
|
|
|
19
19
|
from dataclasses import dataclass
|
|
20
20
|
from threading import Lock, RLock
|
|
21
21
|
|
|
22
|
-
from flwr.common import Context, Error, Message
|
|
22
|
+
from flwr.common import Context, Error, Message, now
|
|
23
23
|
from flwr.common.constant import ErrorCode
|
|
24
24
|
from flwr.common.inflatable import (
|
|
25
25
|
get_all_nested_objects,
|
|
@@ -27,6 +27,7 @@ from flwr.common.inflatable import (
|
|
|
27
27
|
no_object_id_recompute,
|
|
28
28
|
)
|
|
29
29
|
from flwr.common.typing import Run
|
|
30
|
+
from flwr.supercore.constant import MESSAGE_TIME_ENTRY_MAX_AGE_SECONDS
|
|
30
31
|
from flwr.supercore.corestate.in_memory_corestate import InMemoryCoreState
|
|
31
32
|
from flwr.supercore.object_store import ObjectStore
|
|
32
33
|
|
|
@@ -45,6 +46,14 @@ class MessageEntry:
|
|
|
45
46
|
is_retrieved: bool = False
|
|
46
47
|
|
|
47
48
|
|
|
49
|
+
@dataclass
|
|
50
|
+
class TimeEntry:
|
|
51
|
+
"""Data class to represent a time entry."""
|
|
52
|
+
|
|
53
|
+
starting_at: float
|
|
54
|
+
finished_at: float | None = None
|
|
55
|
+
|
|
56
|
+
|
|
48
57
|
class InMemoryNodeState(
|
|
49
58
|
NodeState, InMemoryCoreState
|
|
50
59
|
): # pylint: disable=too-many-instance-attributes
|
|
@@ -63,6 +72,9 @@ class InMemoryNodeState(
|
|
|
63
72
|
# Store run ID to Context mapping
|
|
64
73
|
self.ctx_store: dict[int, Context] = {}
|
|
65
74
|
self.lock_ctx_store = Lock()
|
|
75
|
+
# Store msg ID to TimeEntry mapping
|
|
76
|
+
self.time_store: dict[str, TimeEntry] = {}
|
|
77
|
+
self.lock_time_store = Lock()
|
|
66
78
|
|
|
67
79
|
def set_node_id(self, node_id: int | None) -> None:
|
|
68
80
|
"""Set the node ID."""
|
|
@@ -208,3 +220,52 @@ class InMemoryNodeState(
|
|
|
208
220
|
|
|
209
221
|
# Store the error reply message
|
|
210
222
|
self.store_message(error_reply)
|
|
223
|
+
|
|
224
|
+
def record_message_processing_start(self, message_id: str) -> None:
|
|
225
|
+
"""Record the start time of message processing based on the message ID."""
|
|
226
|
+
with self.lock_time_store:
|
|
227
|
+
self.time_store[message_id] = TimeEntry(starting_at=now().timestamp())
|
|
228
|
+
|
|
229
|
+
def record_message_processing_end(self, message_id: str) -> None:
|
|
230
|
+
"""Record the end time of message processing based on the message ID."""
|
|
231
|
+
with self.lock_time_store:
|
|
232
|
+
if message_id not in self.time_store:
|
|
233
|
+
raise ValueError(
|
|
234
|
+
f"Cannot record end time: Message ID {message_id} not found."
|
|
235
|
+
)
|
|
236
|
+
entry = self.time_store[message_id]
|
|
237
|
+
entry.finished_at = now().timestamp()
|
|
238
|
+
|
|
239
|
+
def get_message_processing_duration(self, message_id: str) -> float:
|
|
240
|
+
"""Get the message processing duration based on the message ID."""
|
|
241
|
+
# Cleanup old message processing times
|
|
242
|
+
self._cleanup_old_message_times()
|
|
243
|
+
with self.lock_time_store:
|
|
244
|
+
if message_id not in self.time_store:
|
|
245
|
+
raise ValueError(f"Message ID {message_id} not found.")
|
|
246
|
+
|
|
247
|
+
entry = self.time_store[message_id]
|
|
248
|
+
if entry.starting_at is None or entry.finished_at is None:
|
|
249
|
+
raise ValueError(
|
|
250
|
+
f"Start time or end time for message ID {message_id} is missing."
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
duration = entry.finished_at - entry.starting_at
|
|
254
|
+
return duration
|
|
255
|
+
|
|
256
|
+
def _cleanup_old_message_times(self) -> None:
|
|
257
|
+
"""Remove time entries older than MESSAGE_TIME_ENTRY_MAX_AGE_SECONDS."""
|
|
258
|
+
with self.lock_time_store:
|
|
259
|
+
cutoff = now().timestamp() - MESSAGE_TIME_ENTRY_MAX_AGE_SECONDS
|
|
260
|
+
# Find message IDs for entries that have a finishing_at time
|
|
261
|
+
# before the cutoff, and those that don't exist in msg_store
|
|
262
|
+
to_delete = [
|
|
263
|
+
msg_id
|
|
264
|
+
for msg_id, entry in self.time_store.items()
|
|
265
|
+
if (entry.finished_at and entry.finished_at < cutoff)
|
|
266
|
+
or msg_id not in self.msg_store
|
|
267
|
+
]
|
|
268
|
+
|
|
269
|
+
# Delete the identified entries
|
|
270
|
+
for msg_id in to_delete:
|
|
271
|
+
del self.time_store[msg_id]
|
|
@@ -168,3 +168,48 @@ class NodeState(CoreState):
|
|
|
168
168
|
Sequence[int]
|
|
169
169
|
Sequence of run IDs with pending messages.
|
|
170
170
|
"""
|
|
171
|
+
|
|
172
|
+
@abstractmethod
|
|
173
|
+
def record_message_processing_start(self, message_id: str) -> None:
|
|
174
|
+
"""Record the start time of message processing based on the message ID.
|
|
175
|
+
|
|
176
|
+
Parameters
|
|
177
|
+
----------
|
|
178
|
+
message_id : str
|
|
179
|
+
The ID of the message associated with the start time.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
@abstractmethod
|
|
183
|
+
def record_message_processing_end(self, message_id: str) -> None:
|
|
184
|
+
"""Record the end time of message processing based on the message ID.
|
|
185
|
+
|
|
186
|
+
Parameters
|
|
187
|
+
----------
|
|
188
|
+
message_id : str
|
|
189
|
+
The ID of the message associated with the end time.
|
|
190
|
+
|
|
191
|
+
Raises
|
|
192
|
+
------
|
|
193
|
+
ValueError
|
|
194
|
+
If the message ID is not found.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
@abstractmethod
|
|
198
|
+
def get_message_processing_duration(self, message_id: str) -> float:
|
|
199
|
+
"""Get the message processing duration based on the message ID.
|
|
200
|
+
|
|
201
|
+
Parameters
|
|
202
|
+
----------
|
|
203
|
+
message_id : str
|
|
204
|
+
The ID of the message.
|
|
205
|
+
|
|
206
|
+
Returns
|
|
207
|
+
-------
|
|
208
|
+
float
|
|
209
|
+
The processing duration in seconds.
|
|
210
|
+
|
|
211
|
+
Raises
|
|
212
|
+
------
|
|
213
|
+
ValueError
|
|
214
|
+
If the message ID is not found, or if start/end times are missing.
|
|
215
|
+
"""
|
|
@@ -41,7 +41,7 @@ from flwr.common.inflatable_protobuf_utils import (
|
|
|
41
41
|
from flwr.common.inflatable_utils import pull_and_inflate_object_from_tree, push_objects
|
|
42
42
|
from flwr.common.logger import log
|
|
43
43
|
from flwr.common.message import remove_content_from_message
|
|
44
|
-
from flwr.common.retry_invoker import
|
|
44
|
+
from flwr.common.retry_invoker import make_simple_grpc_retry_invoker, wrap_stub
|
|
45
45
|
from flwr.common.serde import (
|
|
46
46
|
context_from_proto,
|
|
47
47
|
context_to_proto,
|
|
@@ -103,14 +103,14 @@ def run_clientapp( # pylint: disable=R0913, R0914, R0917
|
|
|
103
103
|
flwr_dir_ = get_flwr_dir(flwr_dir)
|
|
104
104
|
try:
|
|
105
105
|
stub = ClientAppIoStub(channel)
|
|
106
|
-
|
|
106
|
+
wrap_stub(stub, make_simple_grpc_retry_invoker())
|
|
107
107
|
|
|
108
108
|
# Start app heartbeat
|
|
109
109
|
heartbeat_sender = HeartbeatSender(make_app_heartbeat_fn_grpc(stub, token))
|
|
110
110
|
heartbeat_sender.start()
|
|
111
111
|
|
|
112
112
|
# Pull Message, Context, Run and (optional) FAB from SuperNode
|
|
113
|
-
message, context, run, fab =
|
|
113
|
+
message, context, run, fab = pull_appinputs(stub=stub, token=token)
|
|
114
114
|
|
|
115
115
|
try:
|
|
116
116
|
|
|
@@ -152,7 +152,7 @@ def run_clientapp( # pylint: disable=R0913, R0914, R0917
|
|
|
152
152
|
reply_message = Message(Error(code=e_code, reason=reason), reply_to=message)
|
|
153
153
|
|
|
154
154
|
# Push Message and Context to SuperNode
|
|
155
|
-
_ =
|
|
155
|
+
_ = push_appoutputs(
|
|
156
156
|
stub=stub, token=token, message=reply_message, context=context
|
|
157
157
|
)
|
|
158
158
|
|
|
@@ -165,15 +165,15 @@ def run_clientapp( # pylint: disable=R0913, R0914, R0917
|
|
|
165
165
|
)
|
|
166
166
|
|
|
167
167
|
|
|
168
|
-
def
|
|
168
|
+
def pull_appinputs(
|
|
169
169
|
stub: ClientAppIoStub, token: str
|
|
170
170
|
) -> tuple[Message, Context, Run, Fab | None]:
|
|
171
|
-
"""Pull
|
|
171
|
+
"""Pull AppInputs from SuperNode."""
|
|
172
172
|
masked_token = mask_string(token)
|
|
173
|
-
log(INFO, "[flwr-clientapp] Pull `
|
|
173
|
+
log(INFO, "[flwr-clientapp] Pull `AppInputs` for token %s", masked_token)
|
|
174
174
|
try:
|
|
175
175
|
# Pull Context, Run and (optional) FAB
|
|
176
|
-
res: PullAppInputsResponse = stub.
|
|
176
|
+
res: PullAppInputsResponse = stub.PullAppInputs(
|
|
177
177
|
PullAppInputsRequest(token=token)
|
|
178
178
|
)
|
|
179
179
|
context = context_from_proto(res.context)
|
|
@@ -201,16 +201,16 @@ def pull_clientappinputs(
|
|
|
201
201
|
message.metadata.__dict__["_message_id"] = object_tree.object_id
|
|
202
202
|
return message, context, run, fab
|
|
203
203
|
except grpc.RpcError as e:
|
|
204
|
-
log(ERROR, "[
|
|
204
|
+
log(ERROR, "[PullAppInputs] gRPC error occurred: %s", str(e))
|
|
205
205
|
raise e
|
|
206
206
|
|
|
207
207
|
|
|
208
|
-
def
|
|
208
|
+
def push_appoutputs(
|
|
209
209
|
stub: ClientAppIoStub, token: str, message: Message, context: Context
|
|
210
210
|
) -> PushAppOutputsResponse:
|
|
211
|
-
"""Push
|
|
211
|
+
"""Push AppOutputs to SuperNode."""
|
|
212
212
|
masked_token = mask_string(token)
|
|
213
|
-
log(INFO, "[flwr-clientapp] Push `
|
|
213
|
+
log(INFO, "[flwr-clientapp] Push `AppOutputs` for token %s", masked_token)
|
|
214
214
|
# Set message ID
|
|
215
215
|
message.metadata.__dict__["_message_id"] = message.object_id
|
|
216
216
|
proto_message = message_to_proto(remove_content_from_message(message))
|
|
@@ -250,10 +250,10 @@ def push_clientappoutputs(
|
|
|
250
250
|
)
|
|
251
251
|
|
|
252
252
|
# Push Context
|
|
253
|
-
res: PushAppOutputsResponse = stub.
|
|
253
|
+
res: PushAppOutputsResponse = stub.PushAppOutputs(
|
|
254
254
|
PushAppOutputsRequest(token=token, context=proto_context)
|
|
255
255
|
)
|
|
256
256
|
return res
|
|
257
257
|
except grpc.RpcError as e:
|
|
258
|
-
log(ERROR, "[
|
|
258
|
+
log(ERROR, "[PushAppOutputs] gRPC error occurred: %s", str(e))
|
|
259
259
|
raise e
|
|
@@ -129,11 +129,11 @@ class ClientAppIoServicer(clientappio_pb2_grpc.ClientAppIoServicer):
|
|
|
129
129
|
|
|
130
130
|
return GetRunResponse(run=run_to_proto(run))
|
|
131
131
|
|
|
132
|
-
def
|
|
132
|
+
def PullAppInputs(
|
|
133
133
|
self, request: PullAppInputsRequest, context: grpc.ServicerContext
|
|
134
134
|
) -> PullAppInputsResponse:
|
|
135
135
|
"""Pull Message, Context, and Run."""
|
|
136
|
-
log(DEBUG, "ClientAppIo.
|
|
136
|
+
log(DEBUG, "ClientAppIo.PullAppInputs")
|
|
137
137
|
|
|
138
138
|
# Initialize state and ffs connection
|
|
139
139
|
state = self.state_factory.state()
|
|
@@ -176,11 +176,11 @@ class ClientAppIoServicer(clientappio_pb2_grpc.ClientAppIoServicer):
|
|
|
176
176
|
fab=fab_to_proto(fab),
|
|
177
177
|
)
|
|
178
178
|
|
|
179
|
-
def
|
|
179
|
+
def PushAppOutputs(
|
|
180
180
|
self, request: PushAppOutputsRequest, context: grpc.ServicerContext
|
|
181
181
|
) -> PushAppOutputsResponse:
|
|
182
182
|
"""Push Message and Context."""
|
|
183
|
-
log(DEBUG, "ClientAppIo.
|
|
183
|
+
log(DEBUG, "ClientAppIo.PushAppOutputs")
|
|
184
184
|
|
|
185
185
|
# Initialize state connection
|
|
186
186
|
state = self.state_factory.state()
|
|
@@ -223,6 +223,9 @@ class ClientAppIoServicer(clientappio_pb2_grpc.ClientAppIoServicer):
|
|
|
223
223
|
# Retrieve message for this run
|
|
224
224
|
message = state.get_messages(run_ids=[run_id], is_reply=False)[0]
|
|
225
225
|
|
|
226
|
+
# Record message processing start time
|
|
227
|
+
state.record_message_processing_start(message_id=message.metadata.message_id)
|
|
228
|
+
|
|
226
229
|
# Retrieve the object tree for the message
|
|
227
230
|
object_tree = store.get_object_tree(message.metadata.message_id)
|
|
228
231
|
|
|
@@ -248,13 +251,18 @@ class ClientAppIoServicer(clientappio_pb2_grpc.ClientAppIoServicer):
|
|
|
248
251
|
)
|
|
249
252
|
raise RuntimeError("This line should never be reached.")
|
|
250
253
|
|
|
254
|
+
# Record message processing end time
|
|
255
|
+
state.record_message_processing_end(
|
|
256
|
+
message_id=request.messages_list[0].metadata.reply_to_message_id
|
|
257
|
+
)
|
|
258
|
+
|
|
251
259
|
# Store Message object to descendants mapping and preregister objects
|
|
252
260
|
objects_to_push: set[str] = set()
|
|
253
261
|
for object_tree in request.message_object_trees:
|
|
254
262
|
objects_to_push |= set(store.preregister(run_id, object_tree))
|
|
263
|
+
|
|
255
264
|
# Save the message to the state
|
|
256
265
|
state.store_message(message_from_proto(request.messages_list[0]))
|
|
257
|
-
|
|
258
266
|
return PushAppMessagesResponse(objects_to_push=objects_to_push)
|
|
259
267
|
|
|
260
268
|
def SendAppHeartbeat(
|
|
@@ -32,10 +32,10 @@ from cryptography.hazmat.primitives.asymmetric import ec, ed25519
|
|
|
32
32
|
from cryptography.hazmat.primitives.serialization.ssh import load_ssh_public_key
|
|
33
33
|
from grpc import RpcError
|
|
34
34
|
|
|
35
|
+
from flwr.app.user_config import UserConfig
|
|
35
36
|
from flwr.client.grpc_adapter_client.connection import grpc_adapter
|
|
36
37
|
from flwr.client.grpc_rere_client.connection import grpc_request_response
|
|
37
38
|
from flwr.common import GRPC_MAX_MESSAGE_LENGTH, Context, Error, Message, RecordDict
|
|
38
|
-
from flwr.common.address import parse_address
|
|
39
39
|
from flwr.common.config import get_flwr_dir, get_fused_config_from_fab
|
|
40
40
|
from flwr.common.constant import (
|
|
41
41
|
CLIENTAPPIO_API_DEFAULT_SERVER_ADDRESS,
|
|
@@ -60,12 +60,12 @@ from flwr.common.inflatable_utils import (
|
|
|
60
60
|
push_object_contents_from_iterable,
|
|
61
61
|
)
|
|
62
62
|
from flwr.common.logger import log
|
|
63
|
-
from flwr.common.retry_invoker import RetryInvoker,
|
|
63
|
+
from flwr.common.retry_invoker import RetryInvoker, make_simple_grpc_retry_invoker
|
|
64
64
|
from flwr.common.telemetry import EventType
|
|
65
|
-
from flwr.common.typing import Fab, Run, RunNotRunningException
|
|
66
|
-
from flwr.common.version import package_version
|
|
65
|
+
from flwr.common.typing import Fab, Run, RunNotRunningException
|
|
67
66
|
from flwr.proto.clientappio_pb2_grpc import add_ClientAppIoServicer_to_server
|
|
68
67
|
from flwr.proto.message_pb2 import ObjectTree # pylint: disable=E0611
|
|
68
|
+
from flwr.supercore.address import parse_address, resolve_bind_address
|
|
69
69
|
from flwr.supercore.ffs import Ffs, FfsFactory
|
|
70
70
|
from flwr.supercore.grpc_health import run_health_server_grpc_no_tls
|
|
71
71
|
from flwr.supercore.object_store import ObjectStore, ObjectStoreFactory
|
|
@@ -74,6 +74,7 @@ from flwr.supercore.primitives.asymmetric_ed25519 import (
|
|
|
74
74
|
decode_base64url,
|
|
75
75
|
verify_signature,
|
|
76
76
|
)
|
|
77
|
+
from flwr.supercore.version import package_version
|
|
77
78
|
from flwr.supernode.nodestate import NodeState, NodeStateFactory
|
|
78
79
|
from flwr.supernode.servicer.clientappio import ClientAppIoServicer
|
|
79
80
|
|
|
@@ -212,7 +213,10 @@ def start_client_internal(
|
|
|
212
213
|
# Launch the SuperExec if the isolation mode is `subprocess`
|
|
213
214
|
if isolation == ISOLATION_MODE_SUBPROCESS:
|
|
214
215
|
command = ["flower-superexec", "--insecure"]
|
|
215
|
-
command += [
|
|
216
|
+
command += [
|
|
217
|
+
"--appio-api-address",
|
|
218
|
+
resolve_bind_address(clientappio_api_address),
|
|
219
|
+
]
|
|
216
220
|
command += ["--plugin-type", ExecPluginType.CLIENT_APP]
|
|
217
221
|
command += ["--parent-pid", str(os.getpid())]
|
|
218
222
|
# pylint: disable-next=consider-using-with
|
|
@@ -432,7 +436,7 @@ def _pull_and_store_message( # pylint: disable=too-many-positional-arguments
|
|
|
432
436
|
def _push_messages(
|
|
433
437
|
state: NodeState,
|
|
434
438
|
object_store: ObjectStore,
|
|
435
|
-
send: Callable[[Message, ObjectTree], set[str]],
|
|
439
|
+
send: Callable[[Message, ObjectTree, float], set[str]],
|
|
436
440
|
push_object: Callable[[int, str, bytes], None],
|
|
437
441
|
) -> None:
|
|
438
442
|
"""Push reply messages to the SuperLink."""
|
|
@@ -480,9 +484,12 @@ def _push_messages(
|
|
|
480
484
|
|
|
481
485
|
# Send the message
|
|
482
486
|
try:
|
|
483
|
-
|
|
487
|
+
clientapp_runtime = state.get_message_processing_duration(
|
|
488
|
+
message_id=message.metadata.reply_to_message_id,
|
|
489
|
+
)
|
|
490
|
+
# Send the reply message with its ObjectTree and ClientApp runtime
|
|
484
491
|
# Get the IDs of objects to send
|
|
485
|
-
ids_obj_to_send = send(message, object_tree)
|
|
492
|
+
ids_obj_to_send = send(message, object_tree, clientapp_runtime)
|
|
486
493
|
|
|
487
494
|
# Push object contents from the ObjectStore
|
|
488
495
|
run_id = message.metadata.run_id
|
|
@@ -539,7 +546,7 @@ def _init_connection( # pylint: disable=too-many-positional-arguments
|
|
|
539
546
|
tuple[
|
|
540
547
|
int,
|
|
541
548
|
Callable[[], tuple[Message, ObjectTree] | None],
|
|
542
|
-
Callable[[Message, ObjectTree], set[str]],
|
|
549
|
+
Callable[[Message, ObjectTree, float], set[str]],
|
|
543
550
|
Callable[[int], Run],
|
|
544
551
|
Callable[[str, int], Fab],
|
|
545
552
|
Callable[[int, str], bytes],
|
|
@@ -603,7 +610,7 @@ def _make_fleet_connection_retry_invoker(
|
|
|
603
610
|
connection_error_type: type[Exception] = RpcError,
|
|
604
611
|
) -> RetryInvoker:
|
|
605
612
|
"""Create a retry invoker for fleet connection."""
|
|
606
|
-
retry_invoker =
|
|
613
|
+
retry_invoker = make_simple_grpc_retry_invoker()
|
|
607
614
|
retry_invoker.recoverable_exceptions = connection_error_type
|
|
608
615
|
if max_retries is not None:
|
|
609
616
|
retry_invoker.max_tries = max_retries + 1
|