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.
		
		
		
		
		
			
		
			
				
					
					
						
							498 lines
						
					
					
						
							19 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							498 lines
						
					
					
						
							19 KiB
						
					
					
				| package protocol | |
| 
 | |
| import ( | |
| 	"encoding/binary" | |
| 	"fmt" | |
| 	"net" | |
| 	"strconv" | |
| 	"time" | |
| 
 | |
| 	"github.com/seaweedfs/seaweedfs/weed/glog" | |
| ) | |
| 
 | |
| // CoordinatorRegistryInterface defines the interface for coordinator registry operations | |
| type CoordinatorRegistryInterface interface { | |
| 	IsLeader() bool | |
| 	GetLeaderAddress() string | |
| 	WaitForLeader(timeout time.Duration) (string, error) | |
| 	AssignCoordinator(consumerGroup string, requestingGateway string) (*CoordinatorAssignment, error) | |
| 	GetCoordinator(consumerGroup string) (*CoordinatorAssignment, error) | |
| } | |
| 
 | |
| // CoordinatorAssignment represents a consumer group coordinator assignment | |
| type CoordinatorAssignment struct { | |
| 	ConsumerGroup     string | |
| 	CoordinatorAddr   string | |
| 	CoordinatorNodeID int32 | |
| 	AssignedAt        time.Time | |
| 	LastHeartbeat     time.Time | |
| } | |
| 
 | |
| func (h *Handler) handleFindCoordinator(correlationID uint32, apiVersion uint16, requestBody []byte) ([]byte, error) { | |
| 	glog.V(2).Infof("FindCoordinator: version=%d, correlation=%d, bodyLen=%d", apiVersion, correlationID, len(requestBody)) | |
| 	switch apiVersion { | |
| 	case 0: | |
| 		glog.V(4).Infof("FindCoordinator - Routing to V0 handler") | |
| 		return h.handleFindCoordinatorV0(correlationID, requestBody) | |
| 	case 1, 2: | |
| 		glog.V(4).Infof("FindCoordinator - Routing to V1-2 handler (non-flexible)") | |
| 		return h.handleFindCoordinatorV2(correlationID, requestBody) | |
| 	case 3: | |
| 		glog.V(4).Infof("FindCoordinator - Routing to V3 handler (flexible)") | |
| 		return h.handleFindCoordinatorV3(correlationID, requestBody) | |
| 	default: | |
| 		return nil, fmt.Errorf("FindCoordinator version %d not supported", apiVersion) | |
| 	} | |
| } | |
| 
 | |
| func (h *Handler) handleFindCoordinatorV0(correlationID uint32, requestBody []byte) ([]byte, error) { | |
| 	// Parse FindCoordinator v0 request: Key (STRING) only | |
|  | |
| 	if len(requestBody) < 2 { // need at least Key length | |
| 		return nil, fmt.Errorf("FindCoordinator request too short") | |
| 	} | |
| 
 | |
| 	offset := 0 | |
| 
 | |
| 	if len(requestBody) < offset+2 { // coordinator_key_size(2) | |
| 		return nil, fmt.Errorf("FindCoordinator request missing data (need %d bytes, have %d)", offset+2, len(requestBody)) | |
| 	} | |
| 
 | |
| 	// Parse coordinator key (group ID for consumer groups) | |
| 	coordinatorKeySize := binary.BigEndian.Uint16(requestBody[offset : offset+2]) | |
| 	offset += 2 | |
| 
 | |
| 	if len(requestBody) < offset+int(coordinatorKeySize) { | |
| 		return nil, fmt.Errorf("FindCoordinator request missing coordinator key (need %d bytes, have %d)", offset+int(coordinatorKeySize), len(requestBody)) | |
| 	} | |
| 
 | |
| 	coordinatorKey := string(requestBody[offset : offset+int(coordinatorKeySize)]) | |
| 	offset += int(coordinatorKeySize) | |
| 
 | |
| 	// Parse coordinator type (v1+ only, default to 0 for consumer groups in v0) | |
| 	_ = int8(0) // Consumer group coordinator (unused in v0) | |
|  | |
| 	// Find the appropriate coordinator for this group | |
| 	coordinatorHost, coordinatorPort, nodeID, err := h.findCoordinatorForGroup(coordinatorKey) | |
| 	if err != nil { | |
| 		return nil, fmt.Errorf("failed to find coordinator for group %s: %w", coordinatorKey, err) | |
| 	} | |
| 
 | |
| 	// Return hostname instead of IP address for client connectivity | |
| 	// Clients need to connect to the same hostname they originally connected to | |
| 	_ = coordinatorHost // originalHost | |
| 	coordinatorHost = h.getClientConnectableHost(coordinatorHost) | |
| 
 | |
| 	// Build response | |
| 	response := make([]byte, 0, 64) | |
| 
 | |
| 	// NOTE: Correlation ID is handled by writeResponseWithHeader | |
| 	// Do NOT include it in the response body | |
|  | |
| 	// FindCoordinator v0 Response Format (NO throttle_time_ms, NO error_message): | |
| 	// - error_code (INT16) | |
| 	// - node_id (INT32) | |
| 	// - host (STRING) | |
| 	// - port (INT32) | |
|  | |
| 	// Error code (2 bytes, 0 = no error) | |
| 	response = append(response, 0, 0) | |
| 
 | |
| 	// Coordinator node_id (4 bytes) - use direct bit conversion for int32 to uint32 | |
| 	nodeIDBytes := make([]byte, 4) | |
| 	binary.BigEndian.PutUint32(nodeIDBytes, uint32(int32(nodeID))) | |
| 	response = append(response, nodeIDBytes...) | |
| 
 | |
| 	// Coordinator host (string) | |
| 	hostLen := uint16(len(coordinatorHost)) | |
| 	response = append(response, byte(hostLen>>8), byte(hostLen)) | |
| 	response = append(response, []byte(coordinatorHost)...) | |
| 
 | |
| 	// Coordinator port (4 bytes) - validate port range | |
| 	if coordinatorPort < 0 || coordinatorPort > 65535 { | |
| 		return nil, fmt.Errorf("invalid port number: %d", coordinatorPort) | |
| 	} | |
| 	portBytes := make([]byte, 4) | |
| 	binary.BigEndian.PutUint32(portBytes, uint32(coordinatorPort)) | |
| 	response = append(response, portBytes...) | |
| 
 | |
| 	return response, nil | |
| } | |
| 
 | |
