flwr-nightly 1.23.0.dev20251017__py3-none-any.whl → 1.23.0.dev20251021__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

@@ -49,20 +49,20 @@ class ControlStub(object):
49
49
  request_serializer=flwr_dot_proto_dot_control__pb2.PullArtifactsRequest.SerializeToString,
50
50
  response_deserializer=flwr_dot_proto_dot_control__pb2.PullArtifactsResponse.FromString,
51
51
  )
52
- self.CreateNodeCli = channel.unary_unary(
53
- '/flwr.proto.Control/CreateNodeCli',
54
- request_serializer=flwr_dot_proto_dot_control__pb2.CreateNodeCliRequest.SerializeToString,
55
- response_deserializer=flwr_dot_proto_dot_control__pb2.CreateNodeCliResponse.FromString,
52
+ self.RegisterNode = channel.unary_unary(
53
+ '/flwr.proto.Control/RegisterNode',
54
+ request_serializer=flwr_dot_proto_dot_control__pb2.RegisterNodeRequest.SerializeToString,
55
+ response_deserializer=flwr_dot_proto_dot_control__pb2.RegisterNodeResponse.FromString,
56
56
  )
57
- self.DeleteNodeCli = channel.unary_unary(
58
- '/flwr.proto.Control/DeleteNodeCli',
59
- request_serializer=flwr_dot_proto_dot_control__pb2.DeleteNodeCliRequest.SerializeToString,
60
- response_deserializer=flwr_dot_proto_dot_control__pb2.DeleteNodeCliResponse.FromString,
57
+ self.UnregisterNode = channel.unary_unary(
58
+ '/flwr.proto.Control/UnregisterNode',
59
+ request_serializer=flwr_dot_proto_dot_control__pb2.UnregisterNodeRequest.SerializeToString,
60
+ response_deserializer=flwr_dot_proto_dot_control__pb2.UnregisterNodeResponse.FromString,
61
61
  )
62
- self.ListNodesCli = channel.unary_unary(
63
- '/flwr.proto.Control/ListNodesCli',
64
- request_serializer=flwr_dot_proto_dot_control__pb2.ListNodesCliRequest.SerializeToString,
65
- response_deserializer=flwr_dot_proto_dot_control__pb2.ListNodesCliResponse.FromString,
62
+ self.ListNodes = channel.unary_unary(
63
+ '/flwr.proto.Control/ListNodes',
64
+ request_serializer=flwr_dot_proto_dot_control__pb2.ListNodesRequest.SerializeToString,
65
+ response_deserializer=flwr_dot_proto_dot_control__pb2.ListNodesResponse.FromString,
66
66
  )
67
67
 
68
68
 
@@ -118,21 +118,21 @@ class ControlServicer(object):
118
118
  context.set_details('Method not implemented!')
119
119
  raise NotImplementedError('Method not implemented!')
120
120
 
121
- def CreateNodeCli(self, request, context):
122
- """Add SuperNode
121
+ def RegisterNode(self, request, context):
122
+ """Register SuperNode
123
123
  """
124
124
  context.set_code(grpc.StatusCode.UNIMPLEMENTED)
125
125
  context.set_details('Method not implemented!')
126
126
  raise NotImplementedError('Method not implemented!')
127
127
 
128
- def DeleteNodeCli(self, request, context):
129
- """Remove SuperNode
128
+ def UnregisterNode(self, request, context):
129
+ """Unregister SuperNode
130
130
  """
131
131
  context.set_code(grpc.StatusCode.UNIMPLEMENTED)
132
132
  context.set_details('Method not implemented!')
133
133
  raise NotImplementedError('Method not implemented!')
134
134
 
135
- def ListNodesCli(self, request, context):
135
+ def ListNodes(self, request, context):
136
136
  """List SuperNodes
137
137
  """
138
138
  context.set_code(grpc.StatusCode.UNIMPLEMENTED)
@@ -177,20 +177,20 @@ def add_ControlServicer_to_server(servicer, server):
177
177
  request_deserializer=flwr_dot_proto_dot_control__pb2.PullArtifactsRequest.FromString,
178
178
  response_serializer=flwr_dot_proto_dot_control__pb2.PullArtifactsResponse.SerializeToString,
179
179
  ),
180
- 'CreateNodeCli': grpc.unary_unary_rpc_method_handler(
181
- servicer.CreateNodeCli,
182
- request_deserializer=flwr_dot_proto_dot_control__pb2.CreateNodeCliRequest.FromString,
183
- response_serializer=flwr_dot_proto_dot_control__pb2.CreateNodeCliResponse.SerializeToString,
180
+ 'RegisterNode': grpc.unary_unary_rpc_method_handler(
181
+ servicer.RegisterNode,
182
+ request_deserializer=flwr_dot_proto_dot_control__pb2.RegisterNodeRequest.FromString,
183
+ response_serializer=flwr_dot_proto_dot_control__pb2.RegisterNodeResponse.SerializeToString,
184
184
  ),
185
- 'DeleteNodeCli': grpc.unary_unary_rpc_method_handler(
186
- servicer.DeleteNodeCli,
187
- request_deserializer=flwr_dot_proto_dot_control__pb2.DeleteNodeCliRequest.FromString,
188
- response_serializer=flwr_dot_proto_dot_control__pb2.DeleteNodeCliResponse.SerializeToString,
185
+ 'UnregisterNode': grpc.unary_unary_rpc_method_handler(
186
+ servicer.UnregisterNode,
187
+ request_deserializer=flwr_dot_proto_dot_control__pb2.UnregisterNodeRequest.FromString,
188
+ response_serializer=flwr_dot_proto_dot_control__pb2.UnregisterNodeResponse.SerializeToString,
189
189
  ),
190
- 'ListNodesCli': grpc.unary_unary_rpc_method_handler(
191
- servicer.ListNodesCli,
192
- request_deserializer=flwr_dot_proto_dot_control__pb2.ListNodesCliRequest.FromString,
193
- response_serializer=flwr_dot_proto_dot_control__pb2.ListNodesCliResponse.SerializeToString,
190
+ 'ListNodes': grpc.unary_unary_rpc_method_handler(
191
+ servicer.ListNodes,
192
+ request_deserializer=flwr_dot_proto_dot_control__pb2.ListNodesRequest.FromString,
193
+ response_serializer=flwr_dot_proto_dot_control__pb2.ListNodesResponse.SerializeToString,
194
194
  ),
195
195
  }
196
196
  generic_handler = grpc.method_handlers_generic_handler(
@@ -322,7 +322,7 @@ class Control(object):
322
322
  insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
323
323
 
324
324
  @staticmethod
325
- def CreateNodeCli(request,
325
+ def RegisterNode(request,
326
326
  target,
327
327
  options=(),
328
328
  channel_credentials=None,
@@ -332,14 +332,14 @@ class Control(object):
332
332
  wait_for_ready=None,
333
333
  timeout=None,
334
334
  metadata=None):
335
- return grpc.experimental.unary_unary(request, target, '/flwr.proto.Control/CreateNodeCli',
336
- flwr_dot_proto_dot_control__pb2.CreateNodeCliRequest.SerializeToString,
337
- flwr_dot_proto_dot_control__pb2.CreateNodeCliResponse.FromString,
335
+ return grpc.experimental.unary_unary(request, target, '/flwr.proto.Control/RegisterNode',
336
+ flwr_dot_proto_dot_control__pb2.RegisterNodeRequest.SerializeToString,
337
+ flwr_dot_proto_dot_control__pb2.RegisterNodeResponse.FromString,
338
338
  options, channel_credentials,
339
339
  insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
340
340
 
341
341
  @staticmethod
342
- def DeleteNodeCli(request,
342
+ def UnregisterNode(request,
343
343
  target,
344
344
  options=(),
345
345
  channel_credentials=None,
@@ -349,14 +349,14 @@ class Control(object):
349
349
  wait_for_ready=None,
350
350
  timeout=None,
351
351
  metadata=None):
352
- return grpc.experimental.unary_unary(request, target, '/flwr.proto.Control/DeleteNodeCli',
353
- flwr_dot_proto_dot_control__pb2.DeleteNodeCliRequest.SerializeToString,
354
- flwr_dot_proto_dot_control__pb2.DeleteNodeCliResponse.FromString,
352
+ return grpc.experimental.unary_unary(request, target, '/flwr.proto.Control/UnregisterNode',
353
+ flwr_dot_proto_dot_control__pb2.UnregisterNodeRequest.SerializeToString,
354
+ flwr_dot_proto_dot_control__pb2.UnregisterNodeResponse.FromString,
355
355
  options, channel_credentials,
356
356
  insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
357
357
 
358
358
  @staticmethod
359
- def ListNodesCli(request,
359
+ def ListNodes(request,
360
360
  target,
361
361
  options=(),
362
362
  channel_credentials=None,
@@ -366,8 +366,8 @@ class Control(object):
366
366
  wait_for_ready=None,
367
367
  timeout=None,
368
368
  metadata=None):
369
- return grpc.experimental.unary_unary(request, target, '/flwr.proto.Control/ListNodesCli',
370
- flwr_dot_proto_dot_control__pb2.ListNodesCliRequest.SerializeToString,
371
- flwr_dot_proto_dot_control__pb2.ListNodesCliResponse.FromString,
369
+ return grpc.experimental.unary_unary(request, target, '/flwr.proto.Control/ListNodes',
370
+ flwr_dot_proto_dot_control__pb2.ListNodesRequest.SerializeToString,
371
+ flwr_dot_proto_dot_control__pb2.ListNodesResponse.FromString,
372
372
  options, channel_credentials,
373
373
  insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
@@ -44,19 +44,19 @@ class ControlStub:
44
44
  flwr.proto.control_pb2.PullArtifactsResponse]
45
45
  """Pull artifacts generated during a run (flwr pull)"""
46
46
 
47
- CreateNodeCli: grpc.UnaryUnaryMultiCallable[
48
- flwr.proto.control_pb2.CreateNodeCliRequest,
49
- flwr.proto.control_pb2.CreateNodeCliResponse]
50
- """Add SuperNode"""
51
-
52
- DeleteNodeCli: grpc.UnaryUnaryMultiCallable[
53
- flwr.proto.control_pb2.DeleteNodeCliRequest,
54
- flwr.proto.control_pb2.DeleteNodeCliResponse]
55
- """Remove SuperNode"""
56
-
57
- ListNodesCli: grpc.UnaryUnaryMultiCallable[
58
- flwr.proto.control_pb2.ListNodesCliRequest,
59
- flwr.proto.control_pb2.ListNodesCliResponse]
47
+ RegisterNode: grpc.UnaryUnaryMultiCallable[
48
+ flwr.proto.control_pb2.RegisterNodeRequest,
49
+ flwr.proto.control_pb2.RegisterNodeResponse]
50
+ """Register SuperNode"""
51
+
52
+ UnregisterNode: grpc.UnaryUnaryMultiCallable[
53
+ flwr.proto.control_pb2.UnregisterNodeRequest,
54
+ flwr.proto.control_pb2.UnregisterNodeResponse]
55
+ """Unregister SuperNode"""
56
+
57
+ ListNodes: grpc.UnaryUnaryMultiCallable[
58
+ flwr.proto.control_pb2.ListNodesRequest,
59
+ flwr.proto.control_pb2.ListNodesResponse]
60
60
  """List SuperNodes"""
61
61
 
62
62
 
@@ -118,26 +118,26 @@ class ControlServicer(metaclass=abc.ABCMeta):
118
118
  pass
119
119
 
120
120
  @abc.abstractmethod
121
- def CreateNodeCli(self,
122
- request: flwr.proto.control_pb2.CreateNodeCliRequest,
121
+ def RegisterNode(self,
122
+ request: flwr.proto.control_pb2.RegisterNodeRequest,
123
123
  context: grpc.ServicerContext,
124
- ) -> flwr.proto.control_pb2.CreateNodeCliResponse:
125
- """Add SuperNode"""
124
+ ) -> flwr.proto.control_pb2.RegisterNodeResponse:
125
+ """Register SuperNode"""
126
126
  pass
127
127
 
128
128
  @abc.abstractmethod
129
- def DeleteNodeCli(self,
130
- request: flwr.proto.control_pb2.DeleteNodeCliRequest,
129
+ def UnregisterNode(self,
130
+ request: flwr.proto.control_pb2.UnregisterNodeRequest,
131
131
  context: grpc.ServicerContext,
132
- ) -> flwr.proto.control_pb2.DeleteNodeCliResponse:
133
- """Remove SuperNode"""
132
+ ) -> flwr.proto.control_pb2.UnregisterNodeResponse:
133
+ """Unregister SuperNode"""
134
134
  pass
135
135
 
136
136
  @abc.abstractmethod
137
- def ListNodesCli(self,
138
- request: flwr.proto.control_pb2.ListNodesCliRequest,
137
+ def ListNodes(self,
138
+ request: flwr.proto.control_pb2.ListNodesRequest,
139
139
  context: grpc.ServicerContext,
140
- ) -> flwr.proto.control_pb2.ListNodesCliResponse:
140
+ ) -> flwr.proto.control_pb2.ListNodesResponse:
141
141
  """List SuperNodes"""
142
142
  pass
143
143
 
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\"\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')
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\"\xd3\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\x15\n\rregistered_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\x1c\n\x0funregistered_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\x12\n\x10_unregistered_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=389
27
+ _globals['_NODEINFO']._serialized_end=402
28
28
  # @@protoc_insertion_point(module_scope)
flwr/proto/node_pb2.pyi CHANGED
@@ -26,20 +26,20 @@ class NodeInfo(google.protobuf.message.Message):
26
26
  NODE_ID_FIELD_NUMBER: builtins.int
27
27
  OWNER_AID_FIELD_NUMBER: builtins.int
28
28
  STATUS_FIELD_NUMBER: builtins.int
29
- CREATED_AT_FIELD_NUMBER: builtins.int
29
+ REGISTERED_AT_FIELD_NUMBER: builtins.int
30
30
  LAST_ACTIVATED_AT_FIELD_NUMBER: builtins.int
31
31
  LAST_DEACTIVATED_AT_FIELD_NUMBER: builtins.int
32
- DELETED_AT_FIELD_NUMBER: builtins.int
32
+ UNREGISTERED_AT_FIELD_NUMBER: builtins.int
33
33
  ONLINE_UNTIL_FIELD_NUMBER: builtins.int
34
34
  HEARTBEAT_INTERVAL_FIELD_NUMBER: builtins.int
35
35
  PUBLIC_KEY_FIELD_NUMBER: builtins.int
36
36
  node_id: builtins.int
37
37
  owner_aid: typing.Text
38
38
  status: typing.Text
39
- created_at: typing.Text
39
+ registered_at: typing.Text
40
40
  last_activated_at: typing.Text
41
41
  last_deactivated_at: typing.Text
42
- deleted_at: typing.Text
42
+ unregistered_at: typing.Text
43
43
  online_until: builtins.float
44
44
  heartbeat_interval: builtins.float
45
45
  public_key: builtins.bytes
@@ -48,22 +48,22 @@ class NodeInfo(google.protobuf.message.Message):
48
48
  node_id: builtins.int = ...,
49
49
  owner_aid: typing.Text = ...,
50
50
  status: typing.Text = ...,
51
- created_at: typing.Text = ...,
51
+ registered_at: typing.Text = ...,
52
52
  last_activated_at: typing.Optional[typing.Text] = ...,
53
53
  last_deactivated_at: typing.Optional[typing.Text] = ...,
54
- deleted_at: typing.Optional[typing.Text] = ...,
54
+ unregistered_at: typing.Optional[typing.Text] = ...,
55
55
  online_until: typing.Optional[builtins.float] = ...,
56
56
  heartbeat_interval: builtins.float = ...,
57
57
  public_key: builtins.bytes = ...,
58
58
  ) -> None: ...
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"]]: ...
59
+ def HasField(self, field_name: typing_extensions.Literal["_last_activated_at",b"_last_activated_at","_last_deactivated_at",b"_last_deactivated_at","_online_until",b"_online_until","_unregistered_at",b"_unregistered_at","last_activated_at",b"last_activated_at","last_deactivated_at",b"last_deactivated_at","online_until",b"online_until","unregistered_at",b"unregistered_at"]) -> builtins.bool: ...
60
+ def ClearField(self, field_name: typing_extensions.Literal["_last_activated_at",b"_last_activated_at","_last_deactivated_at",b"_last_deactivated_at","_online_until",b"_online_until","_unregistered_at",b"_unregistered_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","registered_at",b"registered_at","status",b"status","unregistered_at",b"unregistered_at"]) -> None: ...
63
61
  @typing.overload
64
62
  def WhichOneof(self, oneof_group: typing_extensions.Literal["_last_activated_at",b"_last_activated_at"]) -> typing.Optional[typing_extensions.Literal["last_activated_at"]]: ...
65
63
  @typing.overload
66
64
  def WhichOneof(self, oneof_group: typing_extensions.Literal["_last_deactivated_at",b"_last_deactivated_at"]) -> typing.Optional[typing_extensions.Literal["last_deactivated_at"]]: ...
67
65
  @typing.overload
68
66
  def WhichOneof(self, oneof_group: typing_extensions.Literal["_online_until",b"_online_until"]) -> typing.Optional[typing_extensions.Literal["online_until"]]: ...
67
+ @typing.overload
68
+ def WhichOneof(self, oneof_group: typing_extensions.Literal["_unregistered_at",b"_unregistered_at"]) -> typing.Optional[typing_extensions.Literal["unregistered_at"]]: ...
69
69
  global___NodeInfo = NodeInfo
flwr/server/app.py CHANGED
@@ -235,6 +235,17 @@ def run_superlink() -> None:
235
235
 
236
236
  # If supernode authentication is disabled, warn users
237
237
  enable_supernode_auth: bool = args.enable_supernode_auth
238
+ if enable_supernode_auth and args.insecure:
239
+ url_v = f"https://flower.ai/docs/framework/v{package_version}/en/"
240
+ page = "how-to-authenticate-supernodes.html"
241
+ flwr_exit(
242
+ ExitCode.SUPERLINK_INVALID_ARGS,
243
+ "The `--enable-supernode-auth` flag requires encrypted TLS communications. "
244
+ "Please provide TLS certificates using the `--ssl-certfile`, "
245
+ "`--ssl-keyfile` and `--ssl-ca-certfile` arguments to your SuperLink. "
246
+ "Please refer to the Flower documentation for more information: "
247
+ f"{url_v}{page}",
248
+ )
238
249
  if not enable_supernode_auth:
239
250
  log(
240
251
  WARN,
@@ -15,6 +15,7 @@
15
15
  """Fleet API gRPC request-response servicer."""
16
16
 
17
17
 
18
+ import threading
18
19
  from logging import DEBUG, ERROR, INFO
19
20
 
20
21
  import grpc
@@ -53,6 +54,7 @@ from flwr.proto.run_pb2 import GetRunRequest, GetRunResponse # pylint: disable=
53
54
  from flwr.server.superlink.fleet.message_handler import message_handler
54
55
  from flwr.server.superlink.linkstate import LinkStateFactory
55
56
  from flwr.server.superlink.utils import abort_grpc_context
57
+ from flwr.supercore.constant import NodeStatus
56
58
  from flwr.supercore.ffs import FfsFactory
57
59
  from flwr.supercore.object_store import ObjectStoreFactory
58
60
 
@@ -71,6 +73,7 @@ class FleetServicer(fleet_pb2_grpc.FleetServicer):
71
73
  self.ffs_factory = ffs_factory
72
74
  self.objectstore_factory = objectstore_factory
73
75
  self.enable_supernode_auth = enable_supernode_auth
76
+ self.lock = threading.Lock()
74
77
 
75
78
  def CreateNode(
76
79
  self, request: CreateNodeRequest, context: grpc.ServicerContext
@@ -88,8 +91,31 @@ class FleetServicer(fleet_pb2_grpc.FleetServicer):
88
91
 
89
92
  # Check if public key is already in use
90
93
  if node_id := state.get_node_id_by_public_key(request.public_key):
91
- # Prepare response with existing node_id
92
- response = CreateNodeResponse(node=Node(node_id=node_id))
94
+
95
+ # Ensure only one request that requires checking the node state
96
+ # is processed at a time. This avoids race conditions when two
97
+ # SuperNodes try to connect at the same time with the same
98
+ # public key.
99
+ with self.lock:
100
+ node_info = state.get_node_info(node_ids=[node_id])[0]
101
+ if node_info.status == NodeStatus.ONLINE:
102
+ # Node is already active
103
+ log(
104
+ ERROR,
105
+ "Public key already in use (node_id=%s)",
106
+ node_id,
107
+ )
108
+ raise ValueError(
109
+ "Public key already in use by an active SuperNode"
110
+ )
111
+
112
+ # Prepare response with existing node_id
113
+ response = CreateNodeResponse(node=Node(node_id=node_id))
114
+ # Awknowledge heartbeat to mark node as online
115
+ state.acknowledge_node_heartbeat(
116
+ node_id=node_id,
117
+ heartbeat_interval=request.heartbeat_interval,
118
+ )
93
119
  else:
94
120
  if self.enable_supernode_auth:
95
121
  # When SuperNode authentication is enabled,
@@ -262,6 +262,7 @@ class InMemoryLinkState(LinkState): # pylint: disable=R0902,R0904
262
262
  node_id: self.nodes[node_id].online_until
263
263
  for node_id in dst_node_ids
264
264
  if node_id in self.nodes
265
+ and self.nodes[node_id].status != NodeStatus.UNREGISTERED
265
266
  },
266
267
  current_time=current,
267
268
  )
@@ -353,11 +354,11 @@ class InMemoryLinkState(LinkState): # pylint: disable=R0902,R0904
353
354
  self.nodes[node_id] = NodeInfo(
354
355
  node_id=node_id,
355
356
  owner_aid=owner_aid,
356
- status=NodeStatus.CREATED,
357
- created_at=now().isoformat(),
357
+ status=NodeStatus.REGISTERED,
358
+ registered_at=now().isoformat(),
358
359
  last_activated_at=None,
359
360
  last_deactivated_at=None,
360
- deleted_at=None,
361
+ unregistered_at=None,
361
362
  online_until=None,
362
363
  heartbeat_interval=heartbeat_interval,
363
364
  public_key=public_key,
@@ -371,16 +372,19 @@ class InMemoryLinkState(LinkState): # pylint: disable=R0902,R0904
371
372
  with self.lock:
372
373
  if (
373
374
  not (node := self.nodes.get(node_id))
374
- or node.status == NodeStatus.DELETED
375
+ or node.status == NodeStatus.UNREGISTERED
375
376
  or owner_aid != self.nodes[node_id].owner_aid
376
377
  ):
377
378
  raise ValueError(
378
- f"Node ID {node_id} already deleted, not found or unauthorized "
379
- "deletion attempt."
379
+ f"Node ID {node_id} already unregistered, not found or "
380
+ "the request was unauthorized."
380
381
  )
381
382
 
382
- node.status = NodeStatus.DELETED
383
- node.deleted_at = now().isoformat()
383
+ node.status = NodeStatus.UNREGISTERED
384
+ current = now()
385
+ node.unregistered_at = current.isoformat()
386
+ # Set online_until to current timestamp on deletion, if it is in the future
387
+ node.online_until = min(node.online_until, current.timestamp())
384
388
 
385
389
  def get_nodes(self, run_id: int) -> set[int]:
386
390
  """Return all available nodes.
@@ -436,7 +440,7 @@ class InMemoryLinkState(LinkState): # pylint: disable=R0902,R0904
436
440
  with self.lock:
437
441
  if (
438
442
  node := self.nodes.get(node_id)
439
- ) is None or node.status == NodeStatus.DELETED:
443
+ ) is None or node.status == NodeStatus.UNREGISTERED:
440
444
  raise ValueError(f"Node ID {node_id} not found")
441
445
  return node.public_key
442
446
 
@@ -450,7 +454,7 @@ class InMemoryLinkState(LinkState): # pylint: disable=R0902,R0904
450
454
  return None
451
455
 
452
456
  node_info = self.nodes[node_id]
453
- if node_info.status == NodeStatus.DELETED:
457
+ if node_info.status == NodeStatus.UNREGISTERED:
454
458
  return None
455
459
  return node_id
456
460
 
@@ -639,11 +643,13 @@ class InMemoryLinkState(LinkState): # pylint: disable=R0902,R0904
639
643
  the node is marked as offline.
640
644
  """
641
645
  with self.lock:
642
- if (node := self.nodes.get(node_id)) and node.status != NodeStatus.DELETED:
646
+ if (
647
+ node := self.nodes.get(node_id)
648
+ ) and node.status != NodeStatus.UNREGISTERED:
643
649
  current_dt = now()
644
650
 
645
651
  # Set timestamp if the status changes
646
- if node.status != NodeStatus.ONLINE: # offline or created
652
+ if node.status != NodeStatus.ONLINE: # offline or registered
647
653
  node.status = NodeStatus.ONLINE
648
654
  node.last_activated_at = current_dt.isoformat()
649
655