Skip to main content

The topic contract

A defined source implies Kafka topics: <source>_land unless it lands on the shared topic, and <source>_load when the source has a transform. Those topics are the engine's for the source's whole life, so nothing here depends on the broker's auto.create.topics.enable - which creates a mis-partitioned, unmanaged topic on first produce, and is unavailable on Confluent Cloud's non-Dedicated tiers anyway.

The deploy creates a source's topics and the delete removes them. The old _load topic of a version that dropped its transform goes once its transform instance has left the deploy repo, the transform's consumer group has no live member, and no consumer group has records left to read on it. The apps reconcile after a later source write, POST /api/v1/sources/reconcile-apps, or the engine's next start deletes it. A transform with no consumer group the engine can name keeps the topic. Everything else is /api/v1/kafka/topics.

A pipeline you run yourself writes these same topics, as a source's origin or its transform, and external-pipelines.md says which and in which consumer group.

A fetcher source pushing to the main topic is the exception at both ends: it arrives on the landing source's main_land, so it creates no landing topic on deploy and removes none on delete. Deleting it otherwise takes main_land out from under the receiver's default flow and every other source landing there.

Deleting a stranded _load topic​

Those checks stop the delete while a writer runs, but not when one starts again in the sub-second gap between the backlog read and the delete. With the broker's auto.create.topics.enable off, its records are refused. With it on, its first produce re-creates _load at broker defaults, which suppresses _land again and strands what lands there until retention. dfe-infra's cluster profile turns auto-create off, while its single profile and dfe-docker's default turn it on.

Once _load is gone the loader reads _land from earliest, the scalo-rs consumer default, so the source's retained _land records load a second time. Those are duplicates, which the at-least-once contract allows, and never loss.

The surface​

RouteWhat it does
GET /api/v1/kafka/topicsPer-topic existence, partitions, replication factor, config and drift against what the source asks for. Reads only.
POST /api/v1/kafka/topics/ensureCreates what is missing. Idempotent, and it never reshapes a topic that already exists.
POST /api/v1/kafka/topics/updateConverges an existing topic: an incremental alter of the configs the spec names, and a partition increase.
POST /api/v1/kafka/topics/removeDeletes one source's topics. Needs confirm=true; the records on them go with them.

Each takes ?source= to narrow to one source and ?dry_run=true to report without changing anything. The topic names are never typed by the caller - the request names a source and the engine derives which topics follow, which is what makes this a governed surface rather than a broker admin shell.

RBAC follows the source scopes: source:read to look, source:deploy to ensure or converge, source:delete to remove. The right to deploy a source is the right to make its topics exist. The CLI is generated from the same spec, so dfe kafka topics-cmd (the list) and dfe kafka topics ensure|update|remove are the same surface over HTTP.

A deployment that runs no bus answers 503 rather than an empty list, so "no topics needed" and "not this deployment's job" are never confused.

What a converge will not do​

RefusedWhy
Partition decreaseKafka has no such operation, and the records already assigned to the partitions it would drop have nowhere to go.
Replication-factor changeThat is a partition reassignment, not a topic alter.
Creating a topic that does not existThat is ensure. Doing it here would hide a source that never deployed.

A refusal is reported apart from a broker failure, because an operator fixes the two in different places.

The dials​

SettingEffect
kafka.topic_partitionsPartition count for topics DFE creates. A converge widens an existing topic to it.
kafka.topic_replication_factorReplication factor for topics DFE creates. Unset takes the dfe-schemas landing default (3) clamped to the broker count the bus reports, so a managed broker that refuses fewer than 3 replicas gets 3. A converge reports a difference and changes nothing.
kafka.topic_retention_msretention.ms on the topics DFE creates and converges. Unset leaves the broker default.
kafka.topic_cleanup_policycleanup.policy, same.
kafka.topic_compression_typecompression.type on the topics DFE creates and converges. Default producer: the broker stores each batch in the codec its producer chose (zstd level 3 across DFE) and never recompresses it. Empty leaves the broker default.
kafka.ensure_topicsWhether the topics are DFE's to manage. Unset follows transport.bus_present.

Only the configs the spec names are ever compared, so a config an operator set outside it is not drift and a converge never proposes to undo it.

kafka.ensure_topics set true also runs one ensure pass over every source at startup, so a deployment restored from its config repo has its topics before anything produces into them. Left unset it does not: a deployment that only carries a bus may not have been told where the broker is, and every boot would spend the admin timeout finding that out.

Reaching the broker​

kafka.provider derives security.protocol and sasl.mechanism; the mechanism is never hand-set. One key names one LISTENER, so a DFE-owned broker on the TLS-off listener the deploy charts stand up is strimzi-no-tls or redpanda-no-tls, not strimzi with something turned down. SCRAM is a challenge-response, so the password never reaches the wire on that listener; no PLAIN or OAUTHBEARER provider has a TLS-off key.

A delete never fails on the topic step: the source is already gone by the time its topics are removed, so an unreachable broker is reported on the response instead. A deploy of a source on the bus does fail on it, with 502 topics_not_created for a topic the broker did not create. An old _load topic never fails it: the deploy reports one it could not delete yet in topics_pending_removal.