github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/dispatch/singleflight/singleflight.go (about) 1 package singleflight 2 3 import ( 4 "context" 5 "encoding/hex" 6 "fmt" 7 "strconv" 8 9 "github.com/prometheus/client_golang/prometheus" 10 "github.com/prometheus/client_golang/prometheus/promauto" 11 "go.opentelemetry.io/otel/attribute" 12 "go.opentelemetry.io/otel/trace" 13 "google.golang.org/grpc/codes" 14 "google.golang.org/grpc/status" 15 "resenje.org/singleflight" 16 17 log "github.com/authzed/spicedb/internal/logging" 18 19 "github.com/authzed/spicedb/internal/dispatch" 20 "github.com/authzed/spicedb/internal/dispatch/keys" 21 v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1" 22 ) 23 24 var ( 25 singleFlightCount = promauto.NewCounterVec(singleFlightCountConfig, []string{"method", "shared"}) 26 singleFlightCountConfig = prometheus.CounterOpts{ 27 Namespace: "spicedb", 28 Subsystem: "dispatch", 29 Name: "single_flight_total", 30 Help: "total number of dispatch requests that were single flighted", 31 } 32 ) 33 34 func New(delegate dispatch.Dispatcher, handler keys.Handler) dispatch.Dispatcher { 35 return &Dispatcher{ 36 delegate: delegate, 37 keyHandler: handler, 38 } 39 } 40 41 type Dispatcher struct { 42 delegate dispatch.Dispatcher 43 keyHandler keys.Handler 44 45 checkGroup singleflight.Group[string, *v1.DispatchCheckResponse] 46 expandGroup singleflight.Group[string, *v1.DispatchExpandResponse] 47 } 48 49 func (d *Dispatcher) DispatchCheck(ctx context.Context, req *v1.DispatchCheckRequest) (*v1.DispatchCheckResponse, error) { 50 key, err := d.keyHandler.CheckDispatchKey(ctx, req) 51 if err != nil { 52 return &v1.DispatchCheckResponse{Metadata: &v1.ResponseMeta{DispatchCount: 1}}, 53 status.Error(codes.Internal, "unexpected DispatchCheck error") 54 } 55 56 keyString := hex.EncodeToString(key) 57 58 // this is in place so that upgrading to a SpiceDB version with traversal bloom does not cause dispatch failures 59 // if this is observed frequently it suggests a callsite is missing setting the bloom filter. 60 // Since there is no bloom filter, there is no guarantee recursion won't happen, so it's safer not to singleflight 61 if len(req.Metadata.TraversalBloom) == 0 { 62 tb, err := v1.NewTraversalBloomFilter(50) 63 if err != nil { 64 return &v1.DispatchCheckResponse{Metadata: &v1.ResponseMeta{DispatchCount: 1}}, status.Error(codes.Internal, fmt.Errorf("unable to create traversal bloom filter: %w", err).Error()) 65 } 66 67 singleFlightCount.WithLabelValues("DispatchCheck", "missing").Inc() 68 req.Metadata.TraversalBloom = tb 69 return d.delegate.DispatchCheck(ctx, req) 70 } 71 72 // Check if the key has already been part of a dispatch. If so, this represents a 73 // likely recursive call, so we dispatch it to the delegate to avoid the singleflight from blocking it. 74 // If the bloom filter presents a false positive, a dispatch will happen, which is a small inefficiency 75 // traded-off to prevent a recursive-call deadlock 76 possiblyLoop, err := req.Metadata.RecordTraversal(keyString) 77 if err != nil { 78 return &v1.DispatchCheckResponse{Metadata: &v1.ResponseMeta{DispatchCount: 1}}, err 79 } else if possiblyLoop { 80 log.Debug().Object("DispatchCheckRequest", req).Str("key", keyString).Msg("potential DispatchCheckRequest loop detected") 81 singleFlightCount.WithLabelValues("DispatchCheck", "loop").Inc() 82 return d.delegate.DispatchCheck(ctx, req) 83 } 84 85 primary := false 86 v, isShared, err := d.checkGroup.Do(ctx, keyString, func(innerCtx context.Context) (*v1.DispatchCheckResponse, error) { 87 primary = true 88 return d.delegate.DispatchCheck(innerCtx, req) 89 }) 90 91 singleFlightCount.WithLabelValues("DispatchCheck", strconv.FormatBool(isShared)).Inc() 92 if err != nil { 93 return &v1.DispatchCheckResponse{Metadata: &v1.ResponseMeta{DispatchCount: 1}}, err 94 } 95 96 span := trace.SpanFromContext(ctx) 97 singleflighted := isShared && !primary 98 span.SetAttributes(attribute.Bool("singleflight", singleflighted)) 99 return v, err 100 } 101 102 func (d *Dispatcher) DispatchExpand(ctx context.Context, req *v1.DispatchExpandRequest) (*v1.DispatchExpandResponse, error) { 103 key, err := d.keyHandler.ExpandDispatchKey(ctx, req) 104 if err != nil { 105 return &v1.DispatchExpandResponse{Metadata: &v1.ResponseMeta{DispatchCount: 1}}, 106 status.Error(codes.Internal, "unexpected DispatchExpand error") 107 } 108 109 keyString := hex.EncodeToString(key) 110 111 // this is in place so that upgrading to a SpiceDB version with traversal bloom does not cause dispatch failures 112 // if this is observed frequently it suggests a callsite is missing setting the bloom filter 113 // Since there is no bloom filter, there is no guarantee recursion won't happen, so it's safer not to singleflight 114 if len(req.Metadata.TraversalBloom) == 0 { 115 tb, err := v1.NewTraversalBloomFilter(50) 116 if err != nil { 117 return &v1.DispatchExpandResponse{Metadata: &v1.ResponseMeta{DispatchCount: 1}}, status.Error(codes.Internal, fmt.Errorf("unable to create traversal bloom filter: %w", err).Error()) 118 } 119 120 singleFlightCount.WithLabelValues("DispatchExpand", "missing").Inc() 121 req.Metadata.TraversalBloom = tb 122 return d.delegate.DispatchExpand(ctx, req) 123 } 124 125 possiblyLoop, err := req.Metadata.RecordTraversal(keyString) 126 if err != nil { 127 return &v1.DispatchExpandResponse{Metadata: &v1.ResponseMeta{DispatchCount: 1}}, err 128 } else if possiblyLoop { 129 log.Debug().Object("DispatchExpand", req).Str("key", keyString).Msg("potential DispatchExpand loop detected") 130 singleFlightCount.WithLabelValues("DispatchExpand", "loop").Inc() 131 return d.delegate.DispatchExpand(ctx, req) 132 } 133 134 v, isShared, err := d.expandGroup.Do(ctx, keyString, func(innerCtx context.Context) (*v1.DispatchExpandResponse, error) { 135 return d.delegate.DispatchExpand(innerCtx, req) 136 }) 137 138 singleFlightCount.WithLabelValues("DispatchExpand", strconv.FormatBool(isShared)).Inc() 139 if err != nil { 140 return &v1.DispatchExpandResponse{Metadata: &v1.ResponseMeta{DispatchCount: 1}}, err 141 } 142 return v, err 143 } 144 145 func (d *Dispatcher) DispatchReachableResources(req *v1.DispatchReachableResourcesRequest, stream dispatch.ReachableResourcesStream) error { 146 return d.delegate.DispatchReachableResources(req, stream) 147 } 148 149 func (d *Dispatcher) DispatchLookupResources(req *v1.DispatchLookupResourcesRequest, stream dispatch.LookupResourcesStream) error { 150 return d.delegate.DispatchLookupResources(req, stream) 151 } 152 153 func (d *Dispatcher) DispatchLookupSubjects(req *v1.DispatchLookupSubjectsRequest, stream dispatch.LookupSubjectsStream) error { 154 return d.delegate.DispatchLookupSubjects(req, stream) 155 } 156 157 func (d *Dispatcher) Close() error { return d.delegate.Close() } 158 func (d *Dispatcher) ReadyState() dispatch.ReadyState { return d.delegate.ReadyState() }