rk_nats 0.2.0 copy "rk_nats: ^0.2.0" to clipboard
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.

0
likes
0
points
182
downloads

Publisher

verified publisherspherex.kz

Weekly Downloads

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.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

ffi

More

Packages that depend on rk_nats

Packages that implement rk_nats