Skip to content

Fatal "Static consumer fenced by other consumer with same group.instance.id" on consumer restart, causes permanent silent stall with no retry #1491

Description

@roman-pepper

Summary

AbstractBatchConsumer.restartConsumer() can trigger Kafka's static-membership fencing against itself, because it starts a replacement consumer with the same group.instance.id before confirming the previous consumer has actually left the group. When this race is lost, bulker treats the resulting fencing error as fatal and never retries -- the consumer for that topic goes permanently silent (no further log lines at all) until the process is manually restarted.

This caused a multi-hour to multi-day silent ingestion outage for us on two separate occasions, since nothing else in the system detects "consumer stopped producing any log output at all" as distinct from "consumer idle because there's nothing to consume."

Where

  • bulker/bulkerapp/app/abstract_consumer.go, GetInstanceId():

    // range partitioner assigner distributes partitions between consumers in alphabetical order
    // since bulker topics mostly have only 1 partition – instance with the lowest instanceId will be assigned for all topic.
    // we use first letters of hash of 'topicId + instanceId' as a beginning of 'group.instance.id'
    // so for each topic the first instance will be different
    // while keeping consistency between restarts (if instanceId is the same)
    firstByte := md5.Sum([]byte(ac.topicId + ac.config.InstanceId))[0]
    return fmt.Sprintf("%x-%s", firstByte, ac.config.InstanceId)

    Since config.InstanceId is fixed for the process lifetime, every consumer recreated for the same topic within the same process reuses the exact same group.instance.id by design.

  • bulker/bulkerapp/app/abstract_batch_consumer.go, restartConsumer():

    bc.Infof("Restarting consumer")
    go func(c *kafka.Consumer) {
        e1 := c.Unsubscribe()
        e2 := c.Close()
        bc.Infof("Previous consumer closed: %v %v", e1, e2)
    }(bc.consumer.Load())
    
    ticker := time.NewTicker(time.Duration(bc.config.KafkaSessionTimeoutMs+15000) * time.Millisecond)
    ...
    case <-ticker.C:
        ...
        _, err := bc.initConsumer(true)

    The old consumer's Close() runs in an unawaited goroutine. The code waits KafkaSessionTimeoutMs + 15s before creating the replacement (presumably to give the old member time to leave the group before the new one claims the same static ID), but there's no actual confirmation that the close completed -- just a fixed timer. Under sustained heavy load, Close() can apparently take longer than that window, and the new consumer joins with the same group.instance.id while the old one hasn't fully left, producing:

    Fatal error: Fatal consumer error: Broker: Static consumer fenced by other consumer with same group.instance.id
    

    This is logged via the normal error path but nothing in the code retries it -- the consumer just stops.

Impact

Once this fires, the affected topic's consumer produces zero further log output of any kind -- not even the periodic degradation/retry warnings -- until the process is externally restarted. For a high-volume topic, this is silent, total data-ingestion loss for that destination with no automatic recovery and no distinguishable signal from "topic is just idle."

Observed conditions

We saw this trigger repeatedly during sustained backlog catch-up (millions of queued messages after an unrelated infra restart), including:

  • Once during a normal ECS rolling deployment (brief two-task overlap, each with the same static ID from a shared image/config -- expected collision)
  • Twice with only one instance running, no deployment overlap at all -- the race triggered purely from bulker's own internal consumer-recreation cycle, roughly ~5-6 minutes into a long-running batch, right as the internal "no successful consume" watchdog fired and tried to restart the consumer

Sample log sequence (one instance, no overlapping tasks):

19:01:54 level=warning msg="[topic] Consumer degradation detected: no successful consume for 5m0s ... Recreating consumer."
19:01:54 level=info msg="[topic] Restarting consumer"
19:01:54 level=info msg="[topic] Previous consumer closed: <nil> <nil>"
19:02:54 level=info msg="[topic] Consumer created: bulkerapp#consumer-7"
19:02:54 level=info msg="[topic] Successfully altered consumer group offsets: ..."
19:02:54 level=info msg="[topic] Consumer created: bulkerapp#consumer-9"
19:02:54 level=error msg="[topic] Consume finished with error: ... Fatal error: Fatal consumer error: Broker: Static consumer fenced by other consumer with same group.instance.id stats: consumed: 1080000 processed: 1080000 ... time: 184.98 s."
(no further log lines for this topic for the next ~17 minutes, until the process was manually restarted)

Note two Consumer created lines within the same second (consumer-7, then consumer-9) -- suggests overlapping restart attempts even within a single process.

Suggested directions

  1. Make restartConsumer actually wait for the old consumer's Unsubscribe()/Close() to complete (or for explicit leave-group confirmation) before calling initConsumer(true), rather than relying on a fixed timer with no completion signal.
  2. Treat the "Static consumer fenced by other consumer with same group.instance.id" error as retryable with backoff rather than fatal-with-no-retry, since it's a transient self-inflicted race rather than a genuine unrecoverable state.
  3. Consider logging a distinct, loud signal when a consumer's goroutine exits after a fatal error (vs. going silent), so downstream monitoring can tell "stopped forever" apart from "just idle."

Happy to provide more log context or test against a patched build if useful.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions