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.
		
		
		
		
		
			
		
			
				
					
					
						
							553 lines
						
					
					
						
							17 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							553 lines
						
					
					
						
							17 KiB
						
					
					
				| package protocol | |
| 
 | |
| import ( | |
| 	"encoding/binary" | |
| 	"fmt" | |
| 	"time" | |
| 
 | |
| 	"github.com/seaweedfs/seaweedfs/weed/mq/kafka/consumer" | |
| ) | |
| 
 | |
| // Heartbeat API (key 12) - Consumer group heartbeat | |
| // Consumers send periodic heartbeats to stay in the group and receive rebalancing signals | |
|  | |
| // HeartbeatRequest represents a Heartbeat request from a Kafka client | |
| type HeartbeatRequest struct { | |
| 	GroupID         string | |
| 	GenerationID    int32 | |
| 	MemberID        string | |
| 	GroupInstanceID string // Optional static membership ID | |
| } | |
| 
 | |
| // HeartbeatResponse represents a Heartbeat response to a Kafka client | |
| type HeartbeatResponse struct { | |
| 	CorrelationID uint32 | |
| 	ErrorCode     int16 | |
| } | |
| 
 | |
| // LeaveGroup API (key 13) - Consumer graceful departure | |
| // Consumers call this when shutting down to trigger immediate rebalancing | |
|  | |
| // LeaveGroupRequest represents a LeaveGroup request from a Kafka client | |
| type LeaveGroupRequest struct { | |
| 	GroupID         string | |
| 	MemberID        string | |
| 	GroupInstanceID string             // Optional static membership ID | |
| 	Members         []LeaveGroupMember // For newer versions, can leave multiple members | |
| } | |
| 
 | |
| // LeaveGroupMember represents a member leaving the group (for batch departures) | |
| type LeaveGroupMember struct { | |
| 	MemberID        string | |
| 	GroupInstanceID string | |
| 	Reason          string // Optional reason for leaving | |
| } | |
| 
 | |
| // LeaveGroupResponse represents a LeaveGroup response to a Kafka client | |
| type LeaveGroupResponse struct { | |
| 	CorrelationID uint32 | |
| 	ErrorCode     int16 | |
| 	Members       []LeaveGroupMemberResponse // Per-member responses for newer versions | |
| } | |
| 
 | |
| // LeaveGroupMemberResponse represents per-member leave group response | |
| type LeaveGroupMemberResponse struct { | |
| 	MemberID        string | |
| 	GroupInstanceID string | |
| 	ErrorCode       int16 | |
| } | |
| 
 | |
| // Error codes specific to consumer coordination are imported from errors.go | |
|  | |
| func (h *Handler) handleHeartbeat(correlationID uint32, apiVersion uint16, requestBody []byte) ([]byte, error) { | |
| 	// Parse Heartbeat request | |
| 	request, err := h.parseHeartbeatRequest(requestBody, apiVersion) | |
| 	if err != nil { | |
| 		return h.buildHeartbeatErrorResponseV(correlationID, ErrorCodeInvalidGroupID, apiVersion), nil | |
| 	} | |
| 
 | |
| 	// Validate request | |
| 	if request.GroupID == "" || request.MemberID == "" { | |
| 		return h.buildHeartbeatErrorResponseV(correlationID, ErrorCodeInvalidGroupID, apiVersion), nil | |
| 	} | |
| 
 | |
| 	// Get consumer group | |
| 	group := h.groupCoordinator.GetGroup(request.GroupID) | |
| 	if group == nil { | |
| 		return h.buildHeartbeatErrorResponseV(correlationID, ErrorCodeInvalidGroupID, apiVersion), nil | |
| 	} | |
| 
 | |
| 	group.Mu.Lock() | |
| 	defer group.Mu.Unlock() | |
| 
 | |
| 	// Update group's last activity | |
| 	group.LastActivity = time.Now() | |
| 
 | |
| 	// Validate member exists | |
| 	member, exists := group.Members[request.MemberID] | |
| 	if !exists { | |
| 		return h.buildHeartbeatErrorResponseV(correlationID, ErrorCodeUnknownMemberID, apiVersion), nil | |
| 	} | |
| 
 | |
| 	// Validate generation | |
| 	if request.GenerationID != group.Generation { | |
| 		return h.buildHeartbeatErrorResponseV(correlationID, ErrorCodeIllegalGeneration, apiVersion), nil | |
| 	} | |
| 
 | |
| 	// Update member's last heartbeat | |
| 	member.LastHeartbeat = time.Now() | |
| 
 | |
| 	// Check if rebalancing is in progress | |
| 	var errorCode int16 = ErrorCodeNone | |
| 	switch group.State { | |
| 	case consumer.GroupStatePreparingRebalance, consumer.GroupStateCompletingRebalance: | |
| 		// Signal the consumer that rebalancing is happening | |
| 		errorCode = ErrorCodeRebalanceInProgress | |
| 	case consumer.GroupStateDead: | |
| 		errorCode = ErrorCodeInvalidGroupID | |
| 	case consumer.GroupStateEmpty: | |
| 		// This shouldn't happen if member exists, but handle gracefully | |
| 		errorCode = ErrorCodeUnknownMemberID | |
| 	case consumer.GroupStateStable: | |
| 		// Normal case - heartbeat accepted | |
| 		errorCode = ErrorCodeNone | |
| 	} | |
| 
 | |
| 	// Build successful response | |
| 	response := HeartbeatResponse{ | |
| 		CorrelationID: correlationID, | |
| 		ErrorCode:     errorCode, | |
| 	} | |
| 
 | |
| 	return h.buildHeartbeatResponseV(response, apiVersion), nil | |
| } | |
| 
 | |
| func (h *Handler) handleLeaveGroup(correlationID uint32, apiVersion uint16, requestBody []byte) ([]byte, error) { | |
| 	// Parse LeaveGroup request | |
| 	request, err := h.parseLeaveGroupRequest(requestBody) | |
| 	if err != nil { | |
| 		return h.buildLeaveGroupErrorResponse(correlationID, ErrorCodeInvalidGroupID, apiVersion), nil | |
| 	} | |
| 
 | |
| 	// Validate request | |
| 	if request.GroupID == "" || request.MemberID == "" { | |
| 		return h.buildLeaveGroupErrorResponse(correlationID, ErrorCodeInvalidGroupID, apiVersion), nil | |
| 	} | |
| 
 | |
| 	// Get consumer group | |
| 	group := h.groupCoordinator.GetGroup(request.GroupID) | |
| 	if group == nil { | |
| 		return h.buildLeaveGroupErrorResponse(correlationID, ErrorCodeInvalidGroupID, apiVersion), nil | |
| 	} | |
| 
 | |
| 	group.Mu.Lock() | |
| 	defer group.Mu.Unlock() | |
| 
 | |
| 	// Update group's last activity | |
| 	group.LastActivity = time.Now() | |
| 
 | |
| 	// Validate member exists | |
| 	member, exists := group.Members[request.MemberID] | |
| 	if !exists { | |
| 		return h.buildLeaveGroupErrorResponse(correlationID, ErrorCodeUnknownMemberID, apiVersion), nil | |
| 	} | |
| 
 | |
| 	// For static members, only remove if GroupInstanceID matches or is not provided | |
| 	if h.groupCoordinator.IsStaticMember(member) { | |
| 		if request.GroupInstanceID != "" && *member.GroupInstanceID != request.GroupInstanceID { | |
| 			return h.buildLeaveGroupErrorResponse(correlationID, ErrorCodeFencedInstanceID, apiVersion), nil | |
| 		} | |
| 		// Unregister static member | |
| 		h.groupCoordinator.UnregisterStaticMemberLocked(group, *member.GroupInstanceID) | |
| 	} | |
| 
 | |
| 	// Remove the member from the group | |
| 	delete(group.Members, request.MemberID) | |
| 
 | |
| 	// Update group state based on remaining members | |
| 	if len(group.Members) == 0 { | |
| 		// Group becomes empty | |
| 		group.State = consumer.GroupStateEmpty | |
| 		group.Generation++ | |
| 		group.Leader = "" | |
| 	} else { | |
| 		// Trigger rebalancing for remaining members | |
| 		group.State = consumer.GroupStatePreparingRebalance | |
| 		group.Generation++ | |
| 
 | |
| 		// If the leaving member was the leader, select a new leader | |
| 		if group.Leader == request.MemberID { | |
| 			// Select first remaining member as new leader | |
| 			for memberID := range group.Members { | |
| 				group.Leader = memberID | |
| 				break | |
| 			} | |
| 		} | |
| 
 | |
| 		// Mark remaining members as pending to trigger rebalancing | |
| 		for _, member := range group.Members { | |
| 			member.State = consumer.MemberStatePending | |
| 		} | |
| 	} | |
| 
 | |
| 	// Update group's subscribed topics (may have changed with member leaving) | |
| 	h.updateGroupSubscriptionFromMembers(group) | |
| 
 | |
| 	// Build successful response | |
| 	response := LeaveGroupResponse{ | |
| 		CorrelationID: correlationID, | |
| 		ErrorCode:     ErrorCodeNone, | |
| 		Members: []LeaveGroupMemberResponse{ | |
| 			{ | |
| 				MemberID:        request.MemberID, | |
| 				GroupInstanceID: request.GroupInstanceID, | |
| 				ErrorCode:       ErrorCodeNone, | |
| 			}, | |
| 		}, | |
| 	} | |
| 
 | |
| 	return h.buildLeaveGroupResponse(response, apiVersion), nil | |
| } | |
| 
 | |
