@@ -7,16 +7,14 @@ import (
77 "net/http/httptest"
88 "sync/atomic"
99 "testing"
10- "testing/synctest"
1110 "time"
1211
1312 clocktesting "k8s.io/utils/clock/testing"
1413
1514 operatorv1 "github.com/openshift/api/operator/v1"
16- "github.com/openshift/library-go/pkg/operator/v1helpers"
17-
1815 "github.com/openshift/library-go/pkg/controller/factory"
1916 "github.com/openshift/library-go/pkg/operator/events"
17+ "github.com/openshift/library-go/pkg/operator/v1helpers"
2018)
2119
2220func newSyncContext (t * testing.T ) factory.SyncContext {
@@ -102,73 +100,6 @@ func TestEndpointAccessibleController_sync(t *testing.T) {
102100 }
103101}
104102
105- // TestEndpointAccessibleController_sync_retry verifies the retry logic for
106- // fast (non-timeout) failures: the controller sleeps for retryInterval between
107- // attempts, and either recovers or gives up after attemptCount tries.
108- func TestEndpointAccessibleController_sync_retry (t * testing.T ) {
109- const attemptCount = 3
110-
111- tests := []struct {
112- name string
113- failFirstN int32 // how many initial requests the server should reject with 500
114- wantErr bool
115- }{
116- {
117- name : "succeeds on last attempt" ,
118- failFirstN : 2 ,
119- wantErr : false ,
120- },
121- {
122- name : "fails after all attempts exhausted" ,
123- failFirstN : 3 ,
124- wantErr : true ,
125- },
126- }
127- for _ , tt := range tests {
128- t .Run (tt .name , func (t * testing.T ) {
129- syncCtx := newSyncContext (t )
130- synctest .Test (t , func (t * testing.T ) {
131- c := & endpointAccessibleController {
132- operatorClient : v1helpers .NewFakeOperatorClient (& operatorv1.OperatorSpec {}, & operatorv1.OperatorStatus {}, nil ),
133- endpointListFn : func () ([]string , error ) {
134- return []string {"http://example.com" }, nil
135- },
136- httpClient : & http.Client {Transport : & failFastTransport {maxFails : tt .failFirstN }},
137- requestTimeout : defaultRequestTimeout ,
138- retryInterval : 10 * time .Second ,
139- attemptCount : attemptCount ,
140- }
141-
142- start := time .Now ()
143- done := make (chan error , 1 )
144- go func () {
145- done <- c .sync (context .Background (), syncCtx )
146- }()
147-
148- // Advance time for each backoff sleep between attempts.
149- backoffs := min (int (tt .failFirstN ), attemptCount - 1 )
150- for range backoffs {
151- synctest .Wait ()
152- time .Sleep (c .retryInterval + time .Millisecond )
153- }
154- synctest .Wait ()
155-
156- err := <- done
157- if (err != nil ) != tt .wantErr {
158- t .Errorf ("sync() error = %v, wantErr %v" , err , tt .wantErr )
159- }
160-
161- // Verify that each retry used the backoff sleep.
162- elapsed := time .Since (start )
163- expectedBackoff := time .Duration (backoffs ) * c .retryInterval
164- if elapsed < expectedBackoff {
165- t .Errorf ("elapsed %v < %v; backoff was skipped for fast failures" , elapsed , expectedBackoff )
166- }
167- })
168- })
169- }
170- }
171-
172103// TestEndpointAccessibleController_sync_retryStaleEndpoint verifies that
173104// endpointListFn is re-invoked on each retry attempt. This covers the upgrade
174105// scenario where Endpoints/EndpointSlices briefly contain a stale pod IP: the
@@ -212,100 +143,3 @@ func TestEndpointAccessibleController_sync_retryStaleEndpoint(t *testing.T) {
212143 }
213144}
214145
215- // TestEndpointAccessibleController_sync_requestTimeout verifies that the
216- // per-request timeout is enforced, that the retry mechanism handles timed-out
217- // requests correctly, and that the backoff sleep is skipped after timeouts
218- // (since the requestTimeout already provided sufficient delay).
219- func TestEndpointAccessibleController_sync_requestTimeout (t * testing.T ) {
220- const attemptCount = 3
221-
222- tests := []struct {
223- name string
224- hangCount int32 // requests that time out before one succeeds
225- wantErr bool
226- }{
227- {
228- name : "succeeds after one timed-out retry" ,
229- hangCount : 1 ,
230- wantErr : false ,
231- },
232- {
233- name : "fails after all retries time out" ,
234- hangCount : 3 ,
235- wantErr : true ,
236- },
237- }
238- for _ , tt := range tests {
239- t .Run (tt .name , func (t * testing.T ) {
240- // Create the sync context outside the bubble: factory.NewSyncContext spawns
241- // a background work-queue goroutine that never exits on its own, which would
242- // deadlock the synctest bubble.
243- syncCtx := newSyncContext (t )
244- synctest .Test (t , func (t * testing.T ) {
245- c := & endpointAccessibleController {
246- operatorClient : v1helpers .NewFakeOperatorClient (& operatorv1.OperatorSpec {}, & operatorv1.OperatorStatus {}, nil ),
247- endpointListFn : func () ([]string , error ) {
248- return []string {"http://example.com" }, nil
249- },
250- httpClient : & http.Client {Transport : & hangingTransport {maxHangs : tt .hangCount }},
251- requestTimeout : defaultRequestTimeout ,
252- retryInterval : 10 * time .Second , // large — would be visible if not skipped
253- attemptCount : attemptCount ,
254- }
255-
256- start := time .Now ()
257- done := make (chan error , 1 )
258- go func () {
259- done <- c .sync (context .Background (), syncCtx )
260- }()
261-
262- for range tt .hangCount {
263- synctest .Wait ()
264- time .Sleep (c .requestTimeout + time .Millisecond )
265- }
266- synctest .Wait ()
267-
268- err := <- done
269- if (err != nil ) != tt .wantErr {
270- t .Errorf ("sync() error = %v, wantErr %v" , err , tt .wantErr )
271- }
272-
273- // Elapsed time should be only hangCount * requestTimeout with no
274- // retryInterval added — backoff is skipped after timeouts.
275- elapsed := time .Since (start )
276- maxExpected := time .Duration (tt .hangCount )* (c .requestTimeout + time .Millisecond ) + time .Second
277- if elapsed > maxExpected {
278- t .Errorf ("elapsed %v exceeds %v; backoff sleep was not skipped after timeout" , elapsed , maxExpected )
279- }
280- })
281- })
282- }
283- }
284-
285- // hangingTransport simulates a slow endpoint: the first maxHangs requests block
286- // until their context is canceled; subsequent requests succeed immediately with 200 OK.
287- type hangingTransport struct {
288- count atomic.Int32
289- maxHangs int32
290- }
291-
292- func (h * hangingTransport ) RoundTrip (req * http.Request ) (* http.Response , error ) {
293- if h .count .Add (1 ) <= h .maxHangs {
294- <- req .Context ().Done ()
295- return nil , req .Context ().Err ()
296- }
297- return & http.Response {StatusCode : http .StatusOK , Body : http .NoBody }, nil
298- }
299-
300- // failFastTransport returns 500 for the first maxFails requests, then 200.
301- type failFastTransport struct {
302- count atomic.Int32
303- maxFails int32
304- }
305-
306- func (f * failFastTransport ) RoundTrip (req * http.Request ) (* http.Response , error ) {
307- if f .count .Add (1 ) <= f .maxFails {
308- return & http.Response {StatusCode : http .StatusInternalServerError , Body : http .NoBody }, nil
309- }
310- return & http.Response {StatusCode : http .StatusOK , Body : http .NoBody }, nil
311- }
0 commit comments