MQTT connector¶
A source (
mqtt_source, via SUBSCRIBE) and a sink (mqtt_sink, via PUBLISH) for MQTT brokers, exchanging string payloads over libmosquitto.
Overview¶
The MQTT connector integrates an MQTT broker (for example Mosquitto) over the libmosquitto synchronous client API. The sink PUBLISHes each input record to a fixed topic; the source SUBSCRIBEs to a topic filter and emits each received message. Both operate on the std::string channel: the payload is treated as an opaque string and published or emitted verbatim, with no Arrow or Parquet encoding. The source can optionally wrap each message as a JSON envelope ({"topic":...,"payload":...}) so that a wildcard subscription can carry the per-message topic. The client wrapper is shared by both sides and handles connection, authentication, optional TLS, and the CONNACK/SUBACK waits.
Dependency and version¶
| Component | Provenance | Version |
|---|---|---|
| libmosquitto | System package via apt (Debian, libmosquitto-dev) / brew (macOS, mosquitto) |
Not pinned by clink |
TLS is built into libmosquitto (OpenSSL), so there is no separate SSL library to discover. tls=true works whenever the installed libmosquitto was compiled with TLS support; if not, the connection fails at connect time with libmosquitto's own error. The connector does not link Arrow.
Enabling it¶
The connector is gated by the CMake option CLINK_WITH_MQTT, with values AUTO (default), ON, or OFF.
AUTO: built only if libmosquitto is found; otherwise the impl is silently skipped.ON: configuration fails if libmosquitto is not found.OFF: the target is not defined.
CMake discovers libmosquitto three ways, most portable first: a pkg-config imported target (libmosquitto.pc, shipped by libmosquitto-dev), mosquitto's own CONFIG package, then a manual header/library probe (covering a Homebrew keg or a bare Linux install with no pkg-config tool). libmosquitto is not installed by the project's Docker build-env scripts, so to build the connector you install it yourself first:
# Debian/Ubuntu
sudo apt-get install -y libmosquitto-dev
# macOS
brew install mosquitto
cmake -S . -B build -DCLINK_WITH_MQTT=ON
cmake --build build --parallel 10
The mosquitto 2.1 umbrella header pulls in <cjson/cJSON.h>; on Homebrew cJSON is a separate keg, so CMake also discovers that header path best-effort and adds it to the include path. Only the header is needed; libmosquitto links cJSON itself.
Factories¶
| Factory name | Direction | Record type |
|---|---|---|
mqtt_source |
Source | std::string |
mqtt_sink |
Sink | std::string |
Configuration¶
Both factories share the connection options. topic is required on both sides; passing no topic causes the operator constructor to throw.
Shared connection options (source and sink)¶
| Option | Required | Default | Description |
|---|---|---|---|
host |
No | localhost |
Broker hostname. |
port |
No | 1883 |
Broker port. |
keepalive |
No | 60 |
Keepalive in seconds (clamped to a minimum of 5). |
username |
No | (empty) | Username for broker authentication; empty means no auth. |
password |
No | (empty) | Password for broker authentication. |
client_id |
No | clink-mqtt-source / clink-mqtt-sink |
Client id base; the subtask index is appended so parallel subtasks hold distinct ids. |
tls |
No | false |
true enables TLS encryption (libmosquitto built-in OpenSSL). |
tls_ca |
No | (empty) | CA certificate file (PEM) used to verify the server. |
tls_capath |
No | (empty) | Directory of CA certificates. |
tls_cert |
No | (empty) | Client certificate file for mutual TLS. |
tls_key |
No | (empty) | Client private key file for mutual TLS. |
tls_verify |
No | true |
false skips server-certificate and hostname verification (self-signed dev only). |
mqtt_sink options¶
| Option | Required | Default | Description |
|---|---|---|---|
topic |
Yes | (none) | Topic to publish to. |
qos |
No | 1 |
Publish QoS: 0 fire-and-forget, 1 at-least-once to broker, 2 once to broker. Must be 0, 1 or 2. |
retain |
No | false |
Set the MQTT retain flag on each published message. |
clean_session |
No | true |
false keeps a persistent broker session for the sink's client id. |
batch_records |
No | 1000 |
Flush after this many buffered records (0 is treated as 1). |
max_bytes |
No | 0 |
Flush after this many buffered payload bytes; 0 disables the byte threshold. |
linger_ms |
No | 0 |
Flush a partial batch this many milliseconds old; 0 disables linger. |
ack_timeout_ms |
No | 30000 |
Bound on the per-flush wait for QoS>0 acknowledgements. |
mqtt_source options¶
| Option | Required | Default | Description |
|---|---|---|---|
topic |
Yes | (none) | Topic filter to subscribe to. |
qos |
No | 1 |
Subscription QoS. Must be 0, 1 or 2. |
shared_group |
No | (empty) | Non-empty subscribes to $share/<group>/<topic> so the broker load-balances across parallel subtasks. Empty means only subtask 0 is active. |
include_topic |
No | false |
true emits each message as {"topic":...,"payload":...} JSON; otherwise the payload is emitted verbatim. |
block_ms |
No | 500 |
libmosquitto loop wait in milliseconds (bounds cancellation latency); a value of 0 or less is reset to 500. |
The source forces clean_session=false (a persistent broker session is required for its recovery model and is not a user-tunable parameter).
Authentication-related options are username and password (broker auth) and the tls_* group (TLS, including tls_cert/tls_key for mutual TLS and tls_verify).
All options are read from the BuildContext parameters in impls/mqtt/src/register_factories.cpp. The defaults are mirrored in the MqttSinkOptions and MqttSourceOptions structs and the ConnectOptions struct in impls/mqtt/include/clink/mqtt/.
SQL usage¶
Not exposed through the SQL frontend; use the programmatic API. There is no connector='mqtt' mapping in src/sql/physical_plan.cpp.
Example¶
Construct the sink and source directly with their option structs (the pattern used by the connector's tests in impls/mqtt/tests/test_mqtt_live.cpp). The example publishes a few records and reads them back from the same topic.
#include "clink/mqtt/mqtt_sink.hpp"
#include "clink/mqtt/mqtt_source.hpp"
using clink::mqtt::MqttSink;
using clink::mqtt::MqttSinkOptions;
using clink::mqtt::MqttSource;
using clink::mqtt::MqttSourceOptions;
// Sink: publish records to a topic at QoS 1 (at-least-once to the broker).
MqttSinkOptions sink_opts;
sink_opts.conn.host = "localhost";
sink_opts.conn.port = 1883;
sink_opts.topic = "clink/demo";
sink_opts.qos = 1;
MqttSink sink(std::move(sink_opts));
sink.open();
clink::Batch<std::string> batch;
batch.emplace(R"({"i":0})");
batch.emplace(R"({"i":1})");
sink.on_data(batch);
sink.flush(); // waits until every QoS>0 message is acked by the broker
sink.close();
// Source: subscribe to the same topic and emit each message verbatim.
MqttSourceOptions src_opts;
src_opts.conn.host = "localhost";
src_opts.conn.port = 1883;
src_opts.topic = "clink/demo";
src_opts.qos = 1;
MqttSource src(std::move(src_opts));
src.open();
// src.produce(emitter) drives one libmosquitto loop iteration per call,
// emitting any messages received during that iteration.
Via the registry, the factories are reached as mqtt_sink and mqtt_source on the string channel, with host, port, topic, qos and the other keys above supplied as BuildContext params (see impls/mqtt/src/register_factories.cpp and impls/mqtt/tests/test_factory_registration.cpp).
Delivery semantics¶
Source. MQTT has no replayable offset or cursor, so the source persists no checkpoint offset (snapshot_offset/restore_offset are the no-op base defaults). Durability comes from the broker session: the source always runs with a stable per-subtask client id and a persistent session (clean_session is forced off by the factory), so the broker retains the subscription and queues QoS 1/2 messages for this client while it is disconnected, redelivering un-acked messages on reconnect. This gives at-least-once across a clean reconnect. libmosquitto auto-acks a QoS 1/2 message when the on_message callback returns, so a process crash after the callback but before the next checkpoint loses those in-flight messages, making delivery best-effort across a hard crash. A downstream idempotent or keyed-dedup consumer is the robust path.
Sink. End-to-end the sink is at-least-once regardless of QoS. flush() (called on the batch thresholds, on linger, and on every checkpoint barrier) waits until every buffered message is acknowledged for QoS>0, so everything buffered is durable in the broker by the barrier. A flush that fails after some messages have reached the broker is replayed from the last checkpoint, re-publishing those messages, so duplicates are possible and must be absorbed downstream. QoS 0 is fire-and-forget with no broker ack and is lossy on any drop; QoS 1 awaits PUBACK; QoS 2 awaits PUBCOMP. On a flush error the sink drops and rebuilds its connection on re-open to avoid a desynced in-flight window.
Limitations¶
- String channel only: payloads are opaque
std::stringvalues, published and emitted verbatim. There is no Arrow or Parquet encoding and no typed (for example int64) channel. - The sink publishes to a single fixed
topic; there is no per-record topic routing. include_topic=trueassumes UTF-8 text payloads; for binary payloads use the default verbatim mode.- Source parallelism: by default (no
shared_group) only subtask 0 subscribes and emits, with other subtasks dormant, so it is best run at parallelism 1. Spreading load across subtasks requiresshared_group, which in turn requires a broker that supports shared subscriptions ($share/...). - No exactly-once end-to-end: the sink is at-least-once at any QoS, and the source is at-least-once across clean reconnects but best-effort across a hard crash (no replayable offset).
- Retained-message clearing and other broker housekeeping are the operator's responsibility, not the connector's.
Testing¶
A live integration test, impls/mqtt/tests/test_mqtt_live.cpp, is skipped unless an environment variable points it at a real broker:
CLINK_MQTT_TEST_URL(for examplemqtt://localhost:1883) enables the plaintext round-trip tests: a sink-to-source round-trip delivering every record verbatim, theinclude_topicJSON envelope, and retained-message delivery to a fresh subscriber.CLINK_MQTT_TLS_TEST_URL(ahost:portpointing at a TLS-enabled broker) enables the TLS round-trip test.CLINK_MQTT_TLS_CAmay supply the CA certificate (PEM); without it, verification is skipped (self-signed dev cert: encrypt but do not authenticate).
There is no dedicated MQTT broker Docker image wired into the test harness; point the environment variable at a broker you run yourself (for example a local eclipse-mosquitto container).
In-process unit tests run without a broker: test_factory_registration.cpp checks that mqtt_source and mqtt_sink are registered on the string channel, and test_mqtt_sink_logic.cpp / test_mqtt_source_logic.cpp exercise the sink and source logic. The test executable is clink_mqtt_tests, labelled mqtt:
ctest --test-dir build -L mqtt