| func (h *Handler) handleFindCoordinatorV2(correlationID uint32, requestBody []byte) ([]byte, error) { | |
| 	// Parse FindCoordinator request (v0-2 non-flex): Key (STRING), v1+ adds KeyType (INT8) | |
|  | |
| 	if len(requestBody) < 2 { // need at least Key length | |
| 		return nil, fmt.Errorf("FindCoordinator request too short") | |
| 	} | |
| 
 | |
| 	offset := 0 | |
| 
 | |
| 	if len(requestBody) < offset+2 { // coordinator_key_size(2) | |
| 		return nil, fmt.Errorf("FindCoordinator request missing data (need %d bytes, have %d)", offset+2, len(requestBody)) | |
| 	} | |
| 
 | |
| 	// Parse coordinator key (group ID for consumer groups) | |
| 	coordinatorKeySize := binary.BigEndian.Uint16(requestBody[offset : offset+2]) | |
| 	offset += 2 | |
| 
 | |
| 	if len(requestBody) < offset+int(coordinatorKeySize) { | |
| 		return nil, fmt.Errorf("FindCoordinator request missing coordinator key (need %d bytes, have %d)", offset+int(coordinatorKeySize), len(requestBody)) | |
| 	} | |
| 
 | |
| 	coordinatorKey := string(requestBody[offset : offset+int(coordinatorKeySize)]) | |
| 	offset += int(coordinatorKeySize) | |
| 
 | |
| 	// Coordinator type present in v1+ (INT8). If absent, default 0. | |
| 	if offset < len(requestBody) { | |
| 		_ = requestBody[offset] // coordinatorType | |
| 		offset++                // Move past the coordinator type byte | |
| 	} | |
| 
 | |
| 	// Find the appropriate coordinator for this group | |
| 	coordinatorHost, coordinatorPort, nodeID, err := h.findCoordinatorForGroup(coordinatorKey) | |
| 	if err != nil { | |
| 		return nil, fmt.Errorf("failed to find coordinator for group %s: %w", coordinatorKey, err) | |
| 	} | |
| 
 | |
| 	// Return hostname instead of IP address for client connectivity | |
| 	// Clients need to connect to the same hostname they originally connected to | |
| 	_ = coordinatorHost // originalHost | |
| 	coordinatorHost = h.getClientConnectableHost(coordinatorHost) | |
| 
 | |
| 	response := make([]byte, 0, 64) | |
| 
 | |
| 	// NOTE: Correlation ID is handled by writeResponseWithHeader | |
| 	// Do NOT include it in the response body | |
|  | |
| 	// FindCoordinator v2 Response Format: | |
| 	// - throttle_time_ms (INT32) | |
| 	// - error_code (INT16) | |
| 	// - error_message (STRING) - nullable | |
| 	// - node_id (INT32) | |
| 	// - host (STRING) | |
| 	// - port (INT32) | |
|  | |
| 	// Throttle time (4 bytes, 0 = no throttling) | |
| 	response = append(response, 0, 0, 0, 0) | |
| 
 | |
| 	// Error code (2 bytes, 0 = no error) | |
| 	response = append(response, 0, 0) | |
| 
 | |
| 	// Error message (nullable string) - null for success | |
| 	response = append(response, 0xff, 0xff) // -1 length indicates null | |
|  | |
| 	// Coordinator node_id (4 bytes) - use direct bit conversion for int32 to uint32 | |
| 	nodeIDBytes := make([]byte, 4) | |
| 	binary.BigEndian.PutUint32(nodeIDBytes, uint32(int32(nodeID))) | |
| 	response = append(response, nodeIDBytes...) | |
| 
 | |
| 	// Coordinator host (string) | |
| 	hostLen := uint16(len(coordinatorHost)) | |
| 	response = append(response, byte(hostLen>>8), byte(hostLen)) | |
| 	response = append(response, []byte(coordinatorHost)...) | |
| 
 | |
| 	// Coordinator port (4 bytes) - validate port range | |
| 	if coordinatorPort < 0 || coordinatorPort > 65535 { | |
| 		return nil, fmt.Errorf("invalid port number: %d", coordinatorPort) | |
| 	} | |
| 	portBytes := make([]byte, 4) | |
| 	binary.BigEndian.PutUint32(portBytes, uint32(coordinatorPort)) | |
| 	response = append(response, portBytes...) | |
| 
 | |
| 	// Debug logging (hex dump removed to reduce CPU usage) | |
| 	if glog.V(4) { | |
| 		glog.V(4).Infof("FindCoordinator v2: Built response - bodyLen=%d, host='%s' (len=%d), port=%d, nodeID=%d", | |
| 			len(response), coordinatorHost, len(coordinatorHost), coordinatorPort, nodeID) | |
| 	} | |
| 
 | |
| 	return response, nil | |
| } | |
| 
 | |
