Amber 6.6.7 даёт мёртвым реле второй шанс через пятнадцать минут
Реле, помеченное как мёртвое, оставалось мёртвым до принудительного закрытия приложения, а поскольку каждая неудачная попытка записывалась дважды, до этого хватало пяти попыток вместо десяти.
Nostr WoT Newsroom
Материал подготовлен редакцией Nostr WoT на основе указанных первоисточников и опубликован автоматически, без индивидуальной проверки человеком.
Amber 6.6.7 был опубликован 5 октября 2026 года. От 6.6.6 его отделяют восемь коммитов и двадцать три файла, и основная часть изменений касается того, как подписывающее приложение решает, что реле больше не стоит набирать.
Сообщённый симптом состоял в том, что реле оставались отключёнными до принудительного закрытия приложения. Сравнение с 6.6.6 показывает две независимые причины, обе в учёте повторных попыток, а не в самих сокетах.
«Мёртв» было навсегда
RelayHealthTracker ведёт по каждому реле серию последовательных неудач подключения и прекращает попытки, когда серия превышает MAX_RECONNECT_ATTEMPTS, равное 10. Мёртвое реле также выбрасывается из набора реле подписки, поэтому периодическое обновление перестаёт открывать к нему сокеты.
В 6.6.6 это состояние сбрасывалось только успешным подключением, сменой сети на уровне операционной системы или ручным переподключением. Трекер хранил простой счётчик неудач, а isDead был сравнением с этим счётчиком. Ничто не делало его истекающим.
Это терпимо, когда каждый сбой приходит как смена сети, и происходит не так. Новая документация в файле перечисляет пропущенные случаи: сеть Wi-Fi, потерявшая доступ в интернет и вернувшая его, слабый сигнал мобильной сети, сбой самого реле. Ни один из них не является сменой сети по умолчанию, поэтому ни один не сбрасывал трекер, и все реле могли оказаться помеченными как мёртвые, пока устройство оставалось в одной внешне исправной сети.
Версия 6.6.7 хранит метку времени рядом со счётчиком и делает состояние «мёртв» истекающим:
const val DEAD_COOLDOWN_MS = 15 * 60 * 1000L
fun isDead(relay: NormalizedRelayUrl): Boolean {
val entry = health[relay] ?: return false
return entry.failures > MAX_RECONNECT_ATTEMPTS && clock() - entry.lastFailureAt < DEAD_COOLDOWN_MS
}Через пятнадцать минут после последней неудачи реле снова становится допустимым, и периодическое обновление возвращает его. Порог не изменился; изменилось то, что его превышение стало временным наказанием, а не приговором. Функция часов внедряется извне, чтобы истечение можно было проверить тестом, не выжидая.
Каждая неудачная попытка считалась дважды
Вторая причина уже и резче. Quartz, библиотека, управляющая пулом реле, сообщает о неудачном наборе как onCannotConnect, за которым следует onDisconnected. Оба обработчика вызывали один и тот же scheduleReconnect, который записывал неудачу.
Поэтому серия росла на два за каждую неудачную попытку. Задокументированный порог 10 достигался на пятой. Комментарий, добавленный в 6.6.7, формулирует обе половины проблемы: подсчёт обоих вызовов вдвое снижал реальный порог, а обычное закрытие со стороны сервера вообще не является сбоем подключения.
Исправление разделяет функцию на две. recordFailureAndScheduleReconnect записывает неудачу и вызывается только из onCannotConnect. scheduleReconnect теперь проверяет isDead и переподключается, ничего не записывая, что и требуется onDisconnected.
Смены сети, которые сменой сети не были
Релиз также пересобирает соединения с реле, когда VPN поднимается или отключается и когда устройство снова получает доступ в интернет, а не только при переключении между Wi-Fi и мобильными данными.
Новый файл NetworkChangeDetector.kt держит это решение в 79 строках без зависимостей от служб Android. Он сводит сеть по умолчанию к снимку из трёх полей: идентификатор сети, набор заявленных транспортов и признак того, что Android подтвердил реальный доступ сети в интернет. Сравнивая последовательные снимки, он сообщает о необходимости сброса, когда меняется идентификатор, когда меняется набор транспортов, когда непроверенная сеть становится проверенной или когда сеть возвращается после потери.
Два из этих случаев относятся к VPN. Сеть VPN заявляет и собственный TRANSPORT_VPN, и свой нижележащий транспорт, поэтому VPN, становящийся сетью по умолчанию, меняет идентификатор, а VPN, переходящий с Wi-Fi на мобильную связь, меняет набор транспортов, хотя сама сеть по умолчанию остаётся той же. Детектор различает оба направления и помечает их, так что сброс несёт с собой причину.
Он столь же прямо говорит о том, что сменой не является. Первый вызов после регистрации относится к той сети, на которой реле уже подключаются, поэтому он записывается как базовая точка и ни о чём не сообщает. Повторные вызовы с тем же состоянием не сообщают ничего. Потеря сети, которая уже была заменена, не сообщает ничего.
ConnectivityService переработан вокруг него, потеряв 46 строк и получив 81. И onAvailable, и onCapabilitiesChanged теперь подают данные в одну функцию onNetworkState. Сбросы сглаживаются NETWORK_SETTLE_MS, одной секундой, поскольку вызовы приходят пачками по мере завершения проверки, а реле следует пересобрать один раз, на итоговой сети. Возможности сети отслеживаются даже при включённом аварийном выключателе, чтобы базовая точка была актуальна, когда реле снова разрешат.
Запросы из браузеров, отличных от Chrome
Отдельная запись восстанавливает совместимость с веб-клиентами. Запрос nostrsigner: может нести параметры либо в extras интента, что NIP-55 описывает для нативных вызовов Android, либо в строке запроса URI, как делает веб-страница. Amber выбирал между ними по одному extra, Browser.EXTRA_APPLICATION_ID.
Chrome его задаёт. Другие браузеры нет. DuckDuckGo назван в списке изменений, и запрос от него шёл по нативному пути и завершался ошибкой "Unknown signer type".
Новый isWebRequest принимает три более слабых признака в дополнение к этому extra: категорию интента BROWSABLE, referrer, разрешающийся в браузер, и параметр type= в строке запроса, проверяемый и в исходном виде, и после декодирования. Extra type по-прежнему имеет приоритет, поскольку означает, что вызывающая сторона воспользовалась API extras и является нативной. Разрешение referrer в браузер требует видимости пакетов, поэтому AndroidManifest.xml добавляет запись <queries> для VIEW вместе с BROWSABLE по https.
Внешний ключ, который IGNORE не покрывал
Последняя запись исправляет аварийное завершение при сохранении разрешения для приложения, только что удалённого или сброшенного. insertPermissions2 был помечен OnConflictStrategy.IGNORE, который покрывает конфликты уникальности, но не нарушения внешнего ключа, поэтому вставка вызывала SQLITE_CONSTRAINT_FOREIGNKEY. Теперь он сначала читает сохранившиеся ключи приложений и отбрасывает разрешения, у которых родителя больше нет, а insertPermissions проходит через него вместо прямого вызова сырой вставки.
Что устанавливают артефакты
Версия 6.6.7 это опубликованный релиз с тегом на коммите dc3f672e, с одиннадцатью пакетами Android и подписанным манифестом контрольных сумм. С ним приходят девятнадцать тестов: четыре на паузу, восемь на детектор смены сети и семь на решение о веб-запросе. Список изменений и диф совпадают по всем четырём пунктам.
Это устанавливает, что делает код. Это не устанавливает, что он запущен хоть на одном устройстве, а магазины и репозитории движутся в своём темпе. Тому, у кого подписывающее приложение теряло реле, стоит проверить версию, установленную в действительности.
Источники
Каждое утверждение в этом материале ссылается на первоисточник.
- Релиз Amber v6.6.7 — greenart7c3/Amber (5 октября 2026 г.)
- Сравнение v6.6.6...v6.6.7 — greenart7c3/Amber (5 октября 2026 г.)
- NIP-55 Android Signer Application — nostr-protocol/nips (5 октября 2026 г.)