Skip to main content

laminar_connectors/rabbitmq/
mod.rs

1//! RabbitMQ source and sink over AMQP 0-9-1 (the `lapin` client).
2//!
3//! The source consumes one classic or quorum queue with manual acknowledgements and releases a
4//! message only after the checkpoint containing it is durable (at-least-once). The sink publishes
5//! to one exchange with publisher confirms and, by default, mandatory routing. Routing, TTL,
6//! dead-lettering, delivery limits, priorities, and queue limits stay broker-native: exchanges and
7//! bindings route, and policies or declaration arguments own the rest.
8
9pub mod config;
10mod connection;
11pub mod metadata;
12mod metrics;
13pub mod sink;
14pub mod source;
15
16pub use sink::RabbitMqSink;
17pub use source::RabbitMqSource;
18
19use std::sync::Arc;
20
21use arrow_schema::Schema;
22
23use crate::config::{ConfigKeySpec, ConnectorInfo};
24use crate::registry::ConnectorRegistry;
25
26/// Registers the RabbitMQ source connector.
27///
28/// # Errors
29///
30/// Returns an error if the source is already registered or the registry is frozen.
31pub fn register_rabbitmq_source(
32    registry: &ConnectorRegistry,
33) -> Result<(), crate::error::ConnectorError> {
34    let info = ConnectorInfo {
35        schema_capabilities: crate::schema::resolution::SchemaCapabilities {
36            built_in_formats: vec![
37                "raw".into(),
38                "bytes".into(),
39                #[cfg(feature = "fpml")]
40                "fpml".into(),
41            ],
42            ..crate::schema::resolution::SchemaCapabilities::declared(false)
43        },
44        name: "rabbitmq".to_string(),
45        display_name: "RabbitMQ Source".to_string(),
46        version: env!("CARGO_PKG_VERSION").to_string(),
47        is_source: true,
48        is_sink: false,
49        config_keys: source_config_keys(),
50    };
51    registry.register_source(
52        "rabbitmq",
53        info,
54        Arc::new(|registry| {
55            Ok(Box::new(RabbitMqSource::new(
56                Arc::new(Schema::empty()),
57                registry.map(Arc::as_ref),
58            )))
59        }),
60    )
61}
62
63/// Registers the RabbitMQ sink connector.
64///
65/// # Errors
66///
67/// Returns an error if the sink is already registered or the registry is frozen.
68pub fn register_rabbitmq_sink(
69    registry: &ConnectorRegistry,
70) -> Result<(), crate::error::ConnectorError> {
71    let info = ConnectorInfo {
72        schema_capabilities: crate::schema::resolution::SchemaCapabilities::declared(true),
73        name: "rabbitmq".to_string(),
74        display_name: "RabbitMQ Sink".to_string(),
75        version: env!("CARGO_PKG_VERSION").to_string(),
76        is_source: false,
77        is_sink: true,
78        config_keys: sink_config_keys(),
79    };
80    registry.register_sink(
81        "rabbitmq",
82        info,
83        Arc::new(|_config, registry| {
84            Ok(Box::new(RabbitMqSink::new(
85                Arc::new(Schema::empty()),
86                registry.map(Arc::as_ref),
87            )))
88        }),
89    )
90}
91
92fn connection_keys() -> Vec<ConfigKeySpec> {
93    use ConfigKeySpec as K;
94    vec![
95        K::required(
96            "url",
97            "AMQP URI(s), comma-separated failover endpoints of one broker or cluster: \
98             amqp[s]://[user:pass@]host[:port]/vhost[?heartbeat=&frame_max=&channel_max=\
99             &connection_timeout=&auth_mechanism=]",
100        ),
101        K::optional("username", "SASL user (instead of URI userinfo)", ""),
102        K::optional(
103            "password",
104            "SASL password; use a ${ENV} secret reference",
105            "",
106        ),
107        K::optional(
108            "connection.name",
109            "Client-provided connection name shown by the broker",
110            "",
111        ),
112        K::optional(
113            "connect.timeout.ms",
114            "Per-endpoint connect bound (or the URI connection_timeout, not both)",
115            "10000",
116        ),
117        K::optional(
118            "tls.ca.location",
119            "PEM CA bundle trusted in addition to the Web PKI roots (amqps)",
120            "",
121        ),
122        K::optional(
123            "tls.cert.location",
124            "PEM client certificate for mutual TLS (amqps)",
125            "",
126        ),
127        K::optional(
128            "tls.key.location",
129            "PKCS#8 PEM client key for mutual TLS (amqps)",
130            "",
131        ),
132    ]
133}
134
135fn source_config_keys() -> Vec<ConfigKeySpec> {
136    use ConfigKeySpec as K;
137    let mut keys = connection_keys();
138    keys.extend([
139        K::required("queue", "Classic or quorum queue to consume"),
140        K::optional(
141            "queue.declare",
142            "Declare the queue idempotently instead of requiring it to exist",
143            "false",
144        ),
145        K::optional("queue.durable", "Durable flag when declaring", "true"),
146        K::optional(
147            "queue.type",
148            "x-queue-type when declaring: classic | quorum",
149            "",
150        ),
151        K::optional(
152            "queue.arguments",
153            "JSON object of declaration-only x-arguments; prefer broker policies for mutable \
154             settings (TTL, dead-lettering, limits)",
155            "",
156        ),
157        K::optional(
158            "queue.bind.exchange",
159            "Exchange to bind the queue to at startup",
160            "",
161        ),
162        K::optional(
163            "queue.bind.routing.keys",
164            "Comma-separated binding keys",
165            "",
166        ),
167        K::optional(
168            "queue.bind.arguments",
169            "JSON object of binding arguments",
170            "",
171        ),
172        K::optional(
173            "prefetch.count",
174            "basic.qos prefetch: most unacknowledged deliveries; size it for one checkpoint \
175             interval of traffic",
176            "1000",
177        ),
178        K::optional(
179            "consumer.tag",
180            "Consumer tag (broker-generated when unset)",
181            "",
182        ),
183        K::optional("consumer.exclusive", "Exclusive consumer", "false"),
184        K::optional(
185            "consumer.arguments",
186            "JSON object of basic.consume arguments",
187            "",
188        ),
189        K::optional(
190            "max.message.bytes",
191            "Largest accepted message body",
192            "16777216",
193        ),
194        K::optional(
195            "max.batch.bytes",
196            "Payload bytes decoded per batch",
197            "33554432",
198        ),
199        K::optional(
200            "on.decode.error",
201            "fail | reject (basic.reject without requeue: dead-lettered by the queue's DLX, \
202             otherwise discarded)",
203            "fail",
204        ),
205        K::optional(
206            "metadata.columns",
207            "Comma-separated delivery metadata columns (exchange, routing_key, redelivered, \
208             delivery_count, message_id, correlation_id, timestamp, content_type, \
209             content_encoding, delivery_mode, priority, expiration, reply_to, type, user_id, \
210             app_id, headers)",
211            "",
212        ),
213    ]);
214    #[cfg(feature = "fpml")]
215    keys.extend(crate::fpml::message::config_keys());
216    keys
217}
218
219fn sink_config_keys() -> Vec<ConfigKeySpec> {
220    use ConfigKeySpec as K;
221    let mut keys = connection_keys();
222    keys.extend([
223        K::optional(
224            "exchange",
225            "Target exchange; empty is the default exchange",
226            "",
227        ),
228        K::optional(
229            "exchange.declare",
230            "Declare the exchange idempotently",
231            "false",
232        ),
233        K::optional("exchange.type", "Exchange type when declaring", ""),
234        K::optional("exchange.durable", "Durable flag when declaring", "true"),
235        K::optional("routing.key", "Literal routing key", ""),
236        K::optional(
237            "routing.key.column",
238            "Utf8 column holding each message's routing key",
239            "",
240        ),
241        K::optional(
242            "mandatory",
243            "Return unroutable messages; a return fails the write",
244            "true",
245        ),
246        K::optional("delivery.mode", "persistent | transient", "persistent"),
247        K::optional(
248            "content.type",
249            "content-type property (defaults by format)",
250            "",
251        ),
252        K::optional("content.encoding", "content-encoding property", ""),
253        K::optional(
254            "message.id.column",
255            "Utf8 column for the message-id property",
256            "",
257        ),
258        K::optional(
259            "correlation.id.column",
260            "Utf8 column for the correlation-id property",
261            "",
262        ),
263        K::optional("priority", "Literal priority property (0-255)", ""),
264        K::optional(
265            "priority.column",
266            "Integer column for the priority property",
267            "",
268        ),
269        K::optional(
270            "expiration.ms",
271            "Per-message TTL property in milliseconds",
272            "",
273        ),
274        K::optional("reply.to", "reply-to property", ""),
275        K::optional("type", "type property", ""),
276        K::optional("app.id", "app-id property", ""),
277        K::optional(
278            "header.columns",
279            "Comma-separated columns written as typed headers",
280            "",
281        ),
282        K::optional("max.pending", "Most publishes awaiting a confirm", "4096"),
283        K::optional(
284            "max.pending.bytes",
285            "Most payload bytes awaiting a confirm",
286            "67108864",
287        ),
288        K::optional("confirm.timeout.ms", "Bound for awaiting confirms", "30000"),
289    ]);
290    keys
291}