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() }