Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ Below are some examples. See [Index](./docs/index.adoc) for the full list.
* Retry Queuing - Retry a failed task with back-off without blocking other tasks flow
* Dynamic Rate Limiting - Apply and update processing rate quota dynamically
* Task Compaction - Crush preceding tasks which its processing results will be overwritten by following task
* Per-Key Quota - Detect bursting keys and isolate their tasks into shaping topics so that they don't dominate processing of other keys

# Performance

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public void init(Config config, Recording recording, ResourceTracker resourceTra
// at that time the offset might not have initialized (reset) to 0. If we start producing tasks before
// it completes fetching the current offset from the broker (which is zero), the offset counter might
// increments before it obtains offset information and consequences to reset the offset to a larger
// value than zero with the default "latest" reset policy.
// value than zero with "latest" reset policy.
props.setProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");

subPartitionRuntime = SubPartitionRuntime.THREAD_POOL;
Expand Down
112 changes: 112 additions & 0 deletions docs/auto-offset-reset.adoc

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this explains why we set auto.offset.reset=earliest by default, and how to override config by users who understand the risks

Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
= Where to Start Consuming (`auto.offset.reset`)
:base_version: 10.0.0
:modules: processor

== Introduction

Since Decaton 10.0.0, Decaton sets `auto.offset.reset` to `earliest` by default.
Kafka's default is `latest` as of Apache Kafka 4.3.

`auto.offset.reset` decides where a processor starts consuming a partition when the partition has no committed offset, or when the offset to resume from no longer exists in the partition (e.g. deleted by retention before being processed).
See https://kafka.apache.org/39/configuration/consumer-configs/#consumerconfigs_auto.offset.reset[Kafka documentation] for details.

Kafka consumers with `latest` can skip tasks silently in the scenarios below.
Decaton uses `earliest` by default because it is safer for at-least-once delivery.

== Why `earliest`

=== Adding partitions

Producers can start producing tasks to new partitions before processors get them assigned.
With `latest`, tasks produced in between are skipped.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now it's added, thank you!


To understand this in detail, you can read https://cwiki.apache.org/confluence/spaces/KAFKA/pages/406619800/KIP-1282+Prevent+data+loss+during+partition+expansion+for+dynamically+added+partitions[KIP-1282: Prevent data loss during partition expansion for dynamically added partitions].

=== Starting to use a topic

When you deploy a producer and a processor of a new consumer group (or change `group.id`), tasks produced before the processor starts are skipped with `latest`.

=== Restarting after committed offsets expired

If all processors of a consumer group are stopped longer than the offset retention of your Kafka cluster (`offsets.retention.minutes`, 7 days by default as of Apache Kafka 4.3), the committed offsets expire.
With `latest`, tasks produced while the processors were stopped are skipped after restarting.

=== Retry and per-key quota

With link:./retry-queueing.adoc[retry queueing] or per-key quota, Decaton produces tasks to the retry topic or shaping topics.
These topics receive tasks only when tasks are retried or shaped, and Decaton commits offsets only for processed tasks, so their partitions can stay without committed offsets for a long time.
For example, a partition of the retry topic has no committed offset until a retried task on it is processed for the first time.

With `latest`, a retried or shaped task on such a partition is skipped if a rebalance happens before the task is processed.
Tasks are often retried during a rebalance because processing can become unstable temporarily (e.g. during a deployment).
A retried task also waits for its backoff before being processed, so this can lead to subtle delivery loss.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah- good point. Now I get why retry-task is problematic in this scenario.


== Trade-off

With `earliest`, tasks in all of the scenarios above are processed.
In exchange, processors may process old tasks:

* A processor of a new consumer group processes all tasks remaining in the topic.
* After committed offsets expired, tasks remaining in the topic are processed again.
* When processors lag behind or are stopped longer than the retention of the topic, they process the remaining old tasks instead of jumping to the end of the partition.

[#options]
== Options if `earliest` doesn't fit

If your processor shouldn't process old tasks (e.g. it sends notifications), you can use options such as the following.
Each has its own risk.

=== Use `latest` or other options

Set `auto.offset.reset` in `consumerConfig`:

[source,java]
----
Properties consumerConfig = new Properties();
...
consumerConfig.setProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest");

ProcessorSubscription subscription =
SubscriptionBuilder.newBuilder("my-decaton-processor")
...
.consumerConfig(consumerConfig)
.buildAndStart();
----

The risk is that tasks are skipped silently in the scenarios above.

If you use `latest` only temporarily (e.g. for the first deployment), partitions on which no task has been processed in the meantime still have no committed offsets after you remove it, and are consumed from the beginning.

=== Prepare committed offsets before starting processors

Keep `earliest`, and create committed offsets for the new consumer group with `kafka-consumer-groups.sh` (the CLI tool of Apache Kafka) before starting processors.
For example:

[source,sh]
----
kafka-consumer-groups.sh \
--bootstrap-server $BOOTSTRAP_SERVERS \
--group $GROUP_ID \
--topic $TOPIC \
--reset-offsets \
--to-latest \
--execute
----

Specify only the origin topic, so that the retry topic and shaping topics are consumed from the beginning.
See https://kafka.apache.org/39/operations/basic-kafka-operations/#managing-consumer-groups[Kafka documentation] for details of `kafka-consumer-groups.sh`.

=== Discard old tasks in the processor

Keep `earliest`, and discard old tasks in your processor based on the timestamp of the task.

`context.metadata().timestampMillis()` is:

* the time when the task was produced, if it was produced by DecatonClient (unless you set it explicitly)
* the timestamp of the Kafka record, if it wasn't produced by DecatonClient
* the value set by the extractor, if you use a custom `TaskExtractor` (0 if not set)

You can also use a timestamp in the headers or the body of the task, if any.

A retried task keeps the timestamp of its original task, so the threshold should be longer than the total period of retries.
A task that wasn't produced by DecatonClient gets a new timestamp when it's shaped (the time when it was produced to the shaping topic).
2 changes: 1 addition & 1 deletion docs/dynamic-property-configuration.adoc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
= Dynamic Property Configuration
:base_version: 9.0.0
:base_version: 10.0.0
:modules: centraldogma,processor
:toc:

Expand Down
4 changes: 3 additions & 1 deletion docs/getting-started.adoc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
= Getting Started Decaton
:base_version: 9.0.0
:base_version: 10.0.0
:modules: common,client,processor,protobuf

Let's start from the most basic usage of Decaton client/processor.
Expand Down Expand Up @@ -163,6 +163,8 @@ public final class ProcessorMain {
----
<1> `subscriptionId` is Decaton's internal-use only ID for metric labeling, logging and so on as of current version.

NOTE: Decaton sets `auto.offset.reset` to `earliest` unless you set it in `consumerConfig`. See link:./auto-offset-reset.adoc[Where to Start Consuming] for the details.

== Test it

=== Preparing topic
Expand Down
3 changes: 2 additions & 1 deletion docs/index.adoc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
= Decaton Documents
:base_version: 9.0.0
:base_version: 10.0.0
:modules:

- link:./why-decaton.adoc[Why Decaton]
Expand All @@ -10,6 +10,7 @@
- link:./task-extractor.adoc[Implement custom task extractor]
- link:./dynamic-property-configuration.adoc[Dynamic property configuration for the processor]
- link:./monitoring.adoc[Monitoring Decaton]
- link:./auto-offset-reset.adoc[Where to Start Consuming (`auto.offset.reset`)]
- Features
* link:./retry-queueing.adoc[Retry Queueing]
* link:./rate-limiting.adoc[Rate Limiting]
Expand Down
2 changes: 1 addition & 1 deletion docs/key-blocking.adoc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
= Key Blocking
:base_version: 9.0.0
:base_version: 10.0.0
:modules: processor

== Introduction
Expand Down
2 changes: 1 addition & 1 deletion docs/monitoring.adoc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
= Monitoring Decaton
:base_version: 9.0.0
:base_version: 10.0.0
:modules: processor

This document guides you how to monitor your Decaton processor applications.
Expand Down
4 changes: 2 additions & 2 deletions docs/rate-limiting.adoc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
= Rate Limiting
:base_version: 9.0.0
:base_version: 10.0.0
:modules: processor

== Introduction
Expand Down Expand Up @@ -32,7 +32,7 @@ The value can have three different meanings:
|-1 (default)
|Unlimited

|Any positive number
|1 to 1,000,000

@Yang-33 Yang-33 Oct 8, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

* <li>(0, 1,000,000]: Do the best to process tasks as much as N per second.
* N may not be kept well if a task takes over a second to process or N is greater than
* actual throughput per second.</li>
* <li>0: Stop all processing but the task currently being processed isn`t interrupted</li>
* <li>-1: Unlimited</li>
* </ul>
* <p>
* See also {@link RateLimiter}.
* <p>
* Reloadable: yes
*/
public static final PropertyDefinition<Long> CONFIG_PROCESSING_RATE =
PropertyDefinition.define("decaton.processing.rate.per.partition", Long.class,
RateLimiter.UNLIMITED,
v -> v instanceof Long
&& RateLimiter.UNLIMITED <= (long) v
&& (long) v <= RateLimiter.MAX_RATE);

(unrelated to this change) document is not correct. Actually the max is 1,000,000

|Do the best to process tasks with the given value per second.
Note that the actual processing rate may not reach the given value if a task takes over a second to process or the value is greater than actual throughput per second.
|===
Expand Down
9 changes: 8 additions & 1 deletion docs/retry-queueing.adoc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
= Retry Queuing
:base_version: 9.0.0
:base_version: 10.0.0
:modules: processor

== Introduction
Expand Down Expand Up @@ -61,6 +61,13 @@ SubscriptionBuilder.newBuilder("testProcessor")
If you are using distributed tracing and want to see retries as part of the same trace,
make sure that RetryConfig contains a KafkaProducerSupplier that will propagate trace information.

[NOTE]
====
Retry topic partitions tend to stay without committed offsets for a long time since they receive tasks only when tasks are retried.
If you set `auto.offset.reset` to `latest` explicitly, retry tasks may be skipped silently on rebalance.
See link:./auto-offset-reset.adoc[Where to Start Consuming] for the details.
====

=== Retry with synchronous processing
After enabling the retry function, you can use it in your processor:

Expand Down
2 changes: 1 addition & 1 deletion docs/runtime.adoc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
= Subpartition Runtime
:base_version: 9.0.0
:base_version: 10.0.0
:modules: processor

This document guides you what Subpartition Runtime is and how to use it.
Expand Down
2 changes: 1 addition & 1 deletion docs/spring-integration.adoc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
= Spring Integration
:base_version: 0.0.43
:base_version: 10.0.0
:modules:

TBC
2 changes: 1 addition & 1 deletion docs/task-batching.adoc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
= Task Batching
:base_version: 9.0.0
:base_version: 10.0.0
:modules: processor

== Introduction
Expand Down
2 changes: 1 addition & 1 deletion docs/task-compaction.adoc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
= Task Compaction
:base_version: 9.0.0
:base_version: 10.0.0
:modules: processor

== Introduction
Expand Down
2 changes: 1 addition & 1 deletion docs/task-extractor.adoc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
= TaskExtractor
:base_version: 9.0.0
:base_version: 10.0.0
:modules: common,protocol,processor

[NOTE]
Expand Down
2 changes: 1 addition & 1 deletion docs/tracing.adoc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
= Tracing
:base_version: 9.0.0
:base_version: 10.0.0
:modules: brave,processor

Decaton can integrate with distributed tracing frameworks so that you can associate the processing of a message
Expand Down
2 changes: 1 addition & 1 deletion docs/why-decaton.adoc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
= Why Decaton
:base_version: 9.0.0
:base_version: 10.0.0
:modules: processor

This document explains why we have decided to create a new consumer framework.
Expand Down
Loading
Loading