Skip to content

Commit b389cdd

Browse files
authored
Add support for multiple InferencePool backends (#4439)
Add support for multiple InferencePool backends on a Route. Problem: A route should be able to have multiple InferencePools in its backendRefs. Solution: Add support for multiple InferencePool backends. Added logic to remove duplicated inference maps. Testing: Added unit tests and enabled correlating GatewayWeightedAcrossTwoInferencePools conformance test. Manually tested situations for multiple inferencepool backends with and without http matches.
1 parent 1481231 commit b389cdd

File tree

13 files changed

+1381
-343
lines changed

13 files changed

+1381
-343
lines changed

internal/controller/nginx/config/maps.go

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package config
22

33
import (
44
"fmt"
5+
"sort"
56
"strings"
67
gotemplate "text/template"
78

@@ -185,14 +186,21 @@ func createAddHeadersMap(name string) shared.Map {
185186

186187
// buildInferenceMaps creates maps for InferencePool Backends.
187188
func buildInferenceMaps(groups []dataplane.BackendGroup) []shared.Map {
188-
inferenceMaps := make([]shared.Map, 0, len(groups))
189+
uniqueMaps := make(map[string]shared.Map)
189190

190191
for _, group := range groups {
191192
for _, backend := range group.Backends {
192193
if backend.EndpointPickerConfig == nil || backend.EndpointPickerConfig.EndpointPickerRef == nil {
193194
continue
194195
}
195196

197+
backendVarName := strings.ReplaceAll(backend.UpstreamName, "-", "_")
198+
mapKey := backendVarName // Use this as the key to detect duplicates
199+
200+
// Skip if we've already processed this upstream
201+
if _, exists := uniqueMaps[mapKey]; exists {
202+
continue
203+
}
196204
// Decide what the map must return when the picker didn’t set a value.
197205
var defaultResult string
198206
switch backend.EndpointPickerConfig.EndpointPickerRef.FailureMode {
@@ -230,14 +238,26 @@ func buildInferenceMaps(groups []dataplane.BackendGroup) []shared.Map {
230238
Result: defaultResult,
231239
})
232240

233-
backendVarName := strings.ReplaceAll(backend.UpstreamName, "-", "_")
234-
235-
inferenceMaps = append(inferenceMaps, shared.Map{
241+
uniqueMaps[mapKey] = shared.Map{
236242
Source: `$inference_workload_endpoint`,
237243
Variable: fmt.Sprintf("$inference_backend_%s", backendVarName),
238244
Parameters: params,
239-
})
245+
}
240246
}
241247
}
248+
249+
// Sort the map keys to ensure deterministic ordering
250+
mapKeys := make([]string, 0, len(uniqueMaps))
251+
for key := range uniqueMaps {
252+
mapKeys = append(mapKeys, key)
253+
}
254+
sort.Strings(mapKeys)
255+
256+
// Build the result slice in sorted order
257+
inferenceMaps := make([]shared.Map, 0, len(uniqueMaps))
258+
for _, key := range mapKeys {
259+
inferenceMaps = append(inferenceMaps, uniqueMaps[key])
260+
}
261+
242262
return inferenceMaps
243263
}

internal/controller/nginx/config/maps_test.go

Lines changed: 167 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -397,55 +397,183 @@ func TestCreateStreamMapsWithEmpty(t *testing.T) {
397397

398398
func TestBuildInferenceMaps(t *testing.T) {
399399
t.Parallel()
400-
g := NewWithT(t)
401400

402-
group := dataplane.BackendGroup{
403-
Backends: []dataplane.Backend{
404-
{
405-
UpstreamName: "upstream1",
406-
EndpointPickerConfig: &dataplane.EndpointPickerConfig{
407-
NsName: "default",
408-
EndpointPickerRef: &inference.EndpointPickerRef{
409-
FailureMode: inference.EndpointPickerFailClose,
401+
tests := []struct {
402+
expectedConfig map[string]struct {
403+
failureMode inference.EndpointPickerFailureMode
404+
defaultResult string
405+
}
406+
name string
407+
backendGroups []dataplane.BackendGroup
408+
expectedMaps int
409+
}{
410+
{
411+
name: "unique backends with different failure modes, result is ordered by upstream name",
412+
backendGroups: []dataplane.BackendGroup{
413+
{
414+
Backends: []dataplane.Backend{
415+
{
416+
UpstreamName: "upstream2",
417+
EndpointPickerConfig: &dataplane.EndpointPickerConfig{
418+
NsName: "default",
419+
EndpointPickerRef: &inference.EndpointPickerRef{
420+
FailureMode: inference.EndpointPickerFailOpen,
421+
},
422+
},
423+
},
424+
{
425+
UpstreamName: "upstream1",
426+
EndpointPickerConfig: &dataplane.EndpointPickerConfig{
427+
NsName: "default",
428+
EndpointPickerRef: &inference.EndpointPickerRef{
429+
FailureMode: inference.EndpointPickerFailClose,
430+
},
431+
},
432+
},
433+
{
434+
UpstreamName: "upstream3",
435+
EndpointPickerConfig: nil,
436+
},
410437
},
411438
},
412439
},
413-
{
414-
UpstreamName: "upstream2",
415-
EndpointPickerConfig: &dataplane.EndpointPickerConfig{
416-
NsName: "default",
417-
EndpointPickerRef: &inference.EndpointPickerRef{
418-
FailureMode: inference.EndpointPickerFailOpen,
440+
expectedMaps: 2,
441+
expectedConfig: map[string]struct {
442+
failureMode inference.EndpointPickerFailureMode
443+
defaultResult string
444+
}{
445+
"upstream1": {
446+
failureMode: inference.EndpointPickerFailClose,
447+
defaultResult: "invalid-backend-ref",
448+
},
449+
"upstream2": {
450+
failureMode: inference.EndpointPickerFailOpen,
451+
defaultResult: "upstream2",
452+
},
453+
},
454+
},
455+
{
456+
name: "duplicate upstreams should be deduplicated",
457+
backendGroups: []dataplane.BackendGroup{
458+
{
459+
Backends: []dataplane.Backend{
460+
{
461+
UpstreamName: "upstream1",
462+
EndpointPickerConfig: &dataplane.EndpointPickerConfig{
463+
NsName: "default",
464+
EndpointPickerRef: &inference.EndpointPickerRef{
465+
FailureMode: inference.EndpointPickerFailClose,
466+
},
467+
},
468+
},
469+
{
470+
UpstreamName: "upstream1", // Duplicate
471+
EndpointPickerConfig: &dataplane.EndpointPickerConfig{
472+
NsName: "default",
473+
EndpointPickerRef: &inference.EndpointPickerRef{
474+
FailureMode: inference.EndpointPickerFailClose,
475+
},
476+
},
477+
},
478+
},
479+
},
480+
{
481+
Backends: []dataplane.Backend{
482+
{
483+
UpstreamName: "upstream1", // Another duplicate
484+
EndpointPickerConfig: &dataplane.EndpointPickerConfig{
485+
NsName: "default",
486+
EndpointPickerRef: &inference.EndpointPickerRef{
487+
FailureMode: inference.EndpointPickerFailClose,
488+
},
489+
},
490+
},
491+
{
492+
UpstreamName: "upstream2",
493+
EndpointPickerConfig: &dataplane.EndpointPickerConfig{
494+
NsName: "default",
495+
EndpointPickerRef: &inference.EndpointPickerRef{
496+
FailureMode: inference.EndpointPickerFailOpen,
497+
},
498+
},
499+
},
419500
},
420501
},
421502
},
422-
{
423-
UpstreamName: "upstream3",
424-
EndpointPickerConfig: nil,
503+
expectedMaps: 2, // Only 2 unique upstreams
504+
expectedConfig: map[string]struct {
505+
failureMode inference.EndpointPickerFailureMode
506+
defaultResult string
507+
}{
508+
"upstream1": {
509+
failureMode: inference.EndpointPickerFailClose,
510+
defaultResult: "invalid-backend-ref",
511+
},
512+
"upstream2": {
513+
failureMode: inference.EndpointPickerFailOpen,
514+
defaultResult: "upstream2",
515+
},
425516
},
426517
},
518+
{
519+
name: "no endpoint picker configs",
520+
backendGroups: []dataplane.BackendGroup{
521+
{
522+
Backends: []dataplane.Backend{
523+
{
524+
UpstreamName: "upstream1",
525+
EndpointPickerConfig: nil,
526+
},
527+
{
528+
UpstreamName: "upstream2",
529+
EndpointPickerConfig: nil,
530+
},
531+
},
532+
},
533+
},
534+
expectedMaps: 0,
535+
},
427536
}
428537

429-
maps := buildInferenceMaps([]dataplane.BackendGroup{group})
430-
g.Expect(maps).To(HaveLen(2))
431-
g.Expect(maps[0].Source).To(Equal("$inference_workload_endpoint"))
432-
g.Expect(maps[0].Variable).To(Equal("$inference_backend_upstream1"))
433-
g.Expect(maps[0].Parameters).To(HaveLen(3))
434-
g.Expect(maps[0].Parameters[0].Value).To(Equal("\"\""))
435-
g.Expect(maps[0].Parameters[0].Result).To(Equal("upstream1"))
436-
g.Expect(maps[0].Parameters[1].Value).To(Equal("~.+"))
437-
g.Expect(maps[0].Parameters[1].Result).To(Equal("$inference_workload_endpoint"))
438-
g.Expect(maps[0].Parameters[2].Value).To(Equal("default"))
439-
g.Expect(maps[0].Parameters[2].Result).To(Equal("invalid-backend-ref"))
538+
for _, tc := range tests {
539+
t.Run(tc.name, func(t *testing.T) {
540+
t.Parallel()
541+
g := NewWithT(t)
542+
543+
maps := buildInferenceMaps(tc.backendGroups)
544+
g.Expect(maps).To(HaveLen(tc.expectedMaps))
440545

441-
// Check the second map
442-
g.Expect(maps[1].Source).To(Equal("$inference_workload_endpoint"))
443-
g.Expect(maps[1].Variable).To(Equal("$inference_backend_upstream2"))
444-
g.Expect(maps[1].Parameters).To(HaveLen(3))
445-
g.Expect(maps[1].Parameters[0].Value).To(Equal("\"\""))
446-
g.Expect(maps[1].Parameters[0].Result).To(Equal("upstream2"))
447-
g.Expect(maps[1].Parameters[1].Value).To(Equal("~.+"))
448-
g.Expect(maps[1].Parameters[1].Result).To(Equal("$inference_workload_endpoint"))
449-
g.Expect(maps[1].Parameters[2].Value).To(Equal("default"))
450-
g.Expect(maps[1].Parameters[2].Result).To(Equal("upstream2"))
546+
// Verify each map has the correct structure
547+
seenUpstreams := make(map[string]bool)
548+
for _, m := range maps {
549+
g.Expect(m.Source).To(Equal("$inference_workload_endpoint"))
550+
g.Expect(m.Parameters).To(HaveLen(3))
551+
552+
// Extract upstream name from variable name
553+
varName := strings.TrimPrefix(m.Variable, "$inference_backend_")
554+
upstreamName := strings.ReplaceAll(varName, "_", "-")
555+
556+
// Verify we haven't seen this upstream before (no duplicates)
557+
g.Expect(seenUpstreams[upstreamName]).To(BeFalse(), "Duplicate upstream found: %s", upstreamName)
558+
seenUpstreams[upstreamName] = true
559+
560+
// Verify parameter structure
561+
g.Expect(m.Parameters[0].Value).To(Equal("\"\""))
562+
g.Expect(m.Parameters[0].Result).To(Equal(upstreamName))
563+
g.Expect(m.Parameters[1].Value).To(Equal("~.+"))
564+
g.Expect(m.Parameters[1].Result).To(Equal("$inference_workload_endpoint"))
565+
g.Expect(m.Parameters[2].Value).To(Equal("default"))
566+
567+
// Verify the default result matches expected failure mode
568+
if expectedConfig, exists := tc.expectedConfig[upstreamName]; exists {
569+
g.Expect(m.Parameters[2].Result).To(Equal(expectedConfig.defaultResult))
570+
}
571+
}
572+
573+
// Verify all expected upstreams are present
574+
for expectedUpstream := range tc.expectedConfig {
575+
g.Expect(seenUpstreams[expectedUpstream]).To(BeTrue(), "Expected upstream not found: %s", expectedUpstream)
576+
}
577+
})
578+
}
451579
}

0 commit comments

Comments
 (0)