Есть функция Fetch. Обычно она берёт данные через load и на успехе кладёт
их в кеш, а на ошибке отдаёт то, что было закешировано раньше:
var store sync.Map
const maxWait = time.Second
func Fetch(key string, load func() (interface{}, error)) (interface{}, error) {
data, e := load()
if e == nil {
store.Store(key, data)
return data, nil
}
fmt.Printf("load failed: %s", e)
if cached, ok := store.Load(key); ok {
return cached, nil
}
return nil, e
}
Проблема: load иногда отвечает очень долго, а бывает, что медленно отвечает
всегда. Доработай Fetch, сигнатуру не меняя.
var store sync.Map
var maxWait = time.Second // var, чтобы тесты могли её уменьшить
var ErrTimeout = errors.New("load timeout")
func Fetch(key string, load func() (any, error)) (any, error)
Требования
load успел за maxWait → поведение прежнее: успех кладётся в кеш и
возвращается, ошибка → закешированное значение или эта ошибка.
load не ответил за maxWait → Fetch возвращается через maxWait:
закешированное значение и nil, а если кеша нет — ErrTimeout.
- Брошенный по таймауту
load доживает в фоне. Если он завершится успехом,
результат попадает в кеш, и следующие Fetch его увидят.
- Пока по ключу идёт
load, новые Fetch того же ключа не запускают свой
load, а ждут уже идущий (тоже не дольше maxWait от своего старта). Иначе
при вечно медленном источнике каждый запрос плодит ещё одну висящую горутину.
Разные ключи друг друга не блокируют.
- Когда все
load завершились, горутин не остаётся. Код проходит go test -race.
Пример
Fetch("rates", fastLoad) // 100, nil — и 100 в кеше
Fetch("rates", slowLoad) // через maxWait: 100, nil (из кеша); slowLoad крутится дальше
Fetch("rates", otherLoad) // пока slowLoad не закончился, otherLoad не вызывается
Устно: что если load зависнет навсегда? Как отдавать устаревшее значение
сразу, не ожидая maxWait (stale-while-revalidate)? Зачем тут sync.Map и
чем она отличается от map + RWMutex?