@@ -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
4251type 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
635663func (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
661794func (c * Client ) selectAvailabilityDomain () string {
0 commit comments