github.com/renbou/grpcbridge@v0.0.2-0.20240416012907-bcbd8b12648a/reflection.go (about)

     1  package grpcbridge
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"net/http"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/renbou/grpcbridge/bridgedesc"
    12  	"github.com/renbou/grpcbridge/grpcadapter"
    13  	"github.com/renbou/grpcbridge/reflection"
    14  	"github.com/renbou/grpcbridge/routing"
    15  	"google.golang.org/grpc"
    16  )
    17  
    18  // RouterOption configures all the components needed to set up grpcbridge routing - gRPC reflection, client pools, logging.
    19  type RouterOption interface {
    20  	applyRouter(*routerOptions)
    21  }
    22  
    23  // ReflectionRouter provides the default [Router] implementation used by grpcbridge.
    24  // It uses the [gRPC reflection Protocol] to retrieve Protobuf contracts of target gRPC services added via the [ReflectionRouter.Add] method,
    25  // which launches a new [reflection.Resolver] to perform polling for contract updates, represented as [bridgedesc.Target] structures, in the background.
    26  //
    27  // Routing is performed using [routing.ServiceRouter] for gRPC requests based on the requested gRPC service name,
    28  // and [routing.PatternRouter] for HTTP requests, supporting familiar pattern-based routing defined by Google's [HTTP to gRPC Transcoding spec].
    29  //
    30  // [grpcadapter.AdaptedClientPool] is used by the various components as a centralized gRPC client pool,
    31  // providing support for features such as gRPC stream receiving/sending cancelation,
    32  // waiting for client liveness and reconnections, and more.
    33  //
    34  // [gRPC reflection Protocol]: https://github.com/grpc/grpc/blob/master/doc/server-reflection.md
    35  // [HTTP to gRPC Transcoding spec]: https://cloud.google.com/endpoints/docs/grpc/transcoding#adding_transcoding_mappings.
    36  type ReflectionRouter struct {
    37  	connpool        *grpcadapter.AdaptedClientPool
    38  	resolverBuilder *reflection.ResolverBuilder
    39  	patternRouter   *routing.PatternRouter
    40  	serviceRouter   *routing.ServiceRouter
    41  
    42  	// mu protects the router state.
    43  	mu      sync.Mutex
    44  	targets map[string]targetState
    45  }
    46  
    47  // NewReflectionRouter constructs a new [*ReflectionRouter] with the given options.
    48  // When no options are provided, the respective components are initialized with their default options.
    49  func NewReflectionRouter(opts ...RouterOption) *ReflectionRouter {
    50  	options := defaultRouterOptions()
    51  
    52  	for _, opt := range opts {
    53  		opt.applyRouter(&options)
    54  	}
    55  
    56  	options.resolverOpts.Logger = options.common.logger
    57  
    58  	connpool := grpcadapter.NewAdaptedClientPool(options.clientPoolOpts)
    59  	resolverBuilder := reflection.NewResolverBuilder(connpool, options.resolverOpts)
    60  	patternRouter := routing.NewPatternRouter(connpool, routing.PatternRouterOpts{Logger: options.common.logger})
    61  	serviceRouter := routing.NewServiceRouter(connpool, routing.ServiceRouterOpts{Logger: options.common.logger})
    62  
    63  	return &ReflectionRouter{
    64  		connpool:        connpool,
    65  		resolverBuilder: resolverBuilder,
    66  		patternRouter:   patternRouter,
    67  		serviceRouter:   serviceRouter,
    68  		targets:         make(map[string]targetState),
    69  	}
    70  }
    71  
    72  // Add adds a new target by an arbitrary name and its gRPC target to the router with the possibility to override the default options.
    73  // It returns true if a new target was added, and false if the target was already present,
    74  // in which case all the components are updated to use the new options.
    75  // Unless a custom connection constructor was specified using [WithConnFunc],
    76  // the target name syntax must adhere to standard gRPC rules defined in https://github.com/grpc/grpc/blob/master/doc/naming.md.
    77  //
    78  // When add returns, it means that the target was added to the router components,
    79  // but no guarantees are made as to when the target can actually be returned by the router.
    80  // For example, the initial resolution of the target's Protobuf contracts can fail,
    81  // in which case the routers will have no information available regarding the target's routes.
    82  //
    83  // TODO(renbou): support option overriding.
    84  // TODO(renbou): support modifications to existing targets.
    85  // TODO(renbou): support using ServiceRouter for HTTP routing when simplified reflection resolving is enabled.
    86  func (r *ReflectionRouter) Add(name string, target string, opts ...RouterOption) (bool, error) {
    87  	r.mu.Lock()
    88  	defer r.mu.Unlock()
    89  
    90  	if _, ok := r.targets[name]; ok {
    91  		return false, errors.New("ReflectionRouter currently doesn't support adding the same target twice")
    92  	} else if len(opts) > 0 {
    93  		return false, errors.New("ReflectionRouter currently doesn't support per-target option overrides")
    94  	}
    95  
    96  	poolController, err := r.connpool.New(name, target)
    97  	if err != nil {
    98  		return false, fmt.Errorf("creating new gRPC client for target %q: %w", target, err)
    99  	}
   100  
   101  	patternWatcher, err := r.patternRouter.Watch(name)
   102  	if err != nil {
   103  		return false, fmt.Errorf("failed to watch target %q with PatternRouter, this should never happen: %w", name, err)
   104  	}
   105  
   106  	serviceWatcher, err := r.serviceRouter.Watch(name)
   107  	if err != nil {
   108  		return false, fmt.Errorf("failed to watch target %q with ServiceRouter, this should never happen: %w", name, err)
   109  	}
   110  
   111  	watcher := &aggregateWatcher{watchers: []closableWatcher{patternWatcher, serviceWatcher}}
   112  	resolver := r.resolverBuilder.Build(name, watcher)
   113  
   114  	r.targets[name] = targetState{resolver: resolver, poolController: poolController, watcher: watcher}
   115  
   116  	return true, nil
   117  }
   118  
   119  // Remove removes a target by its name, stopping the reflection polling and closing the gRPC client, returning true if the target was present.
   120  // No more requests will be routed to the target after this call returns, meaning it will block until all the components acknowledge the removal.
   121  func (r *ReflectionRouter) Remove(name string) bool {
   122  	r.mu.Lock()
   123  	defer r.mu.Unlock()
   124  
   125  	if _, ok := r.targets[name]; !ok {
   126  		return false
   127  	}
   128  
   129  	target := r.targets[name]
   130  
   131  	// Close the watchers first to avoid any errors due to closed clients and such.
   132  	target.watcher.Close()
   133  
   134  	// Then close the resolver to stop polling, after this no more dependencies on the pool client should be present.
   135  	target.resolver.Close()
   136  
   137  	// Close the pooled connection gracefully.
   138  	target.poolController.Close()
   139  
   140  	delete(r.targets, name)
   141  
   142  	return true
   143  }
   144  
   145  // RouteHTTP uses the router's [routing.PatternRouter] instance to perform pattern-based routing for HTTP-originating requests,
   146  // such as simple REST-like HTTP requests, WebSocket streams, and Server-Sent Event streams.
   147  func (r *ReflectionRouter) RouteHTTP(req *http.Request) (grpcadapter.ClientConn, routing.HTTPRoute, error) {
   148  	return r.patternRouter.RouteHTTP(req)
   149  }
   150  
   151  // RouteGRPC uses the router's [routing.ServiceRouter] instance to perform service-based routing for gRPC-like requests.
   152  // This routing method is used for gRPC proxying, as well as gRPC-Web bridging of HTTP and WebSockets.
   153  func (r *ReflectionRouter) RouteGRPC(ctx context.Context) (grpcadapter.ClientConn, routing.GRPCRoute, error) {
   154  	return r.serviceRouter.RouteGRPC(ctx)
   155  }
   156  
   157  // WithDialOpts returns a RouterOption that sets the default gRPC dial options used by [grpcadapter.AdaptedClientPool] for establishing new connections.
   158  // Multiple WithDialOpts calls can be chained together, and they will be aggregated into a single list of options.
   159  func WithDialOpts(opts ...grpc.DialOption) RouterOption {
   160  	return newFuncRouterOption(func(o *routerOptions) {
   161  		o.clientPoolOpts.DefaultOpts = append(o.clientPoolOpts.DefaultOpts, opts...)
   162  	})
   163  }
   164  
   165  // WithConnFunc returns a RouterOption that sets the function that will be used by [grpcadapter.AdaptedClientPool]
   166  // for establishing new connections instead of [grpc.NewClient].
   167  func WithConnFunc(f func(target string, opts ...grpc.DialOption) (*grpc.ClientConn, error)) RouterOption {
   168  	return newFuncRouterOption(func(o *routerOptions) {
   169  		o.clientPoolOpts.NewClientFunc = f
   170  	})
   171  }
   172  
   173  // WithReflectionPollInterval returns a RouterOption that sets the interval at which [reflection.Resolver]
   174  // will poll target gRPC servers for proto changes, by default equal to 5 minutes.
   175  func WithReflectionPollInterval(interval time.Duration) RouterOption {
   176  	return newFuncRouterOption(func(o *routerOptions) {
   177  		o.resolverOpts.PollInterval = interval
   178  	})
   179  }
   180  
   181  // WithDisabledReflectionPolling returns a RouterOption that disables polling for proto changes,
   182  // meaning that [reflection.Resolver] will retrieve the proto descriptors of target servers only once.
   183  // For more granular control over when re-resolving happens, [reflection.ResolverBuilder] should be used to manually create resolvers.
   184  func WithDisabledReflectionPolling() RouterOption {
   185  	return newFuncRouterOption(func(o *routerOptions) {
   186  		o.resolverOpts.PollInterval = 0
   187  	})
   188  }
   189  
   190  type routerOptions struct {
   191  	common         options
   192  	clientPoolOpts grpcadapter.AdaptedClientPoolOpts
   193  	resolverOpts   reflection.ResolverOpts
   194  }
   195  
   196  func defaultRouterOptions() routerOptions {
   197  	return routerOptions{
   198  		common: defaultOptions(),
   199  	}
   200  }
   201  
   202  type funcRouterOption struct {
   203  	f func(*routerOptions)
   204  }
   205  
   206  func (f *funcRouterOption) applyRouter(o *routerOptions) {
   207  	f.f(o)
   208  }
   209  
   210  func newFuncRouterOption(f func(*routerOptions)) RouterOption {
   211  	return &funcRouterOption{f: f}
   212  }
   213  
   214  type targetState struct {
   215  	resolver       *reflection.Resolver
   216  	poolController *grpcadapter.AdaptedClientPoolController
   217  	watcher        closableWatcher
   218  }
   219  
   220  // closableWatcher is implemented by routing.ServiceRouterWatcher and routing.PatternRouterWatcher.
   221  type closableWatcher interface {
   222  	reflection.Watcher
   223  	Close()
   224  }
   225  
   226  type aggregateWatcher struct {
   227  	watchers []closableWatcher
   228  }
   229  
   230  func (a *aggregateWatcher) UpdateDesc(target *bridgedesc.Target) {
   231  	for _, w := range a.watchers {
   232  		w.UpdateDesc(target)
   233  	}
   234  }
   235  
   236  func (a *aggregateWatcher) ReportError(err error) {
   237  	for _, w := range a.watchers {
   238  		w.ReportError(err)
   239  	}
   240  }
   241  
   242  func (a *aggregateWatcher) Close() {
   243  	for _, w := range a.watchers {
   244  		w.Close()
   245  	}
   246  }