Use capacity pressure in endpoint ranking
This commit is contained in:
@@ -7,12 +7,14 @@ import (
|
||||
)
|
||||
|
||||
type EndpointCandidateScoreOptions struct {
|
||||
ChannelClass string
|
||||
PreferredRegion string
|
||||
Now time.Time
|
||||
MaxVerificationAge time.Duration
|
||||
Observations map[string]EndpointCandidateHealthObservation
|
||||
MaxObservationAge time.Duration
|
||||
ChannelClass string
|
||||
PreferredRegion string
|
||||
Now time.Time
|
||||
MaxVerificationAge time.Duration
|
||||
Observations map[string]EndpointCandidateHealthObservation
|
||||
MaxObservationAge time.Duration
|
||||
CapacityPressure map[string]EndpointCandidateCapacityPressure
|
||||
MaxCapacityPressureAge time.Duration
|
||||
}
|
||||
|
||||
type EndpointCandidateHealthObservation struct {
|
||||
@@ -27,6 +29,12 @@ type EndpointCandidateHealthObservation struct {
|
||||
ObservedAt time.Time `json:"observed_at,omitempty"`
|
||||
}
|
||||
|
||||
type EndpointCandidateCapacityPressure struct {
|
||||
EndpointID string `json:"endpoint_id,omitempty"`
|
||||
Count int64 `json:"count"`
|
||||
LastSeenUnixSec int64 `json:"last_seen_unix_sec"`
|
||||
}
|
||||
|
||||
type ScoredPeerEndpointCandidate struct {
|
||||
Candidate PeerEndpointCandidate `json:"candidate"`
|
||||
Score int `json:"score"`
|
||||
@@ -185,6 +193,11 @@ func scorePeerEndpointCandidate(candidate PeerEndpointCandidate, opts EndpointCa
|
||||
score += observationScore
|
||||
reasons = append(reasons, observationReasons...)
|
||||
}
|
||||
if pressure, ok := opts.CapacityPressure[candidate.EndpointID]; ok {
|
||||
pressureScore, pressureReasons := scoreEndpointCandidateCapacityPressure(pressure, opts)
|
||||
score += pressureScore
|
||||
reasons = append(reasons, pressureReasons...)
|
||||
}
|
||||
|
||||
return ScoredPeerEndpointCandidate{
|
||||
Candidate: candidate,
|
||||
@@ -193,6 +206,21 @@ func scorePeerEndpointCandidate(candidate PeerEndpointCandidate, opts EndpointCa
|
||||
}
|
||||
}
|
||||
|
||||
func scoreEndpointCandidateCapacityPressure(pressure EndpointCandidateCapacityPressure, opts EndpointCandidateScoreOptions) (int, []string) {
|
||||
if pressure.Count <= 0 {
|
||||
return 0, nil
|
||||
}
|
||||
if !opts.Now.IsZero() && pressure.LastSeenUnixSec > 0 && opts.MaxCapacityPressureAge > 0 {
|
||||
lastSeen := time.Unix(pressure.LastSeenUnixSec, 0).UTC()
|
||||
age := opts.Now.Sub(lastSeen)
|
||||
if age < 0 || age > opts.MaxCapacityPressureAge {
|
||||
return 0, []string{"capacity:pressure-stale"}
|
||||
}
|
||||
}
|
||||
penalty := boundedInt(int(pressure.Count)*3, 3, 24)
|
||||
return -penalty, []string{"capacity:pressure"}
|
||||
}
|
||||
|
||||
func scoreEndpointCandidateObservation(observation EndpointCandidateHealthObservation, opts EndpointCandidateScoreOptions) (int, []string) {
|
||||
score := 0
|
||||
reasons := []string{"observation:present"}
|
||||
|
||||
@@ -387,6 +387,48 @@ func TestRankPeerEndpointCandidatesTreatsCapacityAsSoftPressure(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRankPeerEndpointCandidatesSpreadsFreshCapacityPressure(t *testing.T) {
|
||||
now := time.Date(2026, 5, 16, 12, 0, 0, 0, time.UTC)
|
||||
ranked := RankPeerEndpointCandidates([]PeerEndpointCandidate{
|
||||
{
|
||||
EndpointID: "node-b-quic-a",
|
||||
NodeID: "node-b",
|
||||
Transport: "direct_quic",
|
||||
Address: "quic://node-b-a.example.test:19443",
|
||||
Reachability: "public",
|
||||
ConnectivityMode: "direct",
|
||||
LastVerifiedAt: &now,
|
||||
},
|
||||
{
|
||||
EndpointID: "node-b-quic-b",
|
||||
NodeID: "node-b",
|
||||
Transport: "direct_quic",
|
||||
Address: "quic://node-b-b.example.test:19443",
|
||||
Reachability: "public",
|
||||
ConnectivityMode: "direct",
|
||||
Priority: 5,
|
||||
LastVerifiedAt: &now,
|
||||
},
|
||||
}, EndpointCandidateScoreOptions{
|
||||
Now: now,
|
||||
MaxVerificationAge: time.Minute,
|
||||
MaxCapacityPressureAge: time.Minute,
|
||||
CapacityPressure: map[string]EndpointCandidateCapacityPressure{
|
||||
"node-b-quic-a": {
|
||||
EndpointID: "node-b-quic-a",
|
||||
Count: 8,
|
||||
LastSeenUnixSec: now.Unix(),
|
||||
},
|
||||
},
|
||||
})
|
||||
if ranked[0].Candidate.EndpointID != "node-b-quic-b" {
|
||||
t.Fatalf("top endpoint = %q, want less pressured endpoint: %+v", ranked[0].Candidate.EndpointID, ranked)
|
||||
}
|
||||
if !containsReason(ranked[1].Reasons, "capacity:pressure") {
|
||||
t.Fatalf("capacity pressure reason missing: %+v", ranked[1].Reasons)
|
||||
}
|
||||
}
|
||||
|
||||
func containsReason(reasons []string, reason string) bool {
|
||||
for _, item := range reasons {
|
||||
if item == reason {
|
||||
|
||||
Reference in New Issue
Block a user