feat: kafka driver change#958
Conversation
| joinCtx, cancel := context.WithTimeout(ctx, 2*time.Minute) | ||
| defer cancel() | ||
| for { | ||
| if joinCtx.Err() != nil { |
There was a problem hiding this comment.
err can be other reasons as well not only time out
|
|
||
| // remove stale consumers before creating new readers | ||
| if err := k.readerManager.RemoveExistingConsumers(ctx, k.client); err != nil { | ||
| return fmt.Errorf("failed to remove existing consumers: %s", err) |
There was a problem hiding this comment.
I was not able to consistently reproduce this issue on my machine under normal conditions. However, after increasing the session timeout to 2 minutes, commenting out the removeExistingConsumer logic, and adding additional debug logging for investigation, I was able to observe that extra consumers were accumulating in the consumer group during rebalancing.
One possible outcome of this situation is that all partitions get assigned to the pre-existing consumers, leaving the newly created consumer with no partitions. In that case, PollFetches eventually times out because there are no assigned partitions to fetch from, causing the sync to exit without processing any records. However, this is only one possible explanation and not a confirmed root cause. There may be other unexpected behaviors or outcomes resulting from the presence of these extra consumers as well.
To further validate the behavior, I re-enabled the removeExistingConsumer logic and added additional logging inside the function to track how many consumers were being removed. Following the same reproduction steps, the logs showed that the function was working as expected and successfully removing the stale consumers.
Here is the reference link for the same :-
https://datazip.atlassian.net/wiki/x/AQCsI
| continue | ||
| } | ||
|
|
||
| message := iter.Next() |
There was a problem hiding this comment.
how many records will be there in a batch?
and also there was a case where message can be large then 1 mb?
There was a problem hiding this comment.
This batch size behavior already existed in Segment, where we had a 10 MB limit, so I kept the same limit in franz-go as well.
What this means is that once the broker has accumulated data up to this limit, it will return the batch. If a single message itself is larger than 10 MB, Kafka will still return that message in a single batch, ignoring the configured fetch limit for that request. I verified this by testing with a 61 MB message.
Regarding how these settings work:
- MinFetchSize: Specifies the minimum amount of data the broker should try to accumulate before returning a fetch response.
- MaxFetchSize: Sets an upper limit on the amount of data returned in a fetch response. However, if the first available message exceeds this limit, Kafka will still return that message in a single batch.
- FetchMaxWaitMs: Specifies how long the broker should wait for data to become available. If the minimum fetch size is not reached within this timeout, the broker returns whatever data is available (or an empty response).
Regarding the default poll timeout we discussed earlier, that behavior is not supported in franz-go. Unlike the Java consumer, franz-go does not enforce a maximum time between polls because heartbeats are handled independently in the background.
Reference: twmb/franz-go#140
| func (b *CustomGroupBalancer) UserData() ([]byte, error) { | ||
| return nil, nil | ||
| // IsCooperative returns false to indicate that the balancer is not cooperative. | ||
| func (b *CustomGroupBalancer) IsCooperative() bool { |
There was a problem hiding this comment.
add comment why we are not using ?
There was a problem hiding this comment.
In Kafka, when isCooperative is set to false, any rebalance causes all consumers to first have their partitions revoked before receiving new assignments. However, in the Franz-go library, there are three rebalance callbacks. During experimentation, I observed that OnPartitionsAssigned is triggered for all consumers on every rebalance, regardless of whether any partitions are assigned to that particular consumer, even when cooperative mode is set to true. Because of this behavior, either approach works for our use case, as the callback is still invoked during rebalances.So due to this we dont need any comment as of odd behavior from franz-go side
|
|
||
| // number of consumers to use | ||
| consumerIDCount := min(b.requiredConsumerIDs, len(members)) | ||
| consumerCount := min(b.requiredConsumerIDs, len(members)) |
There was a problem hiding this comment.
Yes it is not required it was just in staging so didnt removed but removed this now
| // Exit gracefully when a rebalance is detected via assign/revoke callbacks. | ||
| onRebalance := func(_ context.Context, client *kgo.Client, _ map[string][]int32) { | ||
| if r.RebalanceDetected(client) { | ||
| r.exitMode.Store(gracefulExit) |
There was a problem hiding this comment.
this will create problem in MO
There was a problem hiding this comment.
Had a discussion with you regarding it, so no need for it now because rebalance detetection callbacks are only valid in streamchanges not in preCDC.
| // generation id -1 means not yet joined | ||
| // mismatch means readers are on different generations, partition assignment not yet completed | ||
| if currentReaderGenerationID < 0 || (expectedGenerationID >= 0 && expectedGenerationID != currentReaderGenerationID) { | ||
| allReadersJoined = false | ||
| break |
There was a problem hiding this comment.
need to understand again
There was a problem hiding this comment.
// waitForPartitionAssignment blocks until Kafka completes partition assignment
// for all readers in the consumer group.
func (r *ReaderManager) waitForPartitionAssignment(ctx context.Context) error {
joinCtx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
for {
select {
case <-joinCtx.Done():
return fmt.Errorf("timed out waiting for partition assignment on consumer group %s: %s", r.config.ConsumerGroupID, joinCtx.Err())
case <-time.After(2 * time.Second):
var (
allReadersJoined = true
expectedGenerationID int32 = -1
)
for _, kafkaReader := range r.readers {
_, currentReaderGenerationID := kafkaReader.reader.GroupMetadata()
// generation id -1 means not yet joined
// mismatch means readers are on different generations, partition assignment not yet completed
if currentReaderGenerationID < 0 || (expectedGenerationID >= 0 && expectedGenerationID != currentReaderGenerationID) {
allReadersJoined = false
break
}
if expectedGenerationID < 0 {
expectedGenerationID = currentReaderGenerationID
}
}
if allReadersJoined {
r.generationID.Store(expectedGenerationID)
// brief wait to let partition assignment fully propagate before fetching starts.
time.Sleep(2 * time.Second)
logger.Infof("consumer group %s stable: all readers assigned, generation id: %d", r.config.ConsumerGroupID, expectedGenerationID)
return nil
}
}
}
}
There was a problem hiding this comment.
Previously on first run it use to first check all Readers readerId and than wait for 500ms now i changes it andi it will first wait 500ms and than start to check all readers readerId but since in other part of code we are using this and no big changes so i changed it.
| // get current partition metadata and key | ||
| currentPartitionKey := types.PartitionKey{Topic: record.Message.Topic, Partition: record.Message.Partition} | ||
| currentPartitionMeta, exists := k.readerManager.GetPartitionIndex(fmt.Sprintf("%s:%d", record.Message.Topic, record.Message.Partition)) | ||
| currentPartitionMeta, exists := k.readerManager.GetPartitionIndex(kafkapkg.PartitionIndexKey(record.Message.Topic, record.Message.Partition)) |
There was a problem hiding this comment.
function name seem not conveying what actully it does
| if err != nil { | ||
| return fmt.Errorf("error reading message in Kafka CDC sync: %s", err) | ||
| // checked before every poll and every record so a rebalance signal is never delayed by full batch processing. | ||
| if stopProcessing, err := k.readerManager.FetchExitState(); stopProcessing { |
There was a problem hiding this comment.
we should use context here as well
|
|
||
| // Type assert and validate messages | ||
| lastMessages, isValid := lastMessagesMeta.(map[types.PartitionKey]kafka.Message) | ||
| lastMessages, isValid := lastMessagesMeta.(map[types.PartitionKey]*kgo.Record) |
Thnks |
Description
Migrated the Kafka consumer implementation from Segment Kafka-Go to Franz-Go to leverage improved consumer group management, rebalance handling, and lifecycle APIs provided by Franz-Go.
As part of this migration:
Added support for static membership using
instance.idto improve retry and reconnect behavior. This helps avoid unnecessary rebalances during transient failures or consumer restarts when the same instance rejoins the group.Implemented rebalance detection using Franz-Go consumer group callbacks along with generation ID tracking stored in consumer metadata. During sync execution, the active generation is continuously validated against the latest assigned generation to detect stale consumers or lost partition ownership.
Added graceful shutdown handling on successful rebalance detection. Instead of continuing to process records with outdated assignments, the sync now exits cleanly to prevent duplicate processing and stale partition consumption during consumer group transitions.
Fixes #794
Type of change
How Has This Been Tested?
StreamChanges. Verified that retries worked correctly and did not trigger unnecessary consumer group rebalances due to static membership support usinginstance.id.Screenshots or Recordings
N/A
Documentation
Related PR's (If Any):