Skip to content

Commit a42b3cb

Browse files
authored
Requeue at a fixed interval while the consumer group is not empty (#4828)
* Requeue at a fixed interval while the consumer group is not empty Since #4785 the ConsumerGroup finalizer retries deleting a Kafka group that still has members without limit. It returns an error, so the controller requeues with the rate limiter, which doubles the delay from 5ms up to 1000s. A dispatcher stays in the group until its in-flight delivery ends, which takes up to the 60s connect timeout when the subscriber is gone. By then the finalizer has failed about 25 times and its next retry is about 17 minutes away, so the owner (KafkaChannel, Trigger, KafkaSource) stays until something else enqueues the key. Return controller.NewRequeueAfter(5s) instead, as the channel, broker and sink reconcilers do, so the finalizer retries within 5s of the group becoming empty. The status still records the failure. Seen in channel-reconciler-tests-sasl-ssl on #4825: - 11:04:06-11:04:44 "The group is not empty", backoff 63ms..2.5s.. - 11:05:10 the dispatcher closed its consumer after a 60s connect timeout to the deleted subscriber - 11:06:04 the test cleanup gave up with the Channel still present - 11:07:44 the finalizer was removed, when a new leader took the lease * Leave a consumer group in Kafka when it keeps members for 5 minutes The ConsumerGroup finalizer retries while the Kafka group still has members. A dispatcher leaves within about a minute, but a member that Knative does not control never does, for example another application using the same group.id as a KafkaSource. The finalizer then never finishes, and neither does the deletion of the KafkaSource, Trigger or KafkaChannel, or of their namespace. Count the attempts with an expiring counter, as the broker finalizer does for an unreachable Kafka cluster. After 60 attempts 5s apart, about 5 minutes, leave the group in Kafka, as the finalizer did before #4785, and log it.
1 parent b77c152 commit a42b3cb

3 files changed

Lines changed: 58 additions & 14 deletions

File tree

‎control-plane/pkg/reconciler/consumergroup/consumergroup.go‎

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import (
4444
"knative.dev/eventing/pkg/observability"
4545
"knative.dev/eventing/pkg/scheduler"
4646
"knative.dev/pkg/apis"
47+
"knative.dev/pkg/controller"
4748
"knative.dev/pkg/logging"
4849
"knative.dev/pkg/observability/attributekey"
4950
pointer "knative.dev/pkg/ptr"
@@ -131,6 +132,10 @@ type Reconciler struct {
131132
// tried to delete consumer group metadata from Kafka.
132133
DeleteConsumerGroupMetadataCounter *counter.Counter
133134

135+
// NonEmptyGroupCounter is an in-memory counter to count how many times we have
136+
// found the consumer group in Kafka still having members while deleting it.
137+
NonEmptyGroupCounter *counter.Counter
138+
134139
// InitOffsetLatestInitialOffsetCache is the cache for consumer group offset initialization.
135140
//
136141
// When there is high load and multiple consumer group schedule calls, we get many
@@ -257,14 +262,26 @@ func (r *Reconciler) FinalizeKind(ctx context.Context, cg *kafkainternals.Consum
257262
// The group still has active members (dispatchers that haven't observed the contract
258263
// update yet), so retry without consuming the bounded retry budget used for
259264
// unreachable clusters.
265+
// A member can stay until its in-flight event delivery ends, which takes up to the
266+
// 60s connect timeout. Retry at a fixed interval: returning the error would back
267+
// off exponentially, up to 1000s, and retry long after the group became empty.
260268
if errorIsOneOf(err, sarama.ErrNonEmptyGroup) {
261-
return cg.MarkDeleteOffsetFailed("DeleteConsumerGroupOffset", err)
262-
}
263-
// We retry a few times to delete Consumer group metadata from Kafka before giving up.
264-
if v := r.DeleteConsumerGroupMetadataCounter.Inc(string(cg.GetUID())); v <= 5 {
265-
return cg.MarkDeleteOffsetFailed("DeleteConsumerGroupOffset", fmt.Errorf("%w (retry num %d)", err, v))
269+
// A member that stays for about 5 minutes is not a dispatcher on its way out,
270+
// for example another application using the same group.id. Leave the group in
271+
// Kafka rather than block the deletion of the owner forever.
272+
if r.NonEmptyGroupCounter.Inc(string(cg.GetUID())) <= 60 {
273+
_ = cg.MarkDeleteOffsetFailed("DeleteConsumerGroupOffset", err)
274+
return controller.NewRequeueAfter(5 * time.Second)
275+
}
276+
r.NonEmptyGroupCounter.Del(string(cg.GetUID()))
277+
logger.Errorw("Leaving the consumer group in Kafka, it still has members", zap.Error(err))
278+
} else {
279+
// We retry a few times to delete Consumer group metadata from Kafka before giving up.
280+
if v := r.DeleteConsumerGroupMetadataCounter.Inc(string(cg.GetUID())); v <= 5 {
281+
return cg.MarkDeleteOffsetFailed("DeleteConsumerGroupOffset", fmt.Errorf("%w (retry num %d)", err, v))
282+
}
283+
r.DeleteConsumerGroupMetadataCounter.Del(string(cg.GetUID()))
266284
}
267-
r.DeleteConsumerGroupMetadataCounter.Del(string(cg.GetUID()))
268285
}
269286

270287
r.InitOffsetLatestInitialOffsetCache.Expire(keyOf(cg))

‎control-plane/pkg/reconciler/consumergroup/consumergroup_test.go‎

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ const (
6464
testSchedulerKey = "scheduler"
6565
noTestScheduler = "no-scheduler"
6666
wantErrorOnGetClusterAdmin = "wantErrorOnGetClusterAdmin"
67+
nonEmptyGroupAttemptsKey = "nonEmptyGroupAttempts"
6768

6869
systemNamespace = "knative-eventing"
6970
finalizerName = "consumergroups.internal.kafka.eventing.knative.dev"
@@ -1881,6 +1882,7 @@ func TestReconcileKind(t *testing.T) {
18811882
SystemNamespace: systemNamespace,
18821883
AutoscalerConfig: "",
18831884
DeleteConsumerGroupMetadataCounter: counter.NewExpiringCounter(ctx),
1885+
NonEmptyGroupCounter: counter.NewExpiringCounter(ctx),
18841886
InitOffsetLatestInitialOffsetCache: prober.NewLocalExpiringCache[string, prober.Status, struct{}](ctx, time.Second),
18851887
EnqueueKey: func(key string) {},
18861888
}
@@ -2041,6 +2043,7 @@ func TestReconcileKindNoAutoscaler(t *testing.T) {
20412043
},
20422044
SystemNamespace: systemNamespace,
20432045
DeleteConsumerGroupMetadataCounter: counter.NewExpiringCounter(ctx),
2046+
NonEmptyGroupCounter: counter.NewExpiringCounter(ctx),
20442047
InitOffsetLatestInitialOffsetCache: prober.NewLocalExpiringCache[string, prober.Status, struct{}](ctx, time.Second),
20452048
EnqueueKey: func(key string) {},
20462049
}
@@ -2578,7 +2581,7 @@ func TestFinalizeKind(t *testing.T) {
25782581
SkipNamespaceValidation: true, // WantCreates compare the source namespace with configmap namespace, so skip it
25792582
},
25802583
{
2581-
Name: "Finalize error - failed consumer group deletion with " + sarama.ErrNonEmptyGroup.Error(),
2584+
Name: "Finalize requeue - failed consumer group deletion with " + sarama.ErrNonEmptyGroup.Error(),
25822585
Objects: []runtime.Object{
25832586
NewDeletedConsumeGroup(
25842587
ConsumerGroupOwnerRef(SourceAsOwnerReference()),
@@ -2598,13 +2601,6 @@ func TestFinalizeKind(t *testing.T) {
25982601
}),
25992602
kafkatesting.ErrorOnDeleteConsumerGroupTestKey: sarama.ErrNonEmptyGroup,
26002603
},
2601-
WantEvents: []string{
2602-
Eventf(
2603-
corev1.EventTypeWarning,
2604-
"InternalError",
2605-
"failed to delete consumer group offset: unable to delete the consumer group my.group.id: "+sarama.ErrNonEmptyGroup.Error(),
2606-
),
2607-
},
26082604
WantStatusUpdates: []clientgotesting.UpdateActionImpl{
26092605
{
26102606
Object: func() runtime.Object {
@@ -2626,6 +2622,29 @@ func TestFinalizeKind(t *testing.T) {
26262622
},
26272623
SkipNamespaceValidation: true, // WantCreates compare the source namespace with configmap namespace, so skip it
26282624
},
2625+
{
2626+
Name: "Finalize normal - consumer group still has members after 60 attempts",
2627+
Objects: []runtime.Object{
2628+
NewDeletedConsumeGroup(
2629+
ConsumerGroupOwnerRef(SourceAsOwnerReference()),
2630+
ConsumerGroupConsumerSpec(NewConsumerSpec(
2631+
ConsumerConfigs(
2632+
ConsumerBootstrapServersConfig(ChannelBootstrapServers),
2633+
ConsumerGroupIDConfig("my.group.id"),
2634+
),
2635+
)),
2636+
),
2637+
},
2638+
Key: testKey,
2639+
OtherTestData: map[string]interface{}{
2640+
testSchedulerKey: SchedulerFunc(func(_ context.Context, vpod scheduler.VPod) ([]eventingduckv1alpha1.Placement, error) {
2641+
return nil, nil
2642+
}),
2643+
kafkatesting.ErrorOnDeleteConsumerGroupTestKey: sarama.ErrNonEmptyGroup,
2644+
nonEmptyGroupAttemptsKey: 60,
2645+
},
2646+
SkipNamespaceValidation: true, // WantCreates compare the source namespace with configmap namespace, so skip it
2647+
},
26292648
}
26302649

26312650
table.Test(t, NewFactory(nil, func(ctx context.Context, listers *Listers, env *config.Env, row *TableRow) controller.Reconciler {
@@ -2662,6 +2681,7 @@ func TestFinalizeKind(t *testing.T) {
26622681
},
26632682
KafkaFeatureFlags: configapis.DefaultFeaturesConfig(),
26642683
DeleteConsumerGroupMetadataCounter: counter.NewExpiringCounter(ctx),
2684+
NonEmptyGroupCounter: counter.NewExpiringCounter(ctx),
26652685
InitOffsetLatestInitialOffsetCache: prober.NewLocalExpiringCache[string, prober.Status, struct{}](ctx, time.Second),
26662686
}
26672687

@@ -2670,6 +2690,12 @@ func TestFinalizeKind(t *testing.T) {
26702690
r.ExpectedReplicas, _ = meter.Int64Gauge("kn.eventing.replicas.expected")
26712691
r.ReadyReplicas, _ = meter.Int64Gauge("kn.eventing.replicas.ready")
26722692

2693+
// Earlier attempts on the ConsumerGroup, which has no UID in these tests.
2694+
attempts, _ := row.OtherTestData[nonEmptyGroupAttemptsKey].(int)
2695+
for i := 0; i < attempts; i++ {
2696+
r.NonEmptyGroupCounter.Inc("")
2697+
}
2698+
26732699
return consumergroup.NewReconciler(
26742700
ctx,
26752701
logging.FromContext(ctx),

‎control-plane/pkg/reconciler/consumergroup/controller.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ func NewController(ctx context.Context, watcher configmap.Watcher) *controller.I
139139
KedaClient: kedaclient.Get(ctx),
140140
AutoscalerConfig: env.AutoscalerConfigMap,
141141
DeleteConsumerGroupMetadataCounter: counter.NewExpiringCounter(ctx),
142+
NonEmptyGroupCounter: counter.NewExpiringCounter(ctx),
142143
InitOffsetLatestInitialOffsetCache: prober.NewLocalExpiringCache[string, prober.Status, struct{}](ctx, 20*time.Minute),
143144
}
144145

0 commit comments

Comments
 (0)