safing-portbase/database/interface_cache.go

171 lines
4.1 KiB
Go

package database
import (
"context"
"errors"
"time"
"github.com/safing/portbase/database/record"
"github.com/safing/portbase/log"
)
// DelayedCacheWriter must be run by the caller of an interface that uses delayed cache writing.
func (i *Interface) DelayedCacheWriter(ctx context.Context) error {
// Check if the DelayedCacheWriter should be run at all.
if i.options.CacheSize <= 0 || len(i.options.DelayCachedWrites) == 0 {
return errors.New("delayed cache writer is not applicable to this database interface")
}
// Check if backend support the Batcher interface.
batchPut := i.PutMany(i.options.DelayCachedWrites)
// End batchPut immediately and check for an error.
err := batchPut(nil)
if err != nil {
return err
}
for {
// Wait for trigger for writing the cache.
select {
case <-ctx.Done():
// Module is shutting down, flush write cache to database.
i.flushWriteCache()
return nil
case <-i.triggerCacheWrite:
case <-time.After(5 * time.Second):
}
i.flushWriteCache()
}
}
func (i *Interface) flushWriteCache() {
i.writeCacheLock.Lock()
defer i.writeCacheLock.Unlock()
// Check if there is anything to do.
if len(i.writeCache) == 0 {
return
}
// Write the full cache in a batch operation.
batchPut := i.PutMany(i.options.DelayCachedWrites)
for _, r := range i.writeCache {
err := batchPut(r)
if err != nil {
log.Warningf("database: failed to write write-cached entry to %q database: %s", i.options.DelayCachedWrites, err)
}
}
// Finish batch.
err := batchPut(nil)
if err != nil {
log.Warningf("database: failed to finish flushing write cache to %q database: %s", i.options.DelayCachedWrites, err)
}
// Optimized map clearing following the Go1.11 recommendation.
for key := range i.writeCache {
delete(i.writeCache, key)
}
}
// cacheEvictHandler is run by the cache for every entry that gets evicted
// from the cache.
func (i *Interface) cacheEvictHandler(keyData, _ interface{}) {
// Transform the key into a string.
key, ok := keyData.(string)
if !ok {
return
}
// Check if the evicted record is one that is to be written.
i.writeCacheLock.Lock()
r, ok := i.writeCache[key]
if ok {
delete(i.writeCache, key)
}
i.writeCacheLock.Unlock()
if !ok {
return
}
// Write record to database in order to mitigate race conditions where the record would appear
// as non-existent for a short duration.
db, err := getController(r.DatabaseName())
if err != nil {
log.Warningf("database: failed to write evicted cache entry %q: database %q does not exist", key, r.DatabaseName())
return
}
err = db.Put(r)
if err != nil {
log.Warningf("database: failed to write evicted cache entry %q to database: %s", key, err)
}
// Finally, trigger writing the full write cache because a to-be-written
// entry was just evicted from the cache, and this makes it likely that more
// to-be-written entries will be evicted shortly.
select {
case i.triggerCacheWrite <- struct{}{}:
default:
}
}
func (i *Interface) checkCache(key string) record.Record {
// Check if cache is in use.
if i.cache == nil {
return nil
}
// Check if record exists in cache.
cacheVal, err := i.cache.Get(key)
if err == nil {
r, ok := cacheVal.(record.Record)
if ok {
return r
}
}
return nil
}
func (i *Interface) updateCache(r record.Record, write bool) (written bool) {
// Check if cache is in use.
if i.cache == nil {
return false
}
// Check if record should be deleted
if r.Meta().IsDeleted() {
// Remove entry from cache.
i.cache.Remove(r.Key())
// Let write through to database storage.
return false
}
// Update cache with record.
ttl := r.Meta().GetRelativeExpiry()
if ttl >= 0 {
_ = i.cache.SetWithExpire(
r.Key(),
r,
time.Duration(ttl)*time.Second,
)
} else {
_ = i.cache.Set(
r.Key(),
r,
)
}
// Add record to write cache instead if:
// 1. The record is being written.
// 2. Write delaying is active.
// 3. Write delaying is active for the database of this record.
if write && len(i.options.DelayCachedWrites) > 0 && r.DatabaseName() == i.options.DelayCachedWrites {
i.writeCacheLock.Lock()
defer i.writeCacheLock.Unlock()
i.writeCache[r.Key()] = r
return true
}
return false
}