121 lines
2.5 KiB
Go
121 lines
2.5 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package cache
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"time"
|
|
)
|
|
|
|
// Disk is a cache that stores data in a file on-disk. It also supports
|
|
// returning a previously-expired value if refreshing the value in the cache
|
|
// fails.
|
|
type Disk[K comparable, V any] struct {
|
|
key K
|
|
val V
|
|
goodUntil time.Time
|
|
path string
|
|
timeNow func() time.Time // for tests
|
|
|
|
// ServeExpired indicates that if an error occurs when filling the
|
|
// cache, an expired value can be returned instead of an error.
|
|
ServeExpired bool
|
|
}
|
|
|
|
type diskValue[K comparable, V any] struct {
|
|
Key K
|
|
Value V
|
|
Until time.Time // Always UTC
|
|
}
|
|
|
|
func NewDisk[K comparable, V any](path string) (*Disk[K, V], error) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
if !os.IsNotExist(err) {
|
|
return nil, err
|
|
}
|
|
|
|
// Ignore "does not exist" errors
|
|
return &Disk[K, V]{path: path}, nil
|
|
}
|
|
defer f.Close()
|
|
|
|
var dv diskValue[K, V]
|
|
if err := json.NewDecoder(f).Decode(&dv); err != nil {
|
|
// Ignore errors; we'll overwrite when filling.
|
|
return &Disk[K, V]{path: path}, nil
|
|
}
|
|
|
|
return &Disk[K, V]{
|
|
key: dv.Key,
|
|
val: dv.Value,
|
|
goodUntil: dv.Until,
|
|
path: path,
|
|
}, nil
|
|
}
|
|
|
|
// Get will return the cached value, if any, or fill the cache by calling f and
|
|
// return the corresponding value. When the cache is filled, the value will be
|
|
// written to the configured path on-disk, along with the expiry time. Writing
|
|
// to the path on-disk is non-fatal.
|
|
//
|
|
// If f returns an error and c.ServeExpired is true, then a previous expired
|
|
// value can be returned with no error.
|
|
func (d *Disk[K, V]) Get(key K, f FillFunc[V]) (V, error) {
|
|
var now time.Time
|
|
if d.timeNow != nil {
|
|
now = d.timeNow()
|
|
} else {
|
|
now = time.Now()
|
|
}
|
|
|
|
if d.key == key && now.Before(d.goodUntil) {
|
|
return d.val, nil
|
|
}
|
|
|
|
// Re-fill cached entry
|
|
val, until, err := f()
|
|
if err == nil {
|
|
d.key = key
|
|
d.val = val
|
|
d.goodUntil = until
|
|
d.write()
|
|
return val, nil
|
|
}
|
|
|
|
// Never serve an expired entry for the wrong key.
|
|
if d.key == key && d.ServeExpired && !d.goodUntil.IsZero() {
|
|
return d.val, nil
|
|
}
|
|
|
|
var zero V
|
|
return zero, err
|
|
}
|
|
|
|
func (d *Disk[K, V]) write() {
|
|
// Try writing to the file on-disk, but ignore errors.
|
|
b, err := json.Marshal(diskValue[K, V]{
|
|
Key: d.key,
|
|
Value: d.val,
|
|
Until: d.goodUntil.UTC(),
|
|
})
|
|
if err == nil {
|
|
os.WriteFile(d.path, b, 0600)
|
|
}
|
|
}
|
|
|
|
// Forget implements Cache.
|
|
func (d *Disk[K, V]) Forget() {
|
|
d.goodUntil = time.Time{}
|
|
|
|
var zeroKey K
|
|
d.key = zeroKey
|
|
|
|
var zeroVal V
|
|
d.val = zeroVal
|
|
|
|
d.write()
|
|
}
|