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 }