Skip to content

Commit b3ffd88

Browse files
authored
test(e2e): retry post-recovery requests in decode-pod disruption spec (llm-d#1961)
* test(e2e): retry post-recovery requests in decode-pod disruption spec The final recovery check sent unretried requests seconds after the EPP deployment turned Available. Envoy's active gRPC health check of the ext_proc cluster (10s interval) can lag actual EPP readiness in that window, producing a local 500 with no healthy upstream and failing the spec despite recovery having succeeded. Wrap the check in Eventually, requiring three consecutive successes, consistent with the other disruption specs. Signed-off-by: nilig <nili.ifergan@gmail.com> * test(e2e): share the post-recovery probe and verify completion content The streaming disruption spec had the same unretried post-recovery loop and the same exposure to transient gateway 500s. Both specs now poll a shared probe with MustPassRepeatedly(3). tryCompletion verifies finish reason and echoed text, matching runCompletion, so retry tolerance does not weaken what the recovery check asserts. Signed-off-by: nilig <nili.ifergan@gmail.com> --------- Signed-off-by: nilig <nili.ifergan@gmail.com>
1 parent 30385f8 commit b3ffd88

2 files changed

Lines changed: 33 additions & 17 deletions

File tree

test/e2e/disruption_test.go

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,19 @@ func eppPodReady(oldPodName string) func() bool {
8383
}
8484
}
8585

86+
// completionRoutedToNamespace sends one completion and reports an error on any
87+
// failure or namespace-header mismatch, for use inside Eventually blocks.
88+
func completionRoutedToNamespace() error {
89+
nsHdr, _, err := tryCompletion(simplePrompt, simModelName)
90+
if err != nil {
91+
return err
92+
}
93+
if nsHdr != nsName {
94+
return fmt.Errorf("expected namespace %q, got %q", nsName, nsHdr)
95+
}
96+
return nil
97+
}
98+
8699
var _ = ginkgo.Describe("Disruption tests", ginkgo.Ordered, ginkgo.Label(disruptiveTestLabel), func() {
87100
ginkgo.When("A decode pod is killed mid-request", func() {
88101
ginkgo.It("should recover and route to surviving pods", func() {
@@ -112,7 +125,7 @@ var _ = ginkgo.Describe("Disruption tests", ginkgo.Ordered, ginkgo.Label(disrupt
112125

113126
ginkgo.By("Verifying new requests eventually route to a pod other than the killed one")
114127
gomega.Eventually(func() error {
115-
nsHdr, podHdr, _, err := tryCompletion(simplePrompt, simModelName)
128+
nsHdr, podHdr, err := tryCompletion(simplePrompt, simModelName)
116129
if err != nil {
117130
return err
118131
}
@@ -131,11 +144,9 @@ var _ = ginkgo.Describe("Disruption tests", ginkgo.Ordered, ginkgo.Label(disrupt
131144
return len(currentDecode)
132145
}, readyTimeout, 2*time.Second).Should(gomega.Equal(2))
133146

134-
ginkgo.By("Verifying requests succeed after recovery")
135-
for range 3 {
136-
nsHdr, _, _ = runCompletion(simplePrompt, simModelName)
137-
gomega.Expect(nsHdr).Should(gomega.Equal(nsName))
138-
}
147+
ginkgo.By("Verifying requests succeed consistently after recovery")
148+
gomega.Eventually(completionRoutedToNamespace, eppRecoveryTimeout, 1*time.Second).
149+
MustPassRepeatedly(3).Should(gomega.Succeed())
139150
})
140151
})
141152

@@ -183,11 +194,9 @@ var _ = ginkgo.Describe("Disruption tests", ginkgo.Ordered, ginkgo.Label(disrupt
183194
return len(currentDecode)
184195
}, readyTimeout, 2*time.Second).Should(gomega.Equal(2))
185196

186-
ginkgo.By("Verifying requests succeed after recovery")
187-
for range 3 {
188-
nsHdr, _, _ := runCompletion(simplePrompt, simModelName)
189-
gomega.Expect(nsHdr).Should(gomega.Equal(nsName))
190-
}
197+
ginkgo.By("Verifying requests succeed consistently after recovery")
198+
gomega.Eventually(completionRoutedToNamespace, eppRecoveryTimeout, 1*time.Second).
199+
MustPassRepeatedly(3).Should(gomega.Succeed())
191200
})
192201
})
193202

test/e2e/requests_test.go

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ func runCompletion(prompt string, theModel openai.CompletionNewParamsModel) (str
118118

119119
// tryCompletion is like runCompletion but returns an error instead of asserting,
120120
// intended for use inside Eventually blocks where transient failures are acceptable.
121-
func tryCompletion(prompt string, theModel openai.CompletionNewParamsModel) (string, string, string, error) {
121+
func tryCompletion(prompt string, theModel openai.CompletionNewParamsModel) (string, string, error) {
122122
var httpResp *http.Response
123123
completionParams := openai.CompletionNewParams{
124124
Prompt: openai.CompletionNewParamsPromptUnion{OfString: openai.String(prompt)},
@@ -131,16 +131,23 @@ func tryCompletion(prompt string, theModel openai.CompletionNewParamsModel) (str
131131
option.WithRequestTimeout(readyTimeout),
132132
)
133133
if err != nil {
134-
return "", "", "", err
134+
return "", "", err
135135
}
136136
if httpResp == nil {
137-
return "", "", "", errors.New("missing http response")
137+
return "", "", errors.New("missing http response")
138138
}
139139
if len(resp.Choices) != 1 {
140-
return "", "", "", fmt.Errorf("expected 1 choice, got %d", len(resp.Choices))
140+
return "", "", fmt.Errorf("expected 1 choice, got %d", len(resp.Choices))
141141
}
142-
ns, pod, p := extractInferenceHeaders(httpResp)
143-
return ns, pod, p, nil
142+
if resp.Choices[0].FinishReason != openai.CompletionChoiceFinishReasonStop {
143+
return "", "", fmt.Errorf("expected finish reason %q, got %q",
144+
openai.CompletionChoiceFinishReasonStop, resp.Choices[0].FinishReason)
145+
}
146+
if resp.Choices[0].Text != prompt {
147+
return "", "", fmt.Errorf("expected echoed prompt, got %q", resp.Choices[0].Text)
148+
}
149+
ns, pod, _ := extractInferenceHeaders(httpResp)
150+
return ns, pod, nil
144151
}
145152

146153
func runChatCompletion(prompt, modelName string) (string, string, string) {

0 commit comments

Comments
 (0)