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 }