Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pkg/kthena-router/datastore/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -981,6 +981,11 @@ func selectFromWeightedSlice(weights []uint32) int {
totalWeight += int(weight)
}

// Guard against divide-by-zero when all weights are zero
if totalWeight == 0 {
return 0
Comment on lines +984 to +986
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check 0 idea is reasonable.But i dont think we should return zero here(It will cover up configuration errors and directly select the first target.)
for example:

rules:
  - name: my-rule
    targetModels:
      - modelServerName: server-a
        weight: 0
      - modelServerName: server-b
        weight: 0

func (s *store) selectDestination(targets []*aiv1alpha1.TargetModel) (*aiv1alpha1.TargetModel, error) {
weightedSlice, err := toWeightedSlice(targets)
if err != nil {
return nil, err
}
index := selectFromWeightedSlice(weightedSlice)
return targets[index], nil
}

selectDestination will select targets[0] (server-a)

Maybe we can add check in toWeightedSlice to throw an error

func toWeightedSlice(targets []*aiv1alpha1.TargetModel) ([]uint32, error) {
var isWeighted bool
if targets[0].Weight != nil {
isWeighted = true
}
res := make([]uint32, len(targets))
for i, target := range targets {
if (isWeighted && target.Weight == nil) || (!isWeighted && target.Weight != nil) {
return nil, fmt.Errorf("the weight field in targetModel must be either fully specified or not specified")
}
if isWeighted {
res[i] = *target.Weight
} else {
// If weight is not specified, set to 1.
res[i] = 1
}
}
return res, nil
}

	if isWeighted && totalWeight == 0 {
		return nil, fmt.Errorf("the sum of weights in targetModels must be greater than 0")
	}

to inform users that their coniguration is not right

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the suggestion
for this pr i’m intentionally keeping the fix scoped to preventing the runtime panic on the request path returning a deterministic fallback here avoids crashing the router for edgecase or transitional configurations
validating or rejecting misconfiguration earlier (e.g.,, at config or controller level) makes sense but i’d prefer to keep this change defensive and minimal rather than introducing a new panic in the routing path

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rules:
  - name: my-rule
    targetModels:
      - modelServerName: server-a
        weight: 0
      - modelServerName: server-b
        weight: 0

We should deny this CR in validating webhook actually.

	if isWeighted && totalWeight == 0 {
		return nil, fmt.Errorf("the sum of weights in targetModels must be greater than 0")
	}

Also this defensive validating make sense to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense
for this pr i kept the change focused on avoiding the runtime panic in the router path adding validation in the webhook to reject all zero weights sounds like the right place for enforcing configuration correctness and i’m happy to follow up with a separate change for that

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @YaoZengzeng YOu should verify it in validation as well

If all the weight is set to 0. Why do you choose targets[0]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in the all zero case returning targets[0] is just a deterministic fallback to keep the router from crashing on the request path not a correct routing decision
I agree this configuration should ideally be rejected earlier via validation this pr is intentionally scoped to making the runtime path defensive
Happy to align if you’d prefer validation to be included here.

}

randomNum := rng.Intn(totalWeight)

for i, weight := range weights {
Expand Down
66 changes: 66 additions & 0 deletions pkg/kthena-router/datastore/store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1217,3 +1217,69 @@ func TestStoreMatchModelServer(t *testing.T) {
})
}
}

func TestSelectFromWeightedSlice(t *testing.T) {
tests := []struct {
name string
weights []uint32
expectPanic bool
expectedResult int // only checked when not expecting panic and result is deterministic
checkRange bool
minResult int
maxResult int
}{
{
name: "all weights zero does not panic",
weights: []uint32{0, 0, 0},
expectPanic: false,
expectedResult: 0,
},
{
name: "single zero weight does not panic",
weights: []uint32{0},
expectPanic: false,
expectedResult: 0,
},
{
name: "normal weighted selection returns valid index",
weights: []uint32{1, 2, 3},
expectPanic: false,
checkRange: true,
minResult: 0,
maxResult: 2,
},
{
name: "single non-zero weight returns index 0",
weights: []uint32{5},
expectPanic: false,
expectedResult: 0,
},
{
name: "empty weights returns 0",
weights: []uint32{},
expectPanic: false,
expectedResult: 0,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.expectPanic {
assert.Panics(t, func() {
selectFromWeightedSlice(tt.weights)
})
return
}

assert.NotPanics(t, func() {
result := selectFromWeightedSlice(tt.weights)
if tt.checkRange {
assert.GreaterOrEqual(t, result, tt.minResult)
assert.LessOrEqual(t, result, tt.maxResult)
} else {
assert.Equal(t, tt.expectedResult, result)
}
})
})
}
}
Loading