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 }