github.com/renbou/grpcbridge@v0.0.2-0.20240416012907-bcbd8b12648a/routing/service_router.go (about) 1 package routing 2 3 import ( 4 "context" 5 "net/http" 6 "strings" 7 "sync" 8 "sync/atomic" 9 10 "github.com/renbou/grpcbridge/bridgedesc" 11 "github.com/renbou/grpcbridge/bridgelog" 12 "github.com/renbou/grpcbridge/grpcadapter" 13 "github.com/renbou/grpcbridge/internal/httperr" 14 "github.com/renbou/grpcbridge/internal/syncset" 15 "google.golang.org/grpc" 16 "google.golang.org/grpc/codes" 17 "google.golang.org/grpc/status" 18 "google.golang.org/protobuf/reflect/protoreflect" 19 ) 20 21 // ServiceRouterOpts define all the optional settings which can be set for [ServiceRouter]. 22 type ServiceRouterOpts struct { 23 // Logs are discarded by default. 24 Logger bridgelog.Logger 25 } 26 27 func (o ServiceRouterOpts) withDefaults() ServiceRouterOpts { 28 if o.Logger == nil { 29 o.Logger = bridgelog.Discard() 30 } 31 32 return o 33 } 34 35 // ServiceRouter is a router meant for routing HTTP or gRPC requests with the standard gRPC method format of the form "/package.Service/Method". 36 // It relies on all targets having unique gRPC service names, and it doesn't even take the method name into account, 37 // which allows it to run solely based on the knowledge of a target's service names. 38 // This is useful as it allows description resolvers to be greatly simplified or optimized. 39 // For example, the reflection resolver in [github.com/renbou/grpcbridge/reflection] has an OnlyServices option for this specific case. 40 type ServiceRouter struct { 41 pool grpcadapter.ClientPool 42 logger bridgelog.Logger 43 watcherSet *syncset.SyncSet[string] 44 45 // map from gRPC service name to the target name, used for actually routing requests. 46 // using a sync.Map makes sense here because writes to it happen very rarely compared to reads. 47 routes sync.Map // protoreflect.FullName -> serviceRoute 48 49 // used to synchronize updates, doesn't affect reading. 50 mu sync.Mutex 51 52 // map from target name to its gRPC service names, 53 // used for keeping track of a service's routes and manipulating the routing mapping on updates. 54 svcRoutes map[string][]protoreflect.FullName 55 } 56 57 // NewServiceRouter initializes a new [ServiceRouter] with the specified connection pool and options. 58 // 59 // The connection pool will be used to perform a simple retrieval of the connection to a target by its name. 60 // for more complex connection routing this router's methods can be wrapped to return a 61 // different connection based on the matched method and HTTP/GRPC request information. 62 func NewServiceRouter(pool grpcadapter.ClientPool, opts ServiceRouterOpts) *ServiceRouter { 63 opts = opts.withDefaults() 64 65 return &ServiceRouter{ 66 pool: pool, 67 logger: opts.Logger.WithComponent("grpcbridge.routing"), 68 watcherSet: syncset.New[string](), 69 svcRoutes: make(map[string][]protoreflect.FullName), 70 } 71 } 72 73 // RouteGRPC routes the gRPC request based on its method, which is retrieved using [grpc.Method]. 74 // The context is expected to be a stream/request context from a valid gRPC request, 75 // however all the necessary information can be added to it manually for routing some custom requests using [grpc.NewContextWithServerTransportStream]. 76 // 77 // Errors returned by RouteGRPC are gRPC status.Status errors with the code set accordingly. 78 // Currently, the Internal, Unimplemented, and Unavailable codes are returned. 79 // 80 // Performance-wise it is notable that updates to the routing information don't block RouteGRPC, happening fully in the background. 81 func (sr *ServiceRouter) RouteGRPC(ctx context.Context) (grpcadapter.ClientConn, GRPCRoute, error) { 82 rpcName, ok := grpc.Method(ctx) 83 if !ok { 84 sr.logger.Error("no method name in request context, unable to route request") 85 return nil, GRPCRoute{}, status.Errorf(codes.Internal, "grpcbridge: no method name in request context") 86 } 87 88 svc, method, ok := parseRPCName(rpcName) 89 if !ok { 90 // https://github.com/grpc/grpc-go/blob/6fbcd8a889526b3307c3a33cba5b1d2190f0fe11/server.go#L1755 91 return nil, GRPCRoute{}, status.Errorf(codes.Unimplemented, "malformed method name: %q", rpcName) 92 } 93 94 routeAny, ok := sr.routes.Load(svc) 95 if !ok { 96 // https://github.com/grpc/grpc-go/blob/6fbcd8a889526b3307c3a33cba5b1d2190f0fe11/server.go#L1805 97 return nil, GRPCRoute{}, status.Errorf(codes.Unimplemented, "unknown service %v", svc) 98 } 99 100 route := routeAny.(serviceRoute) 101 102 conn, ok := sr.pool.Get(route.target.Name) 103 if !ok { 104 return nil, GRPCRoute{}, status.Errorf(codes.Unavailable, "no connection available to target %q", route.target.Name) 105 } 106 107 return conn, GRPCRoute{ 108 Target: route.target, 109 Service: route.service, 110 Method: bridgedesc.DummyMethod(protoreflect.FullName(svc), protoreflect.Name(method)), 111 }, nil 112 } 113 114 // RouteHTTP implements routing for POST HTTP requests, using the request path as the method name. 115 // It simulates pattern-based routing with default bindings, but is much more efficient than [PatternRouter.RouteHTTP] 116 // because it relies solely on the service name part of the request path and doesn't have to perform any pattern-matching. 117 // 118 // See [ServiceRouter.RouteGRPC] for more details, as this method is very similar, 119 // with the only notable differences being the different status codes returned, which are more suitable for HTTP. 120 // Additionally, an error implementing interface { HTTPStatus() int } can be returned, which should be used to set a custom status code. 121 func (sr *ServiceRouter) RouteHTTP(r *http.Request) (grpcadapter.ClientConn, HTTPRoute, error) { 122 if r.Method != http.MethodPost { 123 return nil, HTTPRoute{}, httperr.Status(http.StatusMethodNotAllowed, status.Errorf(codes.Unimplemented, http.StatusText(http.StatusMethodNotAllowed))) 124 } 125 126 rpcName := r.URL.RawPath 127 128 svc, method, ok := parseRPCName(rpcName) 129 if !ok { 130 // NotFound here because a 404 is what will be given by pattern-based routers and other HTTP-like routers, 131 // it makes more sense than returning a 400 or some other error. 132 return nil, HTTPRoute{}, status.Errorf(codes.NotFound, http.StatusText(http.StatusNotFound)) 133 } 134 135 routeAny, ok := sr.routes.Load(svc) 136 if !ok { 137 return nil, HTTPRoute{}, status.Errorf(codes.NotFound, http.StatusText(http.StatusNotFound)) 138 } 139 140 route := routeAny.(serviceRoute) 141 142 conn, ok := sr.pool.Get(route.target.Name) 143 if !ok { 144 return nil, HTTPRoute{}, status.Errorf(codes.Unavailable, "no connection available to target %q", route.target.Name) 145 } 146 147 methodDesc := bridgedesc.DummyMethod(protoreflect.FullName(svc), protoreflect.Name(method)) 148 binding := bridgedesc.DefaultBinding(methodDesc) 149 150 return conn, HTTPRoute{ 151 Target: route.target, 152 Service: route.service, 153 Method: methodDesc, 154 Binding: binding, 155 PathParams: nil, // PatternRouter also returns nil 156 }, nil 157 } 158 159 // Watch starts watching the specified target for description changes. 160 // It returns a [*ServiceRouterWatcher] through which new updates for this target can be applied. 161 // 162 // It is an error to try Watch()ing the same target multiple times on a single ServiceRouter instance, 163 // See the comment for [PatternRouter.Watch] for some extra detail regarding this API mechanic. 164 func (sr *ServiceRouter) Watch(target string) (*ServiceRouterWatcher, error) { 165 if sr.watcherSet.Add(target) { 166 return &ServiceRouterWatcher{sr: sr, target: target}, nil 167 } 168 169 return nil, ErrAlreadyWatching 170 } 171 172 // ServiceRouterWatcher is a description update watcher created for a specific target in the context of a [ServiceRouter] instance. 173 // New ServiceRouterWatchers are created through [ServiceRouter.Watch]. 174 type ServiceRouterWatcher struct { 175 sr *ServiceRouter 176 target string 177 closed atomic.Bool 178 } 179 180 // UpdateDesc updates the description of the target this watcher is watching. 181 // It follows the same semantics as [PatternRouterWatcher.UpdateDesc], 182 // the documentation for which goes into more detail. 183 func (srw *ServiceRouterWatcher) UpdateDesc(desc *bridgedesc.Target) { 184 if srw.closed.Load() { 185 return 186 } 187 188 if desc.Name != srw.target { 189 srw.sr.logger.Error("ServiceRouterWatcher got update for different target, will ignore", "watcher_target", srw.target, "update_target", desc.Name) 190 return 191 } 192 193 srw.sr.updateRoutes(desc) 194 } 195 196 // ReportError is currently a no-op, present simply to implement the Watcher interface 197 // of the grpcbridge description resolvers, such as the one in [github.com/renbou/grpcbridge/reflection]. 198 func (srw *ServiceRouterWatcher) ReportError(error) { 199 // TODO(renbou): introduce a circuit breaker mechanism when errors are reported multiple times? 200 } 201 202 // Close closes the watcher, preventing further updates from being applied to the router through it. 203 // It is an error to call Close() multiple times on the same watcher, and doing so will result in a panic. 204 func (srw *ServiceRouterWatcher) Close() { 205 if !srw.closed.CompareAndSwap(false, true) { 206 panic("grpcbridge: ServiceRouterWatcher.Close() called multiple times") 207 } 208 209 srw.sr.removeTarget(srw.target) 210 srw.sr.watcherSet.Remove(srw.target) 211 } 212 213 type serviceRoute struct { 214 target *bridgedesc.Target 215 service *bridgedesc.Service 216 } 217 218 func (sr *ServiceRouter) updateRoutes(desc *bridgedesc.Target) { 219 sr.mu.Lock() 220 defer sr.mu.Unlock() 221 222 newSvcRoutes := make([]protoreflect.FullName, 0, len(desc.Services)) 223 presentSvcRoutes := make(map[protoreflect.FullName]struct{}, len(desc.Services)) 224 225 // Handle current routes 226 for i := range desc.Services { 227 svc := &desc.Services[i] 228 229 // Add new routes 230 route, ok := sr.routes.LoadOrStore(svc.Name, serviceRoute{target: desc, service: svc}) 231 if !ok { 232 sr.logger.Debug("adding route", "target", desc.Name, "service", svc.Name) 233 } else if ok && route.(serviceRoute).target.Name != desc.Name { 234 // Since this router has no way to distinguish which routes go where, 235 // it's better to avoid overwriting a route due to some accidental mistake by the user. 236 sr.logger.Warn("ServiceRouter encountered gRPC service route conflict, keeping previous route", 237 "service", svc.Name, 238 "previous_target", route.(serviceRoute).target.Name, 239 "new_target", desc.Name, 240 ) 241 continue 242 } 243 244 // Mark route as present to avoid removing it 245 presentSvcRoutes[svc.Name] = struct{}{} 246 newSvcRoutes = append(newSvcRoutes, svc.Name) 247 } 248 249 // Remove outdated routes 250 for _, route := range sr.svcRoutes[desc.Name] { 251 if _, ok := presentSvcRoutes[route]; !ok { 252 sr.logger.Debug("removing route", "target", desc.Name, "service", route) 253 sr.routes.Delete(route) 254 } 255 } 256 257 sr.svcRoutes[desc.Name] = newSvcRoutes 258 } 259 260 func (sr *ServiceRouter) removeTarget(target string) { 261 sr.mu.Lock() 262 defer sr.mu.Unlock() 263 264 routes := sr.svcRoutes[target] 265 266 for _, route := range routes { 267 sr.routes.Delete(route) 268 } 269 270 delete(sr.svcRoutes, target) 271 } 272 273 func parseRPCName(rpcName string) (protoreflect.FullName, string, bool) { 274 // Support both "/package.Service/Method" and "package.Service/Method" formats. 275 if len(rpcName) > 0 && rpcName[0] == '/' { 276 rpcName = rpcName[1:] 277 } 278 279 svc, method, ok := strings.Cut(rpcName, "/") 280 return protoreflect.FullName(svc), method, ok 281 }