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 }