Skip to content

Kafka listener sources: Kafka producers straight into Materialize (POC) - #40409

Draft
jubrad wants to merge 8 commits into
MaterializeInc:mainfrom
jubrad:kafka-ingest-source
Draft

jubrad wants to merge 8 commits into
MaterializeInc:mainfrom
jubrad:kafka-ingest-source

Conversation

@jubrad

@jubrad jubrad commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

Proof of concept, not for merge as is. Opened as a draft to share the design and the code.

A KAFKA LISTENER source turns a cluster replica into a one-broker Kafka cluster. Unmodified Kafka producers (librdkafka, the Java client, Debezium Server, Kafka Connect, a Materialize Kafka sink) write to it by pointing bootstrap.servers at the replica, with no Kafka cluster in between. The design doc, doc/developer/design/20261030_kafka_listener_source.md, is the place to start.

CREATE SOURCE cdc IN CLUSTER ingest
  FROM KAFKA LISTENER (TOPIC PREFIX 'cdc.public.', SASL USERNAME 'u', SASL PASSWORD SECRET s);
CREATE TABLE orders FROM SOURCE cdc (REFERENCE "cdc.public.orders")
  FORMAT AVRO USING CONFLUENT SCHEMA REGISTRY CONNECTION csr ENVELOPE DEBEZIUM;

How it works

  • The replica is the broker. Each clusterd process runs a Kafka protocol server (mz-kafka-server, on the pure-Rust kafka-protocol crate) on a new kafka port. Ingestion stays on the cluster, scaling and isolated like every other source.
  • A log of every record. The source appends every batch it accepts to its log, a persist shard, and acknowledges it once that append commits, in about 0.1 s. Tables decode their topic's records from the log, so a topic can feed several tables, and a table created later is filled from the start of its topic.
  • Exactly once for idempotent producers. The broker's idempotence rules run against batch rows kept in the same log append as the records, so a resent batch is acknowledged with its original offsets, across restarts. Other producers get at-least-once delivery.
  • The Kafka source's pipeline. The source emits the same SourceMessages a Kafka source would, so formats, envelopes (UPSERT, DEBEZIUM, NONE), INCLUDE metadata and reclocking are shared.
  • Also: VALIDATE ON PRODUCE rejects records a table could not decode; a read-only Confluent-compatible schema registry per replica for inline Avro schemas; transactions and compacted progress topics, so a Materialize Kafka sink can write into a listener exactly once; and SNI routing through balancerd for Kubernetes. orchestratord wiring is not done.

Both features are behind flags that default to off: enable_kafka_listener_source and enable_replica_kafka_listener.

Commits

Grouped by area, for review. Lines are added plus deleted. Code includes comments; tests count integration tests, test data, and in-file #[cfg(test)] modules.

# Commit Code Tests
1 Design doc
2 ccsr: allow a base path in schema registry URLs. Independent of the rest, and can land on its own. 6 45
3 mz-kafka-server: the protocol server, idempotence, transactions, validation hook, schema registry 2,714 1,180
4 SQL: CREATE SOURCE .. FROM KAFKA LISTENER, TOPIC PREFIX with CREATE TABLE .. FROM SOURCE, the connection type 765 204
5 Storage: the source, its log and appender, validation on produce, log finalization 2,014 56
6 Controller and clusterd: replica ports, orchestrator endpoints, advertised addresses 517
7 balancerd: Kafka and schema registry routing by SNI 328 114
Total 6,344 1,599

Not part of a production change:

What Lines
Design doc (commit 1), reviewed rather than shipped as code 787
Demo and end-to-end scripts with their README in misc/kafka-listener (commit 8), for local runs, not CI 1,602
Cargo.lock, generated 68

Measured

On an AWS m6id.16xlarge with 16 producers sending flat out:

  • Plain producers: 976k records/s on 4 workers, 1.71M on 16.
  • Idempotent producers: 1.51M records/s, exactly once, on 16 workers.
  • Correctness: every run held exactly the acknowledged records.

Writing every record to persist twice costs about twice the replica CPU and memory of acknowledging at the table's commit. The extra is persist's encoding and compaction of the log. The design doc covers this and what it flags for persist.

Tests

  • mz-kafka-server integration tests with librdkafka producers: compression, idempotence, SASL, transactions, compacted topics, validation mid-sequence, the schema registry, and the byte-bounded topic queue.
  • test/sqllogictest/kafka_listener.slt for the SQL.
  • Parser testdata.
  • misc/kafka-listener scripts for exactly-once delivery through restarts, tables per topic, the registry, and throughput.

🤖 Generated with Claude Code

jubrad and others added 8 commits November 2, 2026 08:56
A Kafka listener source turns a cluster replica into a one-broker Kafka
cluster: unmodified Kafka producers write to it, and a batch is acknowledged
once it is durable in the source's log, a persist shard. Tables decode their
topic's records from the log.

