diff --git a/hscontrol/state/node_store_test.go b/hscontrol/state/node_store_test.go index 9b1a57c6b..1fa3993d5 100644 --- a/hscontrol/state/node_store_test.go +++ b/hscontrol/state/node_store_test.go @@ -1912,7 +1912,7 @@ var nodeFieldImpact = map[string]string{ "RegisterMethod": "payload", "AuthKeyID": "payload", "AuthKey": "payload", "Expiry": "election", "LastSeen": "payload", "CreatedAt": "payload", "UpdatedAt": "payload", "DeletedAt": "payload", - "ActiveSessions": "payload", "SessionEpoch": "payload", + "ActiveSessions": "payload", "SessionEpoch": "payload", "SessionStart": "election", } // TestUpdateChangesCoversEveryNodeField guards against a new types.Node diff --git a/hscontrol/types/node.go b/hscontrol/types/node.go index a1f1ebd55..0a583142c 100644 --- a/hscontrol/types/node.go +++ b/hscontrol/types/node.go @@ -210,6 +210,12 @@ type Node struct { // returned by Connect as a "Connect ran" sentinel for its cleanup, // and Disconnect logs it. Runtime-only. SessionEpoch uint64 `gorm:"-"` + + // SessionStart is when the node's current run of control sessions + // began: [State.Connect] sets it as the first of them opens, and an + // overlapping reconnect keeps it. Primary election prefers the + // oldest. Runtime-only. + SessionStart time.Time `gorm:"-"` } type Nodes []*Node diff --git a/hscontrol/types/types_clone.go b/hscontrol/types/types_clone.go index bfbe416e6..9b64cc3bf 100644 --- a/hscontrol/types/types_clone.go +++ b/hscontrol/types/types_clone.go @@ -108,6 +108,7 @@ var _NodeCloneNeedsRegeneration = Node(struct { Unhealthy bool ActiveSessions int SessionEpoch uint64 + SessionStart time.Time }{}) // Clone makes a deep copy of PreAuthKey. diff --git a/hscontrol/types/types_view.go b/hscontrol/types/types_view.go index bc43cd044..ee8d0163f 100644 --- a/hscontrol/types/types_view.go +++ b/hscontrol/types/types_view.go @@ -281,7 +281,13 @@ func (v NodeView) ActiveSessions() int { return v.ж.ActiveSessions } // returned by Connect as a "Connect ran" sentinel for its cleanup, // and Disconnect logs it. Runtime-only. func (v NodeView) SessionEpoch() uint64 { return v.ж.SessionEpoch } -func (v NodeView) String() string { return v.ж.String() } + +// SessionStart is when the node's current run of control sessions +// began: [State.Connect] sets it as the first of them opens, and an +// overlapping reconnect keeps it. Primary election prefers the +// oldest. Runtime-only. +func (v NodeView) SessionStart() time.Time { return v.ж.SessionStart } +func (v NodeView) String() string { return v.ж.String() } // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _NodeViewNeedsRegeneration = Node(struct { @@ -311,6 +317,7 @@ var _NodeViewNeedsRegeneration = Node(struct { Unhealthy bool ActiveSessions int SessionEpoch uint64 + SessionStart time.Time }{}) // View returns a read-only view of PreAuthKey.