Skip to content

MINOR: Add compaction replay tests for the group coordinator - #23220

Open
izzyharker wants to merge 7 commits into
apache:trunkfrom
confluentinc:compaction-replay-test
Open

MINOR: Add compaction replay tests for the group coordinator#23220
izzyharker wants to merge 7 commits into
apache:trunkfrom
confluentinc:compaction-replay-test

Conversation

@izzyharker

@izzyharker izzyharker commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Adds GroupMetadataManagerCompactionReplayTest, which tests partition
loading after compaction for scenarios involving offset commit combined
with classic->consumer/streams upgrades, as seen in
KAFKA-19862/KAFKA-20254.

Tests capture records, compact a contiguous stretch of the resulting log
(both prefix and mid-log), and replay the result(s) through a new
GroupCoordinatorShard.

  • testClassicGroupUpgradeToConsumerGroupWithOffsetCommit: A classic
    group has an offset commit, rebalance, consumer member join, online
    upgrade (classic->consumer), new member join. Verified this test fails
    when KAFKA-19862 is reverted.
  • testClassicGroupUpgradeToStreamsGroupWithOffsetCommit: Streams app
    runs on classic protocol, commits offsets, rebalances, then upgrades
    offline to streams protocol. Verified this test fails when KAFKA-20254
    is reverted.

izzyharker and others added 7 commits August 20, 2026 11:24
Adds GroupMetadataManagerCompactionReplayTest, which catches "compaction
makes a partition fail to load" bugs by replaying every realistic compacted
variant of a real group-coordinator record log through a fresh coordinator.

For each scenario it captures the records the coordinator writes (one batch
per write), then sweeps the clean/dirty compaction boundary across every batch
boundary, with tombstones retained and dropped, replaying each variant and
asserting it loads without throwing. Offset-commit records are replayed through
an OffsetMetadataManager sharing the GroupMetadataManager, so the
simple-classic-group creation on the load path is modelled faithfully.

The clean/dirty boundary is only split between batches, never inside one,
because log compaction cleans at segment granularity and a single write's
records are always compacted atomically. Splitting within a batch generates
logs real compaction can never produce (false positives).

Scenarios: rebalance, subscription change, member leave (tombstones), static
member rejoin, classic->consumer upgrade, and consumer/streams groups with an
offset commit. The last two reproduce a bug where, after compaction, the
offset commit sorts before the group records, creating a simple classic group
that the consumer/streams records must load on top of. Both fail if their
respective fixes in getOrMaybeCreatePersisted{Consumer,Streams}Group are
reverted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… isolation

Restore the fresh-coordinator-per-variant isolation in
assertCompactedVariantsLoadCleanly (a Supplier, not a single reused
context) so compacted variants can no longer leak state into one another.

Add two scenarios that reproduce the simple-classic-group load path over a
group that started classic and holds committed offsets:
- testClassicToConsumerUpgradeWithOffsetCommit... (upgrade to consumer)
- testClassicToStreamsMigrationWithOffsetCommit... (offline migration to
  streams)

Both were verified to fail (IllegalStateException "not a consumer/streams
group") when the respective isSimpleGroup() branch is disabled.

Make the docstrings consistent: tag the streams paths to KAFKA-20254,
describe the consumer paths as its pre-existing counterpart, and scope
out the "still owned at epoch" path (covered by GroupMetadataManagerTest)
since the clean-prefix/dirty-suffix model cannot reproduce it. Fix a typo
and trailing whitespace.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the per-batch clean-prefix model with a single cleaning window over
the log: a record is compactable when it is superseded by a later record with
the same key or is a tombstone, and each variant removes every compactable
record in one contiguous stretch. This subsumes the previous prefix-only model
and, because the window can start in the middle of the log, it also produces
the missed-unassignment shape behind KAFKA-19862, which a compacted prefix
cannot express. The separate drop-tombstones flag is gone.

Two constraints keep the variants to logs a load can actually observe: the
window falls on batch boundaries, since the records of one write share a
segment and therefore share the cleaner's verdict; and a tombstone is only
dropped once no earlier record for its key survives, since dropping an
aged-out tombstone happens in a later pass than the one that collapsed the key.

Collapse the nine narrow scenarios into two lifecycle scenarios driven through
the real request paths, as reviewed: a group created on the classic protocol
that commits offsets, rebalances, is upgraded online to the consumer protocol,
rolls its classic members onto the new protocol and scales out; and a Kafka
Streams application migrated offline from the classic protocol to the streams
protocol across three processes. Both mix classic GroupMetadata records and
their tombstone, offset commits and modern group records, and move partitions
or tasks between owners repeatedly.

Verified by reverting each fix in turn and re-running:
  - Streams simple-classic-group handling in
    getOrMaybeCreatePersistedStreamsGroup: "Group ... is not a streams group"
  - KAFKA-19862 ConsumerGroup partition epochs: "Cannot set the epoch of ...
    because the partition is still owned at epoch ..."
  - KAFKA-19862 StreamsGroup process ids: "Cannot remove the process ID ...
    because it does not have any processId"

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reuse of the existing harness:
- StreamsGroupTestUtil.staticHeartbeat/staticJoinHeartbeat now build the
  streams join, leave and reconcile requests.
- Remove follow SyncGroup block, default to SyncGroupRequestBuilder

Deduplication:
- Add leaveStreamsMember, joinConsumerMember, joinStreamsMember and
  sleepCapturing, and fold the two reconciliation loops into a single
  driver.

Compaction model:
- compact() returns a CompactedVariant carrying the records, the cleaned
  window and the removed positions.
- Use a single GroupCoordinatorMetrics instance.
- Pass withLogContext to the OffsetMetadataManager.Builder, as production
  does.

The captured logs are unchanged: both scenarios still produce exactly the
same records, batches and compactable positions as before (69/30/52 for
the consumer scenario, 51/26/42 for the streams one). Verified by
reverting each fix and re-running: KAFKA-20254 fails the streams
scenario, KAFKA-19862 fails both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot added triage PRs from the community tests Test fixes (including flaky tests) group-coordinator labels Aug 20, 2026
@dajac dajac added ci-approved and removed triage PRs from the community labels Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-approved group-coordinator tests Test fixes (including flaky tests)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants