Кэш: по интервалу таблица перечитывается всегда

Поле modified у публичной ссылки Яндекса может «застыть» (кэш на стороне
Диска), и проверка «modified не изменился → не скачиваем» держала старые
данные до неопределённого момента: стенд показывал прежнее расписание, хотя
в таблице правки уже были. Теперь на каждое обновление (и по интервалу, и по
кнопке) таблица скачивается заново; modified только показывается в админке.

Плюс к этому:
- при старте пишем в лог предупреждение, если ссылка в настройках отличается
  от SCHEDULE_LINK из окружения (именно так терялось время в прошлый раз:
  на стенде в настройках была сохранена другая ссылка);
- на странице админки у поля ссылки пояснение, что значение хранится в
  настройках стенда и имеет приоритет над .env.
This commit is contained in:
2026-09-29 13:50:17 +03:00
parent ab92fb44e1
commit d4b47c30bf
5 changed files with 27 additions and 19 deletions

View File

@@ -99,8 +99,8 @@ nginx (https://schedule.our-place.ru) → 127.0.0.1:8090
ошибка, число групп);
- **вставить новую ссылку**: она проверяется скачиванием и разбором таблицы
до сохранения — нерабочая не применится, данные на сайте не пострадают;
- **принудительно обновить расписание** — вне интервала и без проверки
«таблица не менялась» (то же, что делает фоновое обновление, но сейчас);
- **принудительно обновить расписание** — вне интервала, не дожидаясь
фонового обновления (интервал и так перечитывает таблицу целиком);
- **задать интервал обновления** в минутах (`1…1440`).
Как это устроено:
@@ -128,7 +128,7 @@ src/
source.py загрузка файла по публичной ссылке Я.Диска
lessons.py разбор ячеек занятий (общий конвейер ролей)
parsing.py листы → расписание, недели, индексы преподавателей/аудиторий
cache.py ScheduleCache: блокировка, интервал, modified, stale-while-error
cache.py ScheduleCache: блокировка, интервал, stale-while-error
settings.py хранилище настроек админки (ссылка, интервал) в JSON-файле
errors.py ScheduleError
frontend/
@@ -152,7 +152,7 @@ tests/
test_lessons.py все форматы ячеек
test_xlsx.py чтение книги
test_parsing.py разбор листов, недели, индексы
test_api.py API и поведение кэша (TTL/modified/stale, 404/400/502)
test_api.py API и поведение кэша (интервал/stale, 404/400/502)
```
## API

View File

@@ -67,6 +67,13 @@ def default_settings() -> SettingsStore:
def default_cache(settings: SettingsStore | None = None) -> ScheduleCache:
settings = settings or default_settings()
if SCHEDULE_LINK and settings.link and settings.link != SCHEDULE_LINK:
log.warning(
"Ссылка в настройках (%s) отличается от SCHEDULE_LINK из окружения (%s); "
"работаем по ссылке из настроек — её видно и можно поменять в админке",
settings.link,
SCHEDULE_LINK,
)
return ScheduleCache(settings=settings, retry_after=RETRY_AFTER)

View File

@@ -2,8 +2,9 @@
- однопоточная перезагрузка (lock): параллельные запросы во время загрузки
ждут один download вместо N скачиваний;
- TTL (интервал обновления), а также проверка modified таблицы: если таблица
не менялась, скачивание не повторяется;
- интервал обновления: по его истечении таблица перечитывается целиком
(поле modified у публичной ссылки может «застыть», поэтому на него не
опираемся — оно только показывается в админке);
- stale-while-error: если таблица перестала скачиваться, отдаём последнее
удачное состояние и повторяем попытку не чаще, чем раз в retry_after секунд
(вместо скачивания на каждый запрос);
@@ -208,16 +209,10 @@ class ScheduleCache:
if not self._link:
self._fail(ScheduleError(NO_LINK_ERROR), now)
return
# Обновление интервала всегда перечитывает таблицу: поле modified у
# публичной ссылки может «застыть» (кэш на стороне Диска), и тогда
# проверка «таблица не менялась» держала бы старые данные бесконечно.
modified = fetch_table_modified(self._link)
if (
self._data is not None
and modified is not None
and modified == self._modified
):
# таблица не менялась — источник доступен, данные актуальны
self._loaded_at = now
self._error = None
return
try:
self._load(now, modified)
except ScheduleError as exc:

View File

@@ -80,7 +80,8 @@
</div>
<button class="admin-btn" id="save-link" type="button">Сохранить ссылку</button>
</div>
<div class="admin-hint">Ссылка проверяется скачиванием: если не открывается — не сохранится, данные на сайте останутся прежними.</div>
<div class="admin-hint">Ссылка проверяется скачиванием: если не открывается — не сохранится, данные на сайте останутся прежними.
Это значение хранится в настройках стенда и <b>имеет приоритет над .env</b> — именно по нему сайт берёт расписание.</div>
</div>
<div class="admin-card">

View File

@@ -155,11 +155,16 @@ def test_cache_no_repeated_download(client, cache):
assert cache.calls["download"] == 1
def test_cache_modified_unchanged_no_download(cache):
cache._ttl = 0 # даже с истёкшим TTL
def test_interval_refresh_always_downloads(cache):
"""По интервалу таблица перечитывается всегда.
Поле modified у публичной ссылки может «застыть», поэтому на него не
опираемся: иначе стенд показывал бы старые данные бесконечно.
"""
cache._ttl = 0
cache.get()
cache.get()
assert cache.calls["download"] == 1
assert cache.calls["download"] == 2 # оба обновления по интервалу скачали таблицу
assert cache.calls["modified"] >= 2