rk_nats 0.2.0
rk_nats: ^0.2.0 copied to clipboard
NATS and JetStream over a native client, with a durability contract - an acknowledgement means fsynced to disk by default, or says plainly what else it meant.
rk_nats #
NATS и JetStream для Dart поверх нативного клиента: долговечные потоки, долговечные потребители и договор о том, что означает подтверждение.
Зачем пакет существует #
Живых клиентов NATS на Dart, у которых поверхность JetStream не помечена экспериментальной, нет. При этом JetStream — долговечные потоки и потребители — это ровно то, ради чего NATS и берут.
Вторая причина важнее первой.
JetStream подтверждает запись клиенту сразу, а сбрасывает её на диск по
таймеру. Проверка Jepsen для NATS 2.12.1 (опубликована 2025-12-08) в опыте с
согласованным отключением питания потеряла 131 418 из 930 005 подтверждённых
сообщений — около 14 %. Это nats-server#7564, и она открыта до сих пор.
Замерено 2026-07-31 на живых серверах, а не прочитано в заметке о выпуске:
| Сервер | sync_interval |
sync_always |
|---|---|---|
| 2.11.0, настройки по умолчанию | 2 минуты | нет поля |
| 2.14.4, настройки по умолчанию | 2 минуты | нет поля |
2.14.4, sync_interval: "always" |
2 минуты | true |
Последний выпуск, спустя семь месяцев после публикации Jepsen, по-прежнему приезжает с двухминутным окном. Значит настройки долговечности — часть договора пакета, а не то, о чём вспоминают при эксплуатации.
Что означает «подтверждено» здесь #
По умолчанию — сброшено на диск. Политика fsyncOnAck включена без
дополнительных слов, и под ней публикация отклоняется, пока библиотека не
получила доказательство, что сервер делает fsync до подтверждения.
final result = await RkNatsClient.connect(
RkNatsConnectOptions(
servers: ['nats://till-1:4222'],
// durability: RkNatsDurability.fsyncOnAck — уже так
evidence: RkNatsVarzEvidence(await fetchVarz('http://till-1:8222/varz')),
),
libraryPath: libraryPath,
);
final client = result.value!;
print(client.ackMeaning); // RkNatsAckMeaning.fsyncedToDisk
final ack = await client.publish(
stream: 'sales',
subject: 'sales.till17',
payload: utf8.encode(receiptJson),
messageId: 'receipt-000017', // повтор внутри окна — это тот же чек
);
print(ack.value!.ackMeaning); // и здесь тоже, на каждом подтверждении
Три политики, и слабые надо назвать вслух:
| Политика | Подтверждение означает | Скорость (замер ниже) |
|---|---|---|
fsyncOnAck — по умолчанию |
на диске, fsync сделан | 158 сообщ./с |
flushOnAck |
записано, fsync позже; надо назвать окно потери | 2 902 сообщ./с |
ackIsMemoryOnly |
где-то в памяти сервера | 4 137 сообщ./с |
Замер: одна машина, петля обратной связи, одна реплика, последовательная публикация с ожиданием каждого подтверждения. Безопасная настройка в 18 раз медленнее — и всё равно вчетверо быстрее платёжного терминала.
Три ловушки, ради которых написан весь этот код #
Первая. sync_interval не меняется, когда включают fsync на каждую
запись. Сервер с sync_interval: "always" продолжает сообщать две минуты.
Решает поле sync_always, и проверка, читающая только sync_interval, объявит
небезопасным именно тот сервер, который безопасен.
Вторая. Поток в режиме persist_mode: "async" подтверждает до записи —
и делает это на сервере, настроенном на fsync каждой записи. Замерено: 4 198
сообщ./с в режиме async против 158 сообщ./с в обычном на том же сервере. Он не
ждёт диска, а его подтверждение выглядит точно так же, как у того, который ждёт.
Пакет отказывает в создании такого потока под политикой fsyncOnAck.
Третья. nats-server 2.11.0 принимает поток с persist_mode: "async" и
возвращает поле отсутствующим, без ошибки. Поэтому поддержка определяется
опытом, а не сравнением версий, и RkNatsStreamInfo отдельно сообщает
requestedPersistMode и effectivePersistMode.
И отдельно: больше реплик — не замена fsync. Jepsen нашла порчу файлов, расходящуюся через Raft, и расщепление сознания после отказа одного узла при трёх репликах. Репликация защищает от смерти машины, а не от того, что все машины подтвердили то, чего не записали.
Когда брать этот пакет, а когда rk_zenoh #
Они не соперники, и выбор перестаёт быть трудным, как только его произносят.
rk_nats — когда сообщение обязано пережить всё: продажа, уезжающая с кассы
на сервер магазина; движение остатка; всё, что можно переиграть и что нельзя
доставить дважды. JetStream хранит сообщения, помнит, докуда дошёл каждый
потребитель, а листовой узел продолжает принимать записи, пока у магазина лежит
связь.
rk_zenoh — туннель и живое состояние: дотянуться до кассы за чужим
роутером, здоровье и присутствие, экран, следящий за значением. Ткань
публикации-подписки — правильная форма для «что верно сейчас» и неправильная для
«что произошло, по порядку, ровно один раз».
Хотеть оба — нормально. Использовать один вместо другого — ошибка.
Устройство #
rust/— нативная библиотека поверхasync-nats, собирается вcdylibиstaticlibс C ABI.lib/— привязка на Dart.
Граница: одна строка JSON внутрь, одна строка JSON наружу. Отсюда всё остальное —
перечисления пересекают границу по имени, добавление поля не меняет ABI, и
правило освобождения памяти ровно одно. Подробнее — doc/architecture.md и
doc/durability.md.
Сборка нативной части #
Пакет — FFI-плагин Flutter: flutter build сам вызывает cargo и кладёт
библиотеку в приложение на Windows, Linux и Android. Механизм, три разных пути
к cargo и порядок проверки на Mac — в
doc/native-build.md. Каталога hook/ здесь нет и не
будет: само его присутствие ломает dart run, dart test и flutter build.
Вручную:
cd rust && cargo build --release
cd .. && dart test # 49 проверок, 22 из них через C ABI
dart test --tags live --run-skipped # нужны запущенные nats-server
Куда библиотека доезжает:
| Цель | Состояние | Чем доказано |
|---|---|---|
| Windows | доезжает | rk_nats.dll рядом с runner собранного приложения |
| Linux | доезжает | librk_nats.so в bundle/lib/ приложения |
| Android | доезжает | найдена внутри распакованного APK на armeabi-v7a, arm64-v8a, x86_64 |
| macOS, iOS | написано, ни разу не собрано | Mac на проекте нет; файлы существуют, чтобы пакет не оказался неверным в день, когда Mac появится, и ни одно утверждение про них не проверено |
Что не проверено #
Потеря питания. Пакет проверяет настройку, при которой сервер обещает fsync до подтверждения, и стоимость этого обещания измерена. Настоящий обрыв питания на настоящем железе здесь не воспроизводился — эту часть сделала Jepsen, и её вывод и есть причина, по которой пакет устроен так.
Лицензия #
MIT, автор Rob Kim. См. LICENSE.