| func (h *Handler) handleFindCoordinatorV3(correlationID uint32, requestBody []byte) ([]byte, error) { | |
| 	// Parse FindCoordinator v3 request (flexible version): | |
| 	// - Key (COMPACT_STRING with varint length+1) | |
| 	// - KeyType (INT8) | |
| 	// - Tagged fields (varint) | |
|  | |
| 	if len(requestBody) < 2 { | |
| 		return nil, fmt.Errorf("FindCoordinator v3 request too short") | |
| 	} | |
| 
 | |
| 	// HEX DUMP for debugging | |
| 	glog.V(4).Infof("FindCoordinator V3 request body (first 50 bytes): % x", requestBody[:min(50, len(requestBody))]) | |
| 	glog.V(4).Infof("FindCoordinator V3 request body length: %d", len(requestBody)) | |
| 
 | |
| 	offset := 0 | |
| 
 | |
| 	// The first byte is the tagged fields from the REQUEST HEADER that weren't consumed | |
| 	// Skip the tagged fields count (should be 0x00 for no tagged fields) | |
| 	if len(requestBody) > 0 && requestBody[0] == 0x00 { | |
| 		glog.V(4).Infof("FindCoordinator V3: Skipping header tagged fields byte (0x00)") | |
| 		offset = 1 | |
| 	} | |
| 
 | |
| 	// Parse coordinator key (compact string: varint length+1) | |
| 	glog.V(4).Infof("FindCoordinator V3: About to decode varint from bytes: % x", requestBody[offset:min(offset+5, len(requestBody))]) | |
| 	coordinatorKeyLen, bytesRead, err := DecodeUvarint(requestBody[offset:]) | |
| 	if err != nil || bytesRead <= 0 { | |
| 		return nil, fmt.Errorf("failed to decode coordinator key length: %w (bytes: % x)", err, requestBody[offset:min(offset+5, len(requestBody))]) | |
| 	} | |
| 	offset += bytesRead | |
| 
 | |
| 	glog.V(4).Infof("FindCoordinator V3: coordinatorKeyLen (varint)=%d, bytesRead=%d, offset now=%d", coordinatorKeyLen, bytesRead, offset) | |
| 	glog.V(4).Infof("FindCoordinator V3: Next bytes after varint: % x", requestBody[offset:min(offset+20, len(requestBody))]) | |
| 
 | |
| 	if coordinatorKeyLen == 0 { | |
| 		return nil, fmt.Errorf("coordinator key cannot be null in v3") | |
| 	} | |
| 	// Compact strings in Kafka use length+1 encoding: | |
| 	// varint=0 means null, varint=1 means empty string, varint=n+1 means string of length n | |
| 	coordinatorKeyLen-- // Decode: actual length = varint - 1 | |
|  | |
| 	glog.V(4).Infof("FindCoordinator V3: actual coordinatorKeyLen after decoding: %d", coordinatorKeyLen) | |
| 
 | |
| 	if len(requestBody) < offset+int(coordinatorKeyLen) { | |
| 		return nil, fmt.Errorf("FindCoordinator v3 request missing coordinator key") | |
| 	} | |
| 
 | |
| 	coordinatorKey := string(requestBody[offset : offset+int(coordinatorKeyLen)]) | |
| 	offset += int(coordinatorKeyLen) | |
| 
 | |
| 	// Parse coordinator type (INT8) | |
| 	if offset < len(requestBody) { | |
| 		_ = requestBody[offset] // coordinatorType | |
| 		offset++ | |
| 	} | |
| 
 | |
| 	// Skip tagged fields (we don't need them for now) | |
| 	if offset < len(requestBody) { | |
| 		_, bytesRead, tagErr := DecodeUvarint(requestBody[offset:]) | |
| 		if tagErr == nil && bytesRead > 0 { | |
| 			offset += bytesRead | |
| 			// TODO: Parse tagged fields if needed | |
| 		} | |
| 	} | |
| 
 | |
| 	// Find the appropriate coordinator for this group | |
| 	coordinatorHost, coordinatorPort, nodeID, err := h.findCoordinatorForGroup(coordinatorKey) | |
| 	if err != nil { | |
| 		return nil, fmt.Errorf("failed to find coordinator for group %s: %w", coordinatorKey, err) | |
| 	} | |
| 
 | |
| 	// Return hostname instead of IP address for client connectivity | |
| 	_ = coordinatorHost // originalHost | |
| 	coordinatorHost = h.getClientConnectableHost(coordinatorHost) | |
| 
 | |
| 	// Build response (v3 is flexible, uses compact strings and tagged fields) | |
| 	response := make([]byte, 0, 64) | |
| 
 | |
| 	// NOTE: Correlation ID is handled by writeResponseWithHeader | |
| 	// Do NOT include it in the response body | |
|  | |
| 	// FindCoordinator v3 Response Format (FLEXIBLE): | |
| 	// - throttle_time_ms (INT32) | |
| 	// - error_code (INT16) | |
| 	// - error_message (COMPACT_NULLABLE_STRING with varint length+1, 0 = null) | |
| 	// - node_id (INT32) | |
| 	// - host (COMPACT_STRING with varint length+1) | |
| 	// - port (INT32) | |
| 	// - tagged_fields (varint, 0 = no tags) | |
|  | |
| 	// Throttle time (4 bytes, 0 = no throttling) | |
| 	response = append(response, 0, 0, 0, 0) | |
| 
 | |
| 	// Error code (2 bytes, 0 = no error) | |
| 	response = append(response, 0, 0) | |
| 
 | |
| 	// Error message (compact nullable string) - null for success | |
| 	// Compact nullable string: 0 = null, 1 = empty string, n+1 = string of length n | |
| 	response = append(response, 0) // 0 = null | |
|  | |
| 	// Coordinator node_id (4 bytes) - use direct bit conversion for int32 to uint32 | |
| 	nodeIDBytes := make([]byte, 4) | |
| 	binary.BigEndian.PutUint32(nodeIDBytes, uint32(int32(nodeID))) | |
| 	response = append(response, nodeIDBytes...) | |
| 
 | |
| 	// Coordinator host (compact string: varint length+1) | |
| 	hostLen := uint32(len(coordinatorHost)) | |
| 	response = append(response, EncodeUvarint(hostLen+1)...) // +1 for compact string encoding | |
| 	response = append(response, []byte(coordinatorHost)...) | |
| 
 | |
| 	// Coordinator port (4 bytes) - validate port range | |
| 	if coordinatorPort < 0 || coordinatorPort > 65535 { | |
| 		return nil, fmt.Errorf("invalid port number: %d", coordinatorPort) | |
| 	} | |
| 	portBytes := make([]byte, 4) | |
| 	binary.BigEndian.PutUint32(portBytes, uint32(coordinatorPort)) | |
| 	response = append(response, portBytes...) | |
| 
 | |
| 	// Tagged fields (0 = no tags) | |
| 	response = append(response, 0) | |
| 
 | |
| 	return response, nil | |
| } | |
| 
 | |
| // findCoordinatorForGroup determines the coordinator gateway for a consumer group | |
| // Uses gateway leader for distributed coordinator assignment (first-come-first-serve) | |
| func (h *Handler) findCoordinatorForGroup(groupID string) (host string, port int, nodeID int32, err error) { | |
| 	// Get the coordinator registry from the handler | |
| 	registry := h.GetCoordinatorRegistry() | |
| 	if registry == nil { | |
| 		// Fallback to current gateway if no registry available | |
| 		gatewayAddr := h.GetGatewayAddress() | |
| 		if gatewayAddr == "" { | |
| 			return "", 0, 0, fmt.Errorf("no coordinator registry and no gateway address configured") | |
| 		} | |
| 		host, port, err := h.parseGatewayAddress(gatewayAddr) | |
| 		if err != nil { | |
| 			return "", 0, 0, fmt.Errorf("failed to parse gateway address: %w", err) | |
| 		} | |
| 		nodeID = 1 | |
| 		return host, port, nodeID, nil | |
| 	} | |
| 
 | |
| 	// If this gateway is the leader, handle the assignment directly | |
| 	if registry.IsLeader() { | |
| 		return h.handleCoordinatorAssignmentAsLeader(groupID, registry) | |
| 	} | |
| 
 | |
| 	// If not the leader, contact the leader to get/assign coordinator | |
| 	// But first check if we can quickly become the leader or if there's already a leader | |
| 	if leader := registry.GetLeaderAddress(); leader != "" { | |
| 		// If the leader is this gateway, handle assignment directly | |
| 		if leader == h.GetGatewayAddress() { | |
| 			return h.handleCoordinatorAssignmentAsLeader(groupID, registry) | |
| 		} | |
| 	} | |
| 	return h.requestCoordinatorFromLeader(groupID, registry) | |
| } | |
| 
 | |
