github.com/qiniu/go-sdk/v7@v7.27.0/internal/cache/cache.go (about) 1 package cache 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "io" 8 "os" 9 "path/filepath" 10 "reflect" 11 "sort" 12 "sync" 13 "sync/atomic" 14 "time" 15 16 "github.com/gofrs/flock" 17 "golang.org/x/sync/singleflight" 18 ) 19 20 type ( 21 CacheValue interface { 22 IsEqual(CacheValue) bool 23 ShouldRefresh() bool 24 IsValid() bool 25 } 26 27 Cache struct { 28 compactInterval time.Duration 29 cacheMap map[string]cacheValue 30 cacheMapMutex sync.Mutex 31 lastCompactTime time.Time 32 persistentFile *persistentFile 33 group singleflight.Group 34 flushing uint32 35 } 36 37 persistentFile struct { 38 valueType reflect.Type 39 cacheFilePath string 40 persistentDuration time.Duration 41 lastPersistentTime time.Time 42 handleError func(error) 43 } 44 45 cacheValue struct { 46 Value CacheValue `json:"value"` 47 CreatedAt time.Time `json:"created_at"` 48 } 49 50 unmarshalledCacheEntry struct { 51 Key string `json:"key"` 52 Value json.RawMessage `json:"value"` 53 CreatedAt time.Time `json:"created_at"` 54 } 55 ) 56 57 func NewCache(compactInterval time.Duration) *Cache { 58 return &Cache{ 59 compactInterval: compactInterval, 60 cacheMap: make(map[string]cacheValue), 61 lastCompactTime: time.Now(), 62 } 63 } 64 65 func NewPersistentCache( 66 valueType reflect.Type, 67 persistentFilePath string, 68 compactInterval time.Duration, 69 persistentDuration time.Duration, 70 handleError func(error), 71 ) (*Cache, error) { 72 c := &Cache{ 73 persistentFile: &persistentFile{ 74 valueType: valueType, 75 cacheFilePath: persistentFilePath, 76 persistentDuration: persistentDuration, 77 lastPersistentTime: time.Now(), 78 handleError: handleError, 79 }, 80 cacheMap: nil, 81 compactInterval: compactInterval, 82 lastCompactTime: time.Now(), 83 } 84 // 为了兼容上层接口,此处允许加载本地缓存文件失败 85 _ = c.loadLocalCache() 86 return c, nil 87 } 88 89 type GetResult uint8 90 91 const ( 92 GetResultFromCache GetResult = 0 93 GetResultFromCacheAndRefreshAsync GetResult = 1 94 GetResultFromFallback GetResult = 2 95 GetResultFromInvalidCache GetResult = 3 96 NoResultGot GetResult = 4 97 ) 98 99 func (cache *Cache) loadLocalCache() error { 100 defer func() { 101 if cache.cacheMap == nil { 102 cache.cacheMap = make(map[string]cacheValue) 103 } 104 }() 105 106 persistentFilePath := cache.persistentFile.cacheFilePath 107 err := os.MkdirAll(filepath.Dir(persistentFilePath), 0700) 108 if err != nil { 109 return err 110 } 111 unlockFunc, err := lockCachePersistentFile(persistentFilePath, false, cache.persistentFile.handleError) 112 if err != nil { 113 return err 114 } 115 defer unlockFunc() 116 117 file, closeFunc, err := openCachePersistentFile(persistentFilePath, cache.persistentFile.handleError) 118 if err != nil { 119 return err 120 } 121 defer closeFunc() 122 123 cacheMap, err := loadCacheMapFrom(cache.persistentFile.valueType, file) 124 if err != nil { 125 return err 126 } 127 128 cache.cacheMap = cacheMap 129 return nil 130 } 131 132 func (cache *Cache) Get(key string, fallback func() (CacheValue, error)) (CacheValue, GetResult) { 133 cache.cacheMapMutex.Lock() 134 value, ok := cache.cacheMap[key] 135 cache.cacheMapMutex.Unlock() 136 137 defer func() { 138 go cache.flush() 139 }() 140 141 if ok && value.Value.IsValid() { 142 if value.Value.ShouldRefresh() { 143 cache.doFallbackAsync(key, fallback) 144 return value.Value, GetResultFromCacheAndRefreshAsync 145 } else { 146 return value.Value, GetResultFromCache 147 } 148 } 149 150 newValue, err := cache.doFallback(key, fallback) 151 if err != nil { 152 if ok { 153 return value.Value, GetResultFromInvalidCache 154 } else { 155 return nil, NoResultGot 156 } 157 } 158 cache.set(key, newValue, false) 159 return newValue, GetResultFromFallback 160 } 161 162 func (cache *Cache) doFallback(key string, fallback func() (CacheValue, error)) (CacheValue, error) { 163 newValue, err, _ := cache.group.Do(key, func() (interface{}, error) { return fallback() }) 164 if err != nil { 165 return nil, err 166 } 167 return newValue.(CacheValue), nil 168 } 169 170 func (cache *Cache) doFallbackAsync(key string, fallback func() (CacheValue, error)) { 171 go func() { 172 newValue, err := cache.doFallback(key, fallback) 173 if err == nil { 174 cache.Set(key, newValue) 175 } 176 }() 177 } 178 179 func (cache *Cache) Set(key string, value CacheValue) { 180 cache.set(key, value, true) 181 } 182 183 func (cache *Cache) set(key string, value CacheValue, willFlushAsync bool) { 184 cache.checkType(value) 185 if !value.IsValid() { 186 return 187 } 188 189 now := time.Now() 190 cache.cacheMapMutex.Lock() 191 cache.cacheMap[key] = cacheValue{Value: value, CreatedAt: now} 192 cache.cacheMapMutex.Unlock() 193 194 if willFlushAsync { 195 go cache.flush() 196 } 197 } 198 199 func (cache *Cache) checkType(cacheValue CacheValue) { 200 if pf := cache.persistentFile; pf != nil { 201 if cacheValueType := reflect.TypeOf(cacheValue); !cacheValueType.AssignableTo(pf.valueType) { 202 panic(fmt.Sprintf("cannot assign %s to %s", cacheValueType, pf.valueType)) 203 } 204 } 205 } 206 207 func (cache *Cache) flush() { 208 if !atomic.CompareAndSwapUint32(&cache.flushing, 0, 1) { 209 return 210 } 211 defer atomic.StoreUint32(&cache.flushing, 0) 212 213 if cache.lastCompactTime.Add(cache.compactInterval).Before(time.Now()) { 214 cache.doCompact() 215 cache.lastCompactTime = time.Now() 216 } 217 218 if pf := cache.persistentFile; pf != nil { 219 if pf.lastPersistentTime.Add(pf.persistentDuration).Before(time.Now()) { 220 cache.doPersistent() 221 pf.lastPersistentTime = time.Now() 222 } 223 } 224 } 225 226 func (cache *Cache) doCompact() { 227 cache.cacheMapMutex.Lock() 228 defer cache.cacheMapMutex.Unlock() 229 230 var toDeleted []string 231 for key, value := range cache.cacheMap { 232 if !value.Value.IsValid() { 233 toDeleted = append(toDeleted, key) 234 } 235 } 236 for _, toDeletedKey := range toDeleted { 237 delete(cache.cacheMap, toDeletedKey) 238 } 239 } 240 241 func (cache *Cache) doPersistent() { 242 var ( 243 cacheFilePath = cache.persistentFile.cacheFilePath 244 handleError = cache.persistentFile.handleError 245 valueType = cache.persistentFile.valueType 246 ) 247 248 unlockFunc, err := lockCachePersistentFile(cacheFilePath, true, handleError) 249 if err != nil { 250 return 251 } 252 defer unlockFunc() 253 254 cache.cacheMapMutex.Lock() 255 defer cache.cacheMapMutex.Unlock() 256 257 file, closeFunc, err := openCachePersistentFile(cacheFilePath, handleError) 258 if err != nil { 259 return 260 } 261 defer closeFunc() 262 263 newCacheMap, err := loadCacheMapFrom(valueType, file) 264 if err != nil { 265 if handleError != nil { 266 handleError(err) 267 } 268 return 269 } 270 if isCacheMapEqual(cache.cacheMap, newCacheMap) { 271 return 272 } 273 mergeCacheMap(cache.cacheMap, newCacheMap) 274 275 if _, err = file.Seek(0, io.SeekStart); err != nil { 276 if handleError != nil { 277 handleError(err) 278 } 279 return 280 } 281 if err = file.Truncate(0); err != nil { 282 if handleError != nil { 283 handleError(err) 284 } 285 return 286 } 287 if err = saveCacheMapTo(file, cache.cacheMap); err != nil && handleError != nil { 288 handleError(err) 289 } 290 } 291 292 func loadCacheMapFrom(valueType reflect.Type, r io.Reader) (map[string]cacheValue, error) { 293 decoder := json.NewDecoder(r) 294 cacheMap := make(map[string]cacheValue) 295 for decoder.More() { 296 var entry unmarshalledCacheEntry 297 if err := decoder.Decode(&entry); err != nil { 298 return nil, err 299 } 300 ptrValue := reflect.New(valueType) 301 if err := json.Unmarshal(entry.Value, ptrValue.Interface()); err != nil { 302 return nil, err 303 } 304 value := ptrValue.Elem().Interface().(CacheValue) 305 if value.IsValid() { 306 cacheMap[entry.Key] = cacheValue{Value: value, CreatedAt: entry.CreatedAt} 307 } 308 } 309 return cacheMap, nil 310 } 311 312 func isCacheMapEqual(left, right map[string]cacheValue) bool { 313 if len(left) != len(right) { 314 return false 315 } 316 leftKeys := sort.StringSlice(getCacheMapKeys(left)) 317 leftKeys.Sort() 318 rightKeys := sort.StringSlice(getCacheMapKeys(right)) 319 rightKeys.Sort() 320 if !reflect.DeepEqual(leftKeys, rightKeys) { 321 return false 322 } 323 for _, key := range leftKeys { 324 leftValue := left[key] 325 rightValue := right[key] 326 if !leftValue.Value.IsEqual(rightValue.Value) { 327 return false 328 } 329 } 330 return true 331 } 332 333 func getCacheMapKeys(m map[string]cacheValue) []string { 334 keys := make([]string, 0, len(m)) 335 for k := range m { 336 keys = append(keys, k) 337 } 338 return keys 339 } 340 341 func mergeCacheMap(left, right map[string]cacheValue) { 342 for newKey, newValue := range right { 343 existedCacheValue, exists := left[newKey] 344 if exists && existedCacheValue.CreatedAt.Before(newValue.CreatedAt) || !exists { 345 left[newKey] = newValue 346 } 347 } 348 } 349 350 func saveCacheMapTo(w io.Writer, m map[string]cacheValue) error { 351 encoder := json.NewEncoder(w) 352 for k, v := range m { 353 rawMessage, err := json.Marshal(v.Value) 354 if err != nil { 355 return err 356 } 357 if err = encoder.Encode(unmarshalledCacheEntry{ 358 Key: k, 359 Value: rawMessage, 360 CreatedAt: v.CreatedAt, 361 }); err != nil { 362 return err 363 } 364 } 365 return nil 366 } 367 368 func lockCachePersistentFile(cacheFilePath string, ex bool, handleError func(error)) (context.CancelFunc, error) { 369 var ( 370 lockFilePath = cacheFilePath + ".lock" 371 lockFile = flock.New(lockFilePath) 372 err error 373 ) 374 if ex { 375 err = lockFile.Lock() 376 } else { 377 err = lockFile.RLock() 378 } 379 if err != nil { 380 if handleError != nil { 381 handleError(err) 382 } 383 return nil, err 384 } 385 return func() { 386 if err := lockFile.Unlock(); err != nil && handleError != nil { 387 handleError(err) 388 } 389 }, nil 390 } 391 392 func openCachePersistentFile(cacheFile string, handleError func(error)) (*os.File, context.CancelFunc, error) { 393 file, err := os.OpenFile(cacheFile, os.O_CREATE|os.O_RDWR, 0600) 394 if err != nil { 395 if handleError != nil { 396 handleError(err) 397 } 398 return nil, nil, err 399 } 400 return file, func() { 401 if err := file.Close(); err != nil && handleError != nil { 402 handleError(err) 403 } 404 }, nil 405 }