@@ -494,48 +494,61 @@ func (s *composeService) isSwarmEnabled(ctx context.Context) (bool, error) {
494494 return swarmEnabled .val , swarmEnabled .err
495495}
496496
497+ // runtimeVersionCache caches a version string after a successful lookup.
498+ // Transient errors (including context cancellation) are not cached so that
499+ // subsequent calls can retry with a fresh context.
497500type runtimeVersionCache struct {
498- once sync.Once
499- val string
500- err error
501+ mu sync.Mutex
502+ val string
501503}
502504
505+ // RuntimeVersion returns the raw API version reported by the daemon.
506+ // Callers that need the negotiated/effective client API version should use
507+ // CurrentAPIVersion instead.
503508func (s * composeService ) RuntimeVersion (ctx context.Context ) (string , error ) {
504- // RuntimeVersion returns the raw API version reported by the daemon.
505- // Callers that need the negotiated/effective client API version should use
506- // CurrentAPIVersion instead.
507- s .runtimeVersion .once .Do (func () {
508- version , err := s .apiClient ().ServerVersion (ctx , client.ServerVersionOptions {})
509- if err != nil {
510- s .runtimeVersion .err = err
511- return
512- }
513- s .runtimeVersion .val = version .APIVersion
514- })
515- return s .runtimeVersion .val , s .runtimeVersion .err
509+ s .runtimeVersion .mu .Lock ()
510+ defer s .runtimeVersion .mu .Unlock ()
511+ if s .runtimeVersion .val != "" {
512+ return s .runtimeVersion .val , nil
513+ }
514+ version , err := s .apiClient ().ServerVersion (ctx , client.ServerVersionOptions {})
515+ if err != nil {
516+ return "" , err
517+ }
518+ s .runtimeVersion .val = version .APIVersion
519+ return s .runtimeVersion .val , nil
516520}
517521
518522// CurrentAPIVersion returns the API version currently used by the Docker client.
519523// Trigger negotiation first so version-gated request shaping matches the version
520524// that subsequent API calls will actually use.
525+ //
526+ // Lock ordering: currentAPIVersion.mu must be acquired before runtimeVersion.mu
527+ // (via the RuntimeVersion fallback). No code path should reverse this order.
521528func (s * composeService ) CurrentAPIVersion (ctx context.Context ) (string , error ) {
522- s .currentAPIVersion .once .Do (func () {
523- _ , err := s .apiClient ().Ping (ctx , client.PingOptions {NegotiateAPIVersion : true })
524- if err != nil {
525- s .currentAPIVersion .err = err
526- return
527- }
529+ s .currentAPIVersion .mu .Lock ()
530+ defer s .currentAPIVersion .mu .Unlock ()
531+ if s .currentAPIVersion .val != "" {
532+ return s .currentAPIVersion .val , nil
533+ }
528534
529- version := s .apiClient ().ClientVersion ()
530- if version != "" {
531- s .currentAPIVersion .val = version
532- return
533- }
535+ _ , err := s .apiClient ().Ping (ctx , client.PingOptions {NegotiateAPIVersion : true })
536+ if err != nil {
537+ return "" , err
538+ }
534539
535- // Defensive fallback for unexpected client implementations or mocks that
536- // do not populate ClientVersion after a successful negotiated ping.
537- s .currentAPIVersion .val , s .currentAPIVersion .err = s .RuntimeVersion (ctx )
538- })
540+ version := s .apiClient ().ClientVersion ()
541+ if version != "" {
542+ s .currentAPIVersion .val = version
543+ return s .currentAPIVersion .val , nil
544+ }
539545
540- return s .currentAPIVersion .val , s .currentAPIVersion .err
546+ // Defensive fallback for unexpected client implementations or mocks that
547+ // do not populate ClientVersion after a successful negotiated ping.
548+ val , err := s .RuntimeVersion (ctx )
549+ if err != nil {
550+ return "" , err
551+ }
552+ s .currentAPIVersion .val = val
553+ return s .currentAPIVersion .val , nil
541554}
0 commit comments