Skip to main content

llingr_kafka/
lib.rs

1// SPDX-FileCopyrightText: Copyright (c) 2026 The llingr-rs-kafka Authors
2// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Llingr-Commercial
3
4//! The llingr concurrent message processing engine with a Kafka broker,
5//! batteries included.
6//!
7//! One pre-baked crate: the `llingr-demux` Go engine and its franz-go broker
8//! layer compile into your binary as a static c-archive during `cargo build`.
9//! The pure-Go client speaks Kafka and every Kafka-compatible broker,
10//! RedPanda and Amazon MSK included. Per-key concurrent processing without
11//! head-of-line blocking, contiguous offset commit guarantees, engine logs
12//! on the process-global [`log`] facade under the target [`LOG_TARGET`],
13//! and baked-in Prometheus metrics activated at runtime with [`Metrics`].
14//! There are no cargo features and no broker selection.
15//!
16//! # Quick start
17//!
18//! ```rust,no_run
19//! use llingr_kafka::{AutoOffsetReset, Builder, Message, Metrics, Options, Traits};
20//! use llingr_kafka::{DeadLetterHandler, ProcessHandler};
21//!
22//! struct MyProcessor;
23//!
24//! impl ProcessHandler for MyProcessor {
25//!     fn process(&self, msg: &Message) -> Result<Traits, Box<dyn std::error::Error>> {
26//!         // Keys are frequently PII: log coordinates, never key contents.
27//!         println!("partition={} offset={}", msg.partition(), msg.offset());
28//!         Ok(Traits::none())
29//!     }
30//! }
31//!
32//! // Required alongside the processor: failed messages need somewhere to go.
33//! // Logging is the bare minimum; a real DLQ topic or table is recommended.
34//! struct MyDeadLetters;
35//!
36//! impl DeadLetterHandler for MyDeadLetters {
37//!     fn handle(&self, msg: &Message, error_msg: &str) -> Result<(), Box<dyn std::error::Error>> {
38//!         eprintln!("dead-letter partition={} offset={} reason={}", msg.partition(), msg.offset(), error_msg);
39//!         Ok(())
40//!     }
41//! }
42//!
43//! fn main() -> Result<(), Box<dyn std::error::Error>> {
44//!     let engine = Builder::new("orders", MyProcessor, MyDeadLetters)
45//!         .brokers("broker1:9092,broker2:9092")
46//!         .consumer_group("orders-svc")
47//!         .options(Options::new().auto_offset_reset(AutoOffsetReset::Earliest))
48//!         .metrics(Metrics::serve("0.0.0.0:9464", "/metrics"))
49//!         .build()?;
50//!
51//!     let stop = engine.stopper(); // Send closure for signal-watcher threads
52//!     # drop(stop);
53//!     engine.run()?; // BLOCKS until stop() or an emergency shutdown
54//!     Ok(())
55//! }
56//! ```
57//!
58//! # Operational constraints
59//!
60//! Hosting the Go runtime in-process has hard rules. Read these before
61//! shipping.
62//!
63//! - **One engine per process.** The Go runtime is a process-global
64//!   singleton. A second [`Builder::build`] returns a clean error.
65//!
66//! - **The Go runtime initialises at process start.** The statically linked
67//!   engine registers a loader initialiser: the Go runtime comes up before
68//!   `main()` runs, not at `build()`, bringing its signal handlers for
69//!   SIGSEGV, SIGBUS, SIGFPE, SIGPROF, and SIGURG with it. The engine never
70//!   claims SIGINT or SIGTERM, so registering handlers for those works at
71//!   any point; `signal-hook`, for example, chains to any pre-existing
72//!   handler. Do not install non-chaining handlers for the fault signals,
73//!   and anything touching them must use `SA_ONSTACK`.
74//!
75//! - **Do NOT call stop() from a signal handler.** Go code is never
76//!   async-signal-safe: calling stop() or any engine function from inside a
77//!   signal handler can deadlock or crash the process. Use
78//!   `signal_hook::flag` to set an atomic flag, then poll it from a normal
79//!   thread and call [`stop`](Llingr::stop).
80//!
81//! - **Panics.** Rust panics in handlers are caught at the FFI boundary and
82//!   converted to error codes; a panic in the process handler routes the
83//!   message to the dead-letter handler with the reason "panic in process
84//!   callback". This protection relies on the host being built with
85//!   `panic = "unwind"`, the default. Under `panic = "abort"` the first
86//!   handler panic aborts the process; the crate's build script warns
87//!   loudly when a profile does this. Go panics in engine internals abort
88//!   the entire process with no recovery possible.
89//!
90//! - **Thread budget.** Handlers run on Go runtime threads, and a blocking
91//!   handler pins one for its duration. GOMAXPROCS and GODEBUG must be set
92//!   as environment variables before process start; setting them from Rust
93//!   code has no effect. If you also run tokio or rayon, budget threads
94//!   explicitly.
95//!
96//! - **cargo test.** All tests within a single test binary share one Go
97//!   runtime instance. Use `--test-threads=1` or a serialisation crate if
98//!   tests depend on engine state.
99//!
100//! - **musl/Alpine is not supported yet**: an upstream Go limitation; the
101//!   build script fails with the full explanation and links. Use a glibc
102//!   image such as Debian or Ubuntu; a scratch image works because the
103//!   binary is static.
104//!
105//! # Exit and lifecycle hygiene
106//!
107//! - **Borrowed message data is callback-scoped, and retaining it is worse
108//!   than a use-after-free.** The `key`/`value` slices point into a broker
109//!   record buffer and a pooled engine work item, both recycled after the
110//!   callback returns. A stored slice will later read *a different message's
111//!   bytes*: silent wrong data, not necessarily a crash. Copy out anything
112//!   you need to keep with `to_vec()` or `to_string()`.
113//!
114//! - **Shutting down: call [`stop`](Llingr::stop), don't just exit.** The
115//!   call that initiates shutdown drains in-flight work, commits offsets,
116//!   and returns from [`run`](Llingr::run). `std::process::exit` skips the
117//!   drain; that is *safe*, because uncommitted offsets are redelivered at
118//!   least once on restart, but wasteful. Never call `stop()` from inside a
119//!   process or dead-letter handler; signal another thread.
120//!
121//! - **[`emergency_stop`](Llingr::emergency_stop) abandons in-flight work**:
122//!   no drain, no final commit, and duplicates on restart are expected; the
123//!   engine's contract is at-least-once. The shutdown handler receives the
124//!   reason exactly once, on graceful and emergency exits alike.
125//!
126//! - **Liveness is your responsibility.** If a handler stalls, nothing
127//!   crashes: `run()` simply blocks. The engine's own resilience covers the
128//!   broker side: sustained partition poll failure triggers an emergency
129//!   shutdown with the reason after a bail window, ten minutes by default.
130//!
131//! # Architecture
132//!
133//! ```text
134//! Kafka / RedPanda / MSK
135//!     |
136//! Go engine, statically linked (libllingr.a)
137//!     |  franz-go broker client -> llingr-demux pipeline
138//!     |
139//! C FFI boundary (ABI v1, checked at build())
140//!     |
141//! This crate: safe wrapper + llingr-nexus contracts
142//!     |
143//! Your application (ProcessHandler / DeadLetterHandler)
144//! ```
145
146#![deny(missing_docs)]
147
148mod config;
149mod engine;
150mod ffi;
151mod logging;
152mod metrics;
153mod options;
154pub mod snapshot;
155mod trampolines;
156
157// Coverage-focused unit tests over pub(crate) internals.
158#[cfg(test)]
159mod engine_coverage_tests;
160#[cfg(test)]
161mod options_auth_tests;
162#[cfg(test)]
163mod options_gcp_tests;
164#[cfg(test)]
165mod trampolines_coverage_tests;
166
167pub use config::DemuxConfig;
168pub use engine::{Builder, Llingr, LlingrError};
169pub use logging::LOG_TARGET;
170pub use metrics::{Metrics, MetricsHandle, OPENMETRICS_CONTENT_TYPE};
171pub use options::{BalanceStrategy, ClientLogLevel, Options};
172pub use snapshot::Snapshot;
173
174// The contract vocabulary lives in, and is versioned by, the published
175// `llingr-nexus` crate; it is re-exported here so one `use llingr_kafka::...`
176// suffices. Deliberately NOT re-exported: nexus's per-message metrics packet
177// type `Metrics` with its `MetricsHandler` trait, the bandwidth equivalents,
178// and the logger plumbing. The Prometheus integration consumes the packet
179// type internally, the root name `Metrics` belongs to the activation type,
180// and engine logs always flow to the `log` facade.
181pub use llingr_nexus::{
182    AutoOffsetReset, DeadLetterHandler, Header, Headers, Message, ProcessHandler, ShutdownHandler,
183    Timestamp, Traits,
184};