Skip to main content

laminar_connectors/ibmmq/
mod.rs

1//! IBM MQ source and sink over MQI, using IBM's client library loaded at run time.
2//!
3//! The source gets messages under syncpoint and commits each checkpoint's unit of work only after
4//! the checkpoint is durable (at-least-once). Poison messages follow the queue's own `BOTHRESH` and
5//! `BOQNAME`, moved atomically within the same unit of work. The sink puts under syncpoint and
6//! commits each unit of work before a write returns. Blocking MQI calls run on one owned thread per
7//! connector generation, never on the async runtime.
8
9pub mod config;
10mod library;
11pub mod metadata;
12mod metrics;
13mod session;
14pub mod sink;
15pub mod source;
16mod worker;
17
18pub use sink::IbmMqSink;
19pub use source::IbmMqSource;
20
21use std::sync::Arc;
22
23use arrow_schema::Schema;
24
25use crate::config::{ConfigKeySpec, ConnectorInfo};
26use crate::registry::ConnectorRegistry;
27
28/// Registers the IBM MQ source connector.
29///
30/// # Errors
31///
32/// Returns an error if the source is already registered or the registry is frozen.
33pub fn register_ibmmq_source(
34    registry: &ConnectorRegistry,
35) -> Result<(), crate::error::ConnectorError> {
36    let info = ConnectorInfo {
37        schema_capabilities: crate::schema::resolution::SchemaCapabilities {
38            built_in_formats: vec![
39                "raw".into(),
40                "bytes".into(),
41                #[cfg(feature = "fpml")]
42                "fpml".into(),
43            ],
44            ..crate::schema::resolution::SchemaCapabilities::declared(false)
45        },
46        name: "ibmmq".to_string(),
47        display_name: "IBM MQ Source".to_string(),
48        version: env!("CARGO_PKG_VERSION").to_string(),
49        is_source: true,
50        is_sink: false,
51        config_keys: source_config_keys(),
52    };
53    registry.register_source(
54        "ibmmq",
55        info,
56        Arc::new(|registry| {
57            Ok(Box::new(IbmMqSource::new(
58                Arc::new(Schema::empty()),
59                registry.map(Arc::as_ref),
60            )))
61        }),
62    )
63}
64
65/// Registers the IBM MQ sink connector.
66///
67/// # Errors
68///
69/// Returns an error if the sink is already registered or the registry is frozen.
70pub fn register_ibmmq_sink(
71    registry: &ConnectorRegistry,
72) -> Result<(), crate::error::ConnectorError> {
73    let info = ConnectorInfo {
74        schema_capabilities: crate::schema::resolution::SchemaCapabilities::declared(true),
75        name: "ibmmq".to_string(),
76        display_name: "IBM MQ Sink".to_string(),
77        version: env!("CARGO_PKG_VERSION").to_string(),
78        is_source: false,
79        is_sink: true,
80        config_keys: sink_config_keys(),
81    };
82    registry.register_sink(
83        "ibmmq",
84        info,
85        Arc::new(|_config, registry| {
86            Ok(Box::new(IbmMqSink::new(
87                Arc::new(Schema::empty()),
88                registry.map(Arc::as_ref),
89            )))
90        }),
91    )
92}
93
94fn connection_keys() -> Vec<ConfigKeySpec> {
95    use ConfigKeySpec as K;
96    vec![
97        K::optional("queue.manager", "Queue manager name (MQCONNX)", ""),
98        K::optional(
99            "channel",
100            "Server-connection channel (MQCD ChannelName)",
101            "",
102        ),
103        K::optional(
104            "connection.name",
105            "CONNAME list host(port)[,host(port)] (MQCD ConnectionName)",
106            "",
107        ),
108        K::optional(
109            "ccdt.url",
110            "Client channel definition table URL (MQCNO CCDTUrl)",
111            "",
112        ),
113        K::optional("username", "MQCSP user", ""),
114        K::optional(
115            "password",
116            "MQCSP password; use a ${ENV} secret reference",
117            "",
118        ),
119        K::optional("application.name", "MQCNO application name", "laminardb"),
120        K::optional(
121            "tls.cipher.spec",
122            "SSLCIPH CipherSpec, e.g. ANY_TLS13_OR_HIGHER",
123            "",
124        ),
125        K::optional(
126            "tls.key.repository",
127            "Key repository (MQSCO KeyRepository)",
128            "",
129        ),
130        K::optional(
131            "tls.key.repository.password",
132            "Key repository password (secret)",
133            "",
134        ),
135        K::optional("tls.certificate.label", "Client certificate label", ""),
136        K::optional(
137            "tls.peer.name",
138            "SSLPEER filter the server certificate must match",
139            "",
140        ),
141        K::optional(
142            "reconnect",
143            "disabled | qmgr | any (MQCNO reconnect; the source supports disabled only)",
144            "disabled",
145        ),
146        K::optional("client.library", "Path to the IBM MQ client libmqm_r", ""),
147        K::optional(
148            "operation.timeout.ms",
149            "How long one MQI operation may take before the generation is abandoned",
150            "30000",
151        ),
152    ]
153}
154
155fn source_config_keys() -> Vec<ConfigKeySpec> {
156    use ConfigKeySpec as K;
157    let mut keys = connection_keys();
158    keys.extend([
159        K::required("queue", "Queue to get from under syncpoint"),
160        K::optional(
161            "wait.interval.ms",
162            "MQGMO WaitInterval for the first message of a poll",
163            "100",
164        ),
165        K::optional(
166            "max.message.bytes",
167            "Receive buffer; longer messages are never truncated",
168            "4194304",
169        ),
170        K::optional("max.batch.bytes", "Payload bytes per poll", "16777216"),
171        K::optional(
172            "max.uow.messages",
173            "Messages per unit of work before intake waits for a checkpoint",
174            "10000",
175        ),
176        K::optional(
177            "max.uow.bytes",
178            "Payload bytes per unit of work before intake waits for a checkpoint",
179            "67108864",
180        ),
181        K::optional(
182            "on.decode.error",
183            "fail | backout-queue (move to BOQNAME in the same unit of work)",
184            "fail",
185        ),
186        K::optional(
187            "convert",
188            "MQGMO_CONVERT to UTF-8 for convertible formats",
189            "false",
190        ),
191        K::optional(
192            "metadata.columns",
193            "Comma-separated MQMD/property columns (msg_id, correl_id, group_id, msg_seq_number, \
194             msg_flags, msg_type, format, ccsid, encoding, persistence, priority, expiry, \
195             backout_count, reply_to_queue, reply_to_queue_manager, put_timestamp, \
196             put_application_name, user_identifier, properties)",
197            "",
198        ),
199    ]);
200    #[cfg(feature = "fpml")]
201    keys.extend(crate::fpml::message::config_keys());
202    keys
203}
204
205fn sink_config_keys() -> Vec<ConfigKeySpec> {
206    use ConfigKeySpec as K;
207    let mut keys = connection_keys();
208    keys.extend([
209        K::required("queue", "Target queue"),
210        K::optional(
211            "target.queue.manager",
212            "MQOD ObjectQMgrName for a remote queue",
213            "",
214        ),
215        K::optional(
216            "persistence",
217            "persistent | not-persistent | as-queue-definition",
218            "persistent",
219        ),
220        K::optional("priority", "Literal MQMD Priority (0-9)", ""),
221        K::optional("priority.column", "Integer column for MQMD Priority", ""),
222        K::optional("expiry", "MQMD Expiry in tenths of a second", ""),
223        K::optional(
224            "message.id.column",
225            "Binary/Utf8 column for MsgId (else a new id)",
226            "",
227        ),
228        K::optional(
229            "correlation.id.column",
230            "Binary/Utf8 column for CorrelId",
231            "",
232        ),
233        K::optional("reply.to.queue", "MQMD ReplyToQ", ""),
234        K::optional("reply.to.queue.manager", "MQMD ReplyToQMgr", ""),
235        K::optional(
236            "message.format",
237            "MQMD Format override (defaults by codec)",
238            "",
239        ),
240        K::optional("ccsid", "MQMD CodedCharSetId of text payloads", "1208"),
241        K::optional(
242            "property.columns",
243            "Columns written as typed message properties",
244            "",
245        ),
246        K::optional(
247            "max.uow.messages",
248            "Messages per committed unit of work",
249            "1000",
250        ),
251    ]);
252    keys
253}