1use std::sync::Arc;
25
26use rainier_broadcast::kafka::{KafkaBroadcaster, KafkaRelay};
27use rainier_cache::LockManager;
28use rainier_config::Config;
29use rainier_drivers::kafka::{
30 KafkaClient, KafkaConnector, KafkaCredentials, KafkaRecord, SaslMechanism,
31};
32use rainier_queue::KafkaQueue;
33use rainier_support::{Error, Result};
34use serde::Serialize;
35
36use crate::keys;
37
38pub fn connector(config: &Config) -> Result<Option<KafkaConnector>> {
43 let brokers = config.get(keys::KAFKA_BROKERS).unwrap_or_default();
44
45 if brokers.trim().is_empty() {
46 return Ok(None);
47 }
48
49 let mut connector = KafkaConnector::parse(&brokers)
50 .with_client_id(config.get(keys::APP_NAME).unwrap_or_else(|| "rainier".into()));
51
52 if config.get(keys::KAFKA_TLS).unwrap_or(false) {
53 connector = connector.with_tls();
54 }
55
56 let username = config.get(keys::KAFKA_USERNAME).unwrap_or_default();
57 if !username.is_empty() {
58 let mechanism = mechanism(&config.get(keys::KAFKA_SASL_MECHANISM).unwrap_or_default())?;
59 let password = config.get(keys::KAFKA_PASSWORD).unwrap_or_default();
60
61 connector =
62 connector.with_credentials(KafkaCredentials::new(mechanism, username, password));
63 }
64
65 Ok(Some(connector))
66}
67
68pub async fn client(config: &Config) -> Result<Arc<KafkaClient>> {
76 let connector = connector(config)?.ok_or_else(|| {
77 Error::internal(
78 "Kafka is not configured. Set `KAFKA_BROKERS` to a comma-separated list of brokers.",
79 )
80 })?;
81
82 Ok(Arc::new(KafkaClient::connect(&connector).await?))
83}
84
85pub fn broadcaster(config: &Config, client: Arc<KafkaClient>) -> KafkaBroadcaster {
87 let topic = topic(config, keys::KAFKA_BROADCAST_TOPIC, "broadcasts");
88
89 KafkaBroadcaster::new(client).on_topic(topic)
90}
91
92pub fn relay(config: &Config, client: Arc<KafkaClient>) -> KafkaRelay {
94 KafkaRelay::new(client, topic(config, keys::KAFKA_BROADCAST_TOPIC, "broadcasts"))
95}
96
97pub fn queue(config: &Config, client: Arc<KafkaClient>, locks: LockManager) -> Result<KafkaQueue> {
104 let queue = KafkaQueue::new(client, locks)?
105 .in_group(config.get(keys::KAFKA_GROUP).unwrap_or_else(|| "rainier".into()));
106
107 Ok(match config.get(keys::KAFKA_TOPIC_PREFIX) {
108 Some(prefix) if !prefix.is_empty() => queue.with_topic_prefix(prefix),
109 _ => queue,
110 })
111}
112
113fn record_for_event<E: Serialize>(name: &str, event: &E, key: String) -> Result<KafkaRecord> {
120 let body = serde_json::to_vec(event)
121 .map_err(|e| Error::internal(format!("an event published to Kafka must serialise: {e}")))?;
122
123 Ok(KafkaRecord::new(body).keyed(key).header("event", name.to_string()))
124}
125
126pub fn publish_events<E>(
147 events: &rainier_events::Dispatcher,
148 client: Arc<KafkaClient>,
149 topic: impl Into<String>,
150 key_of: impl Fn(&E) -> String + Send + Sync + 'static,
151) where
152 E: rainier_events::Event + Serialize,
153{
154 let topic = topic.into();
155 let name = E::event_name();
156
157 events.listen::<E, _>(move |event: Arc<E>| {
158 let client = Arc::clone(&client);
159 let topic = topic.clone();
160 let key = key_of(&event);
161
162 Box::pin(async move {
163 match record_for_event(name, &*event, key) {
164 Ok(record) => {
165 if let Err(e) = client.produce(&topic, vec![record]).await {
166 tracing::error!(topic, event = name, error = %e, "could not publish an event");
167 }
168 }
169 Err(e) => tracing::error!(topic, event = name, error = %e, "could not serialise an event"),
170 }
171 Ok(())
172 })
173 });
174}
175
176fn topic(config: &Config, key: rainier_config::Key<String>, fallback: &str) -> String {
178 let prefix = config.get(keys::KAFKA_TOPIC_PREFIX).unwrap_or_default();
179
180 let name = config
181 .get(key)
182 .filter(|configured| !configured.is_empty())
183 .unwrap_or_else(|| fallback.to_string());
184
185 format!("{prefix}{name}")
186}
187
188fn mechanism(name: &str) -> Result<SaslMechanism> {
194 match name.trim().to_ascii_lowercase().replace('_', "-").as_str() {
195 "" | "plain" => Ok(SaslMechanism::Plain),
196 "scram-sha-256" => Ok(SaslMechanism::ScramSha256),
197 "scram-sha-512" => Ok(SaslMechanism::ScramSha512),
198 other => Err(Error::internal(format!(
199 "`KAFKA_SASL_MECHANISM={other}` is not a mechanism. Use `plain`, `scram-sha-256` or \
200 `scram-sha-512`."
201 ))),
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208 use rainier_config::Env;
209
210 fn config(env: &str) -> Config {
211 let config = Config::new();
212 let env = Env::parse(env).isolated();
213
214 config.set(keys::KAFKA_BROKERS, env.string("KAFKA_BROKERS", "")).unwrap();
215 config.set(keys::KAFKA_TLS, env.bool("KAFKA_TLS", false)).unwrap();
216 config.set(keys::KAFKA_USERNAME, env.string("KAFKA_USERNAME", "")).unwrap();
217 config.set(keys::KAFKA_PASSWORD, env.string("KAFKA_PASSWORD", "")).unwrap();
218 config
219 .set(keys::KAFKA_SASL_MECHANISM, env.string("KAFKA_SASL_MECHANISM", "plain"))
220 .unwrap();
221 config.set(keys::KAFKA_TOPIC_PREFIX, env.string("KAFKA_TOPIC_PREFIX", "")).unwrap();
222 config
223 .set(keys::KAFKA_BROADCAST_TOPIC, env.string("KAFKA_BROADCAST_TOPIC", "broadcasts"))
224 .unwrap();
225 config.set(keys::APP_NAME, env.string("APP_NAME", "rainier")).unwrap();
226 config
227 }
228
229 #[test]
230 fn no_brokers_is_not_an_error() {
231 assert!(connector(&config("APP_ENV=local")).unwrap().is_none());
233 }
234
235 #[test]
236 fn the_broker_list_becomes_a_connector() {
237 let connector = connector(&config("KAFKA_BROKERS=kafka-1:9092,kafka-2\nAPP_NAME=checkout"))
238 .unwrap()
239 .expect("brokers were configured");
240
241 assert_eq!(connector.brokers(), ["kafka-1:9092", "kafka-2:9092"]);
242 assert!(!connector.is_tls());
243 assert!(connector.credentials().is_none());
244 }
245
246 #[test]
247 fn credentials_are_only_used_when_a_username_is_set() {
248 let with = connector(&config(
249 "KAFKA_BROKERS=kafka:9092\nKAFKA_USERNAME=svc\nKAFKA_PASSWORD=secret\n\
250 KAFKA_SASL_MECHANISM=scram-sha-512",
251 ))
252 .unwrap()
253 .unwrap();
254
255 let credentials = with.credentials().expect("a username was configured");
256 assert_eq!(credentials.mechanism(), SaslMechanism::ScramSha512);
257 assert_eq!(credentials.username(), "svc");
258
259 let without =
260 connector(&config("KAFKA_BROKERS=kafka:9092\nKAFKA_PASSWORD=secret")).unwrap().unwrap();
261 assert!(without.credentials().is_none(), "a password alone authenticates nobody");
262 }
263
264 #[test]
265 fn a_misspelled_mechanism_stops_the_boot() {
266 let error = connector(&config(
269 "KAFKA_BROKERS=kafka:9092\nKAFKA_USERNAME=svc\nKAFKA_SASL_MECHANISM=scram-512",
270 ))
271 .unwrap_err();
272
273 assert!(error.message().contains("scram-sha-512"), "{}", error.message());
274 }
275
276 #[test]
277 fn mechanism_names_are_forgiving_about_case_and_underscores() {
278 assert_eq!(mechanism("SCRAM_SHA_256").unwrap(), SaslMechanism::ScramSha256);
279 assert_eq!(mechanism(" plain ").unwrap(), SaslMechanism::Plain);
280 assert_eq!(mechanism("").unwrap(), SaslMechanism::Plain);
281 }
282
283 #[test]
284 fn the_prefix_reaches_the_topic_names() {
285 let config = config("KAFKA_BROKERS=kafka:9092\nKAFKA_TOPIC_PREFIX=checkout.");
286
287 assert_eq!(
288 topic(&config, keys::KAFKA_BROADCAST_TOPIC, "broadcasts"),
289 "checkout.broadcasts"
290 );
291 }
292
293 #[test]
294 fn a_named_broadcast_topic_wins_over_the_default() {
295 let config = config("KAFKA_BROKERS=kafka:9092\nKAFKA_BROADCAST_TOPIC=events");
296
297 assert_eq!(topic(&config, keys::KAFKA_BROADCAST_TOPIC, "broadcasts"), "events");
298 }
299
300 #[test]
301 fn an_event_becomes_a_keyed_record_carrying_its_name() {
302 #[derive(serde::Serialize)]
303 struct OrderShipped {
304 order_id: u64,
305 }
306
307 let record =
308 record_for_event("OrderShipped", &OrderShipped { order_id: 7 }, "7".into()).unwrap();
309
310 assert_eq!(record.key.as_deref(), Some(&b"7"[..]));
311 assert_eq!(record.headers.get("event").map(Vec::as_slice), Some(&b"OrderShipped"[..]));
312
313 let body: serde_json::Value = serde_json::from_slice(&record.value).unwrap();
316 assert_eq!(body["order_id"], 7);
317 }
318
319 #[test]
320 fn tls_is_off_unless_it_is_asked_for() {
321 let plain = connector(&config("KAFKA_BROKERS=kafka:9092")).unwrap().unwrap();
322 assert!(!plain.is_tls());
323
324 let secured =
325 connector(&config("KAFKA_BROKERS=kafka:9092\nKAFKA_TLS=true")).unwrap().unwrap();
326 assert!(secured.is_tls());
327 }
328}