|
|
@@ -12,13 +12,14 @@ import (
|
|
|
|
|
|
// ConsumerMessage encapsulates a Kafka message returned by the consumer.
|
|
|
type ConsumerMessage struct {
|
|
|
- Key, Value []byte
|
|
|
- Topic string
|
|
|
- Partition int32
|
|
|
- Offset int64
|
|
|
+ Headers []*RecordHeader // only set if kafka is version 0.11+
|
|
|
Timestamp time.Time // only set if kafka is version 0.10+, inner message timestamp
|
|
|
BlockTimestamp time.Time // only set if kafka is version 0.10+, outer (compressed) block timestamp
|
|
|
- Headers []*RecordHeader // only set if kafka is version 0.11+
|
|
|
+
|
|
|
+ Key, Value []byte
|
|
|
+ Topic string
|
|
|
+ Partition int32
|
|
|
+ Offset int64
|
|
|
}
|
|
|
|
|
|
// ConsumerError is what is provided to the user when an error occurs.
|
|
|
@@ -45,11 +46,6 @@ func (ce ConsumerErrors) Error() string {
|
|
|
// Consumer manages PartitionConsumers which process Kafka messages from brokers. You MUST call Close()
|
|
|
// on a consumer to avoid leaks, it will not be garbage-collected automatically when it passes out of
|
|
|
// scope.
|
|
|
-//
|
|
|
-// Sarama's Consumer type does not currently support automatic consumer-group rebalancing and offset tracking.
|
|
|
-// For Zookeeper-based tracking (Kafka 0.8.2 and earlier), the https://github.com/wvanbergen/kafka library
|
|
|
-// builds on Sarama to add this support. For Kafka-based tracking (Kafka 0.9 and later), the
|
|
|
-// https://github.com/bsm/sarama-cluster library builds on Sarama to add this support.
|
|
|
type Consumer interface {
|
|
|
|
|
|
// Topics returns the set of available topics as retrieved from the cluster
|
|
|
@@ -77,13 +73,11 @@ type Consumer interface {
|
|
|
}
|
|
|
|
|
|
type consumer struct {
|
|
|
- client Client
|
|
|
- conf *Config
|
|
|
- ownClient bool
|
|
|
-
|
|
|
- lock sync.Mutex
|
|
|
+ conf *Config
|
|
|
children map[string]map[int32]*partitionConsumer
|
|
|
brokerConsumers map[*Broker]*brokerConsumer
|
|
|
+ client Client
|
|
|
+ lock sync.Mutex
|
|
|
}
|
|
|
|
|
|
// NewConsumer creates a new consumer using the given broker addresses and configuration.
|
|
|
@@ -92,18 +86,19 @@ func NewConsumer(addrs []string, config *Config) (Consumer, error) {
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
|
}
|
|
|
-
|
|
|
- c, err := NewConsumerFromClient(client)
|
|
|
- if err != nil {
|
|
|
- return nil, err
|
|
|
- }
|
|
|
- c.(*consumer).ownClient = true
|
|
|
- return c, nil
|
|
|
+ return newConsumer(client)
|
|
|
}
|
|
|
|
|
|
// NewConsumerFromClient creates a new consumer using the given client. It is still
|
|
|
// necessary to call Close() on the underlying client when shutting down this consumer.
|
|
|
func NewConsumerFromClient(client Client) (Consumer, error) {
|
|
|
+ // For clients passed in by the client, ensure we don't
|
|
|
+ // call Close() on it.
|
|
|
+ cli := &nopCloserClient{client}
|
|
|
+ return newConsumer(cli)
|
|
|
+}
|
|
|
+
|
|
|
+func newConsumer(client Client) (Consumer, error) {
|
|
|
// Check that we are not dealing with a closed Client before processing any other arguments
|
|
|
if client.Closed() {
|
|
|
return nil, ErrClosedClient
|
|
|
@@ -120,10 +115,7 @@ func NewConsumerFromClient(client Client) (Consumer, error) {
|
|
|
}
|
|
|
|
|
|
func (c *consumer) Close() error {
|
|
|
- if c.ownClient {
|
|
|
- return c.client.Close()
|
|
|
- }
|
|
|
- return nil
|
|
|
+ return c.client.Close()
|
|
|
}
|
|
|
|
|
|
func (c *consumer) Topics() ([]string, error) {
|
|
|
@@ -263,7 +255,7 @@ func (c *consumer) abandonBrokerConsumer(brokerWorker *brokerConsumer) {
|
|
|
// or a separate goroutine. Check out the Consumer examples to see implementations of these different approaches.
|
|
|
//
|
|
|
// To terminate such a for/range loop while the loop is executing, call AsyncClose. This will kick off the process of
|
|
|
-// consumer tear-down & return imediately. Continue to loop, servicing the Messages channel until the teardown process
|
|
|
+// consumer tear-down & return immediately. Continue to loop, servicing the Messages channel until the teardown process
|
|
|
// AsyncClose initiated closes it (thus terminating the for/range loop). If you've already ceased reading Messages, call
|
|
|
// Close; this will signal the PartitionConsumer's goroutines to begin shutting down (just like AsyncClose), but will
|
|
|
// also drain the Messages channel, harvest all errors & return them once cleanup has completed.
|
|
|
@@ -300,22 +292,22 @@ type PartitionConsumer interface {
|
|
|
|
|
|
type partitionConsumer struct {
|
|
|
highWaterMarkOffset int64 // must be at the top of the struct because https://golang.org/pkg/sync/atomic/#pkg-note-BUG
|
|
|
- consumer *consumer
|
|
|
- conf *Config
|
|
|
- topic string
|
|
|
- partition int32
|
|
|
|
|
|
+ consumer *consumer
|
|
|
+ conf *Config
|
|
|
broker *brokerConsumer
|
|
|
messages chan *ConsumerMessage
|
|
|
errors chan *ConsumerError
|
|
|
feeder chan *FetchResponse
|
|
|
|
|
|
trigger, dying chan none
|
|
|
- responseResult error
|
|
|
closeOnce sync.Once
|
|
|
-
|
|
|
- fetchSize int32
|
|
|
- offset int64
|
|
|
+ topic string
|
|
|
+ partition int32
|
|
|
+ responseResult error
|
|
|
+ fetchSize int32
|
|
|
+ offset int64
|
|
|
+ retries int32
|
|
|
}
|
|
|
|
|
|
var errTimedOut = errors.New("timed out feeding messages to the user") // not user-facing
|
|
|
@@ -334,12 +326,20 @@ func (child *partitionConsumer) sendError(err error) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+func (child *partitionConsumer) computeBackoff() time.Duration {
|
|
|
+ if child.conf.Consumer.Retry.BackoffFunc != nil {
|
|
|
+ retries := atomic.AddInt32(&child.retries, 1)
|
|
|
+ return child.conf.Consumer.Retry.BackoffFunc(int(retries))
|
|
|
+ }
|
|
|
+ return child.conf.Consumer.Retry.Backoff
|
|
|
+}
|
|
|
+
|
|
|
func (child *partitionConsumer) dispatcher() {
|
|
|
for range child.trigger {
|
|
|
select {
|
|
|
case <-child.dying:
|
|
|
close(child.trigger)
|
|
|
- case <-time.After(child.conf.Consumer.Retry.Backoff):
|
|
|
+ case <-time.After(child.computeBackoff()):
|
|
|
if child.broker != nil {
|
|
|
child.consumer.unrefBrokerConsumer(child.broker)
|
|
|
child.broker = nil
|
|
|
@@ -453,6 +453,10 @@ feederLoop:
|
|
|
for response := range child.feeder {
|
|
|
msgs, child.responseResult = child.parseResponse(response)
|
|
|
|
|
|
+ if child.responseResult == nil {
|
|
|
+ atomic.StoreInt32(&child.retries, 0)
|
|
|
+ }
|
|
|
+
|
|
|
for i, msg := range msgs {
|
|
|
messageSelect:
|
|
|
select {
|
|
|
@@ -513,13 +517,13 @@ func (child *partitionConsumer) parseMessages(msgSet *MessageSet) ([]*ConsumerMe
|
|
|
}
|
|
|
}
|
|
|
if len(messages) == 0 {
|
|
|
- return nil, ErrIncompleteResponse
|
|
|
+ child.offset++
|
|
|
}
|
|
|
return messages, nil
|
|
|
}
|
|
|
|
|
|
func (child *partitionConsumer) parseRecords(batch *RecordBatch) ([]*ConsumerMessage, error) {
|
|
|
- var messages []*ConsumerMessage
|
|
|
+ messages := make([]*ConsumerMessage, 0, len(batch.Records))
|
|
|
|
|
|
for _, rec := range batch.Records {
|
|
|
offset := batch.FirstOffset + rec.OffsetDelta
|
|
|
@@ -542,7 +546,7 @@ func (child *partitionConsumer) parseRecords(batch *RecordBatch) ([]*ConsumerMes
|
|
|
child.offset = offset + 1
|
|
|
}
|
|
|
if len(messages) == 0 {
|
|
|
- child.offset += 1
|
|
|
+ child.offset++
|
|
|
}
|
|
|
return messages, nil
|
|
|
}
|
|
|
@@ -628,15 +632,13 @@ func (child *partitionConsumer) parseResponse(response *FetchResponse) ([]*Consu
|
|
|
return messages, nil
|
|
|
}
|
|
|
|
|
|
-// brokerConsumer
|
|
|
-
|
|
|
type brokerConsumer struct {
|
|
|
consumer *consumer
|
|
|
broker *Broker
|
|
|
input chan *partitionConsumer
|
|
|
newSubscriptions chan []*partitionConsumer
|
|
|
- wait chan none
|
|
|
subscriptions map[*partitionConsumer]none
|
|
|
+ wait chan none
|
|
|
acks sync.WaitGroup
|
|
|
refs int
|
|
|
}
|
|
|
@@ -658,14 +660,14 @@ func (c *consumer) newBrokerConsumer(broker *Broker) *brokerConsumer {
|
|
|
return bc
|
|
|
}
|
|
|
|
|
|
+// The subscriptionManager constantly accepts new subscriptions on `input` (even when the main subscriptionConsumer
|
|
|
+// goroutine is in the middle of a network request) and batches it up. The main worker goroutine picks
|
|
|
+// up a batch of new subscriptions between every network request by reading from `newSubscriptions`, so we give
|
|
|
+// it nil if no new subscriptions are available. We also write to `wait` only when new subscriptions is available,
|
|
|
+// so the main goroutine can block waiting for work if it has none.
|
|
|
func (bc *brokerConsumer) subscriptionManager() {
|
|
|
var buffer []*partitionConsumer
|
|
|
|
|
|
- // The subscriptionManager constantly accepts new subscriptions on `input` (even when the main subscriptionConsumer
|
|
|
- // goroutine is in the middle of a network request) and batches it up. The main worker goroutine picks
|
|
|
- // up a batch of new subscriptions between every network request by reading from `newSubscriptions`, so we give
|
|
|
- // it nil if no new subscriptions are available. We also write to `wait` only when new subscriptions is available,
|
|
|
- // so the main goroutine can block waiting for work if it has none.
|
|
|
for {
|
|
|
if len(buffer) > 0 {
|
|
|
select {
|
|
|
@@ -698,10 +700,10 @@ done:
|
|
|
close(bc.newSubscriptions)
|
|
|
}
|
|
|
|
|
|
+//subscriptionConsumer ensures we will get nil right away if no new subscriptions is available
|
|
|
func (bc *brokerConsumer) subscriptionConsumer() {
|
|
|
<-bc.wait // wait for our first piece of work
|
|
|
|
|
|
- // the subscriptionConsumer ensures we will get nil right away if no new subscriptions is available
|
|
|
for newSubscriptions := range bc.newSubscriptions {
|
|
|
bc.updateSubscriptions(newSubscriptions)
|
|
|
|
|
|
@@ -747,8 +749,8 @@ func (bc *brokerConsumer) updateSubscriptions(newSubscriptions []*partitionConsu
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+//handleResponses handles the response codes left for us by our subscriptions, and abandons ones that have been closed
|
|
|
func (bc *brokerConsumer) handleResponses() {
|
|
|
- // handles the response codes left for us by our subscriptions, and abandons ones that have been closed
|
|
|
for child := range bc.subscriptions {
|
|
|
result := child.responseResult
|
|
|
child.responseResult = nil
|