| func (h *Handler) parseHeartbeatRequest(data []byte, apiVersion uint16) (*HeartbeatRequest, error) { | |
| 	if len(data) < 8 { | |
| 		return nil, fmt.Errorf("request too short") | |
| 	} | |
| 
 | |
| 	offset := 0 | |
| 	isFlexible := IsFlexibleVersion(12, apiVersion) // Heartbeat API key = 12 | |
|  | |
| 	// ADMINCLIENT COMPATIBILITY FIX: Parse top-level tagged fields at the beginning for flexible versions | |
| 	if isFlexible { | |
| 		_, consumed, err := DecodeTaggedFields(data[offset:]) | |
| 		if err == nil { | |
| 			offset += consumed | |
| 		} | |
| 	} | |
| 
 | |
| 	// Parse GroupID | |
| 	var groupID string | |
| 	if isFlexible { | |
| 		// FLEXIBLE V4+ FIX: GroupID is a compact string | |
| 		groupIDBytes, consumed := parseCompactString(data[offset:]) | |
| 		if consumed == 0 { | |
| 			return nil, fmt.Errorf("invalid group ID compact string") | |
| 		} | |
| 		if groupIDBytes != nil { | |
| 			groupID = string(groupIDBytes) | |
| 		} | |
| 		offset += consumed | |
| 	} else { | |
| 		// Non-flexible parsing (v0-v3) | |
| 		groupIDLength := int(binary.BigEndian.Uint16(data[offset:])) | |
| 		offset += 2 | |
| 		if offset+groupIDLength > len(data) { | |
| 			return nil, fmt.Errorf("invalid group ID length") | |
| 		} | |
| 		groupID = string(data[offset : offset+groupIDLength]) | |
| 		offset += groupIDLength | |
| 	} | |
| 
 | |
| 	// Generation ID (4 bytes) - always fixed-length | |
| 	if offset+4 > len(data) { | |
| 		return nil, fmt.Errorf("missing generation ID") | |
| 	} | |
| 	generationID := int32(binary.BigEndian.Uint32(data[offset:])) | |
| 	offset += 4 | |
| 
 | |
| 	// Parse MemberID | |
| 	var memberID string | |
| 	if isFlexible { | |
| 		// FLEXIBLE V4+ FIX: MemberID is a compact string | |
| 		memberIDBytes, consumed := parseCompactString(data[offset:]) | |
| 		if consumed == 0 { | |
| 			return nil, fmt.Errorf("invalid member ID compact string") | |
| 		} | |
| 		if memberIDBytes != nil { | |
| 			memberID = string(memberIDBytes) | |
| 		} | |
| 		offset += consumed | |
| 	} else { | |
| 		// Non-flexible parsing (v0-v3) | |
| 		if offset+2 > len(data) { | |
| 			return nil, fmt.Errorf("missing member ID length") | |
| 		} | |
| 		memberIDLength := int(binary.BigEndian.Uint16(data[offset:])) | |
| 		offset += 2 | |
| 		if offset+memberIDLength > len(data) { | |
| 			return nil, fmt.Errorf("invalid member ID length") | |
| 		} | |
| 		memberID = string(data[offset : offset+memberIDLength]) | |
| 		offset += memberIDLength | |
| 	} | |
| 
 | |
| 	// Parse GroupInstanceID (nullable string) - for Heartbeat v1+ | |
| 	var groupInstanceID string | |
| 	if apiVersion >= 1 { | |
| 		if isFlexible { | |
| 			// FLEXIBLE V4+ FIX: GroupInstanceID is a compact nullable string | |
| 			groupInstanceIDBytes, consumed := parseCompactString(data[offset:]) | |
| 			if consumed == 0 && len(data) > offset && data[offset] == 0x00 { | |
| 				groupInstanceID = "" // null | |
| 				offset += 1 | |
| 			} else { | |
| 				if groupInstanceIDBytes != nil { | |
| 					groupInstanceID = string(groupInstanceIDBytes) | |
| 				} | |
| 				offset += consumed | |
| 			} | |
| 		} else { | |
| 			// Non-flexible v1-v3: regular nullable string | |
| 			if offset+2 <= len(data) { | |
| 				instanceIDLength := int16(binary.BigEndian.Uint16(data[offset:])) | |
| 				offset += 2 | |
| 				if instanceIDLength == -1 { | |
| 					groupInstanceID = "" // null string | |
| 				} else if instanceIDLength >= 0 && offset+int(instanceIDLength) <= len(data) { | |
| 					groupInstanceID = string(data[offset : offset+int(instanceIDLength)]) | |
| 					offset += int(instanceIDLength) | |
| 				} | |
| 			} | |
| 		} | |
| 	} | |
| 
 | |
| 	// Parse request-level tagged fields (v4+) | |
| 	if isFlexible { | |
| 		if offset < len(data) { | |
| 			_, consumed, err := DecodeTaggedFields(data[offset:]) | |
| 			if err == nil { | |
| 				offset += consumed | |
| 			} | |
| 		} | |
| 	} | |
| 
 | |
| 	return &HeartbeatRequest{ | |
| 		GroupID:         groupID, | |
| 		GenerationID:    generationID, | |
| 		MemberID:        memberID, | |
| 		GroupInstanceID: groupInstanceID, | |
| 	}, nil | |
| } | |
| 
 | |
