Кэширование¶
У каждой SecretModel есть thread-safe TTL-кэш. Идея в том, что вы про
него не думаете — но если вдруг придётся, вот как он устроен.
Один кэш на корень¶
Каждый корневой инстанс SecretModel владеет своим TTLCache.
Вложенные модели используют кэш своего корня (и его бэкенд). Два
несвязанных корня имеют независимые кэши; ротация одного никак не
влияет на другой.
prod = App(stage="prod", backend=b)
dev = App(stage="dev", backend=b)
prod.refresh_all() # очищает только кэш prod; dev не тронут
Ключи¶
Ключ кэша — это резолвленный путь (с подставленной {var}),
опционально с суффиксом @version для пин-версий:
Два разных поля, дающих один ключ, делят одну запись и один фетч в бэкенд.
Семантика TTL¶
Задаётся для каждого поля через ttl= в Secret(...):
| Значение | Поведение |
|---|---|
None (default) |
Запись не истекает. |
0 |
Запись сразу считается истёкшей; каждое чтение в бэкенд. |
> 0 |
Запись живёт ttl секунд. |
Истёкшие записи не удаляются при чтении — они остаются в кэше до тех
пор, пока их не перезапишет следующий фетч или вы не позовёте
invalidate / clear. Именно эти просроченные записи использует
stale_on_error.
Как работает prefetch()¶
prefetch() (и validate="fetch" тоже его вызывает) обходит всё
дерево моделей, делит секреты на версионированные и нет, и:
- Не-версионированные идут одним вызовом
backend.get_batch(unique_paths). Per-key locks захватываются до batch-вызова, чтобы конкурентный читатель не успел запросить тот же путь и не получился двойной фетч. - Версионированные идут по очереди через
backend.get(path, version=...)— ни один batch API не поддерживает разные версии в одном запросе.
Каждое значение приводится к типу поля и кладётся в кэш под своим
ключом. Идемпотентен: повторный prefetch() при тёплом кэше дёшев,
но всё ещё идёт в бэкенд. Для «использовать только кэш» оставьте
дефолтное ленивое поведение.
Конкурентность¶
vaultly сериализует только то, что необходимо:
- Hot-чтения (cache hit) берут только лёгкий лок самого кэша, не per-key. Они масштабируются по потокам.
- Cold-чтения (промах) берут per-key lock, делают повторную проверку кэша, и только потом идут в бэкенд. Несколько потоков, обращающихся к одному холодному ключу, увидят ровно один вызов бэкенда.
- Потоки на разных ключах друг другу не мешают.
refresh(name)держит per-key lock наinvalidate + fetch. Конкурентный читатель либо получит старое значение (на короткий момент), либо подождёт нового фетча.
Инвалидация¶
| API | Эффект |
|---|---|
model.refresh(name) |
Удалить из кэша один секрет и перечитать. |
model.refresh_all() |
Удалить весь кэш. |
| (TTL expiry) | Чтение поднимет miss внутри; идёт фетч в бэкенд. |
Заметьте, что refresh_all() чистит весь кэш, включая поля с
ttl=None. После события ротации это обычно правильный молоток.
Жизненный цикл _fetch_locks¶
_fetch_locks — это KeyedLocks, по одному RLock на резолвленный
путь. Локи накапливаются по мере появления новых ключей. Для большинства
приложений набор путей ограничен формой модели, и это нормально.
Multi-tenant приложения, использующие tenant_id в путях (и видящие
тысячи уникальных путей со временем), могут вызвать
model._fetch_locks.discard(key) или model._fetch_locks.clear()
на подходящих границах для освобождения памяти. То же с _cache:
model._cache.clear().