You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							181 lines
						
					
					
						
							7.4 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							181 lines
						
					
					
						
							7.4 KiB
						
					
					
				
								package offset
							 | 
						|
								
							 | 
						|
								import (
							 | 
						|
									"context"
							 | 
						|
									"encoding/json"
							 | 
						|
									"fmt"
							 | 
						|
									"io"
							 | 
						|
									"time"
							 | 
						|
								
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/filer"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/filer_client"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/mq/topic"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/pb/schema_pb"
							 | 
						|
								)
							 | 
						|
								
							 | 
						|
								// ConsumerGroupPosition represents a consumer's position in a partition
							 | 
						|
								// This can be either a timestamp or an offset
							 | 
						|
								type ConsumerGroupPosition struct {
							 | 
						|
									Type        string `json:"type"`         // "offset" or "timestamp"
							 | 
						|
									Value       int64  `json:"value"`        // The actual offset or timestamp value
							 | 
						|
									OffsetType  string `json:"offset_type"`  // Optional: OffsetType enum name (e.g., "EXACT_OFFSET")
							 | 
						|
									CommittedAt int64  `json:"committed_at"` // Unix timestamp in milliseconds when committed
							 | 
						|
									Metadata    string `json:"metadata"`     // Optional: application-specific metadata
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// ConsumerGroupOffsetStorage handles consumer group offset persistence
							 | 
						|
								// Each consumer group gets its own offset file in a dedicated consumers/ subfolder:
							 | 
						|
								// Path: /topics/{namespace}/{topic}/{version}/{partition}/consumers/{consumer_group}.offset
							 | 
						|
								type ConsumerGroupOffsetStorage interface {
							 | 
						|
									// SaveConsumerGroupOffset saves the committed offset for a consumer group
							 | 
						|
									SaveConsumerGroupOffset(t topic.Topic, p topic.Partition, consumerGroup string, offset int64) error
							 | 
						|
								
							 | 
						|
									// SaveConsumerGroupPosition saves the committed position (offset or timestamp) for a consumer group
							 | 
						|
									SaveConsumerGroupPosition(t topic.Topic, p topic.Partition, consumerGroup string, position *ConsumerGroupPosition) error
							 | 
						|
								
							 | 
						|
									// LoadConsumerGroupOffset loads the committed offset for a consumer group (backward compatible)
							 | 
						|
									LoadConsumerGroupOffset(t topic.Topic, p topic.Partition, consumerGroup string) (int64, error)
							 | 
						|
								
							 | 
						|
									// LoadConsumerGroupPosition loads the committed position for a consumer group
							 | 
						|
									LoadConsumerGroupPosition(t topic.Topic, p topic.Partition, consumerGroup string) (*ConsumerGroupPosition, error)
							 | 
						|
								
							 | 
						|
									// ListConsumerGroups returns all consumer groups for a topic partition
							 | 
						|
									ListConsumerGroups(t topic.Topic, p topic.Partition) ([]string, error)
							 | 
						|
								
							 | 
						|
									// DeleteConsumerGroupOffset removes the offset file for a consumer group
							 | 
						|
									DeleteConsumerGroupOffset(t topic.Topic, p topic.Partition, consumerGroup string) error
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// FilerConsumerGroupOffsetStorage implements ConsumerGroupOffsetStorage using SeaweedFS filer
							 | 
						|
								type FilerConsumerGroupOffsetStorage struct {
							 | 
						|
									filerClientAccessor *filer_client.FilerClientAccessor
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// NewFilerConsumerGroupOffsetStorageWithAccessor creates storage using a shared filer client accessor
							 | 
						|
								func NewFilerConsumerGroupOffsetStorageWithAccessor(filerClientAccessor *filer_client.FilerClientAccessor) *FilerConsumerGroupOffsetStorage {
							 | 
						|
									return &FilerConsumerGroupOffsetStorage{
							 | 
						|
										filerClientAccessor: filerClientAccessor,
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// SaveConsumerGroupOffset saves the committed offset for a consumer group
							 | 
						|
								// Stores as: /topics/{namespace}/{topic}/{version}/{partition}/consumers/{consumer_group}.offset
							 | 
						|
								// This is a convenience method that wraps SaveConsumerGroupPosition
							 | 
						|
								func (f *FilerConsumerGroupOffsetStorage) SaveConsumerGroupOffset(t topic.Topic, p topic.Partition, consumerGroup string, offset int64) error {
							 | 
						|
									position := &ConsumerGroupPosition{
							 | 
						|
										Type:        "offset",
							 | 
						|
										Value:       offset,
							 | 
						|
										OffsetType:  schema_pb.OffsetType_EXACT_OFFSET.String(),
							 | 
						|
										CommittedAt: time.Now().UnixMilli(),
							 | 
						|
									}
							 | 
						|
									return f.SaveConsumerGroupPosition(t, p, consumerGroup, position)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// SaveConsumerGroupPosition saves the committed position (offset or timestamp) for a consumer group
							 | 
						|
								// Stores as JSON: /topics/{namespace}/{topic}/{version}/{partition}/consumers/{consumer_group}.offset
							 | 
						|
								func (f *FilerConsumerGroupOffsetStorage) SaveConsumerGroupPosition(t topic.Topic, p topic.Partition, consumerGroup string, position *ConsumerGroupPosition) error {
							 | 
						|
									partitionDir := topic.PartitionDir(t, p)
							 | 
						|
									consumersDir := fmt.Sprintf("%s/consumers", partitionDir)
							 | 
						|
									offsetFileName := fmt.Sprintf("%s.offset", consumerGroup)
							 | 
						|
								
							 | 
						|
									// Marshal position to JSON
							 | 
						|
									jsonBytes, err := json.Marshal(position)
							 | 
						|
									if err != nil {
							 | 
						|
										return fmt.Errorf("failed to marshal position to JSON: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return f.filerClientAccessor.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
							 | 
						|
										return filer.SaveInsideFiler(client, consumersDir, offsetFileName, jsonBytes)
							 | 
						|
									})
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// LoadConsumerGroupOffset loads the committed offset for a consumer group
							 | 
						|
								// This method provides backward compatibility and returns just the offset value
							 | 
						|
								func (f *FilerConsumerGroupOffsetStorage) LoadConsumerGroupOffset(t topic.Topic, p topic.Partition, consumerGroup string) (int64, error) {
							 | 
						|
									position, err := f.LoadConsumerGroupPosition(t, p, consumerGroup)
							 | 
						|
									if err != nil {
							 | 
						|
										return -1, err
							 | 
						|
									}
							 | 
						|
									return position.Value, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// LoadConsumerGroupPosition loads the committed position for a consumer group
							 | 
						|
								func (f *FilerConsumerGroupOffsetStorage) LoadConsumerGroupPosition(t topic.Topic, p topic.Partition, consumerGroup string) (*ConsumerGroupPosition, error) {
							 | 
						|
									partitionDir := topic.PartitionDir(t, p)
							 | 
						|
									consumersDir := fmt.Sprintf("%s/consumers", partitionDir)
							 | 
						|
									offsetFileName := fmt.Sprintf("%s.offset", consumerGroup)
							 | 
						|
								
							 | 
						|
									var position *ConsumerGroupPosition
							 | 
						|
									err := f.filerClientAccessor.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
							 | 
						|
										data, err := filer.ReadInsideFiler(client, consumersDir, offsetFileName)
							 | 
						|
										if err != nil {
							 | 
						|
											return err
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										// Parse JSON format
							 | 
						|
										position = &ConsumerGroupPosition{}
							 | 
						|
										if err := json.Unmarshal(data, position); err != nil {
							 | 
						|
											return fmt.Errorf("invalid consumer group offset file format: %w", err)
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										return nil
							 | 
						|
									})
							 | 
						|
								
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, err
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return position, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// ListConsumerGroups returns all consumer groups for a topic partition
							 | 
						|
								func (f *FilerConsumerGroupOffsetStorage) ListConsumerGroups(t topic.Topic, p topic.Partition) ([]string, error) {
							 | 
						|
									partitionDir := topic.PartitionDir(t, p)
							 | 
						|
									consumersDir := fmt.Sprintf("%s/consumers", partitionDir)
							 | 
						|
									var consumerGroups []string
							 | 
						|
								
							 | 
						|
									err := f.filerClientAccessor.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
							 | 
						|
										// Use ListEntries to get directory contents
							 | 
						|
										stream, err := client.ListEntries(context.Background(), &filer_pb.ListEntriesRequest{
							 | 
						|
											Directory: consumersDir,
							 | 
						|
										})
							 | 
						|
										if err != nil {
							 | 
						|
											return err
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										for {
							 | 
						|
											resp, err := stream.Recv()
							 | 
						|
											if err != nil {
							 | 
						|
												if err == io.EOF {
							 | 
						|
													break
							 | 
						|
												}
							 | 
						|
												return err
							 | 
						|
											}
							 | 
						|
								
							 | 
						|
											entry := resp.Entry
							 | 
						|
											if entry != nil && !entry.IsDirectory && entry.Name != "" {
							 | 
						|
												// Check if this is a consumer group offset file (ends with .offset)
							 | 
						|
												if len(entry.Name) > 7 && entry.Name[len(entry.Name)-7:] == ".offset" {
							 | 
						|
													// Extract consumer group name (remove .offset suffix)
							 | 
						|
													consumerGroup := entry.Name[:len(entry.Name)-7]
							 | 
						|
													consumerGroups = append(consumerGroups, consumerGroup)
							 | 
						|
												}
							 | 
						|
											}
							 | 
						|
										}
							 | 
						|
										return nil
							 | 
						|
									})
							 | 
						|
								
							 | 
						|
									return consumerGroups, err
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// DeleteConsumerGroupOffset removes the offset file for a consumer group
							 | 
						|
								func (f *FilerConsumerGroupOffsetStorage) DeleteConsumerGroupOffset(t topic.Topic, p topic.Partition, consumerGroup string) error {
							 | 
						|
									partitionDir := topic.PartitionDir(t, p)
							 | 
						|
									consumersDir := fmt.Sprintf("%s/consumers", partitionDir)
							 | 
						|
									offsetFileName := fmt.Sprintf("%s.offset", consumerGroup)
							 | 
						|
								
							 | 
						|
									return f.filerClientAccessor.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
							 | 
						|
										return filer_pb.DoRemove(context.Background(), client, consumersDir, offsetFileName, false, false, false, false, nil)
							 | 
						|
									})
							 | 
						|
								}
							 |