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
}