github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/datastore/revisions/optimized.go (about)

     1  package revisions
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"math/rand"
     7  	"sync"
     8  	"time"
     9  
    10  	"github.com/benbjohnson/clock"
    11  	"go.opentelemetry.io/otel"
    12  	"go.opentelemetry.io/otel/trace"
    13  	"golang.org/x/sync/singleflight"
    14  
    15  	log "github.com/authzed/spicedb/internal/logging"
    16  	"github.com/authzed/spicedb/pkg/datastore"
    17  )
    18  
    19  var tracer = otel.Tracer("spicedb/internal/datastore/common/revisions")
    20  
    21  // OptimizedRevisionFunction instructs the datastore to compute its own current
    22  // optimized revision given the specific quantization, and return for how long
    23  // it will remain valid.
    24  type OptimizedRevisionFunction func(context.Context) (rev datastore.Revision, validFor time.Duration, err error)
    25  
    26  // NewCachedOptimizedRevisions returns a CachedOptimizedRevisions for the given configuration
    27  func NewCachedOptimizedRevisions(maxRevisionStaleness time.Duration) *CachedOptimizedRevisions {
    28  	return &CachedOptimizedRevisions{
    29  		maxRevisionStaleness: maxRevisionStaleness,
    30  		clockFn:              clock.New(),
    31  	}
    32  }
    33  
    34  // SetOptimizedRevisionFunc must be called after construction, and is the method
    35  // by which one specializes this helper for a specific datastore.
    36  func (cor *CachedOptimizedRevisions) SetOptimizedRevisionFunc(revisionFunc OptimizedRevisionFunction) {
    37  	cor.optimizedFunc = revisionFunc
    38  }
    39  
    40  func (cor *CachedOptimizedRevisions) OptimizedRevision(ctx context.Context) (datastore.Revision, error) {
    41  	span := trace.SpanFromContext(ctx)
    42  	localNow := cor.clockFn.Now()
    43  
    44  	// Subtract a random amount of time from now, to let barely expired candidates get selected
    45  	adjustedNow := localNow
    46  	if cor.maxRevisionStaleness > 0 {
    47  		// nolint:gosec
    48  		// G404 use of non cryptographically secure random number generator is not a security concern here,
    49  		// as we are using it to introduce randomness to the accepted staleness of a revision and reduce the odds of
    50  		// a thundering herd to the datastore
    51  		adjustedNow = localNow.Add(-1 * time.Duration(rand.Int63n(cor.maxRevisionStaleness.Nanoseconds())) * time.Nanosecond)
    52  	}
    53  
    54  	cor.Lock()
    55  	for _, candidate := range cor.candidates {
    56  		if candidate.validThrough.After(adjustedNow) {
    57  			log.Ctx(ctx).Debug().Time("now", localNow).Time("valid", candidate.validThrough).Msg("returning cached revision")
    58  			span.AddEvent("returning cached revision")
    59  			cor.Unlock()
    60  			return candidate.revision, nil
    61  		}
    62  	}
    63  	cor.Unlock()
    64  
    65  	newQuantizedRevision, err, _ := cor.updateGroup.Do("", func() (interface{}, error) {
    66  		log.Ctx(ctx).Debug().Time("now", localNow).Msg("computing new revision")
    67  		span.AddEvent("computing new revision")
    68  
    69  		optimized, validFor, err := cor.optimizedFunc(ctx)
    70  		if err != nil {
    71  			return nil, fmt.Errorf("unable to compute optimized revision: %w", err)
    72  		}
    73  
    74  		rvt := localNow.Add(validFor)
    75  		cor.Lock()
    76  		defer cor.Unlock()
    77  
    78  		// Prune the candidates that have definitely expired
    79  		var numToDrop uint
    80  		for _, candidate := range cor.candidates {
    81  			if candidate.validThrough.Add(cor.maxRevisionStaleness).Before(localNow) {
    82  				numToDrop++
    83  			} else {
    84  				break
    85  			}
    86  		}
    87  		cor.candidates = cor.candidates[numToDrop:]
    88  
    89  		cor.candidates = append(cor.candidates, validRevision{optimized, rvt})
    90  		log.Ctx(ctx).Debug().Time("now", localNow).Time("valid", rvt).Stringer("validFor", validFor).Msg("setting valid through")
    91  
    92  		return optimized, nil
    93  	})
    94  	if err != nil {
    95  		return datastore.NoRevision, err
    96  	}
    97  	return newQuantizedRevision.(datastore.Revision), err
    98  }
    99  
   100  // CachedOptimizedRevisions does caching and deduplication for requests for optimized revisions.
   101  type CachedOptimizedRevisions struct {
   102  	sync.Mutex
   103  
   104  	maxRevisionStaleness time.Duration
   105  	optimizedFunc        OptimizedRevisionFunction
   106  	clockFn              clock.Clock
   107  
   108  	// these values are read and set by multiple consumers, they're protected
   109  	// by a mutex
   110  	candidates []validRevision
   111  
   112  	// the updategroup consolidates concurrent requests to the database into 1
   113  	updateGroup singleflight.Group
   114  }
   115  
   116  type validRevision struct {
   117  	revision     datastore.Revision
   118  	validThrough time.Time
   119  }