| func (h *Handler) parseLeaveGroupRequest(data []byte) (*LeaveGroupRequest, error) { | |
| 	if len(data) < 4 { | |
| 		return nil, fmt.Errorf("request too short") | |
| 	} | |
| 
 | |
| 	offset := 0 | |
| 
 | |
| 	// GroupID (string) | |
| 	groupIDLength := int(binary.BigEndian.Uint16(data[offset:])) | |
| 	offset += 2 | |
| 	if offset+groupIDLength > len(data) { | |
| 		return nil, fmt.Errorf("invalid group ID length") | |
| 	} | |
| 	groupID := string(data[offset : offset+groupIDLength]) | |
| 	offset += groupIDLength | |
| 
 | |
| 	// MemberID (string) | |
| 	if offset+2 > len(data) { | |
| 		return nil, fmt.Errorf("missing member ID length") | |
| 	} | |
| 	memberIDLength := int(binary.BigEndian.Uint16(data[offset:])) | |
| 	offset += 2 | |
| 	if offset+memberIDLength > len(data) { | |
| 		return nil, fmt.Errorf("invalid member ID length") | |
| 	} | |
| 	memberID := string(data[offset : offset+memberIDLength]) | |
| 	offset += memberIDLength | |
| 
 | |
| 	// GroupInstanceID (string, v3+) - optional field | |
| 	var groupInstanceID string | |
| 	if offset+2 <= len(data) { | |
| 		instanceIDLength := int(binary.BigEndian.Uint16(data[offset:])) | |
| 		offset += 2 | |
| 		if instanceIDLength != 0xFFFF && offset+instanceIDLength <= len(data) { | |
| 			groupInstanceID = string(data[offset : offset+instanceIDLength]) | |
| 		} | |
| 	} | |
| 
 | |
| 	return &LeaveGroupRequest{ | |
| 		GroupID:         groupID, | |
| 		MemberID:        memberID, | |
| 		GroupInstanceID: groupInstanceID, | |
| 		Members:         []LeaveGroupMember{}, // Would parse members array for batch operations | |
| 	}, nil | |
| } | |
| 
 | |
