booblik-client
1. Зона ответственности
То, что тянет к себе потребитель: клиент, продюсер, подписка — и общий кодек провода.
Чем не занимается: не содержит ни селектора, ни сессии, ни брокера. Это и есть причина существования модуля: тому, кто пишет в booblik и читает из него, серверная половина не нужна, но до M-80 он собирал и грузил её вместе с клиентом.
Главный инвариант: здесь нет ничего серверного. Проверяется тем, что модуль не зависит от
:booblik-net — зависимость идёт в обратную сторону.
2. Контракт
Формат на проводе — protocol-wire. Публичный API зафиксирован в
api/booblik-client.api и сравнивается на каждом check.
2а. Ключевые файлы (якоря кода)
| Файл | Что там |
|---|---|
src/main/kotlin/.../net/wire/ | кодек: Protocol, ApiKey, ErrorCode, запросы и ответы |
src/main/kotlin/.../net/client/BooblikClient.kt | синхронный клиент: послал — прочитал |
src/main/kotlin/.../net/client/BooblikConnection.kt | конвейерное соединение, сопоставление по correlationId |
src/main/kotlin/.../net/client/Producer.kt | аккумулятор записей, единственный в клиенте |
src/main/kotlin/.../net/client/Publishing.kt | хендл топика, маршрутизация по ключу, batch { } |
src/main/kotlin/.../net/client/Subscription.kt | follow/replay, RecordBatch, OffsetStore |
src/main/kotlin/.../net/client/Consumer.kt | низкоуровневый poll() по одной партиции |
3. Как устроено
Кодек лежит здесь, а не в сервере, потому что он общий. Обе стороны обязаны понимать провод одинаково, и модуль, который видят обе, — единственное место, где это проверяет компилятор.
Пакеты остались прежними (ru.workinprogress.booblik.net.*) при переезде. Переименование
ничего бы не улучшило, а сломало бы каждый импорт у всех, кто уже собрался.
4. Зависимости
| Тип | Имя | Для чего |
|---|---|---|
| Module | :booblik-core | Offset, TopicName, PartitionId, AckPolicy |
| Library | kotlinx-coroutines-core | конвейерное соединение, аккумулятор, подписка |
5. Конфигурация
Конфигурационных файлов нет — всё параметрами конструкторов и SubscriptionConfig /
ProducerConfig.
6. Инфраструктура и деплой
Опубликован в reposilite snapshots: io.github.youndie.booblik:booblik-client, последняя
версия — 0.2.0. Адрес и креды приходят из BOOBLIK_REPO_URL / _USER / _SECRET и в
репозитории не лежат. Запуск ручной: .github/workflows/publish.yml, версия — 0.1.<номер прогона>.
0.1.1 собирать не надо: против него нельзя скомпилироваться. Корутины стоят в публичном ABI
обоих модулей, а объявлены были implementation — то есть в POM попадали в runtime, и потребитель
падал на Unresolved reference 'kotlinx' сразу после успешного резолва. Ни гейт, ни зелёная
публикация этого не видели; нашлось сборкой настоящего потребителя из опубликованного артефакта.
С 0.1.2 корутины в compile, и шаг проверки в workflow утверждает именно это.
0.1.2 тоже брать не стоит: аккумулятор Producer терял запись, если таймер сброса срабатывал
в момент её прихода (withTimeoutOrNull на mailbox.receive() при отмене снимает элемент и
выбрасывает). Отправка зависала навсегда, остальные обслуживались как обычно. Та же ошибка, что
проект уже находил и чинил в PartitionWriter; клиент пронёс её мимо тестов, потому что ни один
не гнал два топика с разной частотой через один аккумулятор. Исправлено в 0.1.3, регрессия —
ProducerLostRecordTest.
Выкладываются два модуля, а не этот один. :booblik-client зависит от :booblik-core через
api, поэтому в POM клиента ядро стоит в compile — опубликованный в одиночку клиент дал бы
потребителю ссылку на артефакт, которого в репозитории нет. Шаг проверки в workflow утверждает
именно это: что POM клиента ссылается на ядро той же версии, а не что загрузка не упала.
Сервер (:booblik-net, :booblik-app) наружу не выкладывается: снаружи нужен клиент, а серверная
половина — это то, от чего модуль и отделяли.
7. Локальный запуск
./gradlew :booblik-client:testИнтеграционные тесты клиента живут в :booblik-net: им нужен сервер, а зависимость в эту
сторону не идёт.
8. Сознательные ограничения / грабли
- Клиент — единственный, кто может проверить контрольную сумму. Брокер на zero-copy-пути до
байтов не дотрагивается. Поэтому
CRC32Cздесь не деталь, а обязанность, и это же — самая дорогая строка при возможном переезде на KMP (ресёрч §1.17): реализация JDK компилируется в инструкцию, а написанная руками — нет. follow()обязан владеть своим соединением. Удержанный FETCH занимает его целиком, потому что сессия обслуживает запросы по одному (M-75). Подписка поэтому открывает соединение на партицию сама.- Позиции клиент хранит сам.
OffsetStoreобъявлен и не реализован намеренно; словаcommitв именах нет, потому что брокер о сохранении не знает. - Модуль остаётся Kotlin/JVM. Решение Р8 и его условие пересмотра — появление настоящего потребителя не на JVM. Браузер таким потребителем быть не может: у него нет сырого TCP.