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};