The doc covers the SQL, the protocol server, the log and exactly-once
delivery for idempotent producers, tables per topic, validation on produce,
the replica's schema registry, networking through balancerd, Materialize
Kafka sinks as producers, measurements, and the alternatives considered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Some registries serve the Confluent-compatible API under a path, for example
Apicurio Registry at /apis/ccompat/v7. A schema registry connection's URL may
now include a path, which becomes the base of every request. Previously
CREATE CONNECTION rejected any URL with a non-empty path.

This is independent of the Kafka listener source and can land on its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new crate, mz-kafka-server, that presents itself as a single-broker Kafka
cluster to producers, built on the pure-Rust kafka-protocol crate. It knows
nothing about timely or storage: a consumer registers a topic and receives a
stream of produced batches to acknowledge.

It serves ApiVersions, Metadata, Produce, InitProducerId, SASL/PLAIN, the
transaction APIs, and CreateTopics, ListOffsets and Fetch for compacted
topics, as Materialize Kafka sinks need for progress. It implements the
broker's idempotence rules, validates batches through an optional
per-topic validator with KIP-467 record errors, bounds each topic's queue of
batches in bytes, and serves a read-only Confluent-compatible schema registry
for topics with inline Avro schemas.

Tested with librdkafka producers, a transactional producer, an admin client
and a read_committed consumer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds the Kafka listener source connection and its SQL, behind the
enable_kafka_listener_source feature flag:

    CREATE SOURCE events FROM KAFKA LISTENER (
        TOPIC 'events', SASL USERNAME 'u', SASL PASSWORD SECRET s,
        VALIDATE ON PRODUCE)
      KEY FORMAT TEXT VALUE FORMAT JSON ENVELOPE UPSERT;

    CREATE SOURCE cdc FROM KAFKA LISTENER (TOPIC PREFIX 'cdc.public.');
    CREATE TABLE orders FROM SOURCE cdc (REFERENCE "cdc.public.orders")
      FORMAT AVRO USING CONFLUENT SCHEMA REGISTRY CONNECTION csr
      ENVELOPE DEBEZIUM;

Planning reuses the Kafka source's formats, envelopes, metadata columns and
export details. A source with a TOPIC PREFIX has no relation of its own, and
each table names a topic under the prefix; a topic can feed several tables.
Purification records the defaulted TOPIC so that renaming the source does not
change the topic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The source registers its topics with the replica's Kafka server and appends
every batch it accepts to its log, a persist shard derived from the remap
shard, with its records and, for idempotent producers, a row for
deduplication. A batch is acknowledged once its append commits, in about
0.1 s, and its records are then emitted to every table of its topic. After a
restart, or for a table created later, each table is sent the log's records
from its upper. Resent batches of idempotent producers are acknowledged with
their original offsets, so they get exactly-once delivery.

The log commits in groups: at most one append starts per 50 ms and two are in
flight, and each append is split into batches that build in parallel and
commit together. Records not yet committed by every table of their topic are
bounded by an eighth of the replica's memory. VALIDATE ON PRODUCE decodes each
batch with its table's decoders on the producer's connection before
accepting it. The storage controller finalizes the log when the source is
dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Behind the enable_replica_kafka_listener dyncfg, replicas get a kafka port
(9092) and a kafka-registry port (8081), and clusterd runs the Kafka server
and the schema registry on them. Adding ports restarts StatefulSets, so the
flag applies only to replicas provisioned after it is set.

Orchestrators gain endpoints: a stable address routed to one process of a
service. The Kubernetes orchestrator implements one as a ClusterIP Service
selecting the target pod; the process orchestrator needs none, since local
clients reach each replica's TCP proxy. The controller keeps each cluster's
<cluster>-kafka and <cluster>-kafka-registry endpoints pointed at process 0
of the replica that runs its listener sources. environmentd's
--kafka-listener-advertised-addr-template gives the address replicas
advertise in Metadata responses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
balancerd gains optional Kafka and schema registry listeners. It terminates
TLS with the certificate it already serves, reads the first SNI label as
<environment>-<cluster ID>, resolves a backend from a template such as
cluster-{cluster}-kafka.<namespace>.svc.cluster.local:9092, and proxies the
connection without parsing Kafka. Only labels ending in a cluster ID are
routed.

orchestratord does not pass the new flags yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Scripts for a local environmentd, not meant for CI as they are:

- demo.py: a source, a producer and a query, with the replica's endpoint.
- exactly_once.py: resends identical batches over the raw Kafka protocol
  around replica restarts and checks offsets and rows, then runs librdkafka
  idempotent producers through a restart.
- tables.py: a TOPIC PREFIX source with tables added mid-stream, including a
  second table on a topic with records, through a replica kill.
- registry.py: Confluent's Avro serializer against the replica's registry.
- perf.py: throughput, ack latency, and replica CPU and memory per replica
  size and number of producers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant