Skip to content

Conversation

@DCjanus
Copy link
Contributor

@DCjanus DCjanus commented Jan 5, 2026

Summary

  • Add Admin APIs for KIP-396 ListOffsets and AlterConsumerGroupOffsets to support bulk offset queries and group offset commits.
  • Provide Go types aligned with KIP-396 semantics (OffsetSpec, OffsetAndMetadata).

Key changes

  • Add ListOffsets and AlterConsumerGroupOffsets to ClusterAdmin and wire protocol requests.
  • Add request/response types for offset specs and result structs with leader epoch support.
  • Implement broker fan-out for list offsets and coordinator commit path for alter offsets.
  • Add functional tests covering timestamp list offsets and admin offset commits.

Constraints / tradeoffs

  • OffsetSpec is a new type instead of reusing GetOffset(int64) because GetOffset relies on magic int64 values (earliest/latest) while ListOffsets needs an explicit spec (earliest/latest/timestamp); this matches the Java AdminClient surface defined in KIP-396 and keeps room for future spec variants without breaking changes.

Notes

Signed-off-by: DCjanus <DCjanus@dcjanus.com>
Signed-off-by: DCjanus <DCjanus@dcjanus.com>
Signed-off-by: DCjanus <DCjanus@dcjanus.com>
Signed-off-by: DCjanus <DCjanus@dcjanus.com>
@DCjanus DCjanus force-pushed the pr/kip-396-admin-offsets branch from edad338 to 11e34c1 Compare January 5, 2026 16:07
Signed-off-by: DCjanus <DCjanus@dcjanus.com>
@DCjanus DCjanus closed this Jan 5, 2026
Signed-off-by: DCjanus <DCjanus@dcjanus.com>
Signed-off-by: DCjanus <DCjanus@dcjanus.com>
Signed-off-by: DCjanus <DCjanus@dcjanus.com>
Signed-off-by: DCjanus <DCjanus@dcjanus.com>
Signed-off-by: DCjanus <DCjanus@dcjanus.com>
Signed-off-by: DCjanus <DCjanus@dcjanus.com>
Signed-off-by: DCjanus <DCjanus@dcjanus.com>
Signed-off-by: DCjanus <DCjanus@dcjanus.com>
Signed-off-by: DCjanus <DCjanus@dcjanus.com>
@DCjanus DCjanus reopened this Jan 6, 2026
admin_offsets.go Outdated

// OffsetSpec specifies which offset to look up for a partition.
type OffsetSpec struct {
timestamp int64
Copy link
Contributor

Choose a reason for hiding this comment

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

I’m not sure why we need a ton of constructors, when we can simply export this value.

After all, given the functionality provided, we can already arbitrarily mutate any given OffsetSpec and access the field at will:

offspec := OffsetSpecLatest()
offspec = OffsetSpecForTimestamp(arbitraryTimestamp)
go func() {
	offspec = OffsetSpecForTimestamp(OffsetNewest)
}()
ts := offspec.Timestamp() // write-read race condition

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I’m not sure why we need a ton of constructors, when we can simply export this value.

After all, given the functionality provided, we can already arbitrarily mutate any given OffsetSpec and access the field at will:

offspec := OffsetSpecLatest()
offspec = OffsetSpecForTimestamp(arbitraryTimestamp)
go func() {
	offspec = OffsetSpecForTimestamp(OffsetNewest)
}()
ts := offspec.Timestamp() // write-read race condition

Agree that we should not mix two different input styles for the same concept (as noted in #3419 (comment)).

To stay consistent with existing Sarama APIs (e.g. Client.GetOffset(topic, partition, time int64) which uses OffsetOldest/OffsetNewest), I removed OffsetSpec and made ListOffsets take int64 directly (pass OffsetOldest/OffsetNewest or a millisecond timestamp).

Done in commit f3e40c7.

admin_offsets.go Outdated
close(results)

allResults := make(map[TopicPartitionID]*ListOffsetsResult, len(partitions))
var firstErr error
Copy link
Contributor

Choose a reason for hiding this comment

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

var errs []error
for res := range results {
	if res.err != nil {
		errs = append(errs, res.err)
	}
	...
}

return allResults errors.Join(errs...)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

var errs []error
for res := range results {
	if res.err != nil {
		errs = append(errs, res.err)
	}
	...
}

return allResults errors.Join(errs...)

Addressed in commit bce2339 by aggregating all errors with errors.Join.

admin_offsets.go Outdated

for _, req := range requests {
wg.Add(1)
go func(req *brokerOffsetRequest) {
Copy link
Contributor

Choose a reason for hiding this comment

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

How many requests are we likely to be spinning off here?

It can happen that spinning off too many goroutines will actually be performance-degrading rather than performance-enhancing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

How many requests are we likely to be spinning off here?

It can happen that spinning off too many goroutines will actually be performance-degrading rather than performance-enhancing.

Thanks for the note. The Java AdminClient doesn’t appear to impose an explicit concurrency cap either — it groups by broker and issues one in‑flight request per broker via the admin I/O loop.

Key entry point (no concurrency limiting logic):

In practice, the Java I/O loop is equivalent to spawning goroutines in Go: both drive concurrent in‑flight requests without a per‑broker cap.

Given typical Kafka clusters are well below 1,000 brokers, I believe even 10,000 concurrent in‑flight requests should be relatively easy for modern hardware to handle.

admin.go Outdated
Comment on lines 125 to 130
ListOffsets(partitions map[TopicPartitionID]OffsetSpec, options *ListOffsetsOptions) (map[TopicPartitionID]*ListOffsetsResult, error)

// AlterConsumerGroupOffsets alters offsets for the specified group by committing the provided offsets and metadata.
// The request targets the group's coordinator and returns per-partition results in the response.
// This operation is not transactional so it may succeed for some partitions while fail for others.
AlterConsumerGroupOffsets(group string, offsets map[TopicPartitionID]OffsetAndMetadata, options *AlterConsumerGroupOffsetsOptions) (*OffsetCommitResponse, error)
Copy link
Contributor

Choose a reason for hiding this comment

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

Our current design uses map[string]map[int32]V adding this TopicPartitionID might have been nice if we had started with it, but we haven’t really. https://pkg.go.dev/github.com/IBM/sarama#AlterPartitionReassignmentsResponse

Providing two different ways to do something is likely to increase confusion over any benefits of flattening the maps.

It also reduces the ability to iterate over topics individually, without needing to then iterate over all Topic × Partition combinations and select for Topic.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Our current design uses map[string]map[int32]V adding this TopicPartitionID might have been nice if we had started with it, but we haven’t really. https://pkg.go.dev/github.com/IBM/sarama#AlterPartitionReassignmentsResponse

Providing two different ways to do something is likely to increase confusion over any benefits of flattening the maps.

It also reduces the ability to iterate over topics individually, without needing to then iterate over all Topic × Partition combinations and select for Topic.

Thanks for the point about keeping the API shape consistent. I reverted to the existing map[string]map[int32]V style so callers can iterate by topic without scanning all partitions.

This removes TopicPartitionID from ListOffsets and aligns the input/return maps with the rest of Sarama’s admin APIs.

Done in commit be65797.

admin_offsets.go Outdated
}

// ListOffsetsResult contains the response for a single topic partition.
type ListOffsetsResult struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

I’m not sure this is an Offsets result? As it seems to be single offset result?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Renamed ListOffsetsResult to OffsetResult to reflect the single-partition result; done in commit f9150fe.

admin_offsets.go Outdated
Comment on lines 66 to 75
type brokerOffsetRequest struct {
broker *Broker
request *OffsetRequest
partitions []TopicPartitionID
}

type brokerOffsetResult struct {
result map[TopicPartitionID]*ListOffsetsResult
err error
}
Copy link
Contributor

Choose a reason for hiding this comment

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

We could also isolate/scope these types into the ListOffsets right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could also isolate/scope these types into the ListOffsets right?

Updated in commit 243445e (scoped the helper types inside ListOffsets).

admin_offsets.go Outdated
Comment on lines 95 to 96
req = &brokerOffsetRequest{
broker: broker,
Copy link
Contributor

Choose a reason for hiding this comment

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

I’m not sure why we double track the broker? We’re using it as the key, and as a field in the value?

We could just for broker, req := range requests { … } below, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I’m not sure why we double track the broker? We’re using it as the key, and as a field in the value?

We could just for broker, req := range requests { … } below, right?

Addressed in commit ea32b6e by removing the redundant broker field and passing the broker via the map key in the loop.

@DCjanus DCjanus force-pushed the pr/kip-396-admin-offsets branch from 2e502be to ad782bd Compare January 9, 2026 17:42
DCjanus and others added 10 commits January 10, 2026 02:44
Co-authored-by: Cassondra Foesch <puellanivis@users.noreply.github.com>
Signed-off-by: DCjanus <DCjanus@dcjanus.com>
Co-authored-by: Cassondra Foesch <puellanivis@users.noreply.github.com>
Signed-off-by: DCjanus <DCjanus@dcjanus.com>
Co-authored-by: Cassondra Foesch <puellanivis@users.noreply.github.com>
Signed-off-by: DCjanus <DCjanus@dcjanus.com>
Signed-off-by: DCjanus <DCjanus@dcjanus.com>
Signed-off-by: DCjanus <DCjanus@dcjanus.com>
Signed-off-by: DCjanus <DCjanus@dcjanus.com>
Signed-off-by: DCjanus <DCjanus@dcjanus.com>
Signed-off-by: DCjanus <DCjanus@dcjanus.com>
Signed-off-by: DCjanus <DCjanus@dcjanus.com>
Signed-off-by: DCjanus <DCjanus@dcjanus.com>
Signed-off-by: DCjanus <DCjanus@dcjanus.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

KIP-396: Add Reset/List Offsets Operations to AdminClient

2 participants