Produce, Fetch, ListOffsets & Metadata
Per-API reference — see the API support matrix for the generated version table.
Produce
Append RecordBatches to partition logs. This is the write hot path: the storage engine chapter covers the mechanics end to end; this section covers the wire contract.
Versions: v3–v9 (flexible from v9)
Four gates run before any byte reaches the engine, per partition: the topic
must exist in the registry (else UNKNOWN_TOPIC_OR_PARTITION, 3), the broker
Coordinator must own the partition per the applied assignment.json (else
NOT_LEADER_FOR_PARTITION, 6), the gh #62 self-fence must see a fresh
controller heartbeat — a broker cut off from the controller stops acking
within 3 s (also 6) — and the cluster-wide authorizer must grant topic Write
(else TOPIC_AUTHORIZATION_FAILED, 29). A batch larger than Apache's
message.max.bytes default (1 MiB + 12 bytes batch overhead) is rejected with
MESSAGE_TOO_LARGE (18) before it hits storage.
Inside the engine, under the partition mutex: idempotent-producer
classification runs first — a duplicate (PID/epoch/sequence already in the
5-batch ring window) echoes the cached base offset with error_code = 0 and
never touches the log; out-of-order sequences get OUT_OF_ORDER_SEQUENCE_NUMBER
(45); a stale producer epoch gets INVALID_PRODUCER_EPOCH (47). Accepted
batches have their base offset rewritten in place to the current high
watermark (the v2 CRC covers byte 21 onward, so the 8-byte overwrite is
wire-correct) and the client's bytes land on disk verbatim — the broker never
parses records, only fixed-size header peeks. For acks = -1, the append that
crosses the flush threshold parks on the per-partition committer's group-commit
fsync — one sync_all() cycle serves every concurrent appender —
with KAAS_FLUSH_INTERVAL_MESSAGES (default 1) as the durability dial.
The produce quota is checked once per request over the summed record bytes,
after the appends, and feeds throttle_time_ms in the response.
Deviations from Apache 3.7:
acks = 0still gets a wire response. Apache sends nothing for acks=0 produce requests; kaas's dispatcher has no response-suppression path, so the handler's response is always written back.- The
message.max.bytescap is the fixed Apache default — the per-topicmax.message.bytesconfig override is not consulted. log_append_time_msis always-1: the broker never stamps append time, somessage.timestamp.type=LogAppendTimesemantics are not implemented.- The v8+
record_errors/error_messagefields are never populated — errors are reported per partition, not per record.
Source: crates/kaas-broker/src/handlers/produce.rs,
crates/kaas-codec/src/api/produce.rs; engine side in
crates/kaas-storage/src/partition.rs and
crates/kaas-storage/src/idempotence.rs.
Verified by: unknown_topic_returns_error_code_3 (handler unit test),
produce_fetch_metadata_roundtrip in bins/kaas/tests/smoke.rs,
storage_round_trip_is_byte_identical in bins/kaas/tests/byte_opacity.rs,
and the full transactional round trip in bins/kaas/tests/eos_v2.rs. Shell
suite: scripts/kafka-console-producer.sh,
scripts/kafka-producer-perf-test.sh, scripts/kafka-verifiable-producer.sh.
Fetch
Read RecordBatches from partition logs. The symmetric half of the byte-opacity contract: batch bytes come back off disk undecoded (see the storage hot path).
Versions: v4–v12 (flexible from v12)
The front door mirrors Produce: topic-exists, Coordinator::owns, and a
cluster-wide topic Read ACL check, per partition (there is no self-fence gate
on reads). The handler then picks the read cap: the high watermark for
read_uncommitted, the last stable offset for read_committed (gh #176 —
the LSO is the highest offset with no in-flight transaction extending past
it). The engine copies batch bytes from the segment files into memory; for
read_committed the result is trimmed to whole batches strictly below the
LSO — a batch that straddles the cap is dropped along with everything after
it, since batches are atomic — and aborted_transactions is populated from
the partition's aborted-txn index over the returned range, so the client can
filter aborted records (gh #31). A reader already at or past the cap gets an
empty response. Out-of-range offsets map to OFFSET_OUT_OF_RANGE (1). The
fetch quota is checked once over the summed response bytes and feeds
throttle_time_ms.
The response is materialized bytes — copied from the segments, not
sendfile/spliced. The codec keeps records byte-opaque precisely so a splice
path stays possible; today it is a future optimisation, not what ships.
Deviations from Apache 3.7:
- No KIP-227 fetch sessions:
session_id = 0on every response, regardless of what the client sent. That is Apache's documented marker for "broker doesn't support sessions", so clients fall back to full fetch requests per poll — a deliberate contract, not a gap (non-goals).session_epochandforgotten_topicsare decoded and ignored. - No long-poll:
max_wait_msandmin_bytesare decoded but ignored. An empty fetch returns immediately instead of parking until data arrives or the wait expires; clients simply re-poll. - The leader-epoch fields (
current_leader_epoch,last_fetched_epoch) are not validated — kaas never returnsFENCED_LEADER_EPOCHorUNKNOWN_LEADER_EPOCH, and KIP-320 truncation detection does not apply (there are no followers to diverge from). preferred_read_replicais always-1andrack_idis ignored — no follower fetching, because there is no replication (non-goals).
Source: crates/kaas-broker/src/handlers/fetch.rs,
crates/kaas-codec/src/api/fetch.rs; aborted-txn index in
crates/kaas-storage/src/txn_index.rs.
Verified by: the trim_to_offset_* unit tests and
unknown_topic_returns_error_3 (asserts session_id == 0) in the handler,
produce_fetch_metadata_roundtrip in bins/kaas/tests/smoke.rs, and the
read-committed assertions eos_commit_path_records_visible_to_read_committed
/ eos_abort_path_populates_aborted_transactions in
bins/kaas/tests/eos_v2.rs. Shell suite:
scripts/kafka-console-consumer.sh, scripts/kafka-consumer-perf-test.sh,
scripts/kafka-e2e-latency.sh.
ListOffsets
Resolve a timestamp (or a sentinel) to an offset per partition —
what kafka-get-offsets.sh and every consumer's auto.offset.reset ride on
(KIP-32 semantics).
Versions: v1–v7 (flexible from v6)
The handler checks the topic exists (else UNKNOWN_TOPIC_OR_PARTITION, 3),
then translates the request timestamp: -2 (EARLIEST) returns the partition's
log_start_offset, -1 (LATEST) returns the high watermark — both echoed
with timestamp = -1, matching Apache's sentinel-response shape. Any other
timestamp is passed to the engine's offset_for_timestamp.
Deviations from Apache 3.7:
- Timestamp→offset lookup is not implemented. Segments track their
max_timestamp(gh #5), but the engine-level index that maps a timestamp to an offset is a follow-up:offset_for_timestampreturns the(-1, -1)"no matching offset" sentinel unconditionally, in both the disk and in-memory engines. Every non-sentinel timestamp query — includingMAX_TIMESTAMP(-3, nominally in the registered v7 range) — getsoffset = -1, timestamp = -1witherror_code = 0. Only the-1/-2sentinels resolve. isolation_levelis decoded but ignored: LATEST returns the high watermark even forread_committedclients, where Apache returns the last stable offset.- The tiered-storage-only sentinels (
EARLIEST_LOCAL_TIMESTAMP,EARLIEST_PENDING_UPLOAD_OFFSET) are deliberately unsupported — clients only send them when configured for a remote tier (non-goals); on the wire they fall into the same(-1, -1)path as any other timestamp. leader_epochis always0in success responses.- No authorization or leadership gate: there is no topic Describe ACL check,
and no
Coordinator::ownscheck — a non-leader broker answers from its local engine view (typically offset0) instead ofNOT_LEADER_OR_FOLLOWER. Clients that route by Metadata leadership never hit this in steady state.
Source: crates/kaas-broker/src/handlers/list_offsets.rs,
crates/kaas-codec/src/api/list_offsets.rs; the sentinel-returning engine
lookup in crates/kaas-storage/src/disk.rs.
Verified by: handler unit tests latest_returns_high_watermark,
earliest_returns_log_start_offset, unknown_topic_returns_3. Shell suite:
scripts/kafka-get-offsets.sh (exercises the -2/-1 sentinels before and
after producing — the paths clients actually use).
Metadata
The cluster-discovery API: which brokers exist, who leads each partition, and where to connect — the response every client bootstraps from.
Versions: v1–v10 (flexible from v9)
Per-listener advertisement (gh #125): the handler precomputes one
advertised (host, port) per configured listener from the KAAS_LISTENERS
env, and each connection carries its listener name, so a client that
bootstrapped on :9095 gets :9095 back — not the anonymous listener's port.
Without this, authed-listener clients were routed back to the plain listener
and looped on SCRAM retry (Listeners & auth).
Peer brokers are advertised at their stable in-cluster FQDN with the port of
the listener the client connected on; per-broker external hostname templates
for peers are a follow-up.
Leadership comes from assignment.json via the broker Coordinator: each
partition's leader_id is the assignment entry's broker ordinal, and
controller_id is parsed from the assignment's controller identity
(Controller). Dev mode — and any partition
missing from the assignment (fresh topic, recompute pending) — falls back to
self. An empty request topic list returns every known topic (Apache's "all
topics" contract); unknown requested topics get a per-topic
UNKNOWN_TOPIC_OR_PARTITION (3). replica_nodes and isr_nodes are
[leader] — the truthful shape for a single-writer design with no
replication (non-goals).
Deviations from Apache 3.7:
- Topic IDs serve the all-zero sentinel on the wire. The v10
topic_idfield is encoded, but the value is always the null UUID: the operator mints a real v4 UUID into eachKafkaTopic'sStatus.TopicID(gh #105), and the broker-side observation plumbing exists, yet the watcher callback that feeds the topic registry (bins/kaas/src/main.rs) inserts[0; 16]. KIP-516 is partial: minted operator-side, not yet propagated to the Metadata response. leader_epochis always0(v7+ field) — clients that cache epochs for truncation detection get a constant.allow_auto_topic_creationis decoded and ignored. There is no auto-creation: topics exist when aKafkaTopicCR exists (authored directly or via CreateTopics, which writes the CR).topic_authorized_operations/cluster_authorized_operationsare always0, even when the v8+ request flags ask for them — the ACL bitsets are not computed.rackis always null andis_internalalways false — kaas advertises no internal topics (there is no on-wire__consumer_offsets).- v11+ is not registered; clients negotiate down to v10 via ApiVersions.
Source: crates/kaas-broker/src/handlers/metadata.rs,
crates/kaas-codec/src/api/metadata.rs.
Verified by: handler unit tests per_listener_port_echoed_back,
returns_self_as_only_broker_and_leader, empty_topic_list_returns_all_known,
unknown_topic_returns_per_topic_error_3;
produce_fetch_metadata_roundtrip in bins/kaas/tests/smoke.rs. Shell
suite: scripts/kafka-topics.sh (--list / --describe ride Metadata).