From a14e7c5bf041ac4bee5ba6a4740a69b3127dd5f2 Mon Sep 17 00:00:00 2001 From: Arjan Bal Date: Mon, 13 Oct 2025 11:32:40 +0530 Subject: [PATCH] Enter transient failure on resolver creation failure --- clientconn.go | 4 +--- clientconn_parsed_target_test.go | 12 +++++++++--- resolver_wrapper.go | 32 +++++++++++++++++++++++++------- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/clientconn.go b/clientconn.go index a3c315f2d76e..ec4769fb55d1 100644 --- a/clientconn.go +++ b/clientconn.go @@ -357,9 +357,7 @@ func (cc *ClientConn) exitIdleMode() (err error) { // This needs to be called without cc.mu because this builds a new resolver // which might update state or report error inline, which would then need to // acquire cc.mu. - if err := cc.resolverWrapper.start(); err != nil { - return err - } + cc.resolverWrapper.start() cc.addTraceEvent("exiting idle mode") return nil diff --git a/clientconn_parsed_target_test.go b/clientconn_parsed_target_test.go index 499125a7c16a..ea7ae1d032ed 100644 --- a/clientconn_parsed_target_test.go +++ b/clientconn_parsed_target_test.go @@ -27,6 +27,7 @@ import ( "time" "github.com/google/go-cmp/cmp" + "google.golang.org/grpc/connectivity" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/internal" "google.golang.org/grpc/internal/testutils" @@ -185,6 +186,8 @@ func (s) TestParsedTarget_Success_WithoutCustomDialer(t *testing.T) { } func (s) TestParsedTarget_Failure_WithoutCustomDialer(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) + defer cancel() targets := []string{ "", "unix://a/b/c", @@ -195,10 +198,13 @@ func (s) TestParsedTarget_Failure_WithoutCustomDialer(t *testing.T) { for _, target := range targets { t.Run(target, func(t *testing.T) { - if cc, err := Dial(target, WithTransportCredentials(insecure.NewCredentials())); err == nil { - defer cc.Close() - t.Fatalf("Dial(%q) succeeded cc.parsedTarget = %+v, expected to fail", target, cc.parsedTarget) + cc, err := NewClient(target, WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatalf("NewClient(%q) failed: %v", target, err) } + defer cc.Close() + cc.Connect() + testutils.AwaitState(ctx, t, cc, connectivity.TransientFailure) }) } } diff --git a/resolver_wrapper.go b/resolver_wrapper.go index 80e16a327cd3..986d83fa7a5a 100644 --- a/resolver_wrapper.go +++ b/resolver_wrapper.go @@ -20,6 +20,7 @@ package grpc import ( "context" + "fmt" "strings" "sync" @@ -62,11 +63,15 @@ func newCCResolverWrapper(cc *ClientConn) *ccResolverWrapper { } } -// start builds the name resolver using the resolver.Builder in cc and returns -// any error encountered. It must always be the first operation performed on -// any newly created ccResolverWrapper, except that close may be called instead. -func (ccr *ccResolverWrapper) start() error { - errCh := make(chan error) +// start builds the name resolver using the resolver.Builder in cc. +// If an error is encountered, it will report the error to the load balancing +// policy via cc.ReportError(). This action allows the policy to set the channel +// state to TransientFailure and ensures the error is propagated to new RPCs, +// causing them to fail. +// It must always be the first operation performed on any newly created +// ccResolverWrapper, except that close may be called instead. +func (ccr *ccResolverWrapper) start() { + doneCh := make(chan struct{}) ccr.serializer.TrySchedule(func(ctx context.Context) { if ctx.Err() != nil { return @@ -90,11 +95,24 @@ func (ccr *ccResolverWrapper) start() error { } else { ccr.resolver, err = delegatingresolver.New(ccr.cc.parsedTarget, ccr, opts, ccr.cc.resolverBuilder, ccr.cc.dopts.enableLocalDNSResolution) } - errCh <- err + + if err != nil { + ccr.resolver = &nopResolver{} + ccr.ReportError(fmt.Errorf("resolver creation failed: %v", err)) + } + + doneCh <- struct{}{} }) - return <-errCh + <-doneCh } +type nopResolver struct { +} + +func (*nopResolver) ResolveNow(resolver.ResolveNowOptions) {} + +func (*nopResolver) Close() {} + func (ccr *ccResolverWrapper) resolveNow(o resolver.ResolveNowOptions) { ccr.serializer.TrySchedule(func(ctx context.Context) { if ctx.Err() != nil || ccr.resolver == nil {