NATS JetStream¶
Source and sink for NATS JetStream subjects, carrying each message body as a string.
Overview¶
This connector integrates with NATS JetStream, the persistent, acknowledged streaming layer of NATS, via the libnats (nats.c) client. The source is a durable pull consumer that fetches batches from a subject and emits each message body as a std::string; the sink publishes each string record to a subject using asynchronous JetStream publishes. Only the string record type is supported, and records cross the wire as their raw byte body (the SQL frontend bridges this to a JSON object string). Core NATS pub/sub (fire-and-forget, at-most-once) is not targeted; this connector uses JetStream throughout.
Dependency and version¶
| Component | Provenance | Version |
|---|---|---|
| libnats (nats.c) | System package via apt (Debian: libnats-dev) / brew (macOS: cnats) |
Not pinned by clink |
Arrow is not involved in this connector; records are plain string bodies, not Arrow batches.
Enabling it¶
The connector is controlled by the CLINK_WITH_NATS CMake option (AUTO / ON / OFF, default AUTO). Under AUTO the target is built only if libnats is discovered (via pkg-config libnats, or a manual probe for nats/nats.h and the nats library). Under ON a missing libnats is a fatal configure error. The library is found through standard system paths (/opt/homebrew, /usr/local, /usr).
Install the client first:
- Debian:
apt-get install libnats-dev(seescripts/install-connector-deps.sh) - macOS:
brew install cnats
cmake -S . -B build -DCLINK_WITH_NATS=ON
cmake --build build -j
When enabled, the build defines CLINK_HAS_NATS and produces the clink::nats target.
Factories¶
| Factory name | Direction | Record type |
|---|---|---|
nats_source_string |
Source | std::string |
nats_sink_string |
Sink | std::string |
Configuration¶
All options are parsed in impls/nats/src/register_factories.cpp and map onto the NatsSource::Options / NatsSink::Options structs (impls/nats/include/clink/nats/nats_source.hpp, impls/nats/include/clink/nats/nats_sink.hpp) and the shared NatsConnParams (impls/nats/include/clink/nats/connection_params.hpp).
Connection (source and sink)¶
| Option | Required | Default | Description |
|---|---|---|---|
url |
No | nats://localhost:4222 |
NATS server URL. Comma-separated URLs are accepted by nats.c. |
user |
No | (empty) | Username for user/password auth. Leave blank for no-auth. |
password |
No | (empty) | Password for user/password auth. |
token |
No | (empty) | Token for token auth. Set whichever of user/password or token the server expects. |
client_name |
No | clink |
Client connection name shown in NATS monitoring. |
Source (nats_source_string)¶
| Option | Required | Default | Description |
|---|---|---|---|
subject |
Yes | (none) | Subject to consume. Construction throws if empty. |
stream |
No | (empty) | Bind to this JetStream stream; if blank, the stream is resolved by subject. |
durable |
No | clink |
Durable consumer name, shared across subtasks. |
batch |
No | 256 |
Messages fetched per pull. |
fetch_timeout_ms |
No | 1000 |
Fetch wait in milliseconds; also bounds cancel latency. |
ack_wait_s |
No | 60 |
Consumer AckWait in seconds. Keep larger than the checkpoint interval. |
max_ack_pending |
No | 2048 |
Upper bound on held, unacked messages. |
Sink (nats_sink_string)¶
| Option | Required | Default | Description |
|---|---|---|---|
subject |
Yes | (none) | Subject to publish to. Construction throws if empty. |
max_pending |
No | 4096 |
Asynchronous publish window; provides backpressure. |
publish_timeout_ms |
No | 30000 |
Time to wait for pending publish acks on flush. |
Authentication is configured through the connection options (user/password or token); leave them blank for an unauthenticated server.
SQL usage¶
The connector is mapped in src/sql/physical_plan.cpp under connector='nats' for both source and sink. The source binds to nats_source_string with the json_string_to_row codec; the sink binds to nats_sink_string with row_to_json_string. The sink is at-least-once only: mode='upsert' is rejected, and exactly_once is rejected because no producer dedup key is set.
CREATE TABLE orders (
id BIGINT,
region STRING
) WITH (
connector = 'nats',
url = 'nats://localhost:4222',
subject = 'clink.orders',
stream = 'ORDERS',
durable = 'clink-orders'
);
A sink table uses the same connector='nats' with at least subject set, for example subject='clink.results'.
Example¶
Based on impls/nats/tests/test_nats_live.cpp, using the typed classes directly:
#include "clink/nats/nats_sink.hpp"
#include "clink/nats/nats_source.hpp"
using clink::nats::NatsSink;
using clink::nats::NatsSource;
// Sink: publish three records, then block until JetStream acks them all.
NatsSink::Options so;
so.conn.url = "nats://localhost:4222";
so.subject = "clink.events";
NatsSink sink(std::move(so));
sink.open();
clink::Batch<std::string> out;
out.emplace(R"({"id":1,"v":"a"})");
out.emplace(R"({"id":2,"v":"b"})");
sink.on_data(out);
sink.flush(); // throws on publish-ack timeout so the job replays
// Source: durable pull consumer over the same subject/stream.
NatsSource::Options ro;
ro.conn.url = "nats://localhost:4222";
ro.subject = "clink.events";
ro.stream = "EVENTS";
ro.durable = "clink-events";
NatsSource src(std::move(ro));
src.open();
clink::Emitter<std::string> em([](clink::StreamElement<std::string> e) -> bool {
if (e.is_data()) {
for (const auto& rec : e.as_data()) {
// rec.value() is the raw message body
}
}
return true;
});
src.produce(em);
A JetStream stream bound to the subject must already exist; the connectors do not provision streams (that is an admin task, comparable to a RabbitMQ exchange).
Delivery semantics¶
Both directions are at-least-once.
- Source: a durable explicit-ack pull consumer. The ack is deferred to checkpoint COMMIT, not the barrier:
snapshot_offsetbuckets the messages emitted before each barrier against that checkpoint id,notify_checkpoint_complete(driven from the cluster'sCommitCheckpointdispatch) queues a committed bucket for ack, and the nextproduce()turn issues eachnatsMsg_Ack- so a message is acked only after the checkpoint that captured it is globally durable.notify_checkpoint_abortedreleases a bucket without acking, so an aborted checkpoint (or a crash before commit) leaves those messages for redelivery (JetStream redelivers afterAckWait). Until acked JetStream holds them (bounded bymax_ack_pending); all nats.c calls stay on theproduce()thread. The durable consumer's ack floor is persisted server-side, so recovery needs no local state andrestore_offsetis a no-op. Keepack_wait_slarger than the checkpoint interval so held, unacked messages are not redelivered before their checkpoint commits. - Sink: records are published asynchronously (
js_PublishAsync) for throughput;flush()/on_barrier()calljs_PublishAsyncCompleteto block until the server has acked every pending publish, and throw on timeout so the job replays from the last checkpoint rather than dropping data. JetStream de-dup viaNats-Msg-Idis not set, so replay can duplicate. The SQL planner therefore rejectsexactly_onceandupsert.
Limitations¶
- String record type only; there is no int64 or Arrow channel for this connector.
- Sink requires a pre-existing JetStream stream bound to the target subject; stream provisioning is not performed by the connector.
- No producer dedup key (
Nats-Msg-Id) is set, so exactly-once and upsert are unsupported (enforced by the SQL planner). - The source acks at checkpoint commit (not the barrier), so the earlier crash-window drop is closed; duplicates on replay remain the at-least-once trade-off. This relies on the cluster's per-checkpoint
CommitCheckpointdispatch, wired for both the default (non-fused) deployment and the opt-in par-1 chain fusion (CLINK_PLAN_FUSE_PAR1=1). subjectis required for both source and sink; construction throws if it is empty.
Testing¶
Offline tests in impls/nats/tests/test_nats.cpp cover required-parameter validation and clean failure against a dead endpoint; they need no broker.
The publish-then-consume round-trip lives in impls/nats/tests/test_nats_live.cpp and is skipped unless CLINK_NATS_TEST_ENDPOINT is set to a JetStream-enabled NATS URL (for example nats://localhost:4222). The live test creates the stream up front via the nats.c API, publishes through the sink (acks awaited), then reads the messages back through the source's durable pull consumer.
CLINK_NATS_TEST_ENDPOINT=nats://localhost:4222 \
ctest --test-dir build -L nats
The tests do not pin a Docker image; run any JetStream-enabled NATS server and point CLINK_NATS_TEST_ENDPOINT at it.