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  }