chore(ops): развести crontab по хостам под переезд #3072

Merged
lekss361 merged 1 commit from chore/crontab-split-by-host into main 2026-08-24 02:04:40 +00:00
Owner

Закрывает пункт «crontab разведён между хостами» из #3059.

Проблема

Сегодняшний crontab Beget'а обслуживает оба контура сразу — и остающийся (Forgejo), и уезжающий (tradein / gendesign). После 30.08 это перестанет быть верным, а сам crontab об этом не узнает.

Пять записей начнут ходить в контейнеры, которых на Beget больше нет:

Расписание Задача Во что упрётся
0 5 * * * backfill_houses_dadata docker exec tradein-backend
30 5 * * * geocode_deals_from_houses docker exec tradein-backend
45 5 * * * geocode_deals_nominatim docker exec tradein-backend
30 4 * * * backup-tradein-db.sh dump из tradein-postgres
30 3 * * * ops/backup.sh dump из gendesign-postgres

Падающие по ночам задачи — ещё полбеды. Хуже, что вместе с ними перестанут обновляться sentinel'ы бэкапов. Сторож честно это заметит и упрётся в то, что канал оповещения не настроен (#2203TELEGRAM_* нет ни в одном env-файле). То есть бэкапы обоих продуктовых кластеров пропадут молча.

Зеркальный случай опаснее. Если на Poincare crontab не установить вовсе, там не будет ни одного бэкапа — и узнать об этом неоткуда, потому что сторож пропущенных прогонов живёт в том же crontab'е. Отсутствие бэкапов обнаружилось бы ровно в тот момент, когда они понадобятся.

Решение

Два эталонных файла, а не абзац в раннбуке: переход должен быть явным действием, а не «забыли поправить crontab».

crontab /opt/gendesign/ops/crontab-beget.cron       # на остающемся
crontab /opt/gendesign/ops/crontab-poincare.cron    # на принимающем
Хост Что несёт
Beget backup-forgejo.sh + его сторож + docker-prune
Poincare оба продуктовых бэкапа + два их сторожа + три задачи обогащения/геокодирования + свой docker-prune

