Skip to main content
Rust Client · Developer Preview

Rust Client & Streams

Asynchronous Kafka client and stream processing libraries built natively on Tokio and krabka-protocol.

Developer Preview Notice: krabka-client-rs and krabka-streams-rs are under active development. Core producer, consumer, and admin APIs are functional and tested in CI; streams DSL topology drivers are in developer preview.

The Rust client ecosystem is decoupled from the broker binary, depending on krabka-protocol for wire serialization. It provides memory-safe implementations of Kafka producers, consumers, admin tools, and streaming topologies without relying on librdkafka C-bindings. The client crates live in krabka-client-rs; the streams crate lives in krabka-streams-rs.

Crate Architecture

Crate Subsystem & Features
krabka-client-core Broker connection pool, API version negotiation, typed request dispatch, retry backoff, SASL/TLS authentication.
krabka-client-producer Idempotent producer (InitProducerId, sequence tracking), batch accumulation, and transactional APIs (KIP-447).
krabka-client-consumer Consumer group protocol (rebalance, heartbeats, cooperative sticky assignor) and KIP-932 Share Groups.
krabka-client-admin Administrative client for topic CRUD, partition reassignments, ACLs, configs, quotas, SCRAM credentials, and group management.
krabka-client-coordination Leader election, leases, and fencing tokens as a Kafka client API.
krabka-client-streams KIP-1071 streams-group membership, typed Processor API, KStream/KTable DSL with windowing, Turso-backed state stores, and interactive queries. Lives in krabka-streams-rs.
krabka-schema-serde Schema Registry serialization (Avro, JSON Schema, Protobuf). Lives in krabka-schema-registry.

Adding the Crates

These repositories do not publish to crates.io, and the krabka-* names there still belong to an older repository. Depend on them by git revision, and copy the [patch.crates-io] block from the Cargo.toml at the pinned revision of the repository you depend on, so that krabka-protocol and krabka-units resolve to one copy. The examples below need Rust 1.98.1 or newer, tokio with the macros and rt-multi-thread features, bytes for the producer, and krabka-units for the consumers.

Async Producer Example

The idempotent producer initializes producer epochs and base sequences, which the broker uses to detect duplicate batches. send(record).await? waits for delivery and returns metadata. For pipelining, enqueue(record).await? queues a record and returns a delivery handle; await each handle with delivery.await? to check its result.

Idempotent Producer (Tokio):
// Cargo.toml:
//   krabka-client-producer = { git = "https://github.com/krabka-io/krabka-client-rs", rev = "c4ce581587772b3750d88c61f3389d9626a20235" }
//   plus the [patch.crates-io] block from that revision's Cargo.toml
//   bytes = "1"
//   tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
use std::time::Duration;
use bytes::Bytes;
use krabka_client_producer::{Acks, Compression, Producer, ProducerRecord};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Build an idempotent producer with LZ4 compression and acks=all
    let producer = Producer::builder()
        .bootstrap("localhost:9092")
        .compression(Compression::Lz4)
        .acks(Acks::All)
        .linger(Duration::from_millis(5))
        .build()
        .await?;

    // Send record asynchronously with broker sequence deduplication
    producer
        .send(ProducerRecord {
            topic: "events".into(),
            value: Some(Bytes::from("hello krabka")),
            ..Default::default()
        })
        .await?;

    producer.flush().await?;
    producer.close().await?;
    Ok(())
}

Consumer Groups & Share Groups

The consumer client supports consumer group rebalancing and KIP-932 share-group queues for cooperative record delivery:

Standard Consumer Group:
use krabka_client_consumer::{AutoOffsetReset, Consumer};
use krabka_units::millis;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Build a subscribe-style group consumer
    let mut consumer = Consumer::builder()
        .bootstrap("localhost:9092")
        .group_id("order-processors")
        .client_id("processor-node-1")
        .auto_offset_reset(AutoOffsetReset::Earliest)
        .subscribe(["orders".to_string()])
        .build()
        .await?;

    loop {
        let records = consumer.poll(millis(500)).await?;
        for _record in records {
            // Process each record
        }
        consumer.commit_sync().await?;
    }
}
KIP-932 Share-Group Queue Consumer:
use krabka_client_consumer::{ShareAckMode, ShareAckType, ShareConsumer};
use krabka_units::secs;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // KIP-932 share group: records are acquired per consumer, with no partition assignment
    let mut consumer = ShareConsumer::builder()
        .bootstrap("localhost:9092")
        .group_id("share-workers")
        .subscribe(["jobs".to_string()])
        .ack_mode(ShareAckMode::Explicit)
        .build()
        .await?;

    loop {
        let records = consumer.poll(secs(1)).await?;
        for record in &records {
            consumer.acknowledge(record, ShareAckType::Accept)?;
        }
        consumer.commit().await?;
    }
}

Streams Topology

The Processor API wires nodes by typed handle, so a mismatched edge fails at compile time. TopologyTestDriver runs the built topology in process, with no broker:

Processor API with the test driver:
use krabka_client_streams::{
    NodeHandle, Record, StringSerde, Topology, TopologyTestDriver, impl_processor,
};

struct Upper;
impl_processor! {
    impl Upper: (String, String) -> (String, String) {
        async fn process(&mut self, ctx, r) {
            ctx.forward(Record::new(r.key, r.value.to_uppercase(), r.timestamp));
        }
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut topo = Topology::new();
    let src: NodeHandle<String, String> = topo.add_source("src", ["in"]);
    let up = topo.add_processor("up", || Upper, [&src]);
    topo.add_sink("out", "out", [&up]);
    let built = topo.build("my-app")?;

    // Broker-free test driver: runs the same topology in process
    let mut driver = TopologyTestDriver::new(&built)?;
    driver.pipe_input("in", (StringSerde, StringSerde), Some("k".to_string()), "hello".to_string(), 0);
    let out = driver.read_output("out", (StringSerde, StringSerde));
    assert_eq!(out, Some((Some("k".to_string()), "HELLO".to_string())));
    Ok(())
}