fix(hop): skip priority short-circuit when multiple nodes share the same priority
When two or more nodes have an identical matcher rule they receive identical default priority (rule string length). The priority shortcut assumed the top-priority node was the single authoritative choice and returned it directly, bypassing the selector (FailFilter, BackupFilter, strategy) — so weight, round-robin, and hash were silently ignored. Add a third condition: the top priority must be strictly greater than the second-highest. When multiple nodes share the same highest priority the selector applies normally for load balancing. Add doc comments on NodeMatcherConfig.Priority explaining the three semantic ranges (0=auto, negative=always-selector, positive=shortcut).
This commit is contained in:
+7
-3
@@ -208,13 +208,17 @@ func (p *chainHop) Select(ctx context.Context, opts ...hop.SelectOption) *chain.
|
||||
})
|
||||
|
||||
if nodes[0].Options().Priority > 0 &&
|
||||
!anyBackupNode(nodes) {
|
||||
!anyBackupNode(nodes) &&
|
||||
nodes[0].Options().Priority > nodes[1].Options().Priority {
|
||||
// Priority short-circuit: highest-priority non-backup node wins.
|
||||
// Conditions: (1) top priority > 0 means a matcher indicated routing
|
||||
// specificity, so the top node is authoritative for this request;
|
||||
// (2) no backup node is present, otherwise BackupFilter would be
|
||||
// silently bypassed. When both hold the selector (FailFilter,
|
||||
// BackupFilter, strategy) is skipped and the best node wins directly.
|
||||
// silently bypassed; (3) the top priority is strictly higher than
|
||||
// the second-highest — when multiple nodes share the same matcher
|
||||
// rule they have equal priority and the selector (FailFilter,
|
||||
// BackupFilter, strategy) should still apply for load balancing.
|
||||
// When all three hold the selector is skipped and the best node wins directly.
|
||||
p.logger.Debugf("priority shortcut: node %s selected", nodes[0].Name)
|
||||
return nodes[0]
|
||||
}
|
||||
|
||||
@@ -522,6 +522,31 @@ func TestSelect_WithSelector(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelect_EqualPriorityDoesNotShortcut(t *testing.T) {
|
||||
// Two nodes with identical matchers get the same default priority.
|
||||
// The priority shortcut must NOT trigger when multiple nodes share
|
||||
// the highest priority — the selector should still apply.
|
||||
n1 := chain.NewNode("n1", "127.0.0.1:8080",
|
||||
chain.MatcherNodeOption(&testMatcher{match: true}),
|
||||
chain.PriorityNodeOption(50),
|
||||
)
|
||||
n2 := chain.NewNode("n2", "127.0.0.1:9090",
|
||||
chain.MatcherNodeOption(&testMatcher{match: true}),
|
||||
chain.PriorityNodeOption(50),
|
||||
)
|
||||
sel := &testNodeSelector{selectedIdx: 1} // picks n2
|
||||
h := newTestHop(NodeOption(n1, n2), SelectorOption(sel))
|
||||
defer h.Close()
|
||||
|
||||
node := h.Select(context.Background())
|
||||
if node == nil {
|
||||
t.Fatal("expected node, got nil")
|
||||
}
|
||||
if node.Name != "n2" {
|
||||
t.Errorf("expected 'n2' (selected by selector, not shortcut), got %q", node.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelect_NilNodesSkipped(t *testing.T) {
|
||||
h := newTestHop()
|
||||
defer h.Close()
|
||||
|
||||
Reference in New Issue
Block a user