docker-prune нужен на обоих, и это не «переехавшая» запись, а две разные: на Beget он чистит тома CI-раннеров (#2881, они остаются), на Poincare — образы продуктового стека.

Сторожа едут вместе со своими бэкапами: сторож должен жить там же, где sentinel, иначе он следит за файлом, который никто не обновляет, и кричит вечно.

Побочно исправлено

Логи переведены с /tmp на /opt/gendesign/logs. Половина записей писала в /tmp, который чистится при перезагрузке — именно поэтому историю отказов бэкапов раньше не удавалось восстановить.

Времена оставлены прежние — чтобы при разборе после переезда не гадать, что изменилось: поведение или расписание. Подгонять окна имеет смысл после того, как всё поедет стабильно.

ops/*.cron добавлен в paths: deploy.yml. Файлы деплоем не исполняются, но должны физически лежать в /opt/gendesign — иначе в окне их нечем будет установить. Тот же класс, что #2887.

Test plan

  • 11 расписаний разобраны построчно — 5 полей времени + команда, ошибок 0
  • deploy.yml остаётся валидным YAML после правки paths:
  • Разбор по хостам сверен с фактическим crontab -l на проде — учтены все 11 действующих записей, ни одна не потеряна и ни одна не продублирована
  • Установка на живых хостах — в окне переезда, руками

Что нужно от тебя

Кроме собственно crontab <файл> в окне — перенести на Poincare /etc/default/gendesign-backup и /etc/default/tradein-backup. В git их нет и быть не должно, а без них дампы останутся локальными, то есть на том же диске, что и БД — ровно то, из-за чего заводился #2203.

И создать каталог логов: mkdir -p /opt/gendesign/logs.

Refs #3059, #3057, #2203

Закрывает пункт «crontab разведён между хостами» из #3059. ## Проблема Сегодняшний crontab Beget'а обслуживает **оба контура сразу** — и остающийся (Forgejo), и уезжающий (tradein / gendesign). После 30.08 это перестанет быть верным, а сам crontab об этом не узнает. **Пять записей начнут ходить в контейнеры, которых на Beget больше нет:** | Расписание | Задача | Во что упрётся | |---|---|---| | `0 5 * * *` | `backfill_houses_dadata` | `docker exec tradein-backend` | | `30 5 * * *` | `geocode_deals_from_houses` | `docker exec tradein-backend` | | `45 5 * * *` | `geocode_deals_nominatim` | `docker exec tradein-backend` | | `30 4 * * *` | `backup-tradein-db.sh` | dump из `tradein-postgres` | | `30 3 * * *` | `ops/backup.sh` | dump из `gendesign-postgres` | Падающие по ночам задачи — ещё полбеды. Хуже, что вместе с ними **перестанут обновляться sentinel'ы бэкапов**. Сторож честно это заметит и упрётся в то, что канал оповещения не настроен (#2203 — `TELEGRAM_*` нет ни в одном env-файле). То есть **бэкапы обоих продуктовых кластеров пропадут молча**. **Зеркальный случай опаснее.** Если на Poincare crontab не установить вовсе, там не будет **ни одного бэкапа** — и узнать об этом неоткуда, потому что сторож пропущенных прогонов живёт в том же crontab'е. Отсутствие бэкапов обнаружилось бы ровно в тот момент, когда они понадобятся. ## Решение Два эталонных файла, а не абзац в раннбуке: переход должен быть **явным действием**, а не «забыли поправить crontab». ``` crontab /opt/gendesign/ops/crontab-beget.cron # на остающемся crontab /opt/gendesign/ops/crontab-poincare.cron # на принимающем ``` | Хост | Что несёт | |---|---| | **Beget** | `backup-forgejo.sh` + его сторож + `docker-prune` | | **Poincare** | оба продуктовых бэкапа + два их сторожа + три задачи обогащения/геокодирования + свой `docker-prune` | `docker-prune` нужен **на обоих**, и это не «переехавшая» запись, а две разные: на Beget он чистит тома CI-раннеров (#2881, они остаются), на Poincare — образы продуктового стека. Сторожа едут **вместе со своими бэкапами**: сторож должен жить там же, где sentinel, иначе он следит за файлом, который никто не обновляет, и кричит вечно. ### Побочно исправлено **Логи переведены с `/tmp` на `/opt/gendesign/logs`.** Половина записей писала в `/tmp`, который чистится при перезагрузке — именно поэтому историю отказов бэкапов раньше не удавалось восстановить. **Времена оставлены прежние** — чтобы при разборе после переезда не гадать, что изменилось: поведение или расписание. Подгонять окна имеет смысл после того, как всё поедет стабильно. **`ops/*.cron` добавлен в `paths:` `deploy.yml`.** Файлы деплоем не исполняются, но должны физически лежать в `/opt/gendesign` — иначе в окне их нечем будет установить. Тот же класс, что #2887. ## Test plan - [x] **11 расписаний разобраны построчно** — 5 полей времени + команда, ошибок 0 - [x] `deploy.yml` остаётся валидным YAML после правки `paths:` - [x] Разбор по хостам сверен с фактическим `crontab -l` на проде — учтены все 11 действующих записей, ни одна не потеряна и ни одна не продублирована - [ ] Установка на живых хостах — **в окне переезда**, руками ## Что нужно от тебя Кроме собственно `crontab <файл>` в окне — перенести на Poincare `/etc/default/gendesign-backup` и `/etc/default/tradein-backup`. В git их нет и быть не должно, а без них дампы останутся локальными, то есть на том же диске, что и БД — ровно то, из-за чего заводился #2203. И создать каталог логов: `mkdir -p /opt/gendesign/logs`. Refs #3059, #3057, #2203
lekss361 added 1 commit 2026-08-24 01:46:51 +00:00
chore(ops): развести crontab по хостам под переезд
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m1s
CI / backend-tests (pull_request) Successful in 17m15s
e576f8c256
Сегодняшний crontab Beget'а обслуживает ОБА контура сразу — и остающийся
(Forgejo), и уезжающий (tradein/gendesign). После 30.08 это перестанет
быть верным, а сам crontab об этом не узнает.

Пять записей начнут ходить в контейнеры, которых на Beget больше нет:

  0  5 * * *  backfill_houses_dadata      docker exec tradein-backend
  30 5 * * *  geocode_deals_from_houses   docker exec tradein-backend
  45 5 * * *  geocode_deals_nominatim     docker exec tradein-backend
  30 4 * * *  backup-tradein-db.sh        dump из tradein-postgres
  30 3 * * *  ops/backup.sh               dump из gendesign-postgres

Падающие по ночам задачи — ещё полбеды. Хуже, что вместе с ними перестанут
обновляться sentinel'ы бэкапов; сторож честно это заметит и упрётся в то,
что канал оповещения не настроен (#2203: TELEGRAM_* нет ни в одном
env-файле). То есть бэкапы обоих продуктовых кластеров пропадут МОЛЧА.

Зеркальный и более опасный случай: если на Poincare crontab не установить
вовсе, там не будет НИ ОДНОГО бэкапа — и узнать об этом неоткуда, потому
что сторож пропущенных прогонов живёт в том же crontab'е. Отсутствие
бэкапов обнаружилось бы в момент, когда они понадобятся.

Поэтому два эталонных файла, а не инструкция в раннбуке: переход должен
быть явным действием (`crontab ops/crontab-<хост>.cron`), а не «забыли
поправить».

Разбор по хостам:
  Beget    — backup-forgejo.sh + его сторож + docker-prune (здесь остаются
             CI-раннеры, их тома и растут)
  Poincare — оба продуктовых бэкапа + два их сторожа + три задачи
             обогащения/геокодирования + СВОЙ docker-prune

docker-prune нужен на обоих: на Beget он чистит тома раннеров, на Poincare
образы продуктового стека. Это не «переехавшая» запись, а две разные.

Заодно логи переведены с /tmp на /opt/gendesign/logs: половина записей
писала в /tmp, который чистится при перезагрузке — именно поэтому историю
отказов бэкапов раньше не удавалось восстановить.

Времена оставлены прежние: чтобы при разборе後 переезда не гадать, что
изменилось — поведение или расписание.

`ops/*.cron` добавлен в paths deploy.yml: файлы не исполняются деплоем, но
должны физически лежать в /opt/gendesign, иначе в окне их нечем будет
установить (тот же класс, что #2887).

Проверено: 11 расписаний разобраны построчно — 5 полей + команда, ошибок 0;
deploy.yml остаётся валидным YAML.

Refs #3059, #3057, #2203
lekss361 merged commit 0ba52e55db into main 2026-08-24 02:04:40 +00:00
lekss361 deleted branch chore/crontab-split-by-host 2026-08-24 02:04:41 +00:00
Author
Owner

Смержено, файлы доехали до прода, проверено. Заодно поправка к моему же test plan — цифра там неверная.

Проверка после деплоя

-rw-rw-r-- 1 gendesign gendesign 3933 Aug 24 05:05 /opt/gendesign/ops/crontab-beget.cron
-rw-rw-r-- 1 gendesign gendesign 4933 Aug 24 05:05 /opt/gendesign/ops/crontab-poincare.cron

Действующий crontab -l не тронут — как и задумано, файлы эталонные и применяются только руками в окне. Домены отвечают (gendsgn.ru 401, git.gendsgn.ru 200).

Поправка: заданий 10, а не 11

В test plan я написал «все 11 действующих записей учтены». Это неверно — на проде их 10. Цифра 11 — это количество строк в моих двух файлах (3 + 8), и лишняя там появляется намеренно.

Пересчёт с прода, пронумерованный:

# Задача Куда едет
1 backup-tradein-db.sh Poincare
2 ops/backup.sh Poincare
3 backfill_houses_dadata Poincare
4 geocode_deals_from_houses Poincare
5 geocode_deals_nominatim Poincare
6 docker-prune.sh остаётся на Beget
7 backup-forgejo.sh остаётся
8 staleness: main backup Poincare
9 staleness: tradein backup Poincare
10 staleness: forgejo backup остаётся

Итого: 7 переезжают, 3 остаются = 10, всё сходится.

В файлах при этом 3 (Beget) + 8 (Poincare) = 11, потому что docker-prune намеренно продублирован: на Beget он чистит тома CI-раннеров (#2881, они остаются), на Poincare — образы продуктового стека. Это две разные задачи с одинаковым именем скрипта, а не одна переехавшая.

Сам разбор верен: ни одна из десяти не потеряна и ни одна не продублирована случайно. Неверна была только формулировка «11 действующих» — правильно «10 действующих, 11 строк в файлах, разница — осознанный второй prune».

Извиняться за цифру не буду, но оставляю здесь, чтобы тот, кто будет сверять файлы с crontab -l в окне, не решил, что одна запись потерялась.

Смержено, файлы доехали до прода, проверено. Заодно поправка к моему же test plan — цифра там неверная. ## Проверка после деплоя ``` -rw-rw-r-- 1 gendesign gendesign 3933 Aug 24 05:05 /opt/gendesign/ops/crontab-beget.cron -rw-rw-r-- 1 gendesign gendesign 4933 Aug 24 05:05 /opt/gendesign/ops/crontab-poincare.cron ``` Действующий `crontab -l` **не тронут** — как и задумано, файлы эталонные и применяются только руками в окне. Домены отвечают (`gendsgn.ru` 401, `git.gendsgn.ru` 200). ## Поправка: заданий 10, а не 11 В test plan я написал «все **11** действующих записей учтены». Это неверно — **на проде их 10**. Цифра 11 — это количество строк в моих двух файлах (3 + 8), и лишняя там появляется намеренно. Пересчёт с прода, пронумерованный: | # | Задача | Куда едет | |---|---|---| | 1 | `backup-tradein-db.sh` | Poincare | | 2 | `ops/backup.sh` | Poincare | | 3 | `backfill_houses_dadata` | Poincare | | 4 | `geocode_deals_from_houses` | Poincare | | 5 | `geocode_deals_nominatim` | Poincare | | 6 | `docker-prune.sh` | **остаётся** на Beget | | 7 | `backup-forgejo.sh` | **остаётся** | | 8 | staleness: main backup | Poincare | | 9 | staleness: tradein backup | Poincare | | 10 | staleness: forgejo backup | **остаётся** | Итого: **7 переезжают, 3 остаются** = 10, всё сходится. В файлах при этом 3 (Beget) + 8 (Poincare) = 11, потому что **`docker-prune` намеренно продублирован**: на Beget он чистит тома CI-раннеров (#2881, они остаются), на Poincare — образы продуктового стека. Это две разные задачи с одинаковым именем скрипта, а не одна переехавшая. **Сам разбор верен: ни одна из десяти не потеряна и ни одна не продублирована случайно.** Неверна была только формулировка «11 действующих» — правильно «10 действующих, 11 строк в файлах, разница — осознанный второй prune». Извиняться за цифру не буду, но оставляю здесь, чтобы тот, кто будет сверять файлы с `crontab -l` в окне, не решил, что одна запись потерялась.
Author
Owner

Проверил часовой пояс — оказалось, что я весь разбор вёл в неверном допущении, и заодно закрылся вопрос, который в PR даже не был поставлен.

Времена в crontab — местные, а не UTC

Ждал прогона backup-forgejo.sh (15 4 * * *), считая, что это 04:15 UTC. Прогон не появлялся. Оказалось, он уже отработал в 01:15 UTC:

[2026-08-24T01:15:02Z] Dumping Forgejo DB ...
[2026-08-24T01:15:49Z] Forgejo backup done.

а метка файла на диске — Aug 24 04:15. То есть cron считает по местному времени сервера, и 15 4 * * * = 04:15 MSK = 01:15 UTC.

Это ровно тот случай, где расхождение между «что написано» и «когда сработает» стоило бы дорого при переезде: если на новом хосте часовой пояс окажется другим, все одиннадцать записей поедут на несколько часов, причём молча — задачи будут исправно выполняться, просто не тогда.

Проверка обоих хостов

Beget     Time zone: Europe/Moscow (MSK, +0300)
Poincare  Time zone: Europe/Moscow (MSK, +0300)

Совпадают. Значит времена в crontab-beget.cron и crontab-poincare.cron переносятся как есть, без пересчёта — и решение «оставить времена прежними», которое я принял по другой причине (чтобы при разборе не гадать, что изменилось), оказывается верным и по этой.

Но это проверенный факт, а не данность: если Poincare когда-нибудь переставят на UTC — например, при переустановке ОС или по умолчанию у нового провайдера, — расписания сдвинутся на три часа. Добавил бы это отдельной строкой в раннбук #3057: перед установкой crontab сверить timedatectl на обоих хостах.

Поправка к моим же формулировкам

В нескольких местах сегодня я писал «бэкап в 04:15 UTC» и «прогон 04:15 UTC» — неверно, это 04:15 MSK. Там, где я называл время из scrape_schedules.next_run_at (например, окно замера floor_n_pairs 06:31–07:58), ошибки нет: Postgres отдаёт эти значения с явным +00, то есть в UTC, и они не cron'овые, а планировщиковые.

Практическое следствие для проверки #3073 (бэкап конфигурации Forgejo): она произойдёт не сегодня, а на прогоне 01:15 UTC / 04:15 MSK завтра — правка доехала до /opt/gendesign в 03:51 UTC, уже после сегодняшнего прогона.

Проверил часовой пояс — оказалось, что я весь разбор вёл в неверном допущении, и заодно закрылся вопрос, который в PR даже не был поставлен. ## Времена в crontab — местные, а не UTC Ждал прогона `backup-forgejo.sh` (`15 4 * * *`), считая, что это 04:15 UTC. Прогон не появлялся. Оказалось, он **уже отработал в 01:15 UTC**: ``` [2026-08-24T01:15:02Z] Dumping Forgejo DB ... [2026-08-24T01:15:49Z] Forgejo backup done. ``` а метка файла на диске — `Aug 24 04:15`. То есть cron считает по **местному времени сервера**, и `15 4 * * *` = 04:15 MSK = 01:15 UTC. Это ровно тот случай, где расхождение между «что написано» и «когда сработает» стоило бы дорого при переезде: если на новом хосте часовой пояс окажется другим, **все одиннадцать записей поедут на несколько часов**, причём молча — задачи будут исправно выполняться, просто не тогда. ## Проверка обоих хостов ``` Beget Time zone: Europe/Moscow (MSK, +0300) Poincare Time zone: Europe/Moscow (MSK, +0300) ``` **Совпадают.** Значит времена в `crontab-beget.cron` и `crontab-poincare.cron` переносятся как есть, без пересчёта — и решение «оставить времена прежними», которое я принял по другой причине (чтобы при разборе не гадать, что изменилось), оказывается верным и по этой. Но это **проверенный факт, а не данность**: если Poincare когда-нибудь переставят на UTC — например, при переустановке ОС или по умолчанию у нового провайдера, — расписания сдвинутся на три часа. Добавил бы это отдельной строкой в раннбук #3057: **перед установкой crontab сверить `timedatectl` на обоих хостах.** ## Поправка к моим же формулировкам В нескольких местах сегодня я писал «бэкап в 04:15 UTC» и «прогон 04:15 UTC» — неверно, это 04:15 MSK. Там, где я называл время из `scrape_schedules.next_run_at` (например, окно замера `floor_n_pairs` 06:31–07:58), ошибки нет: Postgres отдаёт эти значения с явным `+00`, то есть в UTC, и они не cron'овые, а планировщиковые. Практическое следствие для проверки #3073 (бэкап конфигурации Forgejo): она произойдёт не сегодня, а на прогоне **01:15 UTC / 04:15 MSK завтра** — правка доехала до `/opt/gendesign` в 03:51 UTC, уже после сегодняшнего прогона.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#3072
No description provided.