// Package supply provides a cache of signets for pre-generating signets. package supply import ( "sync" "github.com/safing/jess" "github.com/safing/jess/tools" ) // SignetSupply is cache of signets for pre-generating signets. type SignetSupply struct { lock sync.RWMutex caches map[string]*signetCache notifyEmpty chan struct{} notifyFillable chan struct{} cacheSize int } type signetCache struct { sync.Mutex tool *tools.Tool stock []*jess.Signet stockIterator int stockFillLevel int } // NewSignetSupply returns a new empty *SignetSupply. `cacheSize` specifies how many Signets to cache at maximum (min 1). func NewSignetSupply(cacheSize int) *SignetSupply { if cacheSize < 1 { cacheSize = 1 } return &SignetSupply{ caches: make(map[string]*signetCache), notifyEmpty: make(chan struct{}, 1), notifyFillable: make(chan struct{}, 1), cacheSize: cacheSize, } } // GetSignet returns a new signet from the supply. func (supply *SignetSupply) GetSignet(scheme string) (*jess.Signet, error) { supply.lock.RLock() cache, ok := supply.caches[scheme] supply.lock.RUnlock() // init if !ok { // get tool tool, err := tools.Get(scheme) if err != nil { return nil, err } // create new cache cache = &signetCache{ tool: tool, stock: make([]*jess.Signet, supply.cacheSize), } // save to index supply.lock.Lock() supply.caches[scheme] = cache supply.lock.Unlock() } signet := cache.get() // returned signet from supply if signet != nil { // notify that supply can be filled again select { case supply.notifyFillable <- struct{}{}: default: } return signet, nil } // notify that supply is empty select { case supply.notifyEmpty <- struct{}{}: default: } // generate ad hoc signet = jess.NewSignetBase(cache.tool) err := signet.GenerateKey() if err != nil { return nil, err } return signet, nil } func (sc *signetCache) get() *jess.Signet { sc.Lock() defer sc.Unlock() // get slot signet := sc.stock[sc.stockIterator] if signet == nil { return nil } // reset slot sc.stock[sc.stockIterator] = nil // debugging // fmt.Printf("returning %s: iter=%d fill=%d\n", sc.tool.Info.Name, sc.stockIterator, sc.stockFillLevel-1) // adjust helpers sc.stockFillLevel-- sc.stockIterator = (sc.stockIterator + 1) % len(sc.stock) return signet } // Fill fills all caches with new Signets in the specified amount (up to the cache size), and returns whether the caches are now full. This function is meant to be called periodically (when there is time) with small values for `amount` until the supply is full. func (supply *SignetSupply) Fill(amount int) (full bool, lastErr error) { supply.lock.RLock() defer supply.lock.RUnlock() full = true for _, cache := range supply.caches { cacheIsFull, err := cache.fill(amount) if err != nil { lastErr = err } if !cacheIsFull { full = false } } return } func (sc *signetCache) fill(amount int) (full bool, err error) { sc.Lock() defer sc.Unlock() var signet *jess.Signet fillUpTo := sc.stockFillLevel + amount // check upper bound if fillUpTo > len(sc.stock) { fillUpTo = len(sc.stock) } // generate new signets until wanted fill amount is reached for i := (sc.stockIterator + sc.stockFillLevel) % len(sc.stock); // start at first empty index sc.stockFillLevel < fillUpTo; // continue until fill amount is reached i = (i + 1) % len(sc.stock) /* increase i, but wrap to start */ { // get signet from slot signet = sc.stock[i] // debugging // fmt.Printf("filling %s: i=%d iter=%d fill=%d upto=%d signet=%+v\n", sc.tool.Info.Name, i, sc.stockIterator, sc.stockFillLevel, fillUpTo, signet) if signet != nil { // full return true, nil } // generate new signet = jess.NewSignetBase(sc.tool) err := signet.GenerateKey() if err != nil { return false, err } // reassign sc.stock[i] = signet sc.stockFillLevel++ } return sc.stockFillLevel == len(sc.stock), nil } // Status holds status information about a signet supply. type Status struct { TotalSize int FillLevel int } // Status returns current status information. func (supply *SignetSupply) Status() *Status { supply.lock.RLock() defer supply.lock.RUnlock() status := &Status{ TotalSize: supply.cacheSize * len(supply.caches), } // get current fill level for _, cache := range supply.caches { cache.status(status) } return status } func (sc *signetCache) status(status *Status) { sc.Lock() defer sc.Unlock() status.FillLevel += sc.stockFillLevel }