Кэш: по интервалу таблица перечитывается всегда
Поле modified у публичной ссылки Яндекса может «застыть» (кэш на стороне Диска), и проверка «modified не изменился → не скачиваем» держала старые данные до неопределённого момента: стенд показывал прежнее расписание, хотя в таблице правки уже были. Теперь на каждое обновление (и по интервалу, и по кнопке) таблица скачивается заново; modified только показывается в админке. Плюс к этому: - при старте пишем в лог предупреждение, если ссылка в настройках отличается от SCHEDULE_LINK из окружения (именно так терялось время в прошлый раз: на стенде в настройках была сохранена другая ссылка); - на странице админки у поля ссылки пояснение, что значение хранится в настройках стенда и имеет приоритет над .env.
This commit is contained in:
@@ -99,8 +99,8 @@ nginx (https://schedule.our-place.ru) → 127.0.0.1:8090
|
|||||||
ошибка, число групп);
|
ошибка, число групп);
|
||||||
- **вставить новую ссылку**: она проверяется скачиванием и разбором таблицы
|
- **вставить новую ссылку**: она проверяется скачиванием и разбором таблицы
|
||||||
до сохранения — нерабочая не применится, данные на сайте не пострадают;
|
до сохранения — нерабочая не применится, данные на сайте не пострадают;
|
||||||
- **принудительно обновить расписание** — вне интервала и без проверки
|
- **принудительно обновить расписание** — вне интервала, не дожидаясь
|
||||||
«таблица не менялась» (то же, что делает фоновое обновление, но сейчас);
|
фонового обновления (интервал и так перечитывает таблицу целиком);
|
||||||
- **задать интервал обновления** в минутах (`1…1440`).
|
- **задать интервал обновления** в минутах (`1…1440`).
|
||||||
|
|
||||||
Как это устроено:
|
Как это устроено:
|
||||||
@@ -128,7 +128,7 @@ src/
|
|||||||
source.py загрузка файла по публичной ссылке Я.Диска
|
source.py загрузка файла по публичной ссылке Я.Диска
|
||||||
lessons.py разбор ячеек занятий (общий конвейер ролей)
|
lessons.py разбор ячеек занятий (общий конвейер ролей)
|
||||||
parsing.py листы → расписание, недели, индексы преподавателей/аудиторий
|
parsing.py листы → расписание, недели, индексы преподавателей/аудиторий
|
||||||
cache.py ScheduleCache: блокировка, интервал, modified, stale-while-error
|
cache.py ScheduleCache: блокировка, интервал, stale-while-error
|
||||||
settings.py хранилище настроек админки (ссылка, интервал) в JSON-файле
|
settings.py хранилище настроек админки (ссылка, интервал) в JSON-файле
|
||||||
errors.py ScheduleError
|
errors.py ScheduleError
|
||||||
frontend/
|
frontend/
|
||||||
@@ -152,7 +152,7 @@ tests/
|
|||||||
test_lessons.py все форматы ячеек
|
test_lessons.py все форматы ячеек
|
||||||
test_xlsx.py чтение книги
|
test_xlsx.py чтение книги
|
||||||
test_parsing.py разбор листов, недели, индексы
|
test_parsing.py разбор листов, недели, индексы
|
||||||
test_api.py API и поведение кэша (TTL/modified/stale, 404/400/502)
|
test_api.py API и поведение кэша (интервал/stale, 404/400/502)
|
||||||
```
|
```
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|||||||
@@ -67,6 +67,13 @@ def default_settings() -> SettingsStore:
|
|||||||
|
|
||||||
def default_cache(settings: SettingsStore | None = None) -> ScheduleCache:
|
def default_cache(settings: SettingsStore | None = None) -> ScheduleCache:
|
||||||
settings = settings or default_settings()
|
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)
|
return ScheduleCache(settings=settings, retry_after=RETRY_AFTER)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
- однопоточная перезагрузка (lock): параллельные запросы во время загрузки
|
- однопоточная перезагрузка (lock): параллельные запросы во время загрузки
|
||||||
ждут один download вместо N скачиваний;
|
ждут один download вместо N скачиваний;
|
||||||
- TTL (интервал обновления), а также проверка modified таблицы: если таблица
|
- интервал обновления: по его истечении таблица перечитывается целиком
|
||||||
не менялась, скачивание не повторяется;
|
(поле modified у публичной ссылки может «застыть», поэтому на него не
|
||||||
|
опираемся — оно только показывается в админке);
|
||||||
- stale-while-error: если таблица перестала скачиваться, отдаём последнее
|
- stale-while-error: если таблица перестала скачиваться, отдаём последнее
|
||||||
удачное состояние и повторяем попытку не чаще, чем раз в retry_after секунд
|
удачное состояние и повторяем попытку не чаще, чем раз в retry_after секунд
|
||||||
(вместо скачивания на каждый запрос);
|
(вместо скачивания на каждый запрос);
|
||||||
@@ -208,16 +209,10 @@ class ScheduleCache:
|
|||||||
if not self._link:
|
if not self._link:
|
||||||
self._fail(ScheduleError(NO_LINK_ERROR), now)
|
self._fail(ScheduleError(NO_LINK_ERROR), now)
|
||||||
return
|
return
|
||||||
|
# Обновление интервала всегда перечитывает таблицу: поле modified у
|
||||||
|
# публичной ссылки может «застыть» (кэш на стороне Диска), и тогда
|
||||||
|
# проверка «таблица не менялась» держала бы старые данные бесконечно.
|
||||||
modified = fetch_table_modified(self._link)
|
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:
|
try:
|
||||||
self._load(now, modified)
|
self._load(now, modified)
|
||||||
except ScheduleError as exc:
|
except ScheduleError as exc:
|
||||||
|
|||||||
@@ -80,7 +80,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<button class="admin-btn" id="save-link" type="button">Сохранить ссылку</button>
|
<button class="admin-btn" id="save-link" type="button">Сохранить ссылку</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="admin-hint">Ссылка проверяется скачиванием: если не открывается — не сохранится, данные на сайте останутся прежними.</div>
|
<div class="admin-hint">Ссылка проверяется скачиванием: если не открывается — не сохранится, данные на сайте останутся прежними.
|
||||||
|
Это значение хранится в настройках стенда и <b>имеет приоритет над .env</b> — именно по нему сайт берёт расписание.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="admin-card">
|
<div class="admin-card">
|
||||||
|
|||||||
@@ -155,11 +155,16 @@ def test_cache_no_repeated_download(client, cache):
|
|||||||
assert cache.calls["download"] == 1
|
assert cache.calls["download"] == 1
|
||||||
|
|
||||||
|
|
||||||
def test_cache_modified_unchanged_no_download(cache):
|
def test_interval_refresh_always_downloads(cache):
|
||||||
cache._ttl = 0 # даже с истёкшим TTL
|
"""По интервалу таблица перечитывается всегда.
|
||||||
|
|
||||||
|
Поле modified у публичной ссылки может «застыть», поэтому на него не
|
||||||
|
опираемся: иначе стенд показывал бы старые данные бесконечно.
|
||||||
|
"""
|
||||||
|
cache._ttl = 0
|
||||||
cache.get()
|
cache.get()
|
||||||
cache.get()
|
cache.get()
|
||||||
assert cache.calls["download"] == 1
|
assert cache.calls["download"] == 2 # оба обновления по интервалу скачали таблицу
|
||||||
assert cache.calls["modified"] >= 2
|
assert cache.calls["modified"] >= 2
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user