Skip to content

Commit 7478bda

Browse files
haimariclaude
andcommitted
fix: Implement availability domain caching and request deduplication
- Add AvailabilityDomainCache to cache AD data for 1 hour - Implement RequestDeduplicator to prevent concurrent identical API calls - Add exponential backoff with rate limiting detection for AD API calls - Cache reduces API calls from every provisioning attempt to once per hour - Deduplication prevents multiple simultaneous calls during provisioning bursts - Fixes HTTP 429 "Too Many Requests" errors on /availabilityDomains endpoint - Resolves "no instance type has the required offering" scheduling errors This addresses the root cause where Karpenter was making multiple concurrent calls to the same OCI Identity Service endpoint, overwhelming rate limits. Some requests succeeded (logged in OCI audit) while others were rate limited (429 errors in Karpenter logs), causing inconsistent behavior. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent ef5ea62 commit 7478bda

2 files changed

Lines changed: 160 additions & 13 deletions

File tree

pkg/providers/oci/client.go

Lines changed: 146 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"io"
2424
"strconv"
2525
"strings"
26+
"sync"
2627
"time"
2728

2829
"github.com/oracle/oci-go-sdk/v65/common"
@@ -38,12 +39,33 @@ import (
3839
"sigs.k8s.io/karpenter/pkg/providers/oci/apis/v1alpha1"
3940
)
4041

42+
// AvailabilityDomainCache caches availability domains to avoid repeated API calls
43+
type AvailabilityDomainCache struct {
44+
domains []string
45+
lastFetch time.Time
46+
mutex sync.RWMutex
47+
ttl time.Duration
48+
}
49+
4150
// Client wraps OCI API operations
4251
type Client struct {
4352
config *Config
4453
computeClient core.ComputeClient
4554
containerEngineClient containerengine.ContainerEngineClient
4655
configProvider common.ConfigurationProvider
56+
adCache *AvailabilityDomainCache
57+
requestDeduplicator *RequestDeduplicator
58+
}
59+
60+
// RequestDeduplicator prevents concurrent identical requests
61+
type RequestDeduplicator struct {
62+
inflight map[string]chan result
63+
mutex sync.Mutex
64+
}
65+
66+
type result struct {
67+
value []string
68+
err error
4769
}
4870

4971
// NewClient creates a new OCI client
@@ -111,6 +133,12 @@ func NewClient(config *Config) (*Client, error) {
111133
computeClient: computeClient,
112134
containerEngineClient: containerEngineClient,
113135
configProvider: configProvider,
136+
adCache: &AvailabilityDomainCache{
137+
ttl: 1 * time.Hour, // Cache ADs for 1 hour since they rarely change
138+
},
139+
requestDeduplicator: &RequestDeduplicator{
140+
inflight: make(map[string]chan result),
141+
},
114142
}, nil
115143
}
116144

