Kafka I/O¶
StateFun functions consume and emit through Kafka via declarative
module.yamlspecs. Exactly-once when paired with transactional Kafka and Flink checkpointing.
Ingress¶
kind: io.statefun.kafka.v1/ingress
spec:
id: example/orders
address: kafka.svc:9092
consumerGroupId: example-orders
startupPosition:
type: latest # or earliest, group-offsets, specific-offsets
topics:
- topic: example.orders
valueType: example/Order # registered SDK type
targets:
- example/order-handler
Each entry under topics: maps inbound records to a target function namespace + name. The valueType declares how StateFun decodes the record value - typically a Protobuf type registered in your SDK code.
Full configuration reference¶
Every supported field with its default:
kind: io.statefun.kafka.v1/ingress
spec:
id: example/orders # required, typename
address: kafka.svc:9092 # optional when set via properties
consumerGroupId: example-orders # optional
forwardHeaders: false # optional, ingress-level default for all topics
invalidRecordHandling: # optional, default: type skip + logLevel warn
type: skip # skip | fail
logLevel: warn # skip only: debug | info | warn | error
autoOffsetResetPosition: latest # optional: earliest | latest
startupPosition: # optional, default: latest
type: group-offsets
topics: # required, at least one entry
- topic: example.orders
valueType: example/Order # required, registered SDK type
forwardHeaders: true # optional, overrides the ingress-level value
invalidRecordHandling: # optional, replaces the ingress-level object wholesale
type: fail
targets: # required, at least one function
- example/order-handler
properties: # optional, passed through to the Kafka consumer
- fetch.max.bytes: "52428800"
The same schema is documented in the io.statefun.kafka.v1/ingress binder javadoc (RoutableKafkaIngressBinderV1).
Startup position¶
startupPosition.type |
Meaning |
|---|---|
latest |
Consume only records produced after the consumer starts |
earliest |
Consume from the beginning of each partition |
group-offsets |
Resume from the consumer group's last committed offset (default for restarts) |
specific-offsets |
Pin specific partition offsets (advanced) |
at-timestamp |
Resume from records produced at or after a wall-clock time |
Startup-from-timestamp example¶
Invalid records¶
The routable ingress requires a UTF-8 key and a non-null value on every record: the key is the target function instance id, and a null value (tombstone) has no meaning to a function. invalidRecordHandling decides what happens to records that violate this - as an ingress-level default, with a per-topic override that replaces it wholesale:
kind: io.statefun.kafka.v1/ingress
spec:
invalidRecordHandling:
type: skip # default when omitted
logLevel: warn # skip only: debug, info, warn (default) or error
topics:
- topic: example.orders
invalidRecordHandling:
type: fail # per-topic override: strict contract for this topic
...
type |
Behavior |
|---|---|
skip (default) |
The record is dropped and the job keeps running. Each skipped record is logged individually on the TaskManager and the invalid-record counters increment. |
fail |
The job fails on the first invalid record, with the record coordinates in the exception. This is the strict pre-3.5 behavior. |
Skip logging is one line per record, at logLevel, with no rate limiting:
Skipping invalid record: defect [NULL_VALUE], topic [example.orders], partition [0], offset [42], timestamp [1690000000123], key [order-17], value size [-1]
defect is NULL_KEY or NULL_VALUE. A null key prints as key [null]; a tombstone prints as value size [-1]. An empty key is not invalid: it is a legal address that routes to the function instance with id "".
Alert rules for the counters: Alerting.
Behavior change in 3.5
Previously an invalid record crashed the whole job unconditionally. skip is the new default. Teams alerting on job restarts as their bad-data signal should alert on numInvalidRecordsSkipped instead, or pin type: fail. The null-means-skip contract also applies to custom KafkaIngressDeserializer implementations: a null return, previously a crash, now counts and drops the record.
Egress¶
kind: io.statefun.kafka.v1/egress
spec:
id: example/notifications
address: kafka.svc:9092
deliverySemantic:
type: exactly-once
transactionTimeoutMillis: 60000
Functions emit to a Kafka egress via the SDK:
KafkaEgressMessage outbound = KafkaEgressMessage.forEgress(
TypeName.typeNameFromString("example/notifications"))
.withTopic("example.notifications")
.withKey(orderId)
.withValue(notificationPayload)
.build();
ctx.send(outbound);
The runtime uses Flink transactions to deliver exactly once when paired with a transactional Kafka client and exactly-once Flink checkpointing.
At-least-once vs exactly-once¶
deliverySemantic.type |
Trade-off |
|---|---|
exactly-once |
Strongest guarantee. Slightly higher producer latency due to transactions. Default for production. |
at-least-once |
Higher throughput. Duplicates possible on JM failover (the runtime replays the last checkpoint window). |
Transaction timeout
For exactly-once, set transactionTimeoutMillis higher than your Flink checkpoint interval, but lower than the Kafka broker's transaction.max.timeout.ms (default 15 min). 60 s is a good starting point for sub-minute checkpoint intervals.
Record headers¶
Kafka record headers travel in both directions: functions read the headers of the record that
triggered them via Message#headers() and attach headers to egress records via the
KafkaEgressMessage builder. Ingress header forwarding is opt-in per topic through the
forwardHeaders spec property (default false, settable at ingress level with per-topic
overrides). Semantics match Kafka's own - duplicate keys, ordering, and the null-vs-empty value
distinction are all preserved, and header operations never throw on null input.
See the dedicated guide: Kafka record headers.
Patterns¶
Routing to multiple namespaces¶
TargetFunctions.fromPatternString accepts <namespace>/<name> or <namespace>/*. Comma-lists and wildcard namespaces (*/foo) are not supported. To target multiple namespaces, declare one entry per namespace:
topics:
- topic: orders.events
valueType: example/OrderEvent
targets:
- orders/order-handler
- audit/order-audit
- billing/order-billing
Multi-topic ingresses¶
A single kafka.v1/ingress can declare multiple topics: entries; each has its own valueType and targets:
topics:
- topic: orders.events
valueType: example/OrderEvent
targets: [orders/handler]
- topic: shipments.events
valueType: example/ShipmentEvent
targets: [shipments/handler]
Custom Protobuf types¶
The default io.statefun.types/string works for simple cases. For typed binary payloads, register SimpleType.simpleImmutableTypeFrom(...) in your SDK and reference it as valueType:
public static final Type<Order> ORDER_TYPE =
SimpleType.simpleImmutableTypeFrom(
TypeName.typeNameFromString("example/Order"),
Order::toByteArray,
Order::parseFrom);
Works more reliably than the generic string codec for binary content.
Next steps¶
- Kinesis I/O - same routing model, AWS Kinesis transport.
- Kubernetes deployment - wiring ingress/egress in production.
- Architecture overview - how the dispatcher routes ingress messages.