| func (h *Handler) buildHeartbeatResponse(response HeartbeatResponse) []byte { | |
| 	result := make([]byte, 0, 12) | |
| 
 | |
| 	// NOTE: Correlation ID is handled by writeResponseWithCorrelationID | |
| 	// Do NOT include it in the response body | |
|  | |
| 	// Error code (2 bytes) | |
| 	errorCodeBytes := make([]byte, 2) | |
| 	binary.BigEndian.PutUint16(errorCodeBytes, uint16(response.ErrorCode)) | |
| 	result = append(result, errorCodeBytes...) | |
| 
 | |
| 	// Throttle time (4 bytes, 0 = no throttling) | |
| 	result = append(result, 0, 0, 0, 0) | |
| 
 | |
| 	return result | |
| } | |
| 
 | |
| func (h *Handler) buildHeartbeatResponseV(response HeartbeatResponse, apiVersion uint16) []byte { | |
| 	isFlexible := IsFlexibleVersion(12, apiVersion) // Heartbeat API key = 12 | |
| 	result := make([]byte, 0, 16) | |
| 
 | |
| 	// NOTE: Correlation ID is handled by writeResponseWithCorrelationID | |
| 	// Do NOT include it in the response body | |
|  | |
| 	if isFlexible { | |
| 		// FLEXIBLE V4+ FORMAT | |
| 		// NOTE: Response header tagged fields are handled by writeResponseWithHeader | |
| 		// Do NOT include them in the response body | |
|  | |
| 		// Throttle time (4 bytes, 0 = no throttling) - comes first in flexible format | |
| 		result = append(result, 0, 0, 0, 0) | |
| 
 | |
| 		// Error code (2 bytes) | |
| 		errorCodeBytes := make([]byte, 2) | |
| 		binary.BigEndian.PutUint16(errorCodeBytes, uint16(response.ErrorCode)) | |
| 		result = append(result, errorCodeBytes...) | |
| 
 | |
| 		// Response body tagged fields (varint: 0x00 = empty) | |
| 		result = append(result, 0x00) | |
| 	} else if apiVersion >= 1 { | |
| 		// NON-FLEXIBLE V1-V3 FORMAT: throttle_time_ms BEFORE error_code | |
| 		// CRITICAL FIX: Kafka protocol specifies throttle_time_ms comes FIRST in v1+ | |
|  | |
| 		// Throttle time (4 bytes, 0 = no throttling) - comes first in v1-v3 | |
| 		result = append(result, 0, 0, 0, 0) | |
| 
 | |
| 		// Error code (2 bytes) | |
| 		errorCodeBytes := make([]byte, 2) | |
| 		binary.BigEndian.PutUint16(errorCodeBytes, uint16(response.ErrorCode)) | |
| 		result = append(result, errorCodeBytes...) | |
| 	} else { | |
| 		// V0 FORMAT: Only error_code, NO throttle_time_ms | |
|  | |
| 		// Error code (2 bytes) | |
| 		errorCodeBytes := make([]byte, 2) | |
| 		binary.BigEndian.PutUint16(errorCodeBytes, uint16(response.ErrorCode)) | |
| 		result = append(result, errorCodeBytes...) | |
| 	} | |
| 
 | |
| 	return result | |
| } | |
| 
 | |
| func (h *Handler) buildLeaveGroupResponse(response LeaveGroupResponse, apiVersion uint16) []byte { | |
| 	// LeaveGroup v0 only includes correlation_id and error_code (no throttle_time_ms, no members) | |
| 	if apiVersion == 0 { | |
| 		return h.buildLeaveGroupV0Response(response) | |
| 	} | |
| 
 | |
| 	// For v1+ use the full response format | |
| 	return h.buildLeaveGroupFullResponse(response) | |
| } | |
| 
 | |
| func (h *Handler) buildLeaveGroupV0Response(response LeaveGroupResponse) []byte { | |
| 	result := make([]byte, 0, 6) | |
| 
 | |
| 	// NOTE: Correlation ID is handled by writeResponseWithCorrelationID | |
| 	// Do NOT include it in the response body | |
|  | |
| 	// Error code (2 bytes) - that's it for v0! | |
| 	errorCodeBytes := make([]byte, 2) | |
| 	binary.BigEndian.PutUint16(errorCodeBytes, uint16(response.ErrorCode)) | |
| 	result = append(result, errorCodeBytes...) | |
| 
 | |
| 	return result | |
| } | |
| 
 | |
| func (h *Handler) buildLeaveGroupFullResponse(response LeaveGroupResponse) []byte { | |
| 	estimatedSize := 16 | |
| 	for _, member := range response.Members { | |
| 		estimatedSize += len(member.MemberID) + len(member.GroupInstanceID) + 8 | |
| 	} | |
| 
 | |
| 	result := make([]byte, 0, estimatedSize) | |
| 
 | |
| 	// NOTE: Correlation ID is handled by writeResponseWithCorrelationID | |
| 	// Do NOT include it in the response body | |
|  | |
| 	// For LeaveGroup v1+, throttle_time_ms comes first (4 bytes) | |
| 	result = append(result, 0, 0, 0, 0) | |
| 
 | |
| 	// Error code (2 bytes) | |
| 	errorCodeBytes := make([]byte, 2) | |
| 	binary.BigEndian.PutUint16(errorCodeBytes, uint16(response.ErrorCode)) | |
| 	result = append(result, errorCodeBytes...) | |
| 
 | |
| 	// Members array length (4 bytes) | |
| 	membersLengthBytes := make([]byte, 4) | |
| 	binary.BigEndian.PutUint32(membersLengthBytes, uint32(len(response.Members))) | |
| 	result = append(result, membersLengthBytes...) | |
| 
 | |
| 	// Members | |
| 	for _, member := range response.Members { | |
| 		// Member ID length (2 bytes) | |
| 		memberIDLength := make([]byte, 2) | |
| 		binary.BigEndian.PutUint16(memberIDLength, uint16(len(member.MemberID))) | |
| 		result = append(result, memberIDLength...) | |
| 
 | |
| 		// Member ID | |
| 		result = append(result, []byte(member.MemberID)...) | |
| 
 | |
| 		// Group instance ID length (2 bytes) | |
| 		instanceIDLength := make([]byte, 2) | |
| 		binary.BigEndian.PutUint16(instanceIDLength, uint16(len(member.GroupInstanceID))) | |
| 		result = append(result, instanceIDLength...) | |
| 
 | |
| 		// Group instance ID | |
| 		if len(member.GroupInstanceID) > 0 { | |
| 			result = append(result, []byte(member.GroupInstanceID)...) | |
| 		} | |
| 
 | |
| 		// Error code (2 bytes) | |
| 		memberErrorBytes := make([]byte, 2) | |
| 		binary.BigEndian.PutUint16(memberErrorBytes, uint16(member.ErrorCode)) | |
| 		result = append(result, memberErrorBytes...) | |
| 	} | |
| 
 | |
| 	return result | |
| } | |
| 
 | |
| func (h *Handler) buildHeartbeatErrorResponse(correlationID uint32, errorCode int16) []byte { | |
| 	response := HeartbeatResponse{ | |
| 		CorrelationID: correlationID, | |
| 		ErrorCode:     errorCode, | |
| 	} | |
| 
 | |
| 	return h.buildHeartbeatResponse(response) | |
| } | |
| 
 | |
| func (h *Handler) buildHeartbeatErrorResponseV(correlationID uint32, errorCode int16, apiVersion uint16) []byte { | |
| 	response := HeartbeatResponse{ | |
| 		CorrelationID: correlationID, | |
| 		ErrorCode:     errorCode, | |
| 	} | |
| 
 | |
| 	return h.buildHeartbeatResponseV(response, apiVersion) | |
| } | |
| 
 | |
| func (h *Handler) buildLeaveGroupErrorResponse(correlationID uint32, errorCode int16, apiVersion uint16) []byte { | |
| 	response := LeaveGroupResponse{ | |
| 		CorrelationID: correlationID, | |
| 		ErrorCode:     errorCode, | |
| 		Members:       []LeaveGroupMemberResponse{}, | |
| 	} | |
| 
 | |
| 	return h.buildLeaveGroupResponse(response, apiVersion) | |
| } | |
| 
 | |
| func (h *Handler) updateGroupSubscriptionFromMembers(group *consumer.ConsumerGroup) { | |
| 	// Update group's subscribed topics from remaining members | |
| 	group.SubscribedTopics = make(map[string]bool) | |
| 	for _, member := range group.Members { | |
| 		for _, topic := range member.Subscription { | |
| 			group.SubscribedTopics[topic] = true | |
| 		} | |
| 	} | |
| }
 |