feat: supervise the daemon, and restart one that has lost every peer
Build docker image and push to registry.bitdeals.org / main-build-job (push) Successful in 7m12s
Build docker image and push to registry.bitdeals.org / main-build-job (push) Successful in 7m12s
A PyBitmessage daemon that has dropped to zero network connections does not find its way back. It can sit there for days — testnet1 did, and every BitDeals deal completed on that stand since 2026-08-30 left its escrow unspent, because the guarantor never received a CH3 the node could no longer publish. A daemon that has just started, by contrast, dials hard and reconnects within seconds. The cure was already known; what was missing was anything to notice and apply it. Docker will not: it reacts to a process exiting, never to a healthcheck, and health-driven restarts exist only under Swarm. Doing it from outside means handing a container the docker socket, which is root on the host and a poor trade for a relay with a published port. So the container supervises itself. `run.sh` no longer ends at `exec gosu bitmessage pybitmessage -d`. That made the daemon PID 1, and it is a bad PID 1: daemonize() double-forks and parks the grandfather in `while True: time.sleep(1)`, the final child SIGTERMs it to say "ready", and PID 1 drops that signal for want of a handler. The grandfather slept for ever; `docker stop` therefore reached the real daemon only as the SIGKILL ten seconds later, cutting a startup VACUUM in half — the way a node gets trapped retrying one it can never finish; and a daemon that died on its own left the container Up around a corpse, because what PID 1 was doing had nothing to do with whether the daemon lived. Away from PID 1 that grandfather does die on the ready signal — measured here: the start call returns at once with status 143 and leaves exactly one pybitmessage process behind. That makes starting the daemon an ordinary blocking call, and the supervisor ordinary shell: a trap that stops the daemon through its own API, a restart when it is gone, and the peer rule. What the supervisor does not do is act on a daemon whose API is not answering at all. That is the trapped-VACUUM node; a restart does not cure it and cuts the next VACUUM short as well. watchdog.py reports that case as its own exit code so the loop can leave it to a person. The healthcheck is untouched: it reports, and does not act. Four settings, on by default: BITMESSAGE_WATCHDOG, and _PERIOD, _AFTER, _COOLDOWN. All validated at start, where a typo is visible, rather than hours later as a supervisor that spins or one that never acts. Callers must raise the stop grace period — 90s in compose, or --stop-timeout 90. A clean shutdown took 17.5 s on a small database and grows with it, so under Docker's default ten the daemon is killed mid-write anyway and the supervisor buys nothing. An image cannot set this for itself.
This commit is contained in:
@@ -84,6 +84,10 @@ docker run -d \
|
||||
|-e BITMESSAGE_TRUSTED_PEER|`host:port` единственного пира, к которому узел подключается наружу; больше ни к кому. По умолчанию: пусто — узел выбирает пиров сам. См. «Замечания»|
|
||||
|-e BITMESSAGE_SEND_OUTGOING|Подключается ли узел наружу вообще: `True` или `False`. По умолчанию: `True`. При `False` получается узел, который только принимает входящие, — центр приватного контура|
|
||||
|-e BITMESSAGE_KNOWN_NODES|Список `host:port` через запятую; записывается в `knownnodes.dat` при каждом старте вместо того, что там было. По умолчанию: пусто — файл не трогается. Заодно выключает бутстрап по DNS, см. «Замечания»|
|
||||
|-e BITMESSAGE_WATCHDOG|Перезапускать ли демона, потерявшего всех пиров: `True` или `False`. По умолчанию: `True`. См. «Замечания»|
|
||||
|-e BITMESSAGE_WATCHDOG_PERIOD|Секунд между проверками числа пиров. По умолчанию: `60`|
|
||||
|-e BITMESSAGE_WATCHDOG_AFTER|Сколько проверок подряд без пиров нужно, чтобы вмешаться. По умолчанию: `5` — то есть пять минут при периоде по умолчанию|
|
||||
|-e BITMESSAGE_WATCHDOG_COOLDOWN|Минимальный промежуток между двумя перезапусками, в секундах. По умолчанию: `900`. `0` снимает ограничение|
|
||||
|
||||
# Замечания
|
||||
|
||||
@@ -96,6 +100,31 @@ docker run -d \
|
||||
- Контейнер становится здоровым, когда у демона появилось сетевое соединение,
|
||||
— на новом узле это несколько минут. Начальный период ожидания заодно
|
||||
покрывает стартовый `VACUUM` файла `messages.dat`.
|
||||
- **Демона без пиров перезапускаем, молчащего — нет.** Демон, потерявший всех
|
||||
пиров, сам обратно дорогу не находит: он может простоять на нуле соединений
|
||||
сколько угодно, — а только что запущенный ищет пиров напористо и находит.
|
||||
Поэтому контейнер присматривает за собственным демоном: раз в
|
||||
`BITMESSAGE_WATCHDOG_PERIOD` спрашивает у API число соединений и после
|
||||
`BITMESSAGE_WATCHDOG_AFTER` нулей подряд останавливает демона через его же API
|
||||
и поднимает заново. *Контейнер* при этом не пересоздаётся и никто снаружи не
|
||||
участвует, так что соседний узел сохраняет адреса и видит лишь моргнувший API.
|
||||
|
||||
Демона, у которого API не отвечает вовсе, сторож не трогает намеренно. Это
|
||||
узел, застрявший на стартовом `VACUUM`, которого он не может закончить;
|
||||
перезапуск его не лечит, а лишь обрывает следующий `VACUUM` на середине.
|
||||
Проверка здоровья показывает `unhealthy` в обоих случаях, но вмешиваться стоит
|
||||
только в первый, а во втором нужен человек.
|
||||
|
||||
**Задайте контейнеру `stop_grace_period: 90s`** (compose) или
|
||||
`--stop-timeout 90` (`docker run`). Корректное закрытие `messages.dat` не
|
||||
укладывается в десять секунд, отпущенные Docker по умолчанию, — замерено около
|
||||
тринадцати на небольшой базе, и растёт вместе с файлом, — так что без этого
|
||||
демона убьёт SIGKILL на середине записи, ровно то, ради предотвращения чего
|
||||
супервизор и заведён. Сам образ это задать не может, только вызывающая сторона.
|
||||
|
||||
`BITMESSAGE_WATCHDOG=False` выключает правило о пирах. Супервизор остаётся в
|
||||
любом случае: он же корректно останавливает демона по `docker stop` и
|
||||
поднимает того, кто умер сам, вместо контейнера, стоящего вокруг трупа.
|
||||
- **P2P-порт публикуется только как `8444:8444`.** Пирам демон сообщает порт из
|
||||
собственной конфигурации (`port` в `keys.dat`), а не тот, в который вы его
|
||||
отобразили, поэтому `8555:8444` объявляет сети порт, где никого нет. Для
|
||||
|
||||
Reference in New Issue
Block a user