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.
		
		
		
		
		
			
		
			
				
					
					
						
							164 lines
						
					
					
						
							5.6 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							164 lines
						
					
					
						
							5.6 KiB
						
					
					
				| package broker | |
| 
 | |
| import ( | |
| 	"context" | |
| 	"fmt" | |
| 
 | |
| 	"github.com/seaweedfs/seaweedfs/weed/glog" | |
| 	"github.com/seaweedfs/seaweedfs/weed/mq/topic" | |
| 	"github.com/seaweedfs/seaweedfs/weed/pb/mq_pb" | |
| ) | |
| 
 | |
| // FetchMessage implements Kafka-style stateless message fetching | |
| // This is the recommended API for Kafka gateway and other stateless clients | |
| // | |
| // Key differences from SubscribeMessage: | |
| // 1. Request/Response pattern (not streaming) | |
| // 2. No session state maintained on broker | |
| // 3. Each request is completely independent | |
| // 4. Safe for concurrent calls at different offsets | |
| // 5. No Subscribe loop cancellation/restart complexity | |
| // | |
| // Design inspired by Kafka's Fetch API: | |
| // - Client manages offset tracking | |
| // - Each fetch is independent | |
| // - No shared state between requests | |
| // - Natural support for concurrent reads | |
| func (b *MessageQueueBroker) FetchMessage(ctx context.Context, req *mq_pb.FetchMessageRequest) (*mq_pb.FetchMessageResponse, error) { | |
| 	glog.V(3).Infof("[FetchMessage] CALLED!") // DEBUG: ensure this shows up | |
|  | |
| 	// Validate request | |
| 	if req.Topic == nil { | |
| 		return nil, fmt.Errorf("missing topic") | |
| 	} | |
| 	if req.Partition == nil { | |
| 		return nil, fmt.Errorf("missing partition") | |
| 	} | |
| 
 | |
| 	t := topic.FromPbTopic(req.Topic) | |
| 	partition := topic.FromPbPartition(req.Partition) | |
| 
 | |
| 	glog.V(3).Infof("[FetchMessage] %s/%s partition=%v offset=%d maxMessages=%d maxBytes=%d consumer=%s/%s", | |
| 		t.Namespace, t.Name, partition, req.StartOffset, req.MaxMessages, req.MaxBytes, | |
| 		req.ConsumerGroup, req.ConsumerId) | |
| 
 | |
| 	// Get local partition | |
| 	localPartition, err := b.GetOrGenerateLocalPartition(t, partition) | |
| 	if err != nil { | |
| 		glog.Errorf("[FetchMessage] Failed to get partition: %v", err) | |
| 		return &mq_pb.FetchMessageResponse{ | |
| 			Error:     fmt.Sprintf("partition not found: %v", err), | |
| 			ErrorCode: 1, | |
| 		}, nil | |
| 	} | |
| 	if localPartition == nil { | |
| 		return &mq_pb.FetchMessageResponse{ | |
| 			Error:     "partition not found", | |
| 			ErrorCode: 1, | |
| 		}, nil | |
| 	} | |
| 
 | |
| 	// Set defaults for limits | |
| 	maxMessages := int(req.MaxMessages) | |
| 	if maxMessages <= 0 { | |
| 		maxMessages = 100 // Reasonable default | |
| 	} | |
| 	if maxMessages > 10000 { | |
| 		maxMessages = 10000 // Safety limit | |
| 	} | |
| 
 | |
| 	maxBytes := int(req.MaxBytes) | |
| 	if maxBytes <= 0 { | |
| 		maxBytes = 4 * 1024 * 1024 // 4MB default | |
| 	} | |
| 	if maxBytes > 100*1024*1024 { | |
| 		maxBytes = 100 * 1024 * 1024 // 100MB safety limit | |
| 	} | |
| 
 | |
| 	// TODO: Long poll support disabled for now (causing timeouts) | |
| 	// Check if we should wait for data (long poll support) | |
| 	// shouldWait := req.MaxWaitMs > 0 | |
| 	// if shouldWait { | |
| 	// 	// Wait for data to be available (with timeout) | |
| 	// 	dataAvailable := localPartition.LogBuffer.WaitForDataWithTimeout(req.StartOffset, int(req.MaxWaitMs)) | |
| 	// 	if !dataAvailable { | |
| 	// 		// Timeout - return empty response | |
| 	// 		glog.V(3).Infof("[FetchMessage] Timeout waiting for data at offset %d", req.StartOffset) | |
| 	// 		return &mq_pb.FetchMessageResponse{ | |
| 	// 			Messages:       []*mq_pb.DataMessage{}, | |
| 	// 			HighWaterMark:  localPartition.LogBuffer.GetHighWaterMark(), | |
| 	// 			LogStartOffset: localPartition.LogBuffer.GetLogStartOffset(), | |
| 	// 			EndOfPartition: false, | |
| 	// 			NextOffset:     req.StartOffset, | |
| 	// 		}, nil | |
| 	// 	} | |
| 	// } | |
|  | |
| 	// Check if disk read function is configured | |
| 	if localPartition.LogBuffer.ReadFromDiskFn == nil { | |
| 		glog.Errorf("[FetchMessage] LogBuffer.ReadFromDiskFn is nil! This should not happen.") | |
| 	} else { | |
| 		glog.V(3).Infof("[FetchMessage] LogBuffer.ReadFromDiskFn is configured") | |
| 	} | |
| 
 | |
| 	// Use requested offset directly - let ReadMessagesAtOffset handle disk reads | |
| 	requestedOffset := req.StartOffset | |
| 
 | |
| 	// Read messages from LogBuffer (stateless read) | |
| 	logEntries, nextOffset, highWaterMark, endOfPartition, err := localPartition.LogBuffer.ReadMessagesAtOffset( | |
| 		requestedOffset, | |
| 		maxMessages, | |
| 		maxBytes, | |
| 	) | |
| 
 | |
| 	// CRITICAL: Log the result with full details | |
| 	if len(logEntries) == 0 && highWaterMark > requestedOffset && err == nil { | |
| 		glog.Errorf("[FetchMessage] CRITICAL: ReadMessagesAtOffset returned 0 entries but HWM=%d > requestedOffset=%d (should return data!)", | |
| 			highWaterMark, requestedOffset) | |
| 		glog.Errorf("[FetchMessage] Details: nextOffset=%d, endOfPartition=%v, bufferStartOffset=%d", | |
| 			nextOffset, endOfPartition, localPartition.LogBuffer.GetLogStartOffset()) | |
| 	} | |
| 
 | |
| 	if err != nil { | |
| 		// Check if this is an "offset out of range" error | |
| 		errMsg := err.Error() | |
| 		if len(errMsg) > 0 && (len(errMsg) < 20 || errMsg[:20] != "offset") { | |
| 			glog.Errorf("[FetchMessage] Read error: %v", err) | |
| 		} else { | |
| 			// Offset out of range - this is expected when consumer requests old data | |
| 			glog.V(3).Infof("[FetchMessage] Offset out of range: %v", err) | |
| 		} | |
| 
 | |
| 		// Return empty response with metadata - let client adjust offset | |
| 		return &mq_pb.FetchMessageResponse{ | |
| 			Messages:       []*mq_pb.DataMessage{}, | |
| 			HighWaterMark:  highWaterMark, | |
| 			LogStartOffset: localPartition.LogBuffer.GetLogStartOffset(), | |
| 			EndOfPartition: false, | |
| 			NextOffset:     localPartition.LogBuffer.GetLogStartOffset(), // Suggest starting from earliest available | |
| 			Error:          errMsg, | |
| 			ErrorCode:      2, | |
| 		}, nil | |
| 	} | |
| 
 | |
| 	// Convert to protobuf messages | |
| 	messages := make([]*mq_pb.DataMessage, 0, len(logEntries)) | |
| 	for _, entry := range logEntries { | |
| 		messages = append(messages, &mq_pb.DataMessage{ | |
| 			Key:   entry.Key, | |
| 			Value: entry.Data, | |
| 			TsNs:  entry.TsNs, | |
| 		}) | |
| 	} | |
| 
 | |
| 	glog.V(4).Infof("[FetchMessage] Returning %d messages, nextOffset=%d, highWaterMark=%d, endOfPartition=%v", | |
| 		len(messages), nextOffset, highWaterMark, endOfPartition) | |
| 
 | |
| 	return &mq_pb.FetchMessageResponse{ | |
| 		Messages:       messages, | |
| 		HighWaterMark:  highWaterMark, | |
| 		LogStartOffset: localPartition.LogBuffer.GetLogStartOffset(), | |
| 		EndOfPartition: endOfPartition, | |
| 		NextOffset:     nextOffset, | |
| 	}, nil | |
| }
 |