github.com/safing/portbase@v0.19.5/database/interface_cache.go (about)

     1  package database
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"time"
     7  
     8  	"github.com/safing/portbase/database/record"
     9  	"github.com/safing/portbase/log"
    10  )
    11  
    12  // DelayedCacheWriter must be run by the caller of an interface that uses delayed cache writing.
    13  func (i *Interface) DelayedCacheWriter(ctx context.Context) error {
    14  	// Check if the DelayedCacheWriter should be run at all.
    15  	if i.options.CacheSize <= 0 || i.options.DelayCachedWrites == "" {
    16  		return errors.New("delayed cache writer is not applicable to this database interface")
    17  	}
    18  
    19  	// Check if backend support the Batcher interface.
    20  	batchPut := i.PutMany(i.options.DelayCachedWrites)
    21  	// End batchPut immediately and check for an error.
    22  	err := batchPut(nil)
    23  	if err != nil {
    24  		return err
    25  	}
    26  
    27  	// percentThreshold defines the minimum percentage of entries in the write cache in relation to the cache size that need to be present in order for flushing the cache to the database storage.
    28  	percentThreshold := 25
    29  	thresholdWriteTicker := time.NewTicker(5 * time.Second)
    30  	forceWriteTicker := time.NewTicker(5 * time.Minute)
    31  
    32  	for {
    33  		// Wait for trigger for writing the cache.
    34  		select {
    35  		case <-ctx.Done():
    36  			// The caller is shutting down, flush the cache to storage and exit.
    37  			i.flushWriteCache(0)
    38  			return nil
    39  
    40  		case <-i.triggerCacheWrite:
    41  			// An entry from the cache was evicted that was also in the write cache.
    42  			// This makes it likely that other entries that are also present in the
    43  			// write cache will be evicted soon. Flush the write cache to storage
    44  			// immediately in order to reduce single writes.
    45  			i.flushWriteCache(0)
    46  
    47  		case <-thresholdWriteTicker.C:
    48  			// Often check if the write cache has filled up to a certain degree and
    49  			// flush it to storage before we start evicting to-be-written entries and
    50  			// slow down the hot path again.
    51  			i.flushWriteCache(percentThreshold)
    52  
    53  		case <-forceWriteTicker.C:
    54  			// Once in a while, flush the write cache to storage no matter how much
    55  			// it is filled. We don't want entries lingering around in the write
    56  			// cache forever. This also reduces the amount of data loss in the event
    57  			// of a total crash.
    58  			i.flushWriteCache(0)
    59  		}
    60  	}
    61  }
    62  
    63  // ClearCache clears the read cache.
    64  func (i *Interface) ClearCache() {
    65  	// Check if cache is in use.
    66  	if i.cache == nil {
    67  		return
    68  	}
    69  
    70  	// Clear all cache entries.
    71  	i.cache.Purge()
    72  }
    73  
    74  // FlushCache writes (and thus clears) the write cache.
    75  func (i *Interface) FlushCache() {
    76  	// Check if write cache is in use.
    77  	if i.options.DelayCachedWrites != "" {
    78  		return
    79  	}
    80  
    81  	i.flushWriteCache(0)
    82  }
    83  
    84  func (i *Interface) flushWriteCache(percentThreshold int) {
    85  	i.writeCacheLock.Lock()
    86  	defer i.writeCacheLock.Unlock()
    87  
    88  	// Check if there is anything to do.
    89  	if len(i.writeCache) == 0 {
    90  		return
    91  	}
    92  
    93  	// Check if we reach the given threshold for writing to storage.
    94  	if (len(i.writeCache)*100)/i.options.CacheSize < percentThreshold {
    95  		return
    96  	}
    97  
    98  	// Write the full cache in a batch operation.
    99  	batchPut := i.PutMany(i.options.DelayCachedWrites)
   100  	for _, r := range i.writeCache {
   101  		err := batchPut(r)
   102  		if err != nil {
   103  			log.Warningf("database: failed to write write-cached entry to %q database: %s", i.options.DelayCachedWrites, err)
   104  		}
   105  	}
   106  	// Finish batch.
   107  	err := batchPut(nil)
   108  	if err != nil {
   109  		log.Warningf("database: failed to finish flushing write cache to %q database: %s", i.options.DelayCachedWrites, err)
   110  	}
   111  
   112  	// Optimized map clearing following the Go1.11 recommendation.
   113  	for key := range i.writeCache {
   114  		delete(i.writeCache, key)
   115  	}
   116  }
   117  
   118  // cacheEvictHandler is run by the cache for every entry that gets evicted
   119  // from the cache.
   120  func (i *Interface) cacheEvictHandler(keyData, _ interface{}) {
   121  	// Transform the key into a string.
   122  	key, ok := keyData.(string)
   123  	if !ok {
   124  		return
   125  	}
   126  
   127  	// Check if the evicted record is one that is to be written.
   128  	// Lock the write cache until the end of the function.
   129  	// The read cache is locked anyway for the whole duration.
   130  	i.writeCacheLock.Lock()
   131  	defer i.writeCacheLock.Unlock()
   132  	r, ok := i.writeCache[key]
   133  	if ok {
   134  		delete(i.writeCache, key)
   135  	}
   136  	if !ok {
   137  		return
   138  	}
   139  
   140  	// Write record to database in order to mitigate race conditions where the record would appear
   141  	// as non-existent for a short duration.
   142  	db, err := getController(r.DatabaseName())
   143  	if err != nil {
   144  		log.Warningf("database: failed to write evicted cache entry %q: database %q does not exist", key, r.DatabaseName())
   145  		return
   146  	}
   147  
   148  	r.Lock()
   149  	defer r.Unlock()
   150  
   151  	err = db.Put(r)
   152  	if err != nil {
   153  		log.Warningf("database: failed to write evicted cache entry %q to database: %s", key, err)
   154  	}
   155  
   156  	// Finally, trigger writing the full write cache because a to-be-written
   157  	// entry was just evicted from the cache, and this makes it likely that more
   158  	// to-be-written entries will be evicted shortly.
   159  	select {
   160  	case i.triggerCacheWrite <- struct{}{}:
   161  	default:
   162  	}
   163  }
   164  
   165  func (i *Interface) checkCache(key string) record.Record {
   166  	// Check if cache is in use.
   167  	if i.cache == nil {
   168  		return nil
   169  	}
   170  
   171  	// Check if record exists in cache.
   172  	cacheVal, err := i.cache.Get(key)
   173  	if err == nil {
   174  		r, ok := cacheVal.(record.Record)
   175  		if ok {
   176  			return r
   177  		}
   178  	}
   179  	return nil
   180  }
   181  
   182  // updateCache updates an entry in the interface cache. The given record may
   183  // not be locked, as updating the cache might write an (unrelated) evicted
   184  // record to the database in the process. If this happens while the
   185  // DelayedCacheWriter flushes the write cache with the same record present,
   186  // this will deadlock.
   187  func (i *Interface) updateCache(r record.Record, write bool, remove bool, ttl int64) (written bool) {
   188  	// Check if cache is in use.
   189  	if i.cache == nil {
   190  		return false
   191  	}
   192  
   193  	// Check if record should be deleted
   194  	if remove {
   195  		// Remove entry from cache.
   196  		i.cache.Remove(r.Key())
   197  		// Let write through to database storage.
   198  		return false
   199  	}
   200  
   201  	// Update cache with record.
   202  	if ttl >= 0 {
   203  		_ = i.cache.SetWithExpire(
   204  			r.Key(),
   205  			r,
   206  			time.Duration(ttl)*time.Second,
   207  		)
   208  	} else {
   209  		_ = i.cache.Set(
   210  			r.Key(),
   211  			r,
   212  		)
   213  	}
   214  
   215  	// Add record to write cache instead if:
   216  	// 1. The record is being written.
   217  	// 2. Write delaying is active.
   218  	// 3. Write delaying is active for the database of this record.
   219  	if write && r.DatabaseName() == i.options.DelayCachedWrites {
   220  		i.writeCacheLock.Lock()
   221  		defer i.writeCacheLock.Unlock()
   222  		i.writeCache[r.Key()] = r
   223  		return true
   224  	}
   225  
   226  	return false
   227  }