flwr-nightly 1.23.0.dev20251009__py3-none-any.whl → 1.23.0.dev20251011__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/cli/supernode/create.py +7 -7
- flwr/cli/supernode/ls.py +13 -4
- flwr/common/exit/exit_code.py +10 -0
- flwr/proto/control_pb2.py +6 -6
- flwr/proto/control_pb2.pyi +5 -0
- flwr/proto/node_pb2.py +2 -2
- flwr/proto/node_pb2.pyi +14 -5
- flwr/server/superlink/fleet/vce/vce_api.py +3 -0
- flwr/server/superlink/linkstate/in_memory_linkstate.py +70 -23
- flwr/server/superlink/linkstate/linkstate.py +34 -0
- flwr/server/superlink/linkstate/sqlite_linkstate.py +134 -53
- flwr/supercore/constant.py +15 -0
- flwr/superlink/servicer/control/control_servicer.py +73 -47
- {flwr_nightly-1.23.0.dev20251009.dist-info → flwr_nightly-1.23.0.dev20251011.dist-info}/METADATA +1 -1
- {flwr_nightly-1.23.0.dev20251009.dist-info → flwr_nightly-1.23.0.dev20251011.dist-info}/RECORD +17 -17
- {flwr_nightly-1.23.0.dev20251009.dist-info → flwr_nightly-1.23.0.dev20251011.dist-info}/WHEEL +0 -0
- {flwr_nightly-1.23.0.dev20251009.dist-info → flwr_nightly-1.23.0.dev20251011.dist-info}/entry_points.txt +0 -0
flwr/cli/supernode/create.py
CHANGED
@@ -21,6 +21,7 @@ from pathlib import Path
|
|
21
21
|
from typing import Annotated, Optional
|
22
22
|
|
23
23
|
import typer
|
24
|
+
from cryptography.exceptions import UnsupportedAlgorithm
|
24
25
|
from cryptography.hazmat.primitives import serialization
|
25
26
|
from cryptography.hazmat.primitives.asymmetric import ec
|
26
27
|
from rich.console import Console
|
@@ -32,6 +33,7 @@ from flwr.cli.config_utils import (
|
|
32
33
|
validate_federation_in_project_config,
|
33
34
|
)
|
34
35
|
from flwr.common.constant import FAB_CONFIG_FILE, CliOutputFormat
|
36
|
+
from flwr.common.exit import ExitCode, flwr_exit
|
35
37
|
from flwr.common.logger import print_json_error, redirect_output, restore_output
|
36
38
|
from flwr.proto.control_pb2 import ( # pylint: disable=E0611
|
37
39
|
CreateNodeCliRequest,
|
@@ -163,7 +165,7 @@ def try_load_public_key(public_key_path: Path) -> bytes:
|
|
163
165
|
|
164
166
|
with open(public_key_path, "rb") as key_file:
|
165
167
|
try:
|
166
|
-
public_key = serialization.
|
168
|
+
public_key = serialization.load_ssh_public_key(key_file.read())
|
167
169
|
|
168
170
|
if not isinstance(public_key, ec.EllipticCurvePublicKey):
|
169
171
|
raise ValueError(f"Not an EC public key, got {type(public_key)}")
|
@@ -174,11 +176,9 @@ def try_load_public_key(public_key_path: Path) -> bytes:
|
|
174
176
|
f"EC curve {public_key.curve.name} is not an approved NIST curve"
|
175
177
|
)
|
176
178
|
|
177
|
-
except ValueError as err:
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
bold=True,
|
179
|
+
except (ValueError, UnsupportedAlgorithm) as err:
|
180
|
+
flwr_exit(
|
181
|
+
ExitCode.FLWRCLI_NODE_AUTH_PUBLIC_KEY_INVALID,
|
182
|
+
str(err),
|
182
183
|
)
|
183
|
-
raise typer.Exit(code=1) from err
|
184
184
|
return public_key_to_bytes(public_key)
|
flwr/cli/supernode/ls.py
CHANGED
@@ -47,7 +47,7 @@ from ..utils import flwr_cli_grpc_exc_handler, init_channel, load_cli_auth_plugi
|
|
47
47
|
_NodeListType = tuple[int, str, str, str, str, str, str, str]
|
48
48
|
|
49
49
|
|
50
|
-
def ls( # pylint: disable=R0914
|
50
|
+
def ls( # pylint: disable=R0914, R0913, R0917
|
51
51
|
ctx: typer.Context,
|
52
52
|
app: Annotated[
|
53
53
|
Path,
|
@@ -73,6 +73,13 @@ def ls( # pylint: disable=R0914
|
|
73
73
|
help="Enable verbose output",
|
74
74
|
),
|
75
75
|
] = False,
|
76
|
+
dry_run: Annotated[
|
77
|
+
bool,
|
78
|
+
typer.Option(
|
79
|
+
"--dry-run",
|
80
|
+
help="Simulate the command without contacting any SuperNodes",
|
81
|
+
),
|
82
|
+
] = False,
|
76
83
|
) -> None:
|
77
84
|
"""List SuperNodes in the federation."""
|
78
85
|
# Resolve command used (list or ls)
|
@@ -98,7 +105,7 @@ def ls( # pylint: disable=R0914
|
|
98
105
|
channel = init_channel(app, federation_config, auth_plugin)
|
99
106
|
stub = ControlStub(channel)
|
100
107
|
typer.echo("📄 Listing all nodes...")
|
101
|
-
formatted_nodes = _list_nodes(stub)
|
108
|
+
formatted_nodes = _list_nodes(stub, dry_run=dry_run)
|
102
109
|
restore_output()
|
103
110
|
if output_format == CliOutputFormat.JSON:
|
104
111
|
Console().print_json(_to_json(formatted_nodes, verbose=verbose))
|
@@ -125,10 +132,12 @@ def ls( # pylint: disable=R0914
|
|
125
132
|
captured_output.close()
|
126
133
|
|
127
134
|
|
128
|
-
def _list_nodes(stub: ControlStub) -> list[_NodeListType]:
|
135
|
+
def _list_nodes(stub: ControlStub, dry_run: bool) -> list[_NodeListType]:
|
129
136
|
"""List all nodes."""
|
130
137
|
with flwr_cli_grpc_exc_handler():
|
131
|
-
res: ListNodesCliResponse = stub.ListNodesCli(
|
138
|
+
res: ListNodesCliResponse = stub.ListNodesCli(
|
139
|
+
ListNodesCliRequest(dry_run=dry_run)
|
140
|
+
)
|
132
141
|
|
133
142
|
return _format_nodes(list(res.nodes_info), res.now)
|
134
143
|
|
flwr/common/exit/exit_code.py
CHANGED
@@ -47,6 +47,9 @@ class ExitCode:
|
|
47
47
|
# SuperExec-specific exit codes (400-499)
|
48
48
|
SUPEREXEC_INVALID_PLUGIN_CONFIG = 400
|
49
49
|
|
50
|
+
# FlowerCLI-specific exit codes (500-599)
|
51
|
+
FLWRCLI_NODE_AUTH_PUBLIC_KEY_INVALID = 500
|
52
|
+
|
50
53
|
# Common exit codes (600-699)
|
51
54
|
COMMON_ADDRESS_INVALID = 600
|
52
55
|
COMMON_MISSING_EXTRA_REST = 601
|
@@ -116,6 +119,13 @@ EXIT_CODE_HELP = {
|
|
116
119
|
ExitCode.SUPEREXEC_INVALID_PLUGIN_CONFIG: (
|
117
120
|
"The YAML configuration for the SuperExec plugin is invalid."
|
118
121
|
),
|
122
|
+
# FlowerCLI-specific exit codes (500-599)
|
123
|
+
ExitCode.FLWRCLI_NODE_AUTH_PUBLIC_KEY_INVALID: (
|
124
|
+
"Node authentication requires a valid elliptic curve public key in the "
|
125
|
+
"SSH format and following a NIST standard elliptic curve (e.g. SECP384R1). "
|
126
|
+
"Please ensure that the file path points to a valid public key "
|
127
|
+
"file and try again."
|
128
|
+
),
|
119
129
|
# Common exit codes (600-699)
|
120
130
|
ExitCode.COMMON_ADDRESS_INVALID: (
|
121
131
|
"Please provide a valid URL, IPv4 or IPv6 address."
|
flwr/proto/control_pb2.py
CHANGED
@@ -19,7 +19,7 @@ from flwr.proto import run_pb2 as flwr_dot_proto_dot_run__pb2
|
|
19
19
|
from flwr.proto import node_pb2 as flwr_dot_proto_dot_node__pb2
|
20
20
|
|
21
21
|
|
22
|
-
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18\x66lwr/proto/control.proto\x12\nflwr.proto\x1a\x14\x66lwr/proto/fab.proto\x1a\x1a\x66lwr/proto/transport.proto\x1a\x1b\x66lwr/proto/recorddict.proto\x1a\x14\x66lwr/proto/run.proto\x1a\x15\x66lwr/proto/node.proto\"\xfa\x01\n\x0fStartRunRequest\x12\x1c\n\x03\x66\x61\x62\x18\x01 \x01(\x0b\x32\x0f.flwr.proto.Fab\x12H\n\x0foverride_config\x18\x02 \x03(\x0b\x32/.flwr.proto.StartRunRequest.OverrideConfigEntry\x12\x34\n\x12\x66\x65\x64\x65ration_options\x18\x03 \x01(\x0b\x32\x18.flwr.proto.ConfigRecord\x1aI\n\x13OverrideConfigEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12!\n\x05value\x18\x02 \x01(\x0b\x32\x12.flwr.proto.Scalar:\x02\x38\x01\"2\n\x10StartRunResponse\x12\x13\n\x06run_id\x18\x01 \x01(\x04H\x00\x88\x01\x01\x42\t\n\x07_run_id\"<\n\x11StreamLogsRequest\x12\x0e\n\x06run_id\x18\x01 \x01(\x04\x12\x17\n\x0f\x61\x66ter_timestamp\x18\x02 \x01(\x01\"B\n\x12StreamLogsResponse\x12\x12\n\nlog_output\x18\x01 \x01(\t\x12\x18\n\x10latest_timestamp\x18\x02 \x01(\x01\"1\n\x0fListRunsRequest\x12\x13\n\x06run_id\x18\x01 \x01(\x04H\x00\x88\x01\x01\x42\t\n\x07_run_id\"\x9d\x01\n\x10ListRunsResponse\x12;\n\x08run_dict\x18\x01 \x03(\x0b\x32).flwr.proto.ListRunsResponse.RunDictEntry\x12\x0b\n\x03now\x18\x02 \x01(\t\x1a?\n\x0cRunDictEntry\x12\x0b\n\x03key\x18\x01 \x01(\x04\x12\x1e\n\x05value\x18\x02 \x01(\x0b\x32\x0f.flwr.proto.Run:\x02\x38\x01\"\x18\n\x16GetLoginDetailsRequest\"\x8b\x01\n\x17GetLoginDetailsResponse\x12\x12\n\nauthn_type\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65vice_code\x18\x02 \x01(\t\x12!\n\x19verification_uri_complete\x18\x03 \x01(\t\x12\x12\n\nexpires_in\x18\x04 \x01(\x03\x12\x10\n\x08interval\x18\x05 \x01(\x03\"+\n\x14GetAuthTokensRequest\x12\x13\n\x0b\x64\x65vice_code\x18\x01 \x01(\t\"D\n\x15GetAuthTokensResponse\x12\x14\n\x0c\x61\x63\x63\x65ss_token\x18\x01 \x01(\t\x12\x15\n\rrefresh_token\x18\x02 \x01(\t\" \n\x0eStopRunRequest\x12\x0e\n\x06run_id\x18\x01 \x01(\x04\"\"\n\x0fStopRunResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"&\n\x14PullArtifactsRequest\x12\x0e\n\x06run_id\x18\x01 \x01(\x04\"1\n\x15PullArtifactsResponse\x12\x10\n\x03url\x18\x01 \x01(\tH\x00\x88\x01\x01\x42\x06\n\x04_url\"*\n\x14\x43reateNodeCliRequest\x12\x12\n\npublic_key\x18\x01 \x01(\x0c\"9\n\x15\x43reateNodeCliResponse\x12\x14\n\x07node_id\x18\x01 \x01(\x04H\x00\x88\x01\x01\x42\n\n\x08_node_id\"\'\n\x14\x44\x65leteNodeCliRequest\x12\x0f\n\x07node_id\x18\x01 \x01(\x04\"\x17\n\x15\x44\x65leteNodeCliResponse\"\
|
22
|
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18\x66lwr/proto/control.proto\x12\nflwr.proto\x1a\x14\x66lwr/proto/fab.proto\x1a\x1a\x66lwr/proto/transport.proto\x1a\x1b\x66lwr/proto/recorddict.proto\x1a\x14\x66lwr/proto/run.proto\x1a\x15\x66lwr/proto/node.proto\"\xfa\x01\n\x0fStartRunRequest\x12\x1c\n\x03\x66\x61\x62\x18\x01 \x01(\x0b\x32\x0f.flwr.proto.Fab\x12H\n\x0foverride_config\x18\x02 \x03(\x0b\x32/.flwr.proto.StartRunRequest.OverrideConfigEntry\x12\x34\n\x12\x66\x65\x64\x65ration_options\x18\x03 \x01(\x0b\x32\x18.flwr.proto.ConfigRecord\x1aI\n\x13OverrideConfigEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12!\n\x05value\x18\x02 \x01(\x0b\x32\x12.flwr.proto.Scalar:\x02\x38\x01\"2\n\x10StartRunResponse\x12\x13\n\x06run_id\x18\x01 \x01(\x04H\x00\x88\x01\x01\x42\t\n\x07_run_id\"<\n\x11StreamLogsRequest\x12\x0e\n\x06run_id\x18\x01 \x01(\x04\x12\x17\n\x0f\x61\x66ter_timestamp\x18\x02 \x01(\x01\"B\n\x12StreamLogsResponse\x12\x12\n\nlog_output\x18\x01 \x01(\t\x12\x18\n\x10latest_timestamp\x18\x02 \x01(\x01\"1\n\x0fListRunsRequest\x12\x13\n\x06run_id\x18\x01 \x01(\x04H\x00\x88\x01\x01\x42\t\n\x07_run_id\"\x9d\x01\n\x10ListRunsResponse\x12;\n\x08run_dict\x18\x01 \x03(\x0b\x32).flwr.proto.ListRunsResponse.RunDictEntry\x12\x0b\n\x03now\x18\x02 \x01(\t\x1a?\n\x0cRunDictEntry\x12\x0b\n\x03key\x18\x01 \x01(\x04\x12\x1e\n\x05value\x18\x02 \x01(\x0b\x32\x0f.flwr.proto.Run:\x02\x38\x01\"\x18\n\x16GetLoginDetailsRequest\"\x8b\x01\n\x17GetLoginDetailsResponse\x12\x12\n\nauthn_type\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65vice_code\x18\x02 \x01(\t\x12!\n\x19verification_uri_complete\x18\x03 \x01(\t\x12\x12\n\nexpires_in\x18\x04 \x01(\x03\x12\x10\n\x08interval\x18\x05 \x01(\x03\"+\n\x14GetAuthTokensRequest\x12\x13\n\x0b\x64\x65vice_code\x18\x01 \x01(\t\"D\n\x15GetAuthTokensResponse\x12\x14\n\x0c\x61\x63\x63\x65ss_token\x18\x01 \x01(\t\x12\x15\n\rrefresh_token\x18\x02 \x01(\t\" \n\x0eStopRunRequest\x12\x0e\n\x06run_id\x18\x01 \x01(\x04\"\"\n\x0fStopRunResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"&\n\x14PullArtifactsRequest\x12\x0e\n\x06run_id\x18\x01 \x01(\x04\"1\n\x15PullArtifactsResponse\x12\x10\n\x03url\x18\x01 \x01(\tH\x00\x88\x01\x01\x42\x06\n\x04_url\"*\n\x14\x43reateNodeCliRequest\x12\x12\n\npublic_key\x18\x01 \x01(\x0c\"9\n\x15\x43reateNodeCliResponse\x12\x14\n\x07node_id\x18\x01 \x01(\x04H\x00\x88\x01\x01\x42\n\n\x08_node_id\"\'\n\x14\x44\x65leteNodeCliRequest\x12\x0f\n\x07node_id\x18\x01 \x01(\x04\"\x17\n\x15\x44\x65leteNodeCliResponse\"&\n\x13ListNodesCliRequest\x12\x0f\n\x07\x64ry_run\x18\x01 \x01(\x08\"M\n\x14ListNodesCliResponse\x12(\n\nnodes_info\x18\x01 \x03(\x0b\x32\x14.flwr.proto.NodeInfo\x12\x0b\n\x03now\x18\x02 \x01(\t2\xc5\x06\n\x07\x43ontrol\x12G\n\x08StartRun\x12\x1b.flwr.proto.StartRunRequest\x1a\x1c.flwr.proto.StartRunResponse\"\x00\x12\x44\n\x07StopRun\x12\x1a.flwr.proto.StopRunRequest\x1a\x1b.flwr.proto.StopRunResponse\"\x00\x12O\n\nStreamLogs\x12\x1d.flwr.proto.StreamLogsRequest\x1a\x1e.flwr.proto.StreamLogsResponse\"\x00\x30\x01\x12G\n\x08ListRuns\x12\x1b.flwr.proto.ListRunsRequest\x1a\x1c.flwr.proto.ListRunsResponse\"\x00\x12\\\n\x0fGetLoginDetails\x12\".flwr.proto.GetLoginDetailsRequest\x1a#.flwr.proto.GetLoginDetailsResponse\"\x00\x12V\n\rGetAuthTokens\x12 .flwr.proto.GetAuthTokensRequest\x1a!.flwr.proto.GetAuthTokensResponse\"\x00\x12V\n\rPullArtifacts\x12 .flwr.proto.PullArtifactsRequest\x1a!.flwr.proto.PullArtifactsResponse\"\x00\x12V\n\rCreateNodeCli\x12 .flwr.proto.CreateNodeCliRequest\x1a!.flwr.proto.CreateNodeCliResponse\"\x00\x12V\n\rDeleteNodeCli\x12 .flwr.proto.DeleteNodeCliRequest\x1a!.flwr.proto.DeleteNodeCliResponse\"\x00\x12S\n\x0cListNodesCli\x12\x1f.flwr.proto.ListNodesCliRequest\x1a .flwr.proto.ListNodesCliResponse\"\x00\x62\x06proto3')
|
23
23
|
|
24
24
|
_globals = globals()
|
25
25
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
@@ -71,9 +71,9 @@ if _descriptor._USE_C_DESCRIPTORS == False:
|
|
71
71
|
_globals['_DELETENODECLIRESPONSE']._serialized_start=1398
|
72
72
|
_globals['_DELETENODECLIRESPONSE']._serialized_end=1421
|
73
73
|
_globals['_LISTNODESCLIREQUEST']._serialized_start=1423
|
74
|
-
_globals['_LISTNODESCLIREQUEST']._serialized_end=
|
75
|
-
_globals['_LISTNODESCLIRESPONSE']._serialized_start=
|
76
|
-
_globals['_LISTNODESCLIRESPONSE']._serialized_end=
|
77
|
-
_globals['_CONTROL']._serialized_start=
|
78
|
-
_globals['_CONTROL']._serialized_end=
|
74
|
+
_globals['_LISTNODESCLIREQUEST']._serialized_end=1461
|
75
|
+
_globals['_LISTNODESCLIRESPONSE']._serialized_start=1463
|
76
|
+
_globals['_LISTNODESCLIRESPONSE']._serialized_end=1540
|
77
|
+
_globals['_CONTROL']._serialized_start=1543
|
78
|
+
_globals['_CONTROL']._serialized_end=2380
|
79
79
|
# @@protoc_insertion_point(module_scope)
|
flwr/proto/control_pb2.pyi
CHANGED
@@ -279,8 +279,13 @@ global___DeleteNodeCliResponse = DeleteNodeCliResponse
|
|
279
279
|
|
280
280
|
class ListNodesCliRequest(google.protobuf.message.Message):
|
281
281
|
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
282
|
+
DRY_RUN_FIELD_NUMBER: builtins.int
|
283
|
+
dry_run: builtins.bool
|
282
284
|
def __init__(self,
|
285
|
+
*,
|
286
|
+
dry_run: builtins.bool = ...,
|
283
287
|
) -> None: ...
|
288
|
+
def ClearField(self, field_name: typing_extensions.Literal["dry_run",b"dry_run"]) -> None: ...
|
284
289
|
global___ListNodesCliRequest = ListNodesCliRequest
|
285
290
|
|
286
291
|
class ListNodesCliResponse(google.protobuf.message.Message):
|
flwr/proto/node_pb2.py
CHANGED
@@ -14,7 +14,7 @@ _sym_db = _symbol_database.Default()
|
|
14
14
|
|
15
15
|
|
16
16
|
|
17
|
-
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x66lwr/proto/node.proto\x12\nflwr.proto\"\x17\n\x04Node\x12\x0f\n\x07node_id\x18\x01 \x01(\x04\"\
|
17
|
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x66lwr/proto/node.proto\x12\nflwr.proto\"\x17\n\x04Node\x12\x0f\n\x07node_id\x18\x01 \x01(\x04\"\xc6\x02\n\x08NodeInfo\x12\x0f\n\x07node_id\x18\x01 \x01(\x04\x12\x11\n\towner_aid\x18\x02 \x01(\t\x12\x0e\n\x06status\x18\x03 \x01(\t\x12\x12\n\ncreated_at\x18\x04 \x01(\t\x12\x1e\n\x11last_activated_at\x18\x05 \x01(\tH\x00\x88\x01\x01\x12 \n\x13last_deactivated_at\x18\x06 \x01(\tH\x01\x88\x01\x01\x12\x17\n\ndeleted_at\x18\x07 \x01(\tH\x02\x88\x01\x01\x12\x19\n\x0conline_until\x18\x08 \x01(\x01H\x03\x88\x01\x01\x12\x1a\n\x12heartbeat_interval\x18\t \x01(\x01\x12\x12\n\npublic_key\x18\n \x01(\x0c\x42\x14\n\x12_last_activated_atB\x16\n\x14_last_deactivated_atB\r\n\x0b_deleted_atB\x0f\n\r_online_untilb\x06proto3')
|
18
18
|
|
19
19
|
_globals = globals()
|
20
20
|
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
@@ -24,5 +24,5 @@ if _descriptor._USE_C_DESCRIPTORS == False:
|
|
24
24
|
_globals['_NODE']._serialized_start=37
|
25
25
|
_globals['_NODE']._serialized_end=60
|
26
26
|
_globals['_NODEINFO']._serialized_start=63
|
27
|
-
_globals['_NODEINFO']._serialized_end=
|
27
|
+
_globals['_NODEINFO']._serialized_end=389
|
28
28
|
# @@protoc_insertion_point(module_scope)
|
flwr/proto/node_pb2.pyi
CHANGED
@@ -49,12 +49,21 @@ class NodeInfo(google.protobuf.message.Message):
|
|
49
49
|
owner_aid: typing.Text = ...,
|
50
50
|
status: typing.Text = ...,
|
51
51
|
created_at: typing.Text = ...,
|
52
|
-
last_activated_at: typing.Text = ...,
|
53
|
-
last_deactivated_at: typing.Text = ...,
|
54
|
-
deleted_at: typing.Text = ...,
|
55
|
-
online_until: builtins.float = ...,
|
52
|
+
last_activated_at: typing.Optional[typing.Text] = ...,
|
53
|
+
last_deactivated_at: typing.Optional[typing.Text] = ...,
|
54
|
+
deleted_at: typing.Optional[typing.Text] = ...,
|
55
|
+
online_until: typing.Optional[builtins.float] = ...,
|
56
56
|
heartbeat_interval: builtins.float = ...,
|
57
57
|
public_key: builtins.bytes = ...,
|
58
58
|
) -> None: ...
|
59
|
-
def
|
59
|
+
def HasField(self, field_name: typing_extensions.Literal["_deleted_at",b"_deleted_at","_last_activated_at",b"_last_activated_at","_last_deactivated_at",b"_last_deactivated_at","_online_until",b"_online_until","deleted_at",b"deleted_at","last_activated_at",b"last_activated_at","last_deactivated_at",b"last_deactivated_at","online_until",b"online_until"]) -> builtins.bool: ...
|
60
|
+
def ClearField(self, field_name: typing_extensions.Literal["_deleted_at",b"_deleted_at","_last_activated_at",b"_last_activated_at","_last_deactivated_at",b"_last_deactivated_at","_online_until",b"_online_until","created_at",b"created_at","deleted_at",b"deleted_at","heartbeat_interval",b"heartbeat_interval","last_activated_at",b"last_activated_at","last_deactivated_at",b"last_deactivated_at","node_id",b"node_id","online_until",b"online_until","owner_aid",b"owner_aid","public_key",b"public_key","status",b"status"]) -> None: ...
|
61
|
+
@typing.overload
|
62
|
+
def WhichOneof(self, oneof_group: typing_extensions.Literal["_deleted_at",b"_deleted_at"]) -> typing.Optional[typing_extensions.Literal["deleted_at"]]: ...
|
63
|
+
@typing.overload
|
64
|
+
def WhichOneof(self, oneof_group: typing_extensions.Literal["_last_activated_at",b"_last_activated_at"]) -> typing.Optional[typing_extensions.Literal["last_activated_at"]]: ...
|
65
|
+
@typing.overload
|
66
|
+
def WhichOneof(self, oneof_group: typing_extensions.Literal["_last_deactivated_at",b"_last_deactivated_at"]) -> typing.Optional[typing_extensions.Literal["last_deactivated_at"]]: ...
|
67
|
+
@typing.overload
|
68
|
+
def WhichOneof(self, oneof_group: typing_extensions.Literal["_online_until",b"_online_until"]) -> typing.Optional[typing_extensions.Literal["online_until"]]: ...
|
60
69
|
global___NodeInfo = NodeInfo
|
@@ -63,6 +63,9 @@ def _register_nodes(
|
|
63
63
|
secrets.token_bytes(32),
|
64
64
|
heartbeat_interval=HEARTBEAT_MAX_INTERVAL,
|
65
65
|
)
|
66
|
+
state.acknowledge_node_heartbeat(
|
67
|
+
node_id=node_id, heartbeat_interval=HEARTBEAT_MAX_INTERVAL
|
68
|
+
)
|
66
69
|
nodes_mapping[node_id] = i
|
67
70
|
log(DEBUG, "Registered %i nodes", len(nodes_mapping))
|
68
71
|
return nodes_mapping
|
@@ -19,7 +19,9 @@ import secrets
|
|
19
19
|
import threading
|
20
20
|
from bisect import bisect_right
|
21
21
|
from collections import defaultdict
|
22
|
+
from collections.abc import Sequence
|
22
23
|
from dataclasses import dataclass, field
|
24
|
+
from datetime import datetime
|
23
25
|
from logging import ERROR, WARNING
|
24
26
|
from typing import Optional
|
25
27
|
|
@@ -41,6 +43,7 @@ from flwr.common.typing import Run, RunStatus, UserConfig
|
|
41
43
|
from flwr.proto.node_pb2 import NodeInfo # pylint: disable=E0611
|
42
44
|
from flwr.server.superlink.linkstate.linkstate import LinkState
|
43
45
|
from flwr.server.utils import validate_message
|
46
|
+
from flwr.supercore.constant import NodeStatus
|
44
47
|
|
45
48
|
from .utils import (
|
46
49
|
check_node_availability_for_in_message,
|
@@ -346,17 +349,16 @@ class InMemoryLinkState(LinkState): # pylint: disable=R0902,R0904
|
|
346
349
|
if public_key in self.registered_node_public_keys:
|
347
350
|
raise ValueError("Public key already in use")
|
348
351
|
|
349
|
-
#
|
350
|
-
current = now()
|
352
|
+
# The node is not activated upon creation
|
351
353
|
self.nodes[node_id] = NodeInfo(
|
352
354
|
node_id=node_id,
|
353
|
-
owner_aid=owner_aid,
|
354
|
-
status=
|
355
|
-
created_at=
|
356
|
-
last_activated_at=
|
357
|
-
last_deactivated_at=
|
358
|
-
deleted_at=
|
359
|
-
online_until=
|
355
|
+
owner_aid=owner_aid,
|
356
|
+
status=NodeStatus.CREATED,
|
357
|
+
created_at=now().isoformat(),
|
358
|
+
last_activated_at=None,
|
359
|
+
last_deactivated_at=None,
|
360
|
+
deleted_at=None,
|
361
|
+
online_until=None,
|
360
362
|
heartbeat_interval=heartbeat_interval,
|
361
363
|
public_key=public_key,
|
362
364
|
)
|
@@ -367,13 +369,18 @@ class InMemoryLinkState(LinkState): # pylint: disable=R0902,R0904
|
|
367
369
|
def delete_node(self, owner_aid: str, node_id: int) -> None:
|
368
370
|
"""Delete a node."""
|
369
371
|
with self.lock:
|
370
|
-
if
|
372
|
+
if (
|
373
|
+
not (node := self.nodes.get(node_id))
|
374
|
+
or node.status == NodeStatus.DELETED
|
375
|
+
or owner_aid != self.nodes[node_id].owner_aid
|
376
|
+
):
|
371
377
|
raise ValueError(
|
372
|
-
f"Node ID {node_id} not found or unauthorized
|
378
|
+
f"Node ID {node_id} already deleted, not found or unauthorized "
|
379
|
+
"deletion attempt."
|
373
380
|
)
|
374
381
|
|
375
|
-
node =
|
376
|
-
|
382
|
+
node.status = NodeStatus.DELETED
|
383
|
+
node.deleted_at = now().isoformat()
|
377
384
|
|
378
385
|
def get_nodes(self, run_id: int) -> set[int]:
|
379
386
|
"""Return all available nodes.
|
@@ -386,19 +393,51 @@ class InMemoryLinkState(LinkState): # pylint: disable=R0902,R0904
|
|
386
393
|
with self.lock:
|
387
394
|
if run_id not in self.run_ids:
|
388
395
|
return set()
|
389
|
-
current_time = now().timestamp()
|
390
396
|
return {
|
391
|
-
|
392
|
-
for
|
393
|
-
if info.online_until > current_time
|
397
|
+
node.node_id
|
398
|
+
for node in self.get_node_info(statuses=[NodeStatus.ONLINE])
|
394
399
|
}
|
395
400
|
|
401
|
+
def get_node_info(
|
402
|
+
self,
|
403
|
+
*,
|
404
|
+
node_ids: Optional[Sequence[int]] = None,
|
405
|
+
owner_aids: Optional[Sequence[str]] = None,
|
406
|
+
statuses: Optional[Sequence[str]] = None,
|
407
|
+
) -> Sequence[NodeInfo]:
|
408
|
+
"""Retrieve information about nodes based on the specified filters."""
|
409
|
+
with self.lock:
|
410
|
+
self._check_and_tag_deactivated_nodes()
|
411
|
+
result = []
|
412
|
+
for node in self.nodes.values():
|
413
|
+
if node_ids is not None and node.node_id not in node_ids:
|
414
|
+
continue
|
415
|
+
if owner_aids is not None and node.owner_aid not in owner_aids:
|
416
|
+
continue
|
417
|
+
if statuses is not None and node.status not in statuses:
|
418
|
+
continue
|
419
|
+
result.append(node)
|
420
|
+
return result
|
421
|
+
|
422
|
+
def _check_and_tag_deactivated_nodes(self) -> None:
|
423
|
+
with self.lock:
|
424
|
+
# Set all nodes of "online" status to "offline" if they've offline
|
425
|
+
current_ts = now().timestamp()
|
426
|
+
for node in self.nodes.values():
|
427
|
+
if node.status == NodeStatus.ONLINE:
|
428
|
+
if node.online_until <= current_ts:
|
429
|
+
node.status = NodeStatus.OFFLINE
|
430
|
+
node.last_deactivated_at = datetime.fromtimestamp(
|
431
|
+
node.online_until
|
432
|
+
).isoformat()
|
433
|
+
|
396
434
|
def get_node_public_key(self, node_id: int) -> bytes:
|
397
435
|
"""Get `public_key` for the specified `node_id`."""
|
398
436
|
with self.lock:
|
399
|
-
if (
|
437
|
+
if (
|
438
|
+
node := self.nodes.get(node_id)
|
439
|
+
) is None or node.status == NodeStatus.DELETED:
|
400
440
|
raise ValueError(f"Node ID {node_id} not found")
|
401
|
-
|
402
441
|
return node.public_key
|
403
442
|
|
404
443
|
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
@@ -606,11 +645,19 @@ class InMemoryLinkState(LinkState): # pylint: disable=R0902,R0904
|
|
606
645
|
the node is marked as offline.
|
607
646
|
"""
|
608
647
|
with self.lock:
|
609
|
-
if
|
610
|
-
|
611
|
-
|
648
|
+
if (node := self.nodes.get(node_id)) and node.status != NodeStatus.DELETED:
|
649
|
+
current_dt = now()
|
650
|
+
|
651
|
+
# Set timestamp if the status changes
|
652
|
+
if node.status != NodeStatus.ONLINE: # offline or created
|
653
|
+
node.status = NodeStatus.ONLINE
|
654
|
+
node.last_activated_at = current_dt.isoformat()
|
655
|
+
|
656
|
+
# Refresh `online_until` and `heartbeat_interval`
|
657
|
+
node.online_until = (
|
658
|
+
current_dt.timestamp() + HEARTBEAT_PATIENCE * heartbeat_interval
|
612
659
|
)
|
613
|
-
|
660
|
+
node.heartbeat_interval = heartbeat_interval
|
614
661
|
return True
|
615
662
|
return False
|
616
663
|
|
@@ -16,11 +16,13 @@
|
|
16
16
|
|
17
17
|
|
18
18
|
import abc
|
19
|
+
from collections.abc import Sequence
|
19
20
|
from typing import Optional
|
20
21
|
|
21
22
|
from flwr.common import Context, Message
|
22
23
|
from flwr.common.record import ConfigRecord
|
23
24
|
from flwr.common.typing import Run, RunStatus, UserConfig
|
25
|
+
from flwr.proto.node_pb2 import NodeInfo # pylint: disable=E0611
|
24
26
|
from flwr.supercore.corestate import CoreState
|
25
27
|
|
26
28
|
|
@@ -147,6 +149,38 @@ class LinkState(CoreState): # pylint: disable=R0904
|
|
147
149
|
an empty `Set` MUST be returned.
|
148
150
|
"""
|
149
151
|
|
152
|
+
@abc.abstractmethod
|
153
|
+
def get_node_info(
|
154
|
+
self,
|
155
|
+
*,
|
156
|
+
node_ids: Optional[Sequence[int]] = None,
|
157
|
+
owner_aids: Optional[Sequence[str]] = None,
|
158
|
+
statuses: Optional[Sequence[str]] = None,
|
159
|
+
) -> Sequence[NodeInfo]:
|
160
|
+
"""Retrieve information about nodes based on the specified filters.
|
161
|
+
|
162
|
+
If a filter is set to None, it is ignored.
|
163
|
+
If multiple filters are provided, they are combined using AND logic.
|
164
|
+
|
165
|
+
Parameters
|
166
|
+
----------
|
167
|
+
node_ids : Optional[Sequence[int]] (default: None)
|
168
|
+
Sequence of node IDs to filter by. If a sequence is provided,
|
169
|
+
it is treated as an OR condition.
|
170
|
+
owner_aids : Optional[Sequence[str]] (default: None)
|
171
|
+
Sequence of owner account IDs to filter by. If a sequence is provided,
|
172
|
+
it is treated as an OR condition.
|
173
|
+
statuses : Optional[Sequence[str]] (default: None)
|
174
|
+
Sequence of node status values (e.g., "created", "activated")
|
175
|
+
to filter by. If a sequence is provided, it is treated as an OR condition.
|
176
|
+
|
177
|
+
Returns
|
178
|
+
-------
|
179
|
+
Sequence[NodeInfo]
|
180
|
+
A sequence of NodeInfo objects representing the nodes matching
|
181
|
+
the specified filters.
|
182
|
+
"""
|
183
|
+
|
150
184
|
@abc.abstractmethod
|
151
185
|
def get_node_public_key(self, node_id: int) -> bytes:
|
152
186
|
"""Get `public_key` for the specified `node_id`.
|
@@ -46,10 +46,12 @@ from flwr.common.typing import Run, RunStatus, UserConfig
|
|
46
46
|
|
47
47
|
# pylint: disable=E0611
|
48
48
|
from flwr.proto.error_pb2 import Error as ProtoError
|
49
|
+
from flwr.proto.node_pb2 import NodeInfo
|
49
50
|
from flwr.proto.recorddict_pb2 import RecordDict as ProtoRecordDict
|
50
51
|
|
51
52
|
# pylint: enable=E0611
|
52
53
|
from flwr.server.utils.validator import validate_message
|
54
|
+
from flwr.supercore.constant import NodeStatus
|
53
55
|
|
54
56
|
from .linkstate import LinkState
|
55
57
|
from .utils import (
|
@@ -75,10 +77,10 @@ CREATE TABLE IF NOT EXISTS node(
|
|
75
77
|
owner_aid TEXT,
|
76
78
|
status TEXT,
|
77
79
|
created_at TEXT,
|
78
|
-
last_activated_at TEXT,
|
79
|
-
last_deactivated_at TEXT,
|
80
|
-
deleted_at TEXT,
|
81
|
-
online_until
|
80
|
+
last_activated_at TEXT NULL,
|
81
|
+
last_deactivated_at TEXT NULL,
|
82
|
+
deleted_at TEXT NULL,
|
83
|
+
online_until TIMESTAMP NULL,
|
82
84
|
heartbeat_interval REAL,
|
83
85
|
public_key BLOB UNIQUE
|
84
86
|
);
|
@@ -633,13 +635,13 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
|
|
633
635
|
query,
|
634
636
|
(
|
635
637
|
sint64_node_id, # node_id
|
636
|
-
owner_aid, # owner_aid
|
637
|
-
|
638
|
-
now().isoformat(), # created_at
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
638
|
+
owner_aid, # owner_aid
|
639
|
+
NodeStatus.CREATED, # status
|
640
|
+
now().isoformat(), # created_at
|
641
|
+
None, # last_activated_at
|
642
|
+
None, # last_deactivated_at
|
643
|
+
None, # deleted_at
|
644
|
+
None, # online_until, initialized with offline status
|
643
645
|
heartbeat_interval, # heartbeat_interval
|
644
646
|
public_key, # public_key
|
645
647
|
),
|
@@ -656,24 +658,28 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
|
|
656
658
|
|
657
659
|
def delete_node(self, owner_aid: str, node_id: int) -> None:
|
658
660
|
"""Delete a node."""
|
659
|
-
# Convert the uint64 value to sint64 for SQLite
|
660
661
|
sint64_node_id = convert_uint64_to_sint64(node_id)
|
661
662
|
|
662
|
-
query = "
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
663
|
+
query = """
|
664
|
+
UPDATE node
|
665
|
+
SET status = ?, deleted_at = ?
|
666
|
+
WHERE node_id = ? AND status != ? AND owner_aid = ?
|
667
|
+
RETURNING node_id
|
668
|
+
"""
|
669
|
+
params = (
|
670
|
+
NodeStatus.DELETED,
|
671
|
+
now().isoformat(),
|
672
|
+
sint64_node_id,
|
673
|
+
NodeStatus.DELETED,
|
674
|
+
owner_aid,
|
675
|
+
)
|
667
676
|
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
)
|
675
|
-
except KeyError as exc:
|
676
|
-
log(ERROR, {"query": query, "data": params, "exception": exc})
|
677
|
+
rows = self.query(query, params)
|
678
|
+
if not rows:
|
679
|
+
raise ValueError(
|
680
|
+
f"Node {node_id} already deleted, not found or unauthorized "
|
681
|
+
"deletion attempt."
|
682
|
+
)
|
677
683
|
|
678
684
|
def get_nodes(self, run_id: int) -> set[int]:
|
679
685
|
"""Retrieve all currently stored node IDs as a set.
|
@@ -683,21 +689,84 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
|
|
683
689
|
If the provided `run_id` does not exist or has no matching nodes,
|
684
690
|
an empty `Set` MUST be returned.
|
685
691
|
"""
|
692
|
+
if self.conn is None:
|
693
|
+
raise AttributeError("LinkState not initialized")
|
694
|
+
|
686
695
|
# Convert the uint64 value to sint64 for SQLite
|
687
696
|
sint64_run_id = convert_uint64_to_sint64(run_id)
|
688
697
|
|
689
698
|
# Validate run ID
|
690
|
-
query = "SELECT COUNT(*) FROM run WHERE run_id =
|
691
|
-
|
699
|
+
query = "SELECT COUNT(*) FROM run WHERE run_id = ?"
|
700
|
+
rows = self.query(query, (sint64_run_id,))
|
701
|
+
if rows[0]["COUNT(*)"] == 0:
|
692
702
|
return set()
|
693
703
|
|
694
|
-
#
|
695
|
-
|
696
|
-
|
704
|
+
# Retrieve all online nodes
|
705
|
+
return {
|
706
|
+
node.node_id for node in self.get_node_info(statuses=[NodeStatus.ONLINE])
|
707
|
+
}
|
697
708
|
|
698
|
-
|
699
|
-
|
700
|
-
|
709
|
+
def get_node_info(
|
710
|
+
self,
|
711
|
+
*,
|
712
|
+
node_ids: Optional[Sequence[int]] = None,
|
713
|
+
owner_aids: Optional[Sequence[str]] = None,
|
714
|
+
statuses: Optional[Sequence[str]] = None,
|
715
|
+
) -> Sequence[NodeInfo]:
|
716
|
+
"""Retrieve information about nodes based on the specified filters."""
|
717
|
+
if self.conn is None:
|
718
|
+
raise AttributeError("LinkState is not initialized.")
|
719
|
+
|
720
|
+
with self.conn:
|
721
|
+
# Check and tag offline nodes
|
722
|
+
current_dt = now()
|
723
|
+
# strftime will convert POSIX timestamp to ISO format
|
724
|
+
query = """
|
725
|
+
UPDATE node SET status = ?,
|
726
|
+
last_deactivated_at =
|
727
|
+
strftime("%Y-%m-%dT%H:%M:%f+00:00", online_until, "unixepoch")
|
728
|
+
WHERE online_until <= ? AND status == ?
|
729
|
+
"""
|
730
|
+
params: list[Any] = [
|
731
|
+
NodeStatus.OFFLINE,
|
732
|
+
current_dt.timestamp(),
|
733
|
+
NodeStatus.ONLINE,
|
734
|
+
]
|
735
|
+
self.conn.execute(query, params)
|
736
|
+
|
737
|
+
# Build the WHERE clause based on provided filters
|
738
|
+
conditions = []
|
739
|
+
params = []
|
740
|
+
if node_ids is not None:
|
741
|
+
sint64_node_ids = [
|
742
|
+
convert_uint64_to_sint64(node_id) for node_id in node_ids
|
743
|
+
]
|
744
|
+
placeholders = ",".join(["?"] * len(sint64_node_ids))
|
745
|
+
conditions.append(f"node_id IN ({placeholders})")
|
746
|
+
params.extend(sint64_node_ids)
|
747
|
+
if owner_aids is not None:
|
748
|
+
placeholders = ",".join(["?"] * len(owner_aids))
|
749
|
+
conditions.append(f"owner_aid IN ({placeholders})")
|
750
|
+
params.extend(owner_aids)
|
751
|
+
if statuses is not None:
|
752
|
+
placeholders = ",".join(["?"] * len(statuses))
|
753
|
+
conditions.append(f"status IN ({placeholders})")
|
754
|
+
params.extend(statuses)
|
755
|
+
|
756
|
+
# Construct the final query
|
757
|
+
query = "SELECT * FROM node"
|
758
|
+
if conditions:
|
759
|
+
query += " WHERE " + " AND ".join(conditions)
|
760
|
+
|
761
|
+
rows = self.conn.execute(query, params).fetchall()
|
762
|
+
|
763
|
+
result: list[NodeInfo] = []
|
764
|
+
for row in rows:
|
765
|
+
# Convert sint64 node_id to uint64
|
766
|
+
row["node_id"] = convert_sint64_to_uint64(row["node_id"])
|
767
|
+
result.append(NodeInfo(**row))
|
768
|
+
|
769
|
+
return result
|
701
770
|
|
702
771
|
def get_node_public_key(self, node_id: int) -> bytes:
|
703
772
|
"""Get `public_key` for the specified `node_id`."""
|
@@ -705,8 +774,8 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
|
|
705
774
|
sint64_node_id = convert_uint64_to_sint64(node_id)
|
706
775
|
|
707
776
|
# Query the public key for the given node_id
|
708
|
-
query = "SELECT public_key FROM node WHERE node_id = ?"
|
709
|
-
rows = self.query(query, (sint64_node_id,))
|
777
|
+
query = "SELECT public_key FROM node WHERE node_id = ? AND status != ?;"
|
778
|
+
rows = self.query(query, (sint64_node_id, NodeStatus.DELETED))
|
710
779
|
|
711
780
|
# If no result is found, return None
|
712
781
|
if not rows:
|
@@ -989,26 +1058,38 @@ class SqliteLinkState(LinkState): # pylint: disable=R0904
|
|
989
1058
|
HEARTBEAT_PATIENCE = N allows for N-1 missed heartbeat before
|
990
1059
|
the node is marked as offline.
|
991
1060
|
"""
|
992
|
-
|
1061
|
+
if self.conn is None:
|
1062
|
+
raise AttributeError("LinkState not initialized")
|
993
1063
|
|
994
|
-
|
995
|
-
query = "SELECT 1 FROM node WHERE node_id = ?"
|
996
|
-
if not self.query(query, (sint64_node_id,)):
|
997
|
-
return False
|
1064
|
+
sint64_node_id = convert_uint64_to_sint64(node_id)
|
998
1065
|
|
999
|
-
|
1000
|
-
|
1001
|
-
"
|
1002
|
-
|
1003
|
-
|
1004
|
-
|
1005
|
-
|
1006
|
-
|
1066
|
+
with self.conn:
|
1067
|
+
# Check if node exists and not deleted
|
1068
|
+
query = "SELECT status FROM node WHERE node_id = ? AND status != ?"
|
1069
|
+
row = self.conn.execute(
|
1070
|
+
query, (sint64_node_id, NodeStatus.DELETED)
|
1071
|
+
).fetchone()
|
1072
|
+
if row is None:
|
1073
|
+
return False
|
1074
|
+
|
1075
|
+
# Construct query and params
|
1076
|
+
current_dt = now()
|
1077
|
+
query = "UPDATE node SET online_until = ?, heartbeat_interval = ?"
|
1078
|
+
params: list[Any] = [
|
1079
|
+
current_dt.timestamp() + HEARTBEAT_PATIENCE * heartbeat_interval,
|
1007
1080
|
heartbeat_interval,
|
1008
|
-
|
1009
|
-
|
1010
|
-
|
1011
|
-
|
1081
|
+
]
|
1082
|
+
|
1083
|
+
# Set timestamp if the status changes
|
1084
|
+
if row["status"] != NodeStatus.ONLINE:
|
1085
|
+
query += ", status = ?, last_activated_at = ?"
|
1086
|
+
params += [NodeStatus.ONLINE, current_dt.isoformat()]
|
1087
|
+
|
1088
|
+
# Execute the query, refreshing `online_until` and `heartbeat_interval`
|
1089
|
+
query += " WHERE node_id = ?"
|
1090
|
+
params += [sint64_node_id]
|
1091
|
+
self.conn.execute(query, params)
|
1092
|
+
return True
|
1012
1093
|
|
1013
1094
|
def acknowledge_app_heartbeat(self, run_id: int, heartbeat_interval: float) -> bool:
|
1014
1095
|
"""Acknowledge a heartbeat received from a ServerApp for a given run.
|
flwr/supercore/constant.py
CHANGED
@@ -15,5 +15,20 @@
|
|
15
15
|
"""Constants for Flower infrastructure."""
|
16
16
|
|
17
17
|
|
18
|
+
from __future__ import annotations
|
19
|
+
|
18
20
|
# Top-level key in YAML config for exec plugin settings
|
19
21
|
EXEC_PLUGIN_SECTION = "exec_plugin"
|
22
|
+
|
23
|
+
|
24
|
+
class NodeStatus:
|
25
|
+
"""Event log writer types."""
|
26
|
+
|
27
|
+
CREATED = "created"
|
28
|
+
ONLINE = "online"
|
29
|
+
OFFLINE = "offline"
|
30
|
+
DELETED = "deleted"
|
31
|
+
|
32
|
+
def __new__(cls) -> NodeStatus:
|
33
|
+
"""Prevent instantiation."""
|
34
|
+
raise TypeError(f"{cls.__name__} cannot be instantiated.")
|
@@ -17,7 +17,7 @@
|
|
17
17
|
|
18
18
|
import hashlib
|
19
19
|
import time
|
20
|
-
from collections.abc import Generator
|
20
|
+
from collections.abc import Generator, Sequence
|
21
21
|
from datetime import timedelta
|
22
22
|
from logging import ERROR, INFO
|
23
23
|
from typing import Any, Optional, cast
|
@@ -456,60 +456,86 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
|
|
456
456
|
"""List all SuperNodes."""
|
457
457
|
log(INFO, "ControlServicer.ListNodesCli")
|
458
458
|
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
owner_aid="owner_aid_1",
|
465
|
-
status="created",
|
466
|
-
created_at=(now()).isoformat(),
|
467
|
-
last_activated_at="",
|
468
|
-
last_deactivated_at="",
|
469
|
-
deleted_at="",
|
459
|
+
if self.is_simulation:
|
460
|
+
log(ERROR, "ListNodesCli is not available in simulation mode.")
|
461
|
+
context.abort(
|
462
|
+
grpc.StatusCode.UNIMPLEMENTED,
|
463
|
+
"ListNodesCli is not available in simulation mode.",
|
470
464
|
)
|
471
|
-
|
465
|
+
raise grpc.RpcError() # This line is unreachable
|
472
466
|
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
)
|
467
|
+
nodes_info: Sequence[NodeInfo] = []
|
468
|
+
# If dry run is enabled, create dummy NodeInfo data
|
469
|
+
if request.dry_run:
|
470
|
+
nodes_info = _create_list_nodeif_for_dry_run()
|
471
|
+
|
472
|
+
else:
|
473
|
+
# Init link state
|
474
|
+
state = self.linkstate_factory.state()
|
475
|
+
|
476
|
+
flwr_aid = shared_account_info.get().flwr_aid
|
477
|
+
flwr_aid = _check_flwr_aid_exists(flwr_aid, context)
|
478
|
+
# Retrieve all nodes for the account
|
479
|
+
nodes_info = state.get_node_info(owner_aids=[flwr_aid])
|
480
|
+
|
481
|
+
return ListNodesCliResponse(nodes_info=nodes_info, now=now().isoformat())
|
482
|
+
|
483
|
+
|
484
|
+
def _create_list_nodeif_for_dry_run() -> Sequence[NodeInfo]:
|
485
|
+
"""Create a list of NodeInfo for dry run testing."""
|
486
|
+
nodes_info: list[NodeInfo] = []
|
487
|
+
# A node created (but not connected)
|
488
|
+
nodes_info.append(
|
489
|
+
NodeInfo(
|
490
|
+
node_id=15390646978706312628,
|
491
|
+
owner_aid="owner_aid_1",
|
492
|
+
status="created",
|
493
|
+
created_at=(now()).isoformat(),
|
494
|
+
last_activated_at="",
|
495
|
+
last_deactivated_at="",
|
496
|
+
deleted_at="",
|
484
497
|
)
|
498
|
+
)
|
485
499
|
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
)
|
500
|
+
# A node created and connected
|
501
|
+
nodes_info.append(
|
502
|
+
NodeInfo(
|
503
|
+
node_id=2941141058168602545,
|
504
|
+
owner_aid="owner_aid_2",
|
505
|
+
status="online",
|
506
|
+
created_at=(now()).isoformat(),
|
507
|
+
last_activated_at=(now() + timedelta(hours=0.5)).isoformat(),
|
508
|
+
last_deactivated_at="",
|
509
|
+
deleted_at="",
|
497
510
|
)
|
511
|
+
)
|
498
512
|
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
)
|
513
|
+
# A node created and deleted (never connected)
|
514
|
+
nodes_info.append(
|
515
|
+
NodeInfo(
|
516
|
+
node_id=906971720890549292,
|
517
|
+
owner_aid="owner_aid_3",
|
518
|
+
status="deleted",
|
519
|
+
created_at=(now()).isoformat(),
|
520
|
+
last_activated_at="",
|
521
|
+
last_deactivated_at="",
|
522
|
+
deleted_at=(now() + timedelta(hours=1)).isoformat(),
|
510
523
|
)
|
524
|
+
)
|
511
525
|
|
512
|
-
|
526
|
+
# A node created, deactivate and then deleted
|
527
|
+
nodes_info.append(
|
528
|
+
NodeInfo(
|
529
|
+
node_id=1781174086018058152,
|
530
|
+
owner_aid="owner_aid_4",
|
531
|
+
status="offline",
|
532
|
+
created_at=(now()).isoformat(),
|
533
|
+
last_activated_at=(now() + timedelta(hours=0.5)).isoformat(),
|
534
|
+
last_deactivated_at=(now() + timedelta(hours=1)).isoformat(),
|
535
|
+
deleted_at=(now() + timedelta(hours=1.5)).isoformat(),
|
536
|
+
)
|
537
|
+
)
|
538
|
+
return nodes_info
|
513
539
|
|
514
540
|
|
515
541
|
def _create_list_runs_response(
|
{flwr_nightly-1.23.0.dev20251009.dist-info → flwr_nightly-1.23.0.dev20251011.dist-info}/METADATA
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: flwr-nightly
|
3
|
-
Version: 1.23.0.
|
3
|
+
Version: 1.23.0.dev20251011
|
4
4
|
Summary: Flower: A Friendly Federated AI Framework
|
5
5
|
License: Apache-2.0
|
6
6
|
Keywords: Artificial Intelligence,Federated AI,Federated Analytics,Federated Evaluation,Federated Learning,Flower,Machine Learning
|
{flwr_nightly-1.23.0.dev20251009.dist-info → flwr_nightly-1.23.0.dev20251011.dist-info}/RECORD
RENAMED
@@ -87,9 +87,9 @@ flwr/cli/run/__init__.py,sha256=RPyB7KbYTFl6YRiilCch6oezxrLQrl1kijV7BMGkLbA,790
|
|
87
87
|
flwr/cli/run/run.py,sha256=ED1mDmO1PnSAgtVOrCeWwzwPm6t3aFYSs3Rh36BJzqk,8161
|
88
88
|
flwr/cli/stop.py,sha256=W7ynTYLm0-_1nC5Il4IaZTji6A3GCCm_0R-HQUudAsI,4988
|
89
89
|
flwr/cli/supernode/__init__.py,sha256=DVrTcyCg9NFll6glPLAAA6WPi7boxu6pFY_PRqIyHMk,893
|
90
|
-
flwr/cli/supernode/create.py,sha256=
|
90
|
+
flwr/cli/supernode/create.py,sha256=9KvRO0IrZa4jw0sypAYxFlzzpjmnzf1KW71b-ySxeuI,6383
|
91
91
|
flwr/cli/supernode/delete.py,sha256=SmeKpvVtly8iisNpzJ-MNY-fe0gH2jwwilJs_NMo7do,4528
|
92
|
-
flwr/cli/supernode/ls.py,sha256=
|
92
|
+
flwr/cli/supernode/ls.py,sha256=WEjP-_-5LHzTFuEFIhvnEeFRSaIAndEOrYpX4Ds-ENM,8807
|
93
93
|
flwr/cli/utils.py,sha256=66RqZdF0DKsqmR4ZqE9zZ1bX6opdxD2U50GV41WQDoY,14864
|
94
94
|
flwr/client/__init__.py,sha256=Q0MIF442vLIGSkcwHKq_sIfECQynLARJrumAscq2Q6E,1241
|
95
95
|
flwr/client/client.py,sha256=3HAchxvknKG9jYbB7swNyDj-e5vUWDuMKoLvbT7jCVM,7895
|
@@ -136,7 +136,7 @@ flwr/common/event_log_plugin/__init__.py,sha256=ts3VAL3Fk6Grp1EK_1Qg_V-BfOof9F86
|
|
136
136
|
flwr/common/event_log_plugin/event_log_plugin.py,sha256=4SkVa1Ic-sPlICJShBuggXmXDcQtWQ1KDby4kthFNF0,2064
|
137
137
|
flwr/common/exit/__init__.py,sha256=8W7xaO1iw0vacgmQW7FTFbSh7csNv6XfsgIlnIbNF6U,978
|
138
138
|
flwr/common/exit/exit.py,sha256=DcXJfbpW1g-pQJqSZmps-1MZydd7T7RaarghIf2e4tU,3636
|
139
|
-
flwr/common/exit/exit_code.py,sha256=
|
139
|
+
flwr/common/exit/exit_code.py,sha256=g6g10X85mUlN9seFzXZeMNX8e3f2D290DZiF8p2HBAw,5885
|
140
140
|
flwr/common/exit/exit_handler.py,sha256=uzDdWwhKgc1w5csZS52b86kjmEApmDZKwMn_X0zDZZo,2126
|
141
141
|
flwr/common/exit/signal_handler.py,sha256=wqxykrwgmpFzmEMhpnlM7RtO0PnqIvYiSB1qYahZ5Sk,3710
|
142
142
|
flwr/common/grpc.py,sha256=nHnFC7E84pZVTvd6BhcSYWnGd0jf8t5UmGea04qvilM,9806
|
@@ -191,8 +191,8 @@ flwr/proto/clientappio_pb2.py,sha256=vJjzwWydhg7LruK8cvRAeVQeHPsJztgdIW9nyiPBZF0
|
|
191
191
|
flwr/proto/clientappio_pb2.pyi,sha256=XbFvpZvvrS7QcH5AFXfpRGl4hQvhd3QdKO6x0oTlCCU,165
|
192
192
|
flwr/proto/clientappio_pb2_grpc.py,sha256=iobNROP0qvn5zddx7k-uIi_dJWP3T_BRp_kbKq086i8,17550
|
193
193
|
flwr/proto/clientappio_pb2_grpc.pyi,sha256=Ytf1O1ktKB0Vsuc3AWLIErGjIJYokzKYzi2uA7mdMeg,4785
|
194
|
-
flwr/proto/control_pb2.py,sha256=
|
195
|
-
flwr/proto/control_pb2.pyi,sha256=
|
194
|
+
flwr/proto/control_pb2.py,sha256=5dPV4DfEVhAinofsDctelFSl2TTrjhf6nANU2j329IA,7783
|
195
|
+
flwr/proto/control_pb2.pyi,sha256=Vv3TssjFMGbq_LEECxrkOO-aY5xoPCXvO4Lu6ljYKqY,13600
|
196
196
|
flwr/proto/control_pb2_grpc.py,sha256=Of6tnetwnzvJfJWEXhuTFszz4T_IJbdLJD9uRXBdG04,17182
|
197
197
|
flwr/proto/control_pb2_grpc.pyi,sha256=9z_MRrexz0o3_owST8SvhCjQIIAn-SwMWKIihPHzqdM,4801
|
198
198
|
flwr/proto/error_pb2.py,sha256=PQVWrfjVPo88ql_KgV9nCxyQNCcV9PVfmcw7sOzTMro,1084
|
@@ -223,8 +223,8 @@ flwr/proto/message_pb2.py,sha256=giymevXYEUdpIO-3A0XKsmRabXW1xSz0sIo5oOlbQ8Y,519
|
|
223
223
|
flwr/proto/message_pb2.pyi,sha256=EzXZHy2mtabofrd_ZgKSI6M4QH-soIaRZIZBPwBGPv0,11260
|
224
224
|
flwr/proto/message_pb2_grpc.py,sha256=1oboBPFxaTEXt9Aw7EAj8gXHDCNMhZD2VXqocC9l_gk,159
|
225
225
|
flwr/proto/message_pb2_grpc.pyi,sha256=ff2TSiLVnG6IVQcTGzb2DIH3XRSoAvAo_RMcvbMFyc0,76
|
226
|
-
flwr/proto/node_pb2.py,sha256=
|
227
|
-
flwr/proto/node_pb2.pyi,sha256=
|
226
|
+
flwr/proto/node_pb2.py,sha256=LBIBkiR_TIgAz2RrGn6-wJKGGN8YxStQ3oIwsFB_qCQ,1739
|
227
|
+
flwr/proto/node_pb2.pyi,sha256=IYSwKID4QM80PK_UqkHf2zVH2pBJyfCeOBO4EBLTM74,3747
|
228
228
|
flwr/proto/node_pb2_grpc.py,sha256=1oboBPFxaTEXt9Aw7EAj8gXHDCNMhZD2VXqocC9l_gk,159
|
229
229
|
flwr/proto/node_pb2_grpc.pyi,sha256=ff2TSiLVnG6IVQcTGzb2DIH3XRSoAvAo_RMcvbMFyc0,76
|
230
230
|
flwr/proto/recorddict_pb2.py,sha256=eVkcnxMTFa3rvknRNiFuJ8z8xxPqgw7bV04aFiTe1j4,5290
|
@@ -315,12 +315,12 @@ flwr/server/superlink/fleet/vce/__init__.py,sha256=XOKbAWOzlCqEOQ3M2cBYkH7HKA7Px
|
|
315
315
|
flwr/server/superlink/fleet/vce/backend/__init__.py,sha256=PPH89Yqd1XKm-sRJN6R0WQlKT_b4v54Kzl2yzHAFzM8,1437
|
316
316
|
flwr/server/superlink/fleet/vce/backend/backend.py,sha256=cSrHZ5SjCCvy4vI0pgsyjtx3cDMuMQve8KcKkK-dWWo,2196
|
317
317
|
flwr/server/superlink/fleet/vce/backend/raybackend.py,sha256=cBZYTmfiAsb1HmVUmOQXYLU-UJmJTFWkj1wW4RYRDuc,7218
|
318
|
-
flwr/server/superlink/fleet/vce/vce_api.py,sha256=
|
318
|
+
flwr/server/superlink/fleet/vce/vce_api.py,sha256=EU0DLt4njtKelOpOWfQ7zWW45bSVC6K7pPYfHSyOJwM,13332
|
319
319
|
flwr/server/superlink/linkstate/__init__.py,sha256=OtsgvDTnZLU3k0sUbkHbqoVwW6ql2FDmb6uT6DbNkZo,1064
|
320
|
-
flwr/server/superlink/linkstate/in_memory_linkstate.py,sha256=
|
321
|
-
flwr/server/superlink/linkstate/linkstate.py,sha256=
|
320
|
+
flwr/server/superlink/linkstate/in_memory_linkstate.py,sha256=C3Y0eQC0A8-B9z5h7q-c-bVXOs5umTkNTuXSdsgDKzM,29925
|
321
|
+
flwr/server/superlink/linkstate/linkstate.py,sha256=iL_mRzmdG4BA0q02BxBRf9ljKR80y3gwNR_Cum9aAdo,14772
|
322
322
|
flwr/server/superlink/linkstate/linkstate_factory.py,sha256=8RlosqSpKOoD_vhUUQPY0jtE3A84GeF96Z7sWNkRRcA,2069
|
323
|
-
flwr/server/superlink/linkstate/sqlite_linkstate.py,sha256=
|
323
|
+
flwr/server/superlink/linkstate/sqlite_linkstate.py,sha256=kHGSBIumGmMJ750RnZw8awQ8kfFsCNTw8ko5k-aLq4c,48313
|
324
324
|
flwr/server/superlink/linkstate/utils.py,sha256=IeLh7iGRCHU5MEWOl7iriaSE4L__8GWOa2OleXadK5M,15444
|
325
325
|
flwr/server/superlink/serverappio/__init__.py,sha256=Fy4zJuoccZe5mZSEIpOmQvU6YeXFBa1M4eZuXXmJcn8,717
|
326
326
|
flwr/server/superlink/serverappio/serverappio_grpc.py,sha256=-I7kBbr4w4ZVYwBZoAIle-xHKthFnZrsVfxa6WR8uxA,2310
|
@@ -375,7 +375,7 @@ flwr/supercore/__init__.py,sha256=pqkFoow_E6UhbBlhmoD1gmTH-33yJRhBsIZqxRPFZ7U,75
|
|
375
375
|
flwr/supercore/app_utils.py,sha256=K76Zt6R670b1hUmxOsNc1WUCVYvF7lejXPcCO9K0Q0g,1753
|
376
376
|
flwr/supercore/cli/__init__.py,sha256=EDl2aO-fuQfxSbL-T1W9RAfA2N0hpWHmqX_GSwblJbQ,845
|
377
377
|
flwr/supercore/cli/flower_superexec.py,sha256=JtqYrEWVu3BxLkjavsdohTOwvMwzuFqWP5j4Mo9dqsk,6155
|
378
|
-
flwr/supercore/constant.py,sha256=
|
378
|
+
flwr/supercore/constant.py,sha256=tSZ741Q4siF5LPANvkakwIMh7wd5x1VjqHj0VH0wN1Y,1146
|
379
379
|
flwr/supercore/corestate/__init__.py,sha256=Vau6-L_JG5QzNqtCTa9xCKGGljc09wY8avZmIjSJemg,774
|
380
380
|
flwr/supercore/corestate/corestate.py,sha256=rDAWWeG5DcpCyQso9Z3RhwL4zr2IroPlRMcDzqoSu8s,2328
|
381
381
|
flwr/supercore/ffs/__init__.py,sha256=U3KXwG_SplEvchat27K0LYPoPHzh-cwwT_NHsGlYMt8,908
|
@@ -415,7 +415,7 @@ flwr/superlink/servicer/control/control_account_auth_interceptor.py,sha256=Tbi4W
|
|
415
415
|
flwr/superlink/servicer/control/control_event_log_interceptor.py,sha256=5uBl6VcJlUOgCF0d4kmsmJc1Rs1qxyouaZv0-uH2axs,5969
|
416
416
|
flwr/superlink/servicer/control/control_grpc.py,sha256=MRCaX4I2a5ogjKmhtFs6Mj-VdWemxL2h3gU9QbQmvCA,4183
|
417
417
|
flwr/superlink/servicer/control/control_license_interceptor.py,sha256=T3AzmRt-PPwyTq3hrdpmZHQd5_CpPOk7TtnFZrB-JRY,3349
|
418
|
-
flwr/superlink/servicer/control/control_servicer.py,sha256=
|
418
|
+
flwr/superlink/servicer/control/control_servicer.py,sha256=8nMbXnO9eUnm-H9nikIbyzMQUx7-qZ74PE0X2if_mDE,21377
|
419
419
|
flwr/supernode/__init__.py,sha256=KgeCaVvXWrU3rptNR1y0oBp4YtXbAcrnCcJAiOoWkI4,707
|
420
420
|
flwr/supernode/cli/__init__.py,sha256=JuEMr0-s9zv-PEWKuLB9tj1ocNfroSyNJ-oyv7ati9A,887
|
421
421
|
flwr/supernode/cli/flower_supernode.py,sha256=7aBm0z03OU-npVd1onLCvUotyhSvlZLxAnFkGVMhZcw,8670
|
@@ -430,7 +430,7 @@ flwr/supernode/servicer/__init__.py,sha256=lucTzre5WPK7G1YLCfaqg3rbFWdNSb7ZTt-ca
|
|
430
430
|
flwr/supernode/servicer/clientappio/__init__.py,sha256=7Oy62Y_oijqF7Dxi6tpcUQyOpLc_QpIRZ83NvwmB0Yg,813
|
431
431
|
flwr/supernode/servicer/clientappio/clientappio_servicer.py,sha256=nIHRu38EWK-rpNOkcgBRAAKwYQQWFeCwu0lkO7OPZGQ,10239
|
432
432
|
flwr/supernode/start_client_internal.py,sha256=Y9S1-QlO2WP6eo4JvWzIpfaCoh2aoE7bjEYyxNNnlyg,20777
|
433
|
-
flwr_nightly-1.23.0.
|
434
|
-
flwr_nightly-1.23.0.
|
435
|
-
flwr_nightly-1.23.0.
|
436
|
-
flwr_nightly-1.23.0.
|
433
|
+
flwr_nightly-1.23.0.dev20251011.dist-info/METADATA,sha256=7joOzn562B_dcoRPbxpIH8McdjKFYs45rJk6jNdVFfI,14559
|
434
|
+
flwr_nightly-1.23.0.dev20251011.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
435
|
+
flwr_nightly-1.23.0.dev20251011.dist-info/entry_points.txt,sha256=hxHD2ixb_vJFDOlZV-zB4Ao32_BQlL34ftsDh1GXv14,420
|
436
|
+
flwr_nightly-1.23.0.dev20251011.dist-info/RECORD,,
|
{flwr_nightly-1.23.0.dev20251009.dist-info → flwr_nightly-1.23.0.dev20251011.dist-info}/WHEEL
RENAMED
File without changes
|
File without changes
|