github.com/johnnyeven/libtools@v0.0.0-20191126065708-61829c1adf46/kafka/consumergroup/offset_manager.go (about)

     1  package consumergroup
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"sync"
     7  	"time"
     8  )
     9  
    10  // OffsetManager is the main interface consumergroup requires to manage offsets of the consumergroup.
    11  type OffsetManager interface {
    12  	// InitializePartition is called when the consumergroup is starting to consume a
    13  	// partition. It should return the last processed offset for this partition. Note:
    14  	// the same partition can be initialized multiple times during a single run of a
    15  	// consumer group due to other consumer instances coming online and offline.
    16  	InitializePartition(topic string, partition int32) (int64, error)
    17  
    18  	// MarkAsProcessed tells the offset manager than a certain message has been successfully
    19  	// processed by the consumer, and should be committed. The implementation does not have
    20  	// to store this offset right away, but should return true if it intends to do this at
    21  	// some point.
    22  	//
    23  	// Offsets should generally be increasing if the consumer
    24  	// processes events serially, but this cannot be guaranteed if the consumer does any
    25  	// asynchronous processing. This can be handled in various ways, e.g. by only accepting
    26  	// offsets that are higehr than the offsets seen before for the same partition.
    27  	MarkAsProcessed(topic string, partition int32, offset int64) bool
    28  
    29  	// Flush tells the offset manager to immediately commit offsets synchronously and to
    30  	// return any errors that may have occured during the process.
    31  	Flush() error
    32  
    33  	// FinalizePartition is called when the consumergroup is done consuming a
    34  	// partition. In this method, the offset manager can flush any remaining offsets to its
    35  	// backend store. It should return an error if it was not able to commit the offset.
    36  	// Note: it's possible that the consumergroup instance will start to consume the same
    37  	// partition again after this function is called.
    38  	FinalizePartition(topic string, partition int32, lastOffset int64, timeout time.Duration) error
    39  
    40  	// Close is called when the consumergroup is shutting down. In normal circumstances, all
    41  	// offsets are committed because FinalizePartition is called for all the running partition
    42  	// consumers. You may want to check for this to be true, and try to commit any outstanding
    43  	// offsets. If this doesn't succeed, it should return an error.
    44  	Close() error
    45  }
    46  
    47  var (
    48  	UncleanClose = errors.New("Not all offsets were committed before shutdown was completed")
    49  )
    50  
    51  // OffsetManagerConfig holds configuration setting son how the offset manager should behave.
    52  type OffsetManagerConfig struct {
    53  	CommitInterval time.Duration // Interval between offset flushes to the backend store.
    54  	VerboseLogging bool          // Whether to enable verbose logging.
    55  }
    56  
    57  // NewOffsetManagerConfig returns a new OffsetManagerConfig with sane defaults.
    58  func NewOffsetManagerConfig() *OffsetManagerConfig {
    59  	return &OffsetManagerConfig{
    60  		CommitInterval: 10 * time.Second,
    61  	}
    62  }
    63  
    64  type (
    65  	topicOffsets    map[int32]*partitionOffsetTracker
    66  	offsetsMap      map[string]topicOffsets
    67  	offsetCommitter func(int64) error
    68  )
    69  
    70  type partitionOffsetTracker struct {
    71  	l                      sync.Mutex
    72  	waitingForOffset       int64
    73  	highestProcessedOffset int64
    74  	lastCommittedOffset    int64
    75  	done                   chan struct{}
    76  }
    77  
    78  type zookeeperOffsetManager struct {
    79  	config  *OffsetManagerConfig
    80  	l       sync.RWMutex
    81  	offsets offsetsMap
    82  	cg      *ConsumerGroup
    83  
    84  	closing, closed, flush chan struct{}
    85  	flushErr               chan error
    86  }
    87  
    88  // NewZookeeperOffsetManager returns an offset manager that uses Zookeeper
    89  // to store offsets.
    90  func NewZookeeperOffsetManager(cg *ConsumerGroup, config *OffsetManagerConfig) OffsetManager {
    91  	if config == nil {
    92  		config = NewOffsetManagerConfig()
    93  	}
    94  
    95  	zom := &zookeeperOffsetManager{
    96  		config:   config,
    97  		cg:       cg,
    98  		offsets:  make(offsetsMap),
    99  		closing:  make(chan struct{}),
   100  		closed:   make(chan struct{}),
   101  		flush:    make(chan struct{}),
   102  		flushErr: make(chan error),
   103  	}
   104  
   105  	go zom.offsetCommitter()
   106  
   107  	return zom
   108  }
   109  
   110  func (zom *zookeeperOffsetManager) InitializePartition(topic string, partition int32) (int64, error) {
   111  	zom.l.Lock()
   112  	defer zom.l.Unlock()
   113  
   114  	if zom.offsets[topic] == nil {
   115  		zom.offsets[topic] = make(topicOffsets)
   116  	}
   117  
   118  	nextOffset, err := zom.cg.group.FetchOffset(topic, partition)
   119  	if err != nil {
   120  		return 0, err
   121  	}
   122  
   123  	zom.offsets[topic][partition] = &partitionOffsetTracker{
   124  		highestProcessedOffset: nextOffset - 1,
   125  		lastCommittedOffset:    nextOffset - 1,
   126  		done:                   make(chan struct{}),
   127  	}
   128  
   129  	return nextOffset, nil
   130  }
   131  
   132  func (zom *zookeeperOffsetManager) FinalizePartition(topic string, partition int32, lastOffset int64, timeout time.Duration) error {
   133  	zom.l.RLock()
   134  	tracker := zom.offsets[topic][partition]
   135  	zom.l.RUnlock()
   136  
   137  	if lastOffset >= 0 {
   138  		if lastOffset-tracker.highestProcessedOffset > 0 {
   139  			zom.cg.Logf("%s/%d :: Last processed offset: %d. Waiting up to %ds for another %d messages to process...", topic, partition, tracker.highestProcessedOffset, timeout/time.Second, lastOffset-tracker.highestProcessedOffset)
   140  			if !tracker.waitForOffset(lastOffset, timeout) {
   141  				return fmt.Errorf("TIMEOUT waiting for offset %d. Last committed offset: %d", lastOffset, tracker.lastCommittedOffset)
   142  			}
   143  		}
   144  
   145  		if err := zom.commitOffset(topic, partition, tracker); err != nil {
   146  			return fmt.Errorf("FAILED to commit offset %d to Zookeeper. Last committed offset: %d", tracker.highestProcessedOffset, tracker.lastCommittedOffset)
   147  		}
   148  	}
   149  
   150  	zom.l.Lock()
   151  	delete(zom.offsets[topic], partition)
   152  	zom.l.Unlock()
   153  
   154  	return nil
   155  }
   156  
   157  func (zom *zookeeperOffsetManager) MarkAsProcessed(topic string, partition int32, offset int64) bool {
   158  	zom.l.RLock()
   159  	defer zom.l.RUnlock()
   160  	if p, ok := zom.offsets[topic][partition]; ok {
   161  		return p.markAsProcessed(offset)
   162  	} else {
   163  		return false
   164  	}
   165  }
   166  
   167  func (zom *zookeeperOffsetManager) Flush() error {
   168  	zom.flush <- struct{}{}
   169  	return <-zom.flushErr
   170  }
   171  
   172  func (zom *zookeeperOffsetManager) Close() error {
   173  	close(zom.closing)
   174  	<-zom.closed
   175  
   176  	zom.l.Lock()
   177  	defer zom.l.Unlock()
   178  
   179  	var closeError error
   180  	for _, partitionOffsets := range zom.offsets {
   181  		if len(partitionOffsets) > 0 {
   182  			closeError = UncleanClose
   183  		}
   184  	}
   185  
   186  	return closeError
   187  }
   188  
   189  func (zom *zookeeperOffsetManager) offsetCommitter() {
   190  	var tickerChan <-chan time.Time
   191  	if zom.config.CommitInterval != 0 {
   192  		commitTicker := time.NewTicker(zom.config.CommitInterval)
   193  		tickerChan = commitTicker.C
   194  		defer commitTicker.Stop()
   195  	}
   196  
   197  	for {
   198  		select {
   199  		case <-zom.closing:
   200  			close(zom.closed)
   201  			return
   202  		case <-tickerChan:
   203  			if err := zom.commitOffsets(); err != nil {
   204  				zom.cg.errors <- err
   205  			}
   206  		case <-zom.flush:
   207  			zom.flushErr <- zom.commitOffsets()
   208  		}
   209  	}
   210  }
   211  
   212  func (zom *zookeeperOffsetManager) commitOffsets() error {
   213  	zom.l.RLock()
   214  	defer zom.l.RUnlock()
   215  
   216  	var returnErr error
   217  	for topic, partitionOffsets := range zom.offsets {
   218  		for partition, offsetTracker := range partitionOffsets {
   219  			err := zom.commitOffset(topic, partition, offsetTracker)
   220  			switch err {
   221  			case nil:
   222  				// noop
   223  			default:
   224  				returnErr = err
   225  			}
   226  		}
   227  	}
   228  	return returnErr
   229  }
   230  
   231  func (zom *zookeeperOffsetManager) commitOffset(topic string, partition int32, tracker *partitionOffsetTracker) error {
   232  	err := tracker.commit(func(offset int64) error {
   233  		if offset >= 0 {
   234  			return zom.cg.group.CommitOffset(topic, partition, offset+1)
   235  		} else {
   236  			return nil
   237  		}
   238  	})
   239  
   240  	if err != nil {
   241  		zom.cg.Logf("FAILED to commit offset %d for %s/%d!", tracker.highestProcessedOffset, topic, partition)
   242  	} else if zom.config.VerboseLogging {
   243  		zom.cg.Logf("Committed offset %d for %s/%d!", tracker.lastCommittedOffset, topic, partition)
   244  	}
   245  
   246  	return err
   247  }
   248  
   249  // MarkAsProcessed marks the provided offset as highest processed offset if
   250  // it's higher than any previous offset it has received.
   251  func (pot *partitionOffsetTracker) markAsProcessed(offset int64) bool {
   252  	pot.l.Lock()
   253  	defer pot.l.Unlock()
   254  	if offset > pot.highestProcessedOffset {
   255  		pot.highestProcessedOffset = offset
   256  		if pot.waitingForOffset == pot.highestProcessedOffset {
   257  			close(pot.done)
   258  		}
   259  		return true
   260  	} else {
   261  		return false
   262  	}
   263  }
   264  
   265  // Commit calls a committer function if the highest processed offset is out
   266  // of sync with the last committed offset.
   267  func (pot *partitionOffsetTracker) commit(committer offsetCommitter) error {
   268  	pot.l.Lock()
   269  	defer pot.l.Unlock()
   270  
   271  	if pot.highestProcessedOffset > pot.lastCommittedOffset {
   272  		if err := committer(pot.highestProcessedOffset); err != nil {
   273  			return err
   274  		}
   275  		pot.lastCommittedOffset = pot.highestProcessedOffset
   276  		return nil
   277  	} else {
   278  		return nil
   279  	}
   280  }
   281  
   282  func (pot *partitionOffsetTracker) waitForOffset(offset int64, timeout time.Duration) bool {
   283  	pot.l.Lock()
   284  	if offset > pot.highestProcessedOffset {
   285  		pot.waitingForOffset = offset
   286  		pot.l.Unlock()
   287  		select {
   288  		case <-pot.done:
   289  			return true
   290  		case <-time.After(timeout):
   291  			return false
   292  		}
   293  	} else {
   294  		pot.l.Unlock()
   295  		return true
   296  	}
   297  }