Ограничить блокировку loop() при отправке webhook бюджетом 3 с
BearSSL делает TLS-хендшейк синхронно, поэтому вместо асинхронности блокировка ограничивается по времени. Главный источник залипания — не таймауты HTTPClient, а DNS: WiFiClientSecureCtx::connect(name, port) резолвит имя через WiFi.hostByName() без таймаута и подвешивает loop() на ~10 с. Теперь имя разрешается заранее с лимитом 800 мс, результат попадает в кэш lwIP, и внутренний резолв возвращается мгновенно. Соединение по-прежнему идёт по имени, поэтому SNI и заголовок Host сохраняются. Остаток бюджета уходит в http.setTimeout(): он ограничивает и connect+TLS (через _client->setTimeout() до connect), и чтение ответа (собственный цикл HTTPClient по _tcpTimeout). Худший случай: ~3.5 с вместо прежних 10-15 с. Выходы аварии не затронуты — digitalWrite() и раньше выполнялся до отправки. Проверено: xtensa-lx106-elf-g++ -fsyntax-only -Wall -Wextra против ядра 3.1.2 — в скетче замечаний нет; разбор URL прогнан на 15 граничных случаях. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -143,9 +143,44 @@ TLS-соединение поднимается через `WiFiClientSecure` с
|
||||
сертификат сервера **не проверяется**. Достаточно для отправки в доверенную
|
||||
локальную/корпоративную инфраструктуру, но не защищает от MITM.
|
||||
|
||||
Отправка синхронная и блокирующая: на время POST (до нескольких секунд при
|
||||
недоступном сервере) основной цикл приостанавливается. Дисплей в этот момент
|
||||
не обновляется.
|
||||
### 5.1 Бюджет отправки
|
||||
|
||||
Отправка остаётся синхронной — BearSSL выполняет TLS-хендшейк блокирующе, и
|
||||
полностью асинхронного HTTPS на ESP8266 без хрупких сторонних библиотек нет.
|
||||
Вместо этого блокировка **ограничена по времени** бюджетом
|
||||
`WEBHOOK_BUDGET_MS = 3000 мс`, который расходуется по этапам:
|
||||
|
||||
1. **Разбор URL** (`parseWebhookHost`) — извлекает имя хоста, мгновенно.
|
||||
2. **DNS** — `WiFi.hostByName(host, ip, WEBHOOK_DNS_BUDGET_MS)`, лимит 800 мс.
|
||||
3. **TCP + TLS + чтение ответа** — `http.setTimeout(остаток бюджета)`.
|
||||
|
||||
Шаг 2 существует именно ради потолка: `WiFiClientSecureCtx::connect(name, port)`
|
||||
внутри вызывает `WiFi.hostByName()` **без таймаута** и на мёртвом DNS подвешивает
|
||||
`loop()` примерно на 10 секунд. Предварительный резолв с лимитом кладёт адрес в
|
||||
кэш lwIP, после чего внутренний резолв возвращается мгновенно. Соединение
|
||||
по-прежнему устанавливается по имени, поэтому SNI и заголовок `Host` не ломаются
|
||||
(важно, если webhook живёт за реверс-прокси).
|
||||
|
||||
Шаг 3 опирается на то, что `HTTPClient::connect()` вызывает
|
||||
`_client->setTimeout(_tcpTimeout)` **до** `_client->connect()`, а хендшейк
|
||||
BearSSL ограничен тем же `_timeout`. Чтение ответа ограничено отдельно —
|
||||
собственным циклом `HTTPClient` по `_tcpTimeout`.
|
||||
|
||||
Проверять бюджет между фазами внутри `HTTPClient` нельзя, поэтому потолок не
|
||||
строго 3000 мс. Но провал любой фазы обрывает цепочку, так что лимиты не
|
||||
складываются. Реальные худшие случаи:
|
||||
|
||||
| Сценарий | Задержка |
|
||||
|---|---|
|
||||
| Успешная отправка | 300–1500 мс |
|
||||
| DNS не отвечает | ~800 мс |
|
||||
| IP не отвечает (чёрная дыра) | ~800 мс + остаток бюджета |
|
||||
| TLS не поднимается | то же |
|
||||
| Хендшейк прошёл, ответа нет | ~1500 мс + остаток бюджета ≈ 3.5 с |
|
||||
|
||||
До введения бюджета те же сценарии давали 10–15 секунд.
|
||||
|
||||
Фактическое время каждой отправки пишется в лог: `[HTTP] Код: 200 (412 мс)`.
|
||||
|
||||
---
|
||||
|
||||
@@ -225,10 +260,13 @@ STATUS: OK
|
||||
|
||||
* `setInsecure()` — TLS без проверки сертификата.
|
||||
* Веб-интерфейс и `/reset_wifi` не защищены паролем.
|
||||
* Webhook отправляется синхронно и блокирует цикл; при недоступном сервере
|
||||
реакция контроллера замедляется на время таймаута.
|
||||
* События, произошедшие без Wi-Fi, не буферизуются — webhook просто теряется
|
||||
(в лог пишется `[HTTP] Нет Wi-Fi`).
|
||||
* Webhook отправляется синхронно и блокирует цикл, но не дольше бюджета
|
||||
(см. 5.1). Выходы аварии от этого не зависят: `digitalWrite()` выполняется
|
||||
**до** отправки, поэтому реле и сирена не ждут сервер. Подвисает только
|
||||
обновление OLED, опрос кнопки и веб-панель.
|
||||
* События, произошедшие без Wi-Fi или при неудачной отправке, не буферизуются
|
||||
и не повторяются — webhook теряется, флаги `mainAlarmSent` /
|
||||
`emergencyActiveSent` выставляются независимо от результата POST.
|
||||
* Логика аварии — «оба стоят»: остановка одного насоса штатной ситуацией
|
||||
не считается и никак не сигнализируется.
|
||||
* Webhook URL хранится в EEPROM в открытом виде и отдаётся веб-панелью всем,
|
||||
|
||||
Reference in New Issue
Block a user