# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this is

`libcds-pubsub` — a small C++14 TCP/UDP publisher/subscriber library (zmq-like) built on Boost.Asio,
written for streaming data in LIGO CDS. Messages are `(uint64 key, byte buffer)` pairs; the application
must supply monotonically increasing keys. All I/O runs on library-owned background threads.
License is GPL-3; `external/catch2` is Boost-licensed.

## Build, test, run

Dependencies: CMake ≥ 3.13, Boost (system), pybind11, libzstd (via pkg-config), Catch2 (optional — a
bundled single-header copy under `external/catch2` is used if no system Catch2 is found).

```sh
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build -j
cd build && ctest                      # runs test_pub_sub and test_pub_sub_asan
./build/pub_sub/test_pub_sub "You can send messages through tcp"   # single Catch2 test case by name
./build/pub_sub/test_pub_sub -l       # list test cases
```

Manual/interactive tools (no assertions, run by hand):
- `build/examples/publisher [conn_str]` / `build/examples/subscriber [conn_str]` — default to
  `multi://127.0.0.1/239.192.0.1:9000`. These link against the ASan build of the library
  (`publisher_nosan` exists only to prove the non-sanitized build links).
- `build/pub_sub/test_sender` + `build/pub_sub/test_slow_receiver` — TCP on `127.0.0.1:9000`; the
  receiver deliberately stalls to exercise slow-consumer handling.

Formatting: `scripts/clang_format.sh` runs clang-format (style in `.clang-format`) over `pub_sub`,
`examples`, `python`. The style is unusual (spaces inside parens, aligned declarations) — run the
script rather than hand-formatting.

## Versioning (CMake will fail configure if these disagree)

`version.txt` is the master version; `version_tag.txt` holds an optional CI suffix (normally empty).
CMake checks that the top entry of `debian/changelog` matches `version.txt` + `version_tag.txt` and
errors out otherwise. When bumping the version, update both `version.txt` and `debian/changelog`
(CI uses `scripts/deb_ci_version.sh` to do this via `dch`). The shared-library SOVERSION is set
separately in `pub_sub/CMakeLists.txt` (`PUB_SUB_SOVERSION_*`, libtool current/revision/age scheme).

CI (`.gitlab-ci.yml`) builds Debian packages for buster/bullseye/bookworm/trixie with `dpkg-buildpackage`.

## Architecture

Layout: public headers in `pub_sub/include/cds-pubsub/` (installed), implementation-only headers in
`pub_sub/private/`, sources `pub_sub/*.cc`, Catch2 tests in `pub_sub/tests/`, pybind11 module in
`python/` (exposes `publisher`, `subscriber`, `sub_message` as the `pubsub` Python module).

**Public API** (`pub.hh`, `sub.hh`, `common.hh`): `Publisher` and `Subscriber` are move-only pimpl
facades over `detail::PublisherIntl` / `detail::SubscriberIntl`. `Publisher::publish(key, Message)`
fans out to every configured destination. `Subscriber::subscribe(address, handler)` returns a `SubId`
and invokes `handler(SubMessage)` from the background thread. Optional `PubDebugNotices` /
`SubDebugNotices` virtual-hook classes provide stats/diagnostics callbacks.

**Plugin / pipeline model** (`pub_plugin.hh`, `sub_plugin.hh`, `pipeline_parser.cc`): every
transport and filter is a plugin keyed by URL prefix. A connection string is a `|`-separated
pipeline of stages, e.g. `zstd://3|tcp://127.0.0.1:9000`. Built-in plugins registered in `pub.cc`
/ `sub.cc`:
- `tcp://host:port` — unicast TCP (only transport supporting `reset_subscription`)
- `udp://local_addr/remote_addr:port` — UDP broadcast
- `multi://local_addr/multicast_addr:port` — UDP multicast
- `zstd://[level]` — compression filter; must not be the last publisher stage / first subscriber stage

Pipelines are built back-to-front: the publisher side chains `PublisherInstance::publish` via a
`next_stage`; the subscriber side wraps the user handler in successive `Subscription::filter` calls.
`SubscriptionPluginApi::is_source()` distinguishes sources (transports, must be first) from filters.
Custom plugins are added with `load_plugin()` on either facade.

The built-in publisher transports accept per-destination options as query parameters on their stage
(`tcp://host:port?max_depth=16`), overriding the env-derived defaults; invalid/unknown options throw
from `add_destination`. Plugins still receive the raw stage string (the plugin ABI is unchanged —
out-of-tree plugins exist); the splitting helper is public (`cds-pubsub/stage_address.hh`,
`pub_sub::parse_stage_address` → scheme/path/arguments) so custom plugins can opt in. Option
validation is centralized in `PublisherOptions::parse_max_depth`/`with_arguments` (`pub_intl.hh`).
The built-in publisher plugin classes live in `private/pub_plugins_intl.hh`.

**Wire protocol** (`private/pub_sub_intl.hh`): client sends an 8-byte `SubcriptionRequest` (last key
seen, or `NEXT_PUB_MSG()` = max uint64 for "start with next"), then receives repeated
`BlockHeader{key, size}` (16 bytes) + payload. UDP uses `UdpHeader` chunks (≤9000-byte jumbo frames)
with `UdpResendRequest`-based retransmission. Struct layouts are pinned by `static_assert`s — don't
reorder fields. `MAX_MSG_SIZE()` is 200 MiB.

**Threading / ownership** (`private/pub_tcp.hh`, `pub_udp.hh`, `sub_tcp.hh`): each transport
instance owns its own `boost::asio::io_context` and a single `std::thread` running it. `publish()`
is called on the application thread and hands data to that thread. Publishers keep recent messages
in a `MessageHistory` (`pub_intl.hh`): a bounded deque of shared, immutable `MessageBlock`s with no
inter-block links, handed out only as `std::shared_ptr<const MessageBlock>` — holding a block pins
exactly that block. TCP clients track a key plus at most one in-flight block, so a stalled client
cannot pin the history. Slow consumers are skipped ahead to the oldest retained message; history
depth comes from `PUBLISHER_MAX_DEPTH` (3–60, default 8), read once per `Publisher` in
`create_publisher_intl()` via `PublisherOptions::from_environment()`. Slow-consumer behavior lives
in `PubClientTcp::schedule_write`, with the `PubDebugNotices::slow_client_detected` hook reporting
skips.

Tests for internals include `pub_sub/private` directly (`test_pub_sub` has `private` on its include
path), so private headers are effectively part of the test surface.
