You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
refactor(core) #225: put recovery on producerConfig alone, and move the factory, the derived id, redaction and the deprecation up the stack
This PR now stacks on #426 - producerConfig with the default constructor -
rather than on #420. Everything #420 carries beyond that rung leaves
this branch's tree here and returns above it, in #420 itself, which now stacks on this PR:
ProducerFactory and its contract exception, TransactionalIdDerivation, ProducerConfigRedaction (and
their tests), the deprecation of the instance option with its WARN and removal queue, the README
ownership section and migration note, and the four migrated example apps, which go back to master's.
What recovery is built on instead: the module's protected buildProducer(Map) seam from #426.
replacementProducerWrap() builds another wrapper through it from the same configuration, the caller's
transactional.id included, and hands the id to the ReplacementProducerSource so a failure to build can
still name it. The build runs through UserFunctions.carefullyRun as before: the seam is overridable,
and an Error from the constructor must surface as a build failure rather than escape the recovery
pass. The policy's terminal set loses the factory-contract clause with the factory; Authorization,
UnsupportedVersion and Error stay.
Tests move with it. ProducerRecoveryTest hands ParallelEoSStreamProcessor a PCModule whose seam hands
out the spied MockProducers the factory used to, records the id each build was handed, and asserts it
is the caller's, unchanged, on every build; the log-line and failure-message assertions key on a
per-instance id the test sets, as they keyed on the derived one. ProducerFencingRecoveryIT sets its
own transactional.id so the rogue can fence under it, and drops the derived-shape assertion, which
returns with the derivation. ProducerManagerRecoveryTest's factory-contract case leaves with the
contract. The two option/module test classes are rung 1's.
Records: the plan carries a dated re-cut note at its head rather than a rewrite of its units; the
in-flight note lists the three-PR stack; the roadmap entry, the issue-response draft, the refactoring
queue and the README template (a producer-recovery section only, no ownership section) say what this
PR now says.
Mechanically this commit is the inverse of what #420 becomes: reverting it on top of this
branch is how #420 is re-based, so its history is kept too.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VrpH51xNDodaajE4P2nhFg
A Producer is only required if using the `produce` flows: hand PC either a finished `Producer` instance, or the configuration to build one from as `producerConfig` - any `ProducerConfig` key, serializers included, and PC builds it with `new KafkaProducer<>(config)`, and builds it again if the broker invalidates it (see <<producer-recovery>>).
545
+
In the transactional commit mode, set `transactional.id` in that configuration as you would when building the producer yourself.
544
546
<2> Choose your ordering type, `KEY` in this case.
545
547
This ensures maximum concurrency, while ensuring messages are processed and committed in `KEY` order, making sure no offset is committed unless all offsets before it in it's partition, are completed also.
546
548
<3> The maximum number of concurrent processing operations to be performing at any given time.
547
549
Also, because the library coordinates offsets, `enable.auto.commit` must be disabled in your consumer.
548
-
<4> Producer configuration, only required if using the `produce` flows: PC builds the producer from it through the factory on the next line (the default is `new KafkaProducer<>(config)`; override it to wrap or instrument the producer, keeping every key it is given), and rebuilds it if the broker invalidates it - see <<producer-ownership>> and <<producer-recovery>>.
549
-
Handing PC a finished `Producer` instance still works, deprecated.
550
550
<5> Subscribe to your topics
551
551
552
552
NOTE: Because the library coordinates offsets, `enable.auto.commit` must be disabled.
@@ -635,7 +635,7 @@ There is no guarantee that the messages will be retried again in the same batch.
@@ -794,18 +794,6 @@ The API is source-compatible, so beyond the import lines no source edit is neede
794
794
Committed offset metadata is unchanged too, so an existing consumer group can be upgraded in place without resetting or migrating offsets.
795
795
The library's shared internal utilities moved with the rest, into `bz.stub.parallelconsumer.internal.utils` -- they were always plumbing, they are not part of the published API, and they are now named as such.
796
796
797
-
==== Move from a producer instance to producer configuration
798
-
799
-
The `producer` option - a finished `Producer` instance - is deprecated in favour of `producerConfig` (plus, optionally, `producerFactory`), and its removal is queued for `0.7.0.0`.
800
-
Nothing forces the move in this release, but only the configuration path recovers when the broker invalidates the producer; see <<producer-recovery>>.
801
-
To move:
802
-
803
-
* Replace `.producer(kafkaProducer)` with `.producerConfig(producerProperties)`, the same map you built the producer from, serializers included.
804
-
* Drop any `transactional.id` from that map. PC derives one per running instance, and a value you set is replaced with a WARN.
805
-
* If your cluster grants TransactionalId ACLs, grant a prefixed one on `pc-<L>-<group.id>-` (`<L>` being the length of your `group.id`) - the literal id you used to grant on will no longer be used.
806
-
* If operational tooling keyed on your `transactional.id` - dashboards, alerts, `kafka-transactions.sh` scripts - it now needs the derived prefix instead; the exact id is logged at start-up.
807
-
* If you wrapped or instrumented the producer, do that in a `producerFactory` instead; it receives the resolved configuration and must build from it unchanged.
808
-
809
797
=== From 0.4 to 0.5
810
798
811
799
This version has a breaking change in the API - instead of passing in `ConsumerRecord` instances, it passes in a `PollContext` object which has extra information and utility methods.
@@ -1101,24 +1089,11 @@ It is covered by an existing test, but no negative control was observed for it,
1101
1089
The produce-lock timeout returning the record for retry started out the same way and no longer is: the eager-processing test holds the commit lock shut until a worker's acquisition is observed to time out, then watches that record's retry succeed, and removing the hold turns it red.
1102
1090
It is recorded `PROVED`.
1103
1091
1104
-
[[producer-ownership]]
1105
-
=== Producer ownership
1106
-
1107
-
Supply the producer as configuration - `producerConfig`, the map you would hand `new KafkaProducer<>(config)` - and PC builds the producer itself, through `producerFactory` (default: exactly that constructor; override it to wrap or instrument the producer, keeping every key it is given).
1108
-
PC sets the `transactional.id` where it builds the producer: `pc-<L>-<group.id>-<uuid>`, where `<L>` is the decimal length of the consumer's `group.id`.
1109
-
The prefix `pc-<L>-<group.id>-` is stable for the group, so one prefixed TransactionalId ACL authorises every id PC derives for it, and the length field keeps one group's prefix from being a prefix of another's (`app` versus `app-x`).
1110
-
A `transactional.id` set in `producerConfig` does not take effect there, and PC says so at WARN naming the value it derived instead.
1111
-
Configuration values are never rendered into logs: PC's own log lines and `toString()` show an allow-list of non-secret keys and redact the rest.
1112
-
A factory must return a new producer on every call - PC may discard one and build another from the same configuration - and a factory that returns one it already returned, or drops the `transactional.id` it was given, is rejected at construction naming that contract.
1113
-
1114
-
Supplying a finished `Producer` instance still works for every flow it worked for, and is deprecated: PC cannot read a finished producer's configuration back out, so it can never build another from it.
1115
-
PC logs one WARN at start-up naming the remedy.
1116
-
The instance option's removal is queued for `0.7.0.0`.
1117
-
1118
1092
[[producer-recovery]]
1119
1093
=== Producer recovery
1120
1094
1121
1095
Where PC built the producer, a producer the broker reports invalid is replaced and the work its aborted transaction discarded is processed again, so processing continues instead of the instance stopping.
1096
+
The replacement is built from the same configuration, `transactional.id` included, and initialising it is what fences the producer it replaces; a finished `Producer` instance carries no configuration to rebuild from, so on that path these conditions keep their earlier behaviour.
1122
1097
Recovery is a transactional-mode mechanism: it runs from the transactional commit loop, so in the consumer-commit modes a PC-built producer is not recovered and these conditions surface as the produce failures they were before.
1123
1098
To make the replay possible, transactional mode keeps every completed record until the commit that carries it succeeds - on both producer paths - so memory held per partition grows with `commitInterval`; the default interval keeps it to a fraction of a second of throughput.
1124
1099
The conditions PC treats this way are `ProducerFencedException`, `InvalidProducerEpochException`, `InvalidPidMappingException`, `OutOfOrderSequenceException` (and so `UnknownProducerIdException`) and `CommitFailedException`, on both the commit path and the produce path, unwrapped from the `ExecutionException` a send future raises and the `KafkaException` the client wraps a stored error in.
Copy file name to clipboardExpand all lines: docs/plans/2026-09-02-001-feat-recoverable-producer-fencing-plan.md
+4Lines changed: 4 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -11,6 +11,8 @@ execution: code
11
11
12
12
# Recoverable Producer Fencing - Plan
13
13
14
+
> **Re-cut into a stack, 2026-09-03.** The producer-ownership half of this plan ships in stages rather than in one PR: `producerConfig` with the default constructor is the base rung (astubbs/parallel-consumer#426); recovery (astubbs/parallel-consumer#410) stacks on that rung alone and rebuilds under the `transactional.id` the caller sets; the derived prefix-free id, the `ProducerFactory` and its contract, configuration redaction, and the deprecation of the instance option with the example migration are astubbs/parallel-consumer#420, stacked above recovery. Where the text below says the factory or the derived id is what recovery rebuilds through, read: the same configuration, id included - the mechanisms and decisions are unchanged, only the order they land in.
15
+
14
16
## Goal Capsule
15
17
16
18
**Objective.** Give Parallel Consumer a transactional producer it owns, so that when the broker tells it the producer is no longer usable, PC closes that producer, builds a replacement, and carries on — instead of spinning forever on the produce path or shutting the instance down on the commit path. Tracked as astubbs#225.
@@ -331,6 +333,7 @@ U1 → U2 → U3 → U5 → U6 → U7; U4 has no dependencies and may be built a
-**Files:**`parallel-consumer-core/src/main/java/bz/stub/parallelconsumer/ParallelConsumerOptions.java`, new `parallel-consumer-core/src/main/java/bz/stub/parallelconsumer/ProducerFactory.java`, new `parallel-consumer-core/src/main/java/bz/stub/parallelconsumer/internal/ProducerConfigRedaction.java`, `docs/refactoring.md` (the entry exists; only re-check the release it names), tests in new `parallel-consumer-core/src/test/java/bz/stub/parallelconsumer/ParallelConsumerOptionsProducerConfigTest.java` and new `parallel-consumer-core/src/test/java/bz/stub/parallelconsumer/internal/ProducerConfigRedactionTest.java`.
336
+
<!-- file-refs: N/A - the factory, derivation and redaction files are astubbs#420's, stacked above this PR; see the re-cut note at the head of this plan -->
334
337
-**Approach:**
335
338
1. Add `producerConfig` (`Map<String, Object>`) and `producerFactory` (`ProducerFactory<K, V>`, `@Builder.Default` to a `KafkaProducer` constructor) fields; `isProducerSupplied()` becomes "instance or configuration present".
336
339
2.`validate()`: both `producer` and `producerConfig` set fails with a message naming `Fields.producer` and `Fields.producerConfig` (R17); transactional mode with neither fails as today; `producer` set alone logs the single R19 WARN naming `producerConfig` plus `producerFactory`, the absence of recovery, and the removal release.
@@ -353,6 +356,7 @@ U1 → U2 → U3 → U5 → U6 → U7; U4 has no dependencies and may be built a
353
356
-**Requirements:** R2, R4, R5, R6; KTD2, KTD8.
354
357
-**Dependencies:** U1.
355
358
-**Files:**`parallel-consumer-core/src/main/java/bz/stub/parallelconsumer/internal/PCModule.java`, `parallel-consumer-core/src/main/java/bz/stub/parallelconsumer/internal/ProducerWrapper.java`, new `parallel-consumer-core/src/main/java/bz/stub/parallelconsumer/internal/TransactionalIdDerivation.java`, `parallel-consumer-core/src/test/java/bz/stub/parallelconsumer/internal/PCModuleTestEnv.java` (constructor change follow-through), tests in new `parallel-consumer-core/src/test/java/bz/stub/parallelconsumer/internal/TransactionalIdDerivationTest.java` and new `parallel-consumer-core/src/test/java/bz/stub/parallelconsumer/internal/PcBuiltProducerTest.java`.
359
+
<!-- file-refs: N/A - the factory, derivation and redaction files are astubbs#420's, stacked above this PR; see the re-cut note at the head of this plan -->
356
360
-**Approach:**
357
361
1.`TransactionalIdDerivation`: `prefixFor(groupId)` returns `pc-<L>-<groupId>-`; `derive(groupId, instanceUuid)` appends the UUID; `resolve(producerConfig, commitMode, groupId, uuid)` returns a copy of the config with the id set (transactional mode) or removed (otherwise), emitting the R5 WARN when the caller had set one.
358
362
2.`PCModule`: one `UUID` per module instance; `producerWrap()` builds from the instance when present, otherwise resolves the config and calls the factory; new `replacementProducerWrap()` returns `Optional<Supplier<ProducerWrapper<K, V>>>`, present only on the configuration path, each call resolving the same config and calling the factory again.
(astubbs#225); the deprecation javadoc names the major AFTER the release that ships the deprecation, so the option is not removed in the same version that deprecates it.
128
121
-**Remove the JStream API** (deprecate first) - design ref
129
122
`origin/refactor/deprecate-jstream`@8a8f6508.
130
123
-**Rename the enum to the standard pattern** (public enum rename) -
0 commit comments