@@ -631,31 +659,136 @@ func (c *Client) CreateClusterKubeconfig(ctx context.Context) (string, error) {
631659

632660
// Helper methods
633661

634-
// getAvailabilityDomain gets the first available availability domain
662+
// getAvailabilityDomain gets the first available availability domain with caching and deduplication
635663
func (c *Client) getAvailabilityDomain(ctx context.Context) (string, error) {
664+
logger := log.FromContext(ctx)
665+
666+
// Try cache first
667+
c.adCache.mutex.RLock()
668+
if len(c.adCache.domains) > 0 && time.Since(c.adCache.lastFetch) < c.adCache.ttl {
669+
domain := c.adCache.domains[0]
670+
c.adCache.mutex.RUnlock()
671+
logger.V(1).Info("using cached availability domain", "domain", domain)
672+
return domain, nil
673+
}
674+
c.adCache.mutex.RUnlock()
675+
676+
// Cache expired or empty, fetch new data with deduplication
677+
domains, err := c.getAvailabilityDomainsWithDeduplication(ctx)
678+
if err != nil {
679+
return "", err
680+
}
681+
682+
if len(domains) == 0 {
683+
return "", fmt.Errorf("no availability domains found")
684+
}
685+
686+
// For now, return the first AD. In production, this should be more sophisticated
687+
// based on capacity, spread, and fault domain distribution
688+
return domains[0], nil
689+
}
690+
691+
// getAvailabilityDomainsWithDeduplication fetches availability domains with request deduplication
692+
func (c *Client) getAvailabilityDomainsWithDeduplication(ctx context.Context) ([]string, error) {
693+
logger := log.FromContext(ctx)
694+
key := fmt.Sprintf("ad-%s", c.config.CompartmentID)
695+
696+
c.requestDeduplicator.mutex.Lock()
697+
if ch, exists := c.requestDeduplicator.inflight[key]; exists {
698+
// Another request is in flight, wait for it
699+
c.requestDeduplicator.mutex.Unlock()
700+
logger.V(1).Info("waiting for inflight availability domain request")
701+
select {
702+
case res := <-ch:
703+
return res.value, res.err
704+
case <-ctx.Done():
705+
return nil, ctx.Err()
706+
}
707+
}
708+
709+
// No inflight request, start new one
710+
ch := make(chan result, 1)
711+
c.requestDeduplicator.inflight[key] = ch
712+
c.requestDeduplicator.mutex.Unlock()
713+
714+
// Clean up when done
715+
defer func() {
716+
c.requestDeduplicator.mutex.Lock()
717+
delete(c.requestDeduplicator.inflight, key)
718+
c.requestDeduplicator.mutex.Unlock()
719+
}()
720+
721+
logger.V(1).Info("fetching availability domains from OCI API")
722+
domains, err := c.fetchAvailabilityDomainsFromAPI(ctx)
723+
724+
// Send result to all waiters
725+
res := result{value: domains, err: err}
726+
select {
727+
case ch <- res:
728+
default:
729+
}
730+
731+
if err == nil && len(domains) > 0 {
732+
// Update cache
733+
c.adCache.mutex.Lock()
734+
c.adCache.domains = domains
735+
c.adCache.lastFetch = time.Now()
736+
c.adCache.mutex.Unlock()
737+
logger.Info("cached availability domains", "count", len(domains), "domains", domains)
738+
}
739+
740+
return domains, err
741+
}
742+
743+
// fetchAvailabilityDomainsFromAPI makes the actual API call to OCI
744+
func (c *Client) fetchAvailabilityDomainsFromAPI(ctx context.Context) ([]string, error) {
745+
logger := log.FromContext(ctx)
746+
636747
request := identity.ListAvailabilityDomainsRequest{
637748
CompartmentId: &c.config.CompartmentID,
638749
}
639750

640-
// We need to create an identity client for this
751+
// Create identity client
641752
identityClient, err := identity.NewIdentityClientWithConfigurationProvider(c.configProvider)
642753
if err != nil {
643-
return "", fmt.Errorf("creating identity client: %w", err)
754+
return nil, fmt.Errorf("creating identity client: %w", err)
644755
}
645-
// Note: IdentityClient doesn't have a Close method in the OCI SDK
646756

647-
response, err := identityClient.ListAvailabilityDomains(ctx, request)
648-
if err != nil {
649-
return "", WrapOCIError(err, "availability domains")
650-
}
757+
// Add exponential backoff for rate limiting
758+
var domains []string
759+
err = wait.ExponentialBackoff(wait.Backoff{
760+
Duration: 1 * time.Second,
761+
Factor: 2.0,
762+
Jitter: 0.1,
763+
Steps: 5,
764+
Cap: 30 * time.Second,
765+
}, func() (bool, error) {
766+
response, apiErr := identityClient.ListAvailabilityDomains(ctx, request)
767+
if apiErr != nil {
768+
wrappedErr := WrapOCIError(apiErr, "availability domains")
769+
if IsRateLimitError(wrappedErr) {
770+
logger.V(1).Info("rate limited on availability domains API, retrying", "error", apiErr)
771+
return false, nil // Retry
772+
}
773+
return false, wrappedErr // Don't retry on other errors
774+
}
651775

652-
if len(response.Items) == 0 {
653-
return "", fmt.Errorf("no availability domains found")
776+
// Success - extract domain names
777+
for _, item := range response.Items {
778+
if item.Name != nil {
779+
domains = append(domains, *item.Name)
780+
}
781+
}
782+
return true, nil
783+
})
784+
785+
if err != nil {
786+
logger.Error(err, "failed to fetch availability domains after retries")
787+
return nil, err
654788
}
655789

656-
// For now, return the first AD. In production, this should be more sophisticated
657-
// based on capacity, spread, and fault domain distribution
658-
return *response.Items[0].Name, nil
790+
logger.Info("successfully fetched availability domains", "count", len(domains))
791+
return domains, nil
659792
}
660793

661794
func (c *Client) selectAvailabilityDomain() string {

pkg/providers/oci/errors.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,4 +248,18 @@ func findSubstring(s, substr string) bool {
248248
func extractResourceID(errMsg string) string {
249249
// Simple extraction - in real implementation would be more sophisticated
250250
return "unknown"
251+
}
252+
253+
// IsRateLimitError checks if an error is due to rate limiting
254+
func IsRateLimitError(err error) bool {
255+
if err == nil {
256+
return false
257+
}
258+
259+
errMsg := err.Error()
260+
return contains(errMsg, "TooManyRequests") ||
261+
contains(errMsg, "429") ||
262+
contains(errMsg, "rate limit") ||
263+
contains(errMsg, "throttled") ||
264+
contains(errMsg, "too many requests")
251265
}

0 commit comments

Comments
 (0)