Skip to content

Commit 9f928e1

Browse files
authored
Fix aggregated API server readiness race causing sporadic 503s in envtest suites (#1505)
* Fix aggregated API server readiness race causing sporadic 503s in envtest suites Signed-off-by: Andreas Fritzler <andreas.fritzler@sap.com> * Avoid reusing list objects across serve-readiness probes Signed-off-by: Andreas Fritzler <andreas.fritzler@sap.com> --------- Signed-off-by: Andreas Fritzler <andreas.fritzler@sap.com>
1 parent 99efc63 commit 9f928e1

1 file changed

Lines changed: 91 additions & 0 deletions

File tree

utils/envtest/envtest.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"context"
88
"errors"
99
"fmt"
10+
"net/url"
1011
"os"
1112
"path/filepath"
1213
"reflect"
@@ -18,6 +19,7 @@ import (
1819
"github.com/ironcore-dev/ironcore/utils/envtest/internal/testing/addr"
1920
"github.com/ironcore-dev/ironcore/utils/envtest/internal/testing/certs"
2021
corev1 "k8s.io/api/core/v1"
22+
apierrors "k8s.io/apimachinery/pkg/api/errors"
2123
"k8s.io/apimachinery/pkg/api/meta"
2224
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2325
"k8s.io/apimachinery/pkg/runtime"
@@ -591,6 +593,10 @@ func WaitUntilGroupVersionsDiscoverable(ctx context.Context, cfg *rest.Config, c
591593
return fmt.Errorf("error waiting for GVKs to be discoverable: %w", err)
592594
}
593595

596+
if err := waitUntilGroupVersionsServe(ctx, c, scheme, gvs...); err != nil {
597+
return fmt.Errorf("error waiting for aggregated api server to serve requests: %w", err)
598+
}
599+
594600
log.Info("All group versions and REST mappings are discoverable", "gvs", gvs)
595601
return nil
596602
}
@@ -661,6 +667,91 @@ func WaitUntilAPIServicesReady(ctx context.Context, ext *EnvironmentExtensions,
661667
return nil
662668
}
663669

670+
// poll interval for the aggregated API server serve probe.
671+
const serveReadinessPollInterval = 100 * time.Millisecond
672+
673+
// sustained success window to absorb the 503 startup race after APIService goes Available.
674+
const serveReadinessStableDuration = 500 * time.Millisecond
675+
676+
// waitUntilGroupVersionsServe polls a real List per GV until it succeeds for
677+
// serveReadinessStableDuration, absorbing the aggregated server's 503 window
678+
// after APIService reports Available.
679+
func waitUntilGroupVersionsServe(ctx context.Context, c client.Client, scheme *runtime.Scheme, gvs ...schema.GroupVersion) error {
680+
var listGVKs []schema.GroupVersionKind
681+
for _, gv := range gvs {
682+
if listGVK, ok := firstListGVKForGroupVersion(scheme, gv); ok {
683+
listGVKs = append(listGVKs, listGVK)
684+
}
685+
}
686+
if len(listGVKs) == 0 {
687+
return nil
688+
}
689+
690+
requiredSuccesses := int(serveReadinessStableDuration / serveReadinessPollInterval)
691+
successes := 0
692+
var lastErr error
693+
if err := wait.PollUntilContextCancel(ctx, serveReadinessPollInterval, true, func(ctx context.Context) (bool, error) {
694+
for _, listGVK := range listGVKs {
695+
list, err := scheme.New(listGVK)
696+
if err != nil {
697+
return false, fmt.Errorf("unexpected error constructing list for %s: %w", listGVK, err)
698+
}
699+
if err := c.List(ctx, list.(client.ObjectList)); err != nil {
700+
if !isTransientServeError(err) {
701+
return false, fmt.Errorf("unexpected error probing aggregated api server: %w", err)
702+
}
703+
lastErr = err
704+
successes = 0
705+
return false, nil
706+
}
707+
}
708+
successes++
709+
return successes >= requiredSuccesses, nil
710+
}); err != nil {
711+
if lastErr != nil {
712+
return fmt.Errorf("%w (last error: %v)", err, lastErr)
713+
}
714+
return err
715+
}
716+
return nil
717+
}
718+
719+
// firstListGVKForGroupVersion returns the first list-kind GVK registered for gv, sorted for determinism.
720+
func firstListGVKForGroupVersion(scheme *runtime.Scheme, gv schema.GroupVersion) (schema.GroupVersionKind, bool) {
721+
kinds := make([]string, 0, len(scheme.KnownTypes(gv)))
722+
for kind := range scheme.KnownTypes(gv) {
723+
kinds = append(kinds, kind)
724+
}
725+
sort.Strings(kinds)
726+
for _, kind := range kinds {
727+
obj, err := scheme.New(gv.WithKind(kind))
728+
if err != nil {
729+
continue
730+
}
731+
if _, ok := obj.(client.ObjectList); ok {
732+
return gv.WithKind(kind), true
733+
}
734+
}
735+
return schema.GroupVersionKind{}, false
736+
}
737+
738+
// isTransientServeError reports whether err is a transient startup condition worth retrying.
739+
func isTransientServeError(err error) bool {
740+
if err == nil {
741+
return false
742+
}
743+
switch {
744+
case apierrors.IsServiceUnavailable(err),
745+
apierrors.IsServerTimeout(err),
746+
apierrors.IsTimeout(err),
747+
apierrors.IsTooManyRequests(err),
748+
apierrors.IsInternalError(err):
749+
return true
750+
}
751+
var urlErr *url.Error
752+
return errors.As(err, &urlErr)
753+
}
754+
664755
func WaitUntilAPIServicesReadyWithTimeout(timeout time.Duration, ext *EnvironmentExtensions, cfg *rest.Config, c client.Client, scheme *runtime.Scheme) error {
665756
ctx, cancel := context.WithTimeout(context.Background(), timeout)
666757
defer cancel()

0 commit comments

Comments
 (0)