package spindle import ( "sync" "time" "github.com/bluesky-social/indigo/atproto/syntax" "github.com/hashicorp/golang-lru/v2/expirable" "golang.org/x/time/rate" "tangled.org/core/spindle/models" "tangled.org/core/spindle/xrpc" ) const ( workflowDefRefTTL = 10 * time.Second workflowDefCacheSize = 512 workflowDefLoadsPerSecond = 5 workflowDefLoadBurst = 20 ) // a miss costs the knot a tree read and a blob per workflow type workflowDefCache struct { once sync.Once mu sync.Mutex byRef *expirable.LRU[string, []*models.WorkflowDefinition] limits *expirable.LRU[syntax.DID, *rate.Limiter] } func (c *workflowDefCache) init() { c.byRef = expirable.NewLRU[string, []*models.WorkflowDefinition](workflowDefCacheSize, nil, workflowDefRefTTL) c.limits = expirable.NewLRU[syntax.DID, *rate.Limiter](workflowDefCacheSize, nil, time.Minute) } // only knot reads spend the budget, local clone reads are free func (c *workflowDefCache) admit(repo syntax.DID) error { c.once.Do(c.init) c.mu.Lock() defer c.mu.Unlock() limiter, ok := c.limits.Get(repo) if !ok { limiter = rate.NewLimiter(workflowDefLoadsPerSecond, workflowDefLoadBurst) c.limits.Add(repo, limiter) } if !limiter.Allow() { return xrpc.ErrDefinitionsBusy } return nil } func (c *workflowDefCache) list(repo syntax.DID, ref string, load func() ([]*models.WorkflowDefinition, error)) ([]*models.WorkflowDefinition, error) { c.once.Do(c.init) return cached(c.byRef, repo.String()+"\x00"+ref, load) } func cached[V any](lru *expirable.LRU[string, V], key string, load func() (V, error)) (V, error) { if v, ok := lru.Get(key); ok { return v, nil } v, err := load() if err == nil { lru.Add(key, v) } return v, err }