Add the kafka-basics module
This commit is contained in:
50
kafka-basics/docs/01-the-on-ramp.md
Normal file
50
kafka-basics/docs/01-the-on-ramp.md
Normal file
@@ -0,0 +1,50 @@
|
||||
[Module README](../README.md) · [Producing →](02-producing.md)
|
||||
|
||||
# 1. The on-ramp, and the dependency that is not the one you remember
|
||||
|
||||
Under Spring Boot 3 you added `org.springframework.kafka:spring-kafka` and got a
|
||||
`KafkaTemplate`. Under Boot 4 you get this:
|
||||
|
||||
```
|
||||
No qualifying bean of type 'org.springframework.kafka.core.KafkaTemplate<java.lang.String,
|
||||
com.ankurm.kafkabasics.OrderEvent>' available
|
||||
```
|
||||
|
||||
Boot 4 split the auto-configurations out of `spring-boot-autoconfigure` into per-technology
|
||||
modules. Kafka's now lives in `spring-boot-kafka`, package
|
||||
`org.springframework.boot.kafka.autoconfigure`, and a bare `spring-kafka` dependency does not
|
||||
bring it. The application compiles, the context starts, and there is simply no template.
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-kafka</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
That is the fix, and the same shape applies elsewhere: `spring-boot-starter-amqp` for RabbitMQ,
|
||||
`spring-boot-starter-restclient` for `RestClient.Builder`. The rule of thumb for Boot 4 is that
|
||||
if you are depending on a library directly rather than through a Boot starter, you are probably
|
||||
missing its auto-configuration.
|
||||
|
||||
## What the starter actually gives you
|
||||
|
||||
| Bean | Comes from | Notes |
|
||||
|---|---|---|
|
||||
| `KafkaTemplate<?, ?>` | `KafkaAutoConfiguration` | typed by your injection point |
|
||||
| `ProducerFactory` / `ConsumerFactory` | `KafkaAutoConfiguration` | built from `spring.kafka.*` |
|
||||
| `KafkaListenerContainerFactory` | `KafkaAnnotationDrivenConfiguration` | what `@KafkaListener` binds to |
|
||||
| `KafkaAdmin` | `KafkaAutoConfiguration` | creates `NewTopic` beans at startup |
|
||||
|
||||
`KafkaAdmin` is worth knowing about early: declare a `NewTopic` bean and Boot creates the topic
|
||||
on startup with the partition count and replication factor you asked for. It will **not** change
|
||||
an existing topic's partition count, so a `NewTopic` bean that disagrees with the cluster is
|
||||
silently ignored rather than applied.
|
||||
|
||||
## Versions
|
||||
|
||||
Boot 4.1.1 manages Spring Kafka **4.1.1** and kafka-clients **4.2.1**. Note the second one:
|
||||
Maven Central has kafka-clients 4.3.1, and overriding the managed version to reach it is the
|
||||
kind of change that works until it does not. Let the BOM decide.
|
||||
|
||||
[Producing →](02-producing.md)
|
||||
67
kafka-basics/docs/02-producing.md
Normal file
67
kafka-basics/docs/02-producing.md
Normal file
@@ -0,0 +1,67 @@
|
||||
[← The on-ramp](01-the-on-ramp.md) · [Module README](../README.md) · [Serialisation →](03-serialisation.md)
|
||||
|
||||
# 2. Producing, and the return value everybody throws away
|
||||
|
||||
[`OrderProducer`](../src/main/java/com/ankurm/kafkabasics/OrderProducer.java) has three send
|
||||
methods because there are exactly three useful answers to "when do I find out this failed".
|
||||
|
||||
```java
|
||||
public void sendAndForget(OrderEvent event) {
|
||||
this.template.send(TOPIC, event.orderId(), event); // returns a CompletableFuture. Ignored.
|
||||
}
|
||||
```
|
||||
|
||||
`KafkaTemplate.send` is asynchronous and returns a `CompletableFuture<SendResult<K, V>>`.
|
||||
Discarding it discards the only notification you will get. The method returns normally, the
|
||||
record may never reach the broker, and nothing in your logs says so — the producer's own retry
|
||||
and expiry messages are at `WARN` under `org.apache.kafka`, which most applications turn down.
|
||||
|
||||
This is the most common way to lose messages in a Spring Kafka application, and it looks like
|
||||
correct code.
|
||||
|
||||
```java
|
||||
this.template.send(TOPIC, event.orderId(), event)
|
||||
.whenComplete((result, ex) -> { /* log, meter, compensate */ });
|
||||
```
|
||||
|
||||
Handle the future, or block on it when the caller genuinely must not proceed without a durable
|
||||
write:
|
||||
|
||||
```java
|
||||
SendResult<String, OrderEvent> result = this.template.send(record).get();
|
||||
result.getRecordMetadata().partition(); // where it landed
|
||||
result.getRecordMetadata().offset(); // and at what offset
|
||||
```
|
||||
|
||||
Blocking costs a network round trip plus the replication acknowledgement, so it belongs at the
|
||||
edges of a system, not inside a loop.
|
||||
|
||||
## Durability comes from defaults you did not set
|
||||
|
||||
Spring Boot sets **nothing** on the producer beyond bootstrap servers and serializers. Here is
|
||||
the effective configuration, printed by
|
||||
[`EffectiveConfigTest`](../src/test/java/com/ankurm/kafkabasics/EffectiveConfigTest.java) and
|
||||
committed at [`docs/output/effective-config.txt`](output/effective-config.txt):
|
||||
|
||||
```
|
||||
property kafka-clients default set by Spring Boot
|
||||
acks all -
|
||||
enable.idempotence true -
|
||||
retries 2147483647 -
|
||||
max.in.flight.requests.per.connection 5 -
|
||||
delivery.timeout.ms 120000 -
|
||||
```
|
||||
|
||||
Since Kafka 3.0 the client defaults are `acks=all` and `enable.idempotence=true`, so a stock
|
||||
Boot application already has a durable, deduplicating producer. Two consequences:
|
||||
|
||||
- **You do not need to set `acks=all`.** It is already on.
|
||||
- **An old runbook that sets `acks=1` or `retries=0` is now a downgrade.** Those lines were
|
||||
written when the defaults were weaker, and deleting them makes the system safer, which is the
|
||||
opposite of how configuration usually ages.
|
||||
|
||||
`delivery.timeout.ms` at two minutes is the one worth revisiting: it is the total budget for a
|
||||
send including retries, and a request that exceeds it fails permanently. If your future never
|
||||
completes, that is the clock you are waiting on.
|
||||
|
||||
[Serialisation →](03-serialisation.md)
|
||||
82
kafka-basics/docs/03-serialisation.md
Normal file
82
kafka-basics/docs/03-serialisation.md
Normal file
@@ -0,0 +1,82 @@
|
||||
[← Producing](02-producing.md) · [Module README](../README.md) · [Keys and partitions →](04-keys-and-partitions.md)
|
||||
|
||||
# 3. Serialisation: two Jackson families, and the one your IDE will suggest is the wrong one
|
||||
|
||||
Kafka moves `byte[]`. Everything else is a `Serializer` and a `Deserializer`, and under Spring
|
||||
Boot 4 there is a fork in the road that no tutorial mentions yet.
|
||||
|
||||
Spring Kafka 4.1 ships **two complete JSON families**:
|
||||
|
||||
| Class | Jackson | Mapper type |
|
||||
|---|---|---|
|
||||
| `JsonSerializer` / `JsonDeserializer` / `JsonSerde` | 2.x | `com.fasterxml.jackson.databind.ObjectMapper` |
|
||||
| `JacksonJsonSerializer` / `JacksonJsonDeserializer` / `JacksonJsonSerde` | 3.x | `tools.jackson.databind.json.JsonMapper` |
|
||||
|
||||
Spring Boot 4 is a Jackson 3 application. `spring-boot-starter-jackson` brings
|
||||
`tools.jackson.core:jackson-databind` 3.1.5. But the class named `JsonSerializer` — the one
|
||||
every existing example configures, and the one autocomplete offers first — is the Jackson 2 one.
|
||||
|
||||
It does not fail at startup. It fails on the first payload containing a `java.time` value:
|
||||
|
||||
```
|
||||
org.apache.kafka.common.errors.SerializationException: Can't serialize data
|
||||
[OrderEvent[orderId=o-1, ..., placedAt=2026-08-29T10:15:30Z]] for topic [orders]
|
||||
Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
|
||||
Java 8 date/time type `java.time.Instant` not supported by default:
|
||||
add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling
|
||||
```
|
||||
|
||||
The message tells you to add a Jackson 2 module, and doing so works — and leaves you running two
|
||||
Jackson stacks, one for your HTTP layer and one for your messaging layer, with independent
|
||||
configuration. The better fix is one word:
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
kafka:
|
||||
producer:
|
||||
value-serializer: org.springframework.kafka.support.serializer.JacksonJsonSerializer
|
||||
consumer:
|
||||
value-deserializer: org.springframework.kafka.support.serializer.JacksonJsonDeserializer
|
||||
```
|
||||
|
||||
Jackson 3 handles `java.time` with no module and no configuration, and `BigDecimal` keeps its
|
||||
scale through a round trip — both asserted in
|
||||
[`SerialisationTest`](../src/test/java/com/ankurm/kafkabasics/SerialisationTest.java):
|
||||
|
||||
```
|
||||
{"orderId":"o-1","customerId":"c-1","amount":10.00,"placedAt":"2026-08-29T10:15:30Z"}
|
||||
```
|
||||
|
||||
`amount` is `10.00`, not `10.0` and not `"10.00"`. Use `BigDecimal` for money and this survives;
|
||||
use `double` and it does not.
|
||||
|
||||
## The type header
|
||||
|
||||
The three-argument `serialize(topic, headers, data)` overload writes a `__TypeId__` header
|
||||
naming the class:
|
||||
|
||||
```java
|
||||
headers.lastHeader("__TypeId__") -> "com.ankurm.kafkabasics.OrderEvent"
|
||||
```
|
||||
|
||||
The consumer reads it and builds that class. Which is convenient and is also a remote-code-
|
||||
selection primitive, so the deserializer refuses any class outside its trusted packages:
|
||||
|
||||
```
|
||||
... is not in the trusted packages: [java.util, java.lang]
|
||||
```
|
||||
|
||||
Three ways out, in descending order of preference:
|
||||
|
||||
1. `spring.json.trusted.packages: com.ankurm.kafkabasics` — an allow-list of your own packages.
|
||||
2. `spring.json.value.default.type` — ignore the header, always build this class. Best when
|
||||
producer and consumer are owned by different teams, because it makes the consumer's contract
|
||||
its own decision rather than the producer's.
|
||||
3. `spring.json.trusted.packages: "*"` — do not.
|
||||
|
||||
Option 2 has a second benefit worth stating plainly: the `__TypeId__` header couples the two
|
||||
services by **fully qualified class name**. Renaming or moving a class in the producer breaks
|
||||
every consumer that trusts the header, at runtime, with no compile-time warning. `type.mapping`
|
||||
(a logical name to class map on both sides) is the version of this that survives a refactor.
|
||||
|
||||
[Keys and partitions →](04-keys-and-partitions.md)
|
||||
51
kafka-basics/docs/04-keys-and-partitions.md
Normal file
51
kafka-basics/docs/04-keys-and-partitions.md
Normal file
@@ -0,0 +1,51 @@
|
||||
[← Serialisation](03-serialisation.md) · [Module README](../README.md) · [Consuming →](05-consuming.md)
|
||||
|
||||
# 4. Keys and partitions: the ordering guarantee in disguise
|
||||
|
||||
Kafka orders records **within a partition**. Not within a topic. So the key is not a label — it
|
||||
decides which records are ordered with respect to each other, and it is the most consequential
|
||||
line in a producer.
|
||||
|
||||
Same key, same partition, forever:
|
||||
|
||||
```
|
||||
key partition murmur2 & 0x7fffffff % 3 Math.abs(murmur2) % 3
|
||||
o-1 0 0 0
|
||||
o-2 0 0 2
|
||||
o-3 1 1 1
|
||||
o-4 1 1 1
|
||||
o-5 2 2 0
|
||||
o-6 1 1 1
|
||||
```
|
||||
|
||||
From [`docs/output/key-to-partition.txt`](output/key-to-partition.txt), produced against a real
|
||||
broker by
|
||||
[`KeysAndPartitionsTest`](../src/test/java/com/ankurm/kafkabasics/KeysAndPartitionsTest.java).
|
||||
|
||||
The default partitioner is `murmur2` of the **serialized key bytes**, masked positive, modulo
|
||||
the partition count. Note the third and fourth columns: `& 0x7fffffff` and `Math.abs` disagree
|
||||
on two of six keys, because clearing the sign bit is not the same number as negating it. If you
|
||||
reimplement the partitioner to predict placement — for a test, for a migration, for a routing
|
||||
table — `Math.abs` gives you the right answer about half the time, which is the worst available
|
||||
failure mode.
|
||||
|
||||
## Three consequences
|
||||
|
||||
**A null key is not a key.** Records without one are spread across partitions by the sticky
|
||||
partitioner, so nothing about their relative order is guaranteed. If two events describe the same
|
||||
entity and you did not key them, they can be processed out of order by different consumers.
|
||||
|
||||
**Adding partitions repartitions every key.** The modulus changes, so `o-2` moves. Anything
|
||||
relying on per-key ordering loses it across the resize for records still in flight. Pick the
|
||||
partition count with room to grow; changing it later is a data-ordering event, not a capacity
|
||||
knob.
|
||||
|
||||
**Key cardinality is your parallelism ceiling.** Keying by `customerId` when one customer is 40%
|
||||
of traffic gives you a hot partition that no amount of consumer scaling fixes, because one
|
||||
partition is consumed by exactly one member of a group.
|
||||
|
||||
Choosing the key is choosing what must stay ordered. Order events by `orderId` if operations on
|
||||
one order must not overtake each other; by `customerId` if that is true across a customer's
|
||||
orders. Those are different systems.
|
||||
|
||||
[Consuming →](05-consuming.md)
|
||||
59
kafka-basics/docs/05-consuming.md
Normal file
59
kafka-basics/docs/05-consuming.md
Normal file
@@ -0,0 +1,59 @@
|
||||
[← Keys and partitions](04-keys-and-partitions.md) · [Module README](../README.md) · [Acknowledgement →](06-acknowledgement.md)
|
||||
|
||||
# 5. Consuming
|
||||
|
||||
```java
|
||||
@KafkaListener(topics = "orders", groupId = "orders-basic")
|
||||
public void onOrder(@Payload OrderEvent event,
|
||||
@Header(KafkaHeaders.RECEIVED_PARTITION) int partition,
|
||||
@Header(KafkaHeaders.OFFSET) long offset) { ... }
|
||||
```
|
||||
|
||||
There is no poll loop, no offset commit and no rebalance listener, because the container owns
|
||||
all three. That is the actual value Spring Kafka adds over the plain client, and it is worth
|
||||
knowing what it is doing on your behalf — see [chapter 6](06-acknowledgement.md).
|
||||
|
||||
## The single most common "my listener never fires"
|
||||
|
||||
`auto.offset.reset` decides where a consumer group with **no committed offset** starts. The
|
||||
kafka-clients default is `latest`: skip everything already in the topic. So a brand-new group
|
||||
attached to a topic full of records reads nothing, and the listener you just wrote never runs.
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
kafka:
|
||||
consumer:
|
||||
auto-offset-reset: earliest
|
||||
```
|
||||
|
||||
Boot does not change this default; the value in this module's `application.yaml` does. It only
|
||||
applies when there is no committed offset — once the group has committed once, this setting is
|
||||
irrelevant, which is why the symptom disappears the first time it works and never comes back.
|
||||
|
||||
The other reliable cause is a `groupId` mismatch between the annotation and
|
||||
`spring.kafka.consumer.group-id`. The annotation wins, and a typo there creates a brand-new group
|
||||
that then hits the paragraph above.
|
||||
|
||||
## Concurrency and what it can and cannot buy
|
||||
|
||||
`spring.kafka.listener.concurrency` creates that many consumers in the group, each on its own
|
||||
thread. The ceiling is the partition count: with 3 partitions, `concurrency: 10` gives you three
|
||||
working consumers and seven idle ones. There is no configuration that makes two threads consume
|
||||
one partition, because that would break the ordering guarantee from
|
||||
[chapter 4](04-keys-and-partitions.md).
|
||||
|
||||
## Headers worth knowing
|
||||
|
||||
| Constant | What it carries |
|
||||
|---|---|
|
||||
| `KafkaHeaders.RECEIVED_KEY` | the producer's key, through the key deserializer |
|
||||
| `KafkaHeaders.RECEIVED_PARTITION` | which partition this came from |
|
||||
| `KafkaHeaders.OFFSET` | the offset within that partition |
|
||||
| `KafkaHeaders.RECEIVED_TIMESTAMP` | broker or producer timestamp, per topic config |
|
||||
| `__TypeId__` | the class name the producer wrote — see [chapter 3](03-serialisation.md) |
|
||||
|
||||
Declare the key as `required = false` unless you are certain every record has one. A `null` key
|
||||
on a `required = true` header parameter fails the conversion, and the failure arrives as a
|
||||
deserialization-time error rather than as a null.
|
||||
|
||||
[Acknowledgement →](06-acknowledgement.md)
|
||||
79
kafka-basics/docs/06-acknowledgement.md
Normal file
79
kafka-basics/docs/06-acknowledgement.md
Normal file
@@ -0,0 +1,79 @@
|
||||
[← Consuming](05-consuming.md) · [Module README](../README.md) · [Testing →](07-testing.md)
|
||||
|
||||
# 6. Acknowledgement, and a property that is not where you look for it
|
||||
|
||||
The container's default `AckMode` is **`BATCH`**: commit the offsets of the whole `poll()` batch
|
||||
after the listener has returned for every record in it. Confirmed at runtime by
|
||||
[`EffectiveConfigTest`](../src/test/java/com/ankurm/kafkabasics/EffectiveConfigTest.java):
|
||||
|
||||
```
|
||||
=== listener container ===
|
||||
ackMode BATCH
|
||||
groupId orders-basic
|
||||
```
|
||||
|
||||
That gives at-least-once delivery. A crash after processing and before the commit redelivers the
|
||||
batch, which is why your listener must be idempotent — and why
|
||||
[the error-handling article](https://ankurm.com/spring-kafka-4-1-error-handling-dlt-retry-topics/)
|
||||
is a necessary sequel rather than an optional one.
|
||||
|
||||
## Where `enable.auto.commit` actually lives
|
||||
|
||||
This one is worth the detour, because every debugging session about commits starts in the wrong
|
||||
place.
|
||||
|
||||
- The **kafka-clients default** for `enable.auto.commit` is `true`.
|
||||
- **Spring Boot does not set it.** It is absent from `ConsumerFactory.getConfigurationProperties()`,
|
||||
before the containers start and after.
|
||||
- **`ConsumerFactory.isAutoCommit()` returns `true`** on a stock Boot 4.1 application, because it
|
||||
reads that same absent key and falls back to the client default.
|
||||
- And yet **no consumer auto-commits**, because `ListenerConsumer.determineAutoCommit` checks
|
||||
whether the factory config contains the key and, when it does not, calls
|
||||
`setProperty("enable.auto.commit", "false")` on the **per-container `Properties`** handed to
|
||||
`createConsumer`.
|
||||
|
||||
So the shared factory never learns, its public accessor answers the opposite of the truth, and
|
||||
the real value lives in an override map you cannot reach from application code. All four of
|
||||
those statements are asserted in the test, including the counter-intuitive one:
|
||||
|
||||
```java
|
||||
assertThat(this.consumerFactory.isAutoCommit()).isTrue();
|
||||
```
|
||||
|
||||
If you ever need to know whether a running consumer auto-commits, read the consumer's own
|
||||
metrics or its startup log line, not the factory.
|
||||
|
||||
## Manual acknowledgement
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
kafka:
|
||||
listener:
|
||||
ack-mode: MANUAL
|
||||
```
|
||||
|
||||
```java
|
||||
@KafkaListener(topics = "orders")
|
||||
void onOrder(OrderEvent event, Acknowledgment acknowledgment) {
|
||||
process(event);
|
||||
acknowledgment.acknowledge();
|
||||
}
|
||||
```
|
||||
|
||||
Nothing commits until `acknowledge()` runs, which is what you want when the work must be durable
|
||||
before the offset moves. Two things to know:
|
||||
|
||||
- Asking for `MANUAL` while auto-commit is genuinely enabled is an
|
||||
`IllegalStateException` at container start. It does not happen on a stock configuration —
|
||||
despite `isAutoCommit()` returning `true` — because `determineAutoCommit` sets the container
|
||||
property to `false` before the check runs. [`ManualAckTest`](../src/test/java/com/ankurm/kafkabasics/ManualAckTest.java)
|
||||
pins that down, because it is exactly the kind of interaction that would otherwise be a
|
||||
surprise in production.
|
||||
- **An `Acknowledgment` you forget to call stalls the partition.** Not immediately — the
|
||||
container keeps polling until `max.poll.records` of un-acknowledged records accumulate. So the
|
||||
symptom is a consumer that works for a while and then stops, which reads like a broker problem.
|
||||
|
||||
`MANUAL_IMMEDIATE` commits synchronously on the consumer thread instead of at the end of the
|
||||
batch. It is slower and it is the right choice when redelivery is genuinely expensive.
|
||||
|
||||
[Testing →](07-testing.md)
|
||||
84
kafka-basics/docs/07-testing.md
Normal file
84
kafka-basics/docs/07-testing.md
Normal file
@@ -0,0 +1,84 @@
|
||||
[← Acknowledgement](06-acknowledgement.md) · [Module README](../README.md)
|
||||
|
||||
# 7. Testing without installing Kafka
|
||||
|
||||
Two options, both real brokers, and the choice is less obvious than it looks.
|
||||
|
||||
## `@EmbeddedKafka` — a real broker inside the JVM
|
||||
|
||||
```java
|
||||
@SpringBootTest
|
||||
@EmbeddedKafka(topics = "orders", partitions = 3)
|
||||
class KeysAndPartitionsTest { ... }
|
||||
```
|
||||
|
||||
`spring-kafka-test` starts `EmbeddedKafkaKraftBroker` — the actual Apache Kafka broker classes,
|
||||
in KRaft mode, in-process. No ZooKeeper, no container, no daemon. It binds a random port and
|
||||
exposes it as `${spring.embedded.kafka.brokers}`:
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
kafka:
|
||||
bootstrap-servers: ${spring.embedded.kafka.brokers}
|
||||
```
|
||||
|
||||
It starts in about three seconds and needs nothing installed, which is why every transcript in
|
||||
this module came from it and why `./scripts/run-all.sh` works on a machine with no Docker.
|
||||
|
||||
## Testcontainers — the image you actually deploy
|
||||
|
||||
```java
|
||||
@TestConfiguration(proxyBeanMethods = false)
|
||||
public class TestcontainersConfiguration {
|
||||
|
||||
@Bean
|
||||
@ServiceConnection
|
||||
KafkaContainer kafkaContainer() {
|
||||
return new KafkaContainer(DockerImageName.parse("apache/kafka:4.1.0"));
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
`@ServiceConnection` registers the container's bootstrap servers as the application's, which
|
||||
removes the `@DynamicPropertySource` block that older examples all carry.
|
||||
|
||||
**Two coordinates changed recently and both will bite you:**
|
||||
|
||||
```xml
|
||||
<!-- NOT org.testcontainers:kafka, which stopped at 1.21.4 -->
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>testcontainers-kafka</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
Testcontainers 2.x prefixed every module artifact with `testcontainers-`, and Boot 4.1.1 imports
|
||||
`testcontainers-bom` 2.0.5, which manages only the new names. Using the old coordinate fails
|
||||
with a Maven error that does not mention the rename:
|
||||
|
||||
```
|
||||
'dependencies.dependency.version' for org.testcontainers:kafka:jar is missing
|
||||
```
|
||||
|
||||
The class moved too: use `org.testcontainers.kafka.KafkaContainer` (Apache Kafka, KRaft), not
|
||||
the older `org.testcontainers.containers.KafkaContainer` (Confluent images, ZooKeeper).
|
||||
|
||||
## Which to use
|
||||
|
||||
| | `@EmbeddedKafka` | Testcontainers |
|
||||
|---|---|---|
|
||||
| startup | ~3s | ~10s, plus image pull |
|
||||
| needs Docker | no | yes |
|
||||
| broker version | the client library's | whatever image you name |
|
||||
| TLS, SASL, quotas, partitions | not modelled | real |
|
||||
|
||||
Use `@EmbeddedKafka` for the bulk of a suite and Testcontainers for the handful of tests where
|
||||
the difference between "the broker classes" and "the broker you deploy" matters. The
|
||||
[`TestcontainersConfiguration`](../src/test/java/com/ankurm/kafkabasics/TestcontainersConfiguration.java)
|
||||
in this module is compiled but not exercised by `run-all.sh`, because the machine that
|
||||
regenerates `docs/output/` has no Docker daemon — which is itself the argument for keeping
|
||||
both paths.
|
||||
|
||||
[Module README](../README.md)
|
||||
71
kafka-basics/docs/output/effective-config.txt
Normal file
71
kafka-basics/docs/output/effective-config.txt
Normal file
@@ -0,0 +1,71 @@
|
||||
=== producer ===
|
||||
property kafka-clients default set by Spring Boot
|
||||
--------------------------------------------------------------------------------------------------------
|
||||
acks all -
|
||||
enable.idempotence true -
|
||||
retries 2147483647 -
|
||||
max.in.flight.requests.per.connection 5 -
|
||||
linger.ms 5 -
|
||||
batch.size 16384 -
|
||||
compression.type none -
|
||||
delivery.timeout.ms 120000 -
|
||||
|
||||
=== consumer ===
|
||||
property kafka-clients default set by Spring Boot
|
||||
--------------------------------------------------------------------------------------------------------
|
||||
auto.offset.reset latest earliest
|
||||
enable.auto.commit true -
|
||||
max.poll.records 500 -
|
||||
max.poll.interval.ms 300000 -
|
||||
session.timeout.ms 45000 -
|
||||
heartbeat.interval.ms 3000 -
|
||||
isolation.level read_uncommitted read_uncommitted
|
||||
partition.assignment.strategy [class org.apache.kafka.clients.consumer.RangeAssignor, class org.apache.kafka.clients.consumer.CooperativeStickyAssignor] -
|
||||
|
||||
=== listener container ===
|
||||
=== listener container ===
|
||||
ackMode BATCH
|
||||
groupId orders-basic
|
||||
concurrency
|
||||
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.828 s -- in com.ankurm.kafkabasics.EffectiveConfigTest
|
||||
[INFO] Running com.ankurm.kafkabasics.SerialisationTest
|
||||
JacksonJsonSerializer -> {"orderId":"o-1","customerId":"c-1","amount":10.00,"placedAt":"2026-08-29T10:15:30Z"}
|
||||
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.227 s -- in com.ankurm.kafkabasics.SerialisationTest
|
||||
[INFO] Running com.ankurm.kafkabasics.KeysAndPartitionsTest
|
||||
=== key -> partition, 3 partitions ===
|
||||
key partition murmur2 & 0x7fffffff % 3 Math.abs(murmur2) % 3
|
||||
o-1 0 0 0
|
||||
o-2 0 0 2
|
||||
o-3 1 1 1
|
||||
o-4 1 1 1
|
||||
o-5 2 2 0
|
||||
o-6 1 1 1
|
||||
2026-08-29T09:44:16.725+05:30 INFO 67 --- [kafka-basics] [ntainer#0-0-C-1] com.ankurm.kafkabasics.OrderConsumer : received orderId=o-1 partition=0 offset=0
|
||||
2026-08-29T09:44:16.726+05:30 INFO 67 --- [kafka-basics] [ntainer#0-0-C-1] com.ankurm.kafkabasics.OrderConsumer : received orderId=o-2 partition=0 offset=1
|
||||
2026-08-29T09:44:16.726+05:30 INFO 67 --- [kafka-basics] [ntainer#0-0-C-1] com.ankurm.kafkabasics.OrderConsumer : received orderId=o-1 partition=0 offset=2
|
||||
2026-08-29T09:44:16.726+05:30 INFO 67 --- [kafka-basics] [ntainer#0-0-C-1] com.ankurm.kafkabasics.OrderConsumer : received orderId=o-3 partition=1 offset=0
|
||||
2026-08-29T09:44:16.726+05:30 INFO 67 --- [kafka-basics] [ntainer#0-0-C-1] com.ankurm.kafkabasics.OrderConsumer : received orderId=o-4 partition=1 offset=1
|
||||
2026-08-29T09:44:16.727+05:30 INFO 67 --- [kafka-basics] [ntainer#0-0-C-1] com.ankurm.kafkabasics.OrderConsumer : received orderId=o-6 partition=1 offset=2
|
||||
2026-08-29T09:44:16.727+05:30 INFO 67 --- [kafka-basics] [ntainer#0-0-C-1] com.ankurm.kafkabasics.OrderConsumer : received orderId=o-5 partition=2 offset=0
|
||||
2026-08-29T09:44:16.746+05:30 INFO 67 --- [kafka-basics] [ntainer#0-0-C-1] com.ankurm.kafkabasics.OrderConsumer : received orderId=e-1 partition=0 offset=3
|
||||
2026-08-29T09:44:16.749+05:30 INFO 67 --- [kafka-basics] [ntainer#0-0-C-1] com.ankurm.kafkabasics.OrderConsumer : received orderId=e-2 partition=1 offset=3
|
||||
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.532 s -- in com.ankurm.kafkabasics.KeysAndPartitionsTest
|
||||
[INFO] Running com.ankurm.kafkabasics.ManualAckTest
|
||||
Bootstrap metadata: BootstrapMetadata(records=[ApiMessageAndVersion(FeatureLevelRecord(name='metadata.version', featureLevel=30) at version 0), ApiMessageAndVersion(FeatureLevelRecord(name='eligible.leader.replicas.version', featureLevel=1) at version 0), ApiMessageAndVersion(FeatureLevelRecord(name='group.version', featureLevel=1) at version 0), ApiMessageAndVersion(FeatureLevelRecord(name='share.version', featureLevel=1) at version 0), ApiMessageAndVersion(FeatureLevelRecord(name='streams.version', featureLevel=1) at version 0), ApiMessageAndVersion(FeatureLevelRecord(name='transaction.version', featureLevel=2) at version 0)], metadataVersionLevel=30, source=format command)
|
||||
Formatting metadata directory /tmp/kafka-14668657738428164299/combined_0_0 with metadata.version 4.3-IV0.
|
||||
2026-08-29T09:44:17.569+05:30 INFO 67 --- [kafka-basics] [ main] com.ankurm.kafkabasics.ManualAckTest : Starting ManualAckTest using Java 25.0.4.1 with PID 67 (started by tender-adoring-cori in /tmp/work/smd/kafka-basics)
|
||||
2026-08-29T09:44:17.570+05:30 INFO 67 --- [kafka-basics] [ main] com.ankurm.kafkabasics.ManualAckTest : The following 1 profile is active: "test"
|
||||
2026-08-29T09:44:17.780+05:30 INFO 67 --- [kafka-basics] [ main] com.ankurm.kafkabasics.ManualAckTest : Started ManualAckTest in 0.503 seconds (process running for 6.368)
|
||||
2026-08-29T09:44:18.617+05:30 INFO 67 --- [kafka-basics] [ntainer#0-0-C-1] com.ankurm.kafkabasics.OrderConsumer : received orderId=o-1 partition=0 offset=0
|
||||
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.919 s -- in com.ankurm.kafkabasics.ManualAckTest
|
||||
[INFO]
|
||||
[INFO] Results:
|
||||
[INFO]
|
||||
[INFO] Tests run: 8, Failures: 0, Errors: 0, Skipped: 0
|
||||
[INFO]
|
||||
[INFO] ------------------------------------------------------------------------
|
||||
[INFO] BUILD SUCCESS
|
||||
[INFO] ------------------------------------------------------------------------
|
||||
[INFO] Total time: 9.869 s
|
||||
[INFO] Finished at: 2026-08-29T09:44:20+05:30
|
||||
[INFO] ------------------------------------------------------------------------
|
||||
7
kafka-basics/docs/output/key-to-partition.txt
Normal file
7
kafka-basics/docs/output/key-to-partition.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
key partition murmur2 & 0x7fffffff % 3 Math.abs(murmur2) % 3
|
||||
o-1 0 0 0
|
||||
o-2 0 0 2
|
||||
o-3 1 1 1
|
||||
o-4 1 1 1
|
||||
o-5 2 2 0
|
||||
o-6 1 1 1
|
||||
1
kafka-basics/docs/output/serialised-payload.txt
Normal file
1
kafka-basics/docs/output/serialised-payload.txt
Normal file
@@ -0,0 +1 @@
|
||||
JacksonJsonSerializer -> {"orderId":"o-1","customerId":"c-1","amount":10.00,"placedAt":"2026-08-29T10:15:30Z"}
|
||||
4
kafka-basics/docs/output/tests.txt
Normal file
4
kafka-basics/docs/output/tests.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.828 s -- in com.ankurm.kafkabasics.EffectiveConfigTest
|
||||
Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.227 s -- in com.ankurm.kafkabasics.SerialisationTest
|
||||
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.532 s -- in com.ankurm.kafkabasics.KeysAndPartitionsTest
|
||||
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.919 s -- in com.ankurm.kafkabasics.ManualAckTest
|
||||
Reference in New Issue
Block a user