| // handleCoordinatorAssignmentAsLeader handles coordinator assignment when this gateway is the leader | |
| func (h *Handler) handleCoordinatorAssignmentAsLeader(groupID string, registry CoordinatorRegistryInterface) (host string, port int, nodeID int32, err error) { | |
| 	// Check if coordinator already exists | |
| 	if assignment, err := registry.GetCoordinator(groupID); err == nil && assignment != nil { | |
| 		return h.parseAddress(assignment.CoordinatorAddr, assignment.CoordinatorNodeID) | |
| 	} | |
| 
 | |
| 	// No coordinator exists, assign the requesting gateway (first-come-first-serve) | |
| 	currentGateway := h.GetGatewayAddress() | |
| 	if currentGateway == "" { | |
| 		return "", 0, 0, fmt.Errorf("no gateway address configured for coordinator assignment") | |
| 	} | |
| 	assignment, err := registry.AssignCoordinator(groupID, currentGateway) | |
| 	if err != nil { | |
| 		// Fallback to current gateway on assignment error | |
| 		host, port, parseErr := h.parseGatewayAddress(currentGateway) | |
| 		if parseErr != nil { | |
| 			return "", 0, 0, fmt.Errorf("failed to parse gateway address after assignment error: %w", parseErr) | |
| 		} | |
| 		nodeID = 1 | |
| 		return host, port, nodeID, nil | |
| 	} | |
| 
 | |
| 	return h.parseAddress(assignment.CoordinatorAddr, assignment.CoordinatorNodeID) | |
| } | |
| 
 | |
| // requestCoordinatorFromLeader requests coordinator assignment from the gateway leader | |
| // If no leader exists, it waits for leader election to complete | |
| func (h *Handler) requestCoordinatorFromLeader(groupID string, registry CoordinatorRegistryInterface) (host string, port int, nodeID int32, err error) { | |
| 	// Wait for leader election to complete with a longer timeout for Schema Registry compatibility | |
| 	_, err = h.waitForLeader(registry, 10*time.Second) // 10 second timeout for enterprise clients | |
| 	if err != nil { | |
| 		gatewayAddr := h.GetGatewayAddress() | |
| 		if gatewayAddr == "" { | |
| 			return "", 0, 0, fmt.Errorf("failed to wait for leader and no gateway address configured: %w", err) | |
| 		} | |
| 		host, port, parseErr := h.parseGatewayAddress(gatewayAddr) | |
| 		if parseErr != nil { | |
| 			return "", 0, 0, fmt.Errorf("failed to parse gateway address after leader wait timeout: %w", parseErr) | |
| 		} | |
| 		nodeID = 1 | |
| 		return host, port, nodeID, nil | |
| 	} | |
| 
 | |
| 	// Since we don't have direct RPC between gateways yet, and the leader might be this gateway, | |
| 	// check if we became the leader during the wait | |
| 	if registry.IsLeader() { | |
| 		return h.handleCoordinatorAssignmentAsLeader(groupID, registry) | |
| 	} | |
| 
 | |
| 	// For now, if we can't directly contact the leader (no inter-gateway RPC yet), | |
| 	// use current gateway as fallback. In a full implementation, this would make | |
| 	// an RPC call to the leader gateway. | |
| 	gatewayAddr := h.GetGatewayAddress() | |
| 	if gatewayAddr == "" { | |
| 		return "", 0, 0, fmt.Errorf("no gateway address configured for fallback coordinator") | |
| 	} | |
| 	host, port, parseErr := h.parseGatewayAddress(gatewayAddr) | |
| 	if parseErr != nil { | |
| 		return "", 0, 0, fmt.Errorf("failed to parse gateway address for fallback: %w", parseErr) | |
| 	} | |
| 	nodeID = 1 | |
| 	return host, port, nodeID, nil | |
| } | |
| 
 | |
| // waitForLeader waits for a leader to be elected, with timeout | |
| func (h *Handler) waitForLeader(registry CoordinatorRegistryInterface, timeout time.Duration) (leaderAddress string, err error) { | |
| 
 | |
| 	// Use the registry's efficient wait mechanism | |
| 	leaderAddress, err = registry.WaitForLeader(timeout) | |
| 	if err != nil { | |
| 		return "", err | |
| 	} | |
| 
 | |
| 	return leaderAddress, nil | |
| } | |
| 
 | |
| // parseGatewayAddress parses a gateway address string (host:port) into host and port | |
| func (h *Handler) parseGatewayAddress(address string) (host string, port int, err error) { | |
| 	// Use net.SplitHostPort for proper IPv6 support | |
| 	hostStr, portStr, err := net.SplitHostPort(address) | |
| 	if err != nil { | |
| 		return "", 0, fmt.Errorf("invalid gateway address format: %s", address) | |
| 	} | |
| 
 | |
| 	port, err = strconv.Atoi(portStr) | |
| 	if err != nil { | |
| 		return "", 0, fmt.Errorf("invalid port in gateway address %s: %v", address, err) | |
| 	} | |
| 
 | |
| 	return hostStr, port, nil | |
| } | |
| 
 | |
| // parseAddress parses a gateway address and returns host, port, and nodeID | |
| func (h *Handler) parseAddress(address string, nodeID int32) (host string, port int, nid int32, err error) { | |
| 	// Reuse the correct parseGatewayAddress implementation | |
| 	host, port, err = h.parseGatewayAddress(address) | |
| 	if err != nil { | |
| 		return "", 0, 0, err | |
| 	} | |
| 	nid = nodeID | |
| 	return host, port, nid, nil | |
| } | |
| 
 | |
| // getClientConnectableHost returns the hostname that clients can connect to | |
| // This ensures that FindCoordinator returns the same hostname the client originally connected to | |
| func (h *Handler) getClientConnectableHost(coordinatorHost string) string { | |
| 	// If the coordinator host is an IP address, return the original gateway hostname | |
| 	// This prevents clients from switching to IP addresses which creates new connections | |
| 	if net.ParseIP(coordinatorHost) != nil { | |
| 		// It's an IP address, return the original gateway hostname | |
| 		gatewayAddr := h.GetGatewayAddress() | |
| 		if host, _, err := h.parseGatewayAddress(gatewayAddr); err == nil { | |
| 			// If the gateway address is also an IP, return the IP directly | |
| 			// This handles local/test environments where hostnames aren't resolvable | |
| 			if net.ParseIP(host) != nil { | |
| 				// Both are IPs, return the actual IP address | |
| 				return coordinatorHost | |
| 			} | |
| 			return host | |
| 		} | |
| 		// Fallback to the coordinator host IP itself | |
| 		return coordinatorHost | |
| 	} | |
| 
 | |
| 	// It's already a hostname, return as-is | |
| 	return coordinatorHost | |
| }
 |