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 }