github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/dispatch/combined/combined.go (about)

     1  // Package combined implements a dispatcher that combines caching,
     2  // redispatching and optional cluster dispatching.
     3  package combined
     4  
     5  import (
     6  	"fmt"
     7  	"time"
     8  
     9  	"github.com/authzed/grpcutil"
    10  	"google.golang.org/grpc"
    11  	"google.golang.org/grpc/credentials/insecure"
    12  
    13  	"github.com/authzed/spicedb/internal/dispatch"
    14  	"github.com/authzed/spicedb/internal/dispatch/caching"
    15  	"github.com/authzed/spicedb/internal/dispatch/graph"
    16  	"github.com/authzed/spicedb/internal/dispatch/keys"
    17  	"github.com/authzed/spicedb/internal/dispatch/remote"
    18  	"github.com/authzed/spicedb/internal/dispatch/singleflight"
    19  	log "github.com/authzed/spicedb/internal/logging"
    20  	"github.com/authzed/spicedb/pkg/cache"
    21  	v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1"
    22  )
    23  
    24  // Option is a function-style option for configuring a combined Dispatcher.
    25  type Option func(*optionState)
    26  
    27  type optionState struct {
    28  	metricsEnabled         bool
    29  	prometheusSubsystem    string
    30  	upstreamAddr           string
    31  	upstreamCAPath         string
    32  	grpcPresharedKey       string
    33  	grpcDialOpts           []grpc.DialOption
    34  	cache                  cache.Cache
    35  	concurrencyLimits      graph.ConcurrencyLimits
    36  	remoteDispatchTimeout  time.Duration
    37  	secondaryUpstreamAddrs map[string]string
    38  	secondaryUpstreamExprs map[string]string
    39  }
    40  
    41  // MetricsEnabled enables issuing prometheus metrics
    42  func MetricsEnabled(enabled bool) Option {
    43  	return func(state *optionState) {
    44  		state.metricsEnabled = enabled
    45  	}
    46  }
    47  
    48  // PrometheusSubsystem sets the subsystem name for the prometheus metrics
    49  func PrometheusSubsystem(name string) Option {
    50  	return func(state *optionState) {
    51  		state.prometheusSubsystem = name
    52  	}
    53  }
    54  
    55  // UpstreamAddr sets the optional cluster dispatching upstream address.
    56  func UpstreamAddr(addr string) Option {
    57  	return func(state *optionState) {
    58  		state.upstreamAddr = addr
    59  	}
    60  }
    61  
    62  // UpstreamCAPath sets the optional cluster dispatching upstream certificate
    63  // authority.
    64  func UpstreamCAPath(path string) Option {
    65  	return func(state *optionState) {
    66  		state.upstreamCAPath = path
    67  	}
    68  }
    69  
    70  // SecondaryUpstreamAddrs sets a named map of upstream addresses for secondary
    71  // dispatching.
    72  func SecondaryUpstreamAddrs(addrs map[string]string) Option {
    73  	return func(state *optionState) {
    74  		state.secondaryUpstreamAddrs = addrs
    75  	}
    76  }
    77  
    78  // SecondaryUpstreamExprs sets a named map from dispatch type to the associated
    79  // CEL expression to run to determine which secondary dispatch addresses (if any)
    80  // to use for that incoming request.
    81  func SecondaryUpstreamExprs(addrs map[string]string) Option {
    82  	return func(state *optionState) {
    83  		state.secondaryUpstreamExprs = addrs
    84  	}
    85  }
    86  
    87  // GrpcPresharedKey sets the preshared key used to authenticate for optional
    88  // cluster dispatching.
    89  func GrpcPresharedKey(key string) Option {
    90  	return func(state *optionState) {
    91  		state.grpcPresharedKey = key
    92  	}
    93  }
    94  
    95  // GrpcDialOpts sets the default DialOptions used for gRPC clients
    96  // connecting to the optional cluster dispatching.
    97  func GrpcDialOpts(opts ...grpc.DialOption) Option {
    98  	return func(state *optionState) {
    99  		state.grpcDialOpts = opts
   100  	}
   101  }
   102  
   103  // Cache sets the cache for the dispatcher.
   104  func Cache(c cache.Cache) Option {
   105  	return func(state *optionState) {
   106  		state.cache = c
   107  	}
   108  }
   109  
   110  // ConcurrencyLimits sets the max number of goroutines per operation
   111  func ConcurrencyLimits(limits graph.ConcurrencyLimits) Option {
   112  	return func(state *optionState) {
   113  		state.concurrencyLimits = limits
   114  	}
   115  }
   116  
   117  // RemoteDispatchTimeout sets the maximum timeout for a remote dispatch.
   118  // Defaults to 60s (as defined in the remote dispatcher).
   119  func RemoteDispatchTimeout(remoteDispatchTimeout time.Duration) Option {
   120  	return func(state *optionState) {
   121  		state.remoteDispatchTimeout = remoteDispatchTimeout
   122  	}
   123  }
   124  
   125  // NewDispatcher initializes a Dispatcher that caches and redispatches
   126  // optionally to the provided upstream.
   127  func NewDispatcher(options ...Option) (dispatch.Dispatcher, error) {
   128  	var opts optionState
   129  	for _, fn := range options {
   130  		fn(&opts)
   131  	}
   132  	log.Debug().Str("upstream", opts.upstreamAddr).Msg("configured combined dispatcher")
   133  
   134  	if opts.prometheusSubsystem == "" {
   135  		opts.prometheusSubsystem = "dispatch_client"
   136  	}
   137  
   138  	cachingRedispatch, err := caching.NewCachingDispatcher(opts.cache, opts.metricsEnabled, opts.prometheusSubsystem, &keys.CanonicalKeyHandler{})
   139  	if err != nil {
   140  		return nil, err
   141  	}
   142  
   143  	redispatch := graph.NewDispatcher(cachingRedispatch, opts.concurrencyLimits)
   144  	redispatch = singleflight.New(redispatch, &keys.CanonicalKeyHandler{})
   145  
   146  	// If an upstream is specified, create a cluster dispatcher.
   147  	if opts.upstreamAddr != "" {
   148  		if opts.upstreamCAPath != "" {
   149  			customCertOpt, err := grpcutil.WithCustomCerts(grpcutil.VerifyCA, opts.upstreamCAPath)
   150  			if err != nil {
   151  				return nil, err
   152  			}
   153  			opts.grpcDialOpts = append(opts.grpcDialOpts, customCertOpt)
   154  			opts.grpcDialOpts = append(opts.grpcDialOpts, grpcutil.WithBearerToken(opts.grpcPresharedKey))
   155  		} else {
   156  			opts.grpcDialOpts = append(opts.grpcDialOpts, grpcutil.WithInsecureBearerToken(opts.grpcPresharedKey))
   157  			opts.grpcDialOpts = append(opts.grpcDialOpts, grpc.WithTransportCredentials(insecure.NewCredentials()))
   158  		}
   159  
   160  		opts.grpcDialOpts = append(opts.grpcDialOpts, grpc.WithDefaultCallOptions(grpc.UseCompressor("s2")))
   161  
   162  		conn, err := grpc.Dial(opts.upstreamAddr, opts.grpcDialOpts...)
   163  		if err != nil {
   164  			return nil, err
   165  		}
   166  
   167  		secondaryClients := make(map[string]remote.SecondaryDispatch, len(opts.secondaryUpstreamAddrs))
   168  		for name, addr := range opts.secondaryUpstreamAddrs {
   169  			secondaryConn, err := grpc.Dial(addr, opts.grpcDialOpts...)
   170  			if err != nil {
   171  				return nil, err
   172  			}
   173  			secondaryClients[name] = remote.SecondaryDispatch{
   174  				Name:   name,
   175  				Client: v1.NewDispatchServiceClient(secondaryConn),
   176  			}
   177  		}
   178  
   179  		secondaryExprs := make(map[string]*remote.DispatchExpr, len(opts.secondaryUpstreamExprs))
   180  		for name, exprString := range opts.secondaryUpstreamExprs {
   181  			parsed, err := remote.ParseDispatchExpression(name, exprString)
   182  			if err != nil {
   183  				return nil, fmt.Errorf("error parsing secondary dispatch expr `%s` for method `%s`: %w", exprString, name, err)
   184  			}
   185  			secondaryExprs[name] = parsed
   186  		}
   187  
   188  		redispatch = remote.NewClusterDispatcher(v1.NewDispatchServiceClient(conn), conn, remote.ClusterDispatcherConfig{
   189  			KeyHandler:             &keys.CanonicalKeyHandler{},
   190  			DispatchOverallTimeout: opts.remoteDispatchTimeout,
   191  		}, secondaryClients, secondaryExprs)
   192  		redispatch = singleflight.New(redispatch, &keys.CanonicalKeyHandler{})
   193  	}
   194  
   195  	cachingRedispatch.SetDelegate(redispatch)
   196  
   197  	return cachingRedispatch, nil
   198  }