Files
seaweedfs/weed/pb/filer_pb_direct_read.go
T
Chris Lu af6f69740c Read metadata log chunks the way the mount reads every other chunk (#11018)
* Replay metadata log chunks the way the mount reads every other chunk

The subscription's log-chunk replay built its own lookup, which always
resolves volume server addresses. A mount started with
-volumeServerAccess=filerProxy cannot reach those, so every fresh
subscription failed on the previous minute's persisted segment and
resubscribed a second later, forever. Take the lookup from the caller
instead; the mount hands over the one it uses for file reads, which also
keeps publicUrl and the bounded location cache in play.

Claude-Session: https://claude.ai/code/session_01NGqrxYj7cHUpSrL249n3Z6

* Keep a log chunk read failure off the filer connection

A metadata subscriber reads persisted log chunks over HTTP from volume
servers and hands whatever went wrong back as the subscription's error.
"connection refused" from a volume server then matched the transport
patterns that decide a gRPC channel is dead, so every failed replay
closed the shared filer ClientConn and cancelled the assign and upload
RPCs riding on it with "the client connection is closing". Mark those
read failures so they are judged for what they are.

Claude-Session: https://claude.ai/code/session_01NGqrxYj7cHUpSrL249n3Z6
2026-08-28 14:15:39 -07:00

355 lines
10 KiB
Go

package pb
import (
"container/heap"
"errors"
"fmt"
"io"
"strings"
"sync"
"google.golang.org/protobuf/proto"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/util"
)
// LogFileReaderFn creates an io.ReadCloser for a set of file chunks.
type LogFileReaderFn func(chunks []*filer_pb.FileChunk) (io.ReadCloser, error)
// logEntryChannelSize bounds decoded entries in flight per filer stream.
const logEntryChannelSize = 512
// maxLogEntrySize guards the per-entry allocation against a corrupt size
// prefix, mirroring the filer package's unexported constant.
const maxLogEntrySize = 1 << 30
// ErrLogFileRead marks a failure to read persisted log chunks from volume
// servers. It surfaces as the metadata stream's error, but says nothing about
// the filer connection that carried the stream.
var ErrLogFileRead = errors.New("read log file refs")
// errReaderStopped signals that the entry consumer asked to stop (the merge
// loop aborted or the caller hit a processing error). It is not a read failure.
var errReaderStopped = errors.New("log entry reader stopped")
// PathFilter holds subscription path filtering parameters, matching the
// server-side eachEventNotificationFn filtering logic.
type PathFilter struct {
PathPrefix string
AdditionalPathPrefixes []string
DirectoriesToWatch []string
}
// ReadLogFileRefs reads log file data directly from volume servers using the
// chunk references, merges entries from multiple filers in timestamp order
// (same algorithm as the server's OrderedLogVisitor), applies path filtering,
// and invokes processEventFn for each matching event.
//
// Filers are read in parallel (one goroutine per filer), each streaming
// entries through a bounded channel.
func ReadLogFileRefs(
refs []*filer_pb.LogFileChunkRef,
newReader LogFileReaderFn,
startTsNs, stopTsNs int64,
filter PathFilter,
processEventFn ProcessMetadataFunc,
) (lastTsNs int64, err error) {
if len(refs) == 0 {
return
}
// Group refs by filer ID, preserving order within each filer.
perFiler := make(map[string][]*filer_pb.LogFileChunkRef)
var filerOrder []string
for _, ref := range refs {
if len(ref.Chunks) == 0 {
continue
}
if _, seen := perFiler[ref.FilerId]; !seen {
filerOrder = append(filerOrder, ref.FilerId)
}
perFiler[ref.FilerId] = append(perFiler[ref.FilerId], ref)
}
if len(filerOrder) == 0 {
return
}
return readFilersMerged(filerOrder, perFiler, newReader, startTsNs, stopTsNs, filter, processEventFn)
}
// readFilersMerged reads files from each filer in parallel (one producer
// goroutine per filer streaming decoded entries through a bounded channel),
// then merges entries in timestamp order via min-heap. A single filer is the
// degenerate one-stream merge.
func readFilersMerged(
filerOrder []string,
perFiler map[string][]*filer_pb.LogFileChunkRef,
newReader LogFileReaderFn,
startTsNs, stopTsNs int64,
filter PathFilter,
processEventFn ProcessMetadataFunc,
) (lastTsNs int64, err error) {
type filerStream struct {
filerId string
entryCh chan *filer_pb.LogEntry
}
streams := make([]filerStream, len(filerOrder))
// A genuine (non chunk-not-found) read error must fail the whole replay: the
// caller advances its cursor only on success, so a swallowed error leaves a
// permanent gap. stop aborts the other readers; fatalErr is read on exit.
//
// Closing stop is the only cleanup: producers observe it at every send and
// every file boundary and exit on their own. An abort must not wait for
// them — a producer can be wedged in an uncancellable chunk read, and
// joining it would stall the caller's retry loop behind a dead connection.
stop := make(chan struct{})
var stopOnce sync.Once
closeStop := func() { stopOnce.Do(func() { close(stop) }) }
defer closeStop()
var fatalMu sync.Mutex
var fatalErr error
setFatal := func(e error) {
fatalMu.Lock()
if fatalErr == nil {
fatalErr = e
}
fatalMu.Unlock()
closeStop()
}
fatal := func() error {
fatalMu.Lock()
defer fatalMu.Unlock()
return fatalErr
}
for i, filerId := range filerOrder {
entryCh := make(chan *filer_pb.LogEntry, logEntryChannelSize)
streams[i] = filerStream{filerId: filerId, entryCh: entryCh}
go func(refs []*filer_pb.LogFileChunkRef, ch chan *filer_pb.LogEntry) {
defer close(ch)
readFilerFilesToChannel(refs, newReader, startTsNs, stopTsNs, ch, stop, setFatal)
}(perFiler[filerId], entryCh)
}
// Seed the min-heap with the first entry from each filer
pq := &logEntryHeap{}
heap.Init(pq)
for i := range streams {
select {
case entry, ok := <-streams[i].entryCh:
if ok {
heap.Push(pq, &logEntryHeapItem{entry: entry, filerIdx: i})
}
case <-stop:
return lastTsNs, fatal()
}
}
// Merge loop
for pq.Len() > 0 {
// stop is closed only by setFatal here, so a closed stop means a reader
// aborted; lock-free bail on the hot path.
select {
case <-stop:
return lastTsNs, fatal()
default:
}
item := heap.Pop(pq).(*logEntryHeapItem)
lastTsNs, err = processOneLogEntry(item.entry, filter, processEventFn)
if err != nil {
return
}
select {
case entry, ok := <-streams[item.filerIdx].entryCh:
if ok {
heap.Push(pq, &logEntryHeapItem{entry: entry, filerIdx: item.filerIdx})
}
case <-stop:
return lastTsNs, fatal()
}
}
// All channels closed: every producer has finished (setFatal, if any,
// happened before its close).
if fe := fatal(); fe != nil {
return lastTsNs, fe
}
return
}
func readFilerFilesToChannel(
refs []*filer_pb.LogFileChunkRef,
newReader LogFileReaderFn,
startTsNs, stopTsNs int64,
ch chan *filer_pb.LogEntry,
stop <-chan struct{},
setFatal func(error),
) {
var sent int
sendEntry := func(entry *filer_pb.LogEntry) error {
// Prefer stop over a send the buffer could still absorb, so an aborted
// producer quits at the next entry instead of filling the channel.
select {
case <-stop:
return errReaderStopped
default:
}
select {
case ch <- entry:
sent++
return nil
case <-stop:
return errReaderStopped
}
}
for _, ref := range refs {
select {
case <-stop:
return
default:
}
sentBefore := sent
streamErr := streamLogFileEntries(newReader, ref.Chunks, startTsNs, stopTsNs, sendEntry)
if streamErr == errReaderStopped {
return
}
if streamErr != nil {
if isChunkNotFound(streamErr) {
// A mid-file not-found still delivered the readable prefix; say so
// rather than pretending the whole file was skipped.
glog.V(0).Infof("skip log file filer=%s ts=%d after %d entries: %v", ref.FilerId, ref.FileTsNs, sent-sentBefore, streamErr)
continue
}
setFatal(fmt.Errorf("read log file filer=%s ts=%d: %w", ref.FilerId, ref.FileTsNs, streamErr))
return
}
}
}
func processOneLogEntry(logEntry *filer_pb.LogEntry, filter PathFilter, processEventFn ProcessMetadataFunc) (int64, error) {
event := &filer_pb.SubscribeMetadataResponse{}
// proto.Unmarshal (not UnmarshalVT) validates UTF-8 in string fields, so
// malformed metadata is skipped here instead of reaching path filtering.
if err := proto.Unmarshal(logEntry.Data, event); err != nil {
glog.Errorf("unmarshal log entry: %v", err)
return 0, nil // skip corrupt entries
}
if !matchesFilter(event, filter) {
return event.TsNs, nil
}
if err := processEventFn(event); err != nil {
return event.TsNs, fmt.Errorf("process event: %w", err)
}
return event.TsNs, nil
}
// --- path filtering (mirrors server-side eachEventNotificationFn logic) ---
const systemLogDir = "/topics/.system/log"
func matchesFilter(resp *filer_pb.SubscribeMetadataResponse, filter PathFilter) bool {
fullpath := filer_pb.MetadataEventSourceFullPath(resp)
// Skip internal meta log entries
if strings.HasPrefix(fullpath, systemLogDir) {
return false
}
return filer_pb.MetadataEventMatchesSubscription(resp, filter.PathPrefix, filter.AdditionalPathPrefixes, filter.DirectoriesToWatch)
}
// isChunkNotFound checks if an error indicates a missing volume chunk.
// Matches the server-side isChunkNotFoundError logic.
func isChunkNotFound(err error) bool {
if err == nil {
return false
}
s := err.Error()
return strings.Contains(s, "not found") || strings.Contains(s, "status 404")
}
// --- min-heap for merging entries across filers ---
type logEntryHeapItem struct {
entry *filer_pb.LogEntry
filerIdx int
}
type logEntryHeap []*logEntryHeapItem
func (h logEntryHeap) Len() int { return len(h) }
func (h logEntryHeap) Less(i, j int) bool { return h[i].entry.TsNs < h[j].entry.TsNs }
func (h logEntryHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *logEntryHeap) Push(x any) { *h = append(*h, x.(*logEntryHeapItem)) }
func (h *logEntryHeap) Pop() any {
old := *h
n := len(old)
item := old[n-1]
old[n-1] = nil
*h = old[:n-1]
return item
}
// --- log file parsing (uses io.ReadFull for correct partial-read handling) ---
// streamLogFileEntries reads a log file's chunks and invokes eachFn for every
// entry as it is decoded, without buffering the whole file: a multi-GB log
// file costs one entry plus the chunk reader's buffer at a time, not O(file
// size). An eachFn error stops the read and is propagated verbatim.
func streamLogFileEntries(newReader LogFileReaderFn, chunks []*filer_pb.FileChunk, startTsNs, stopTsNs int64, eachFn func(*filer_pb.LogEntry) error) error {
reader, err := newReader(chunks)
if err != nil {
return fmt.Errorf("create reader: %w", err)
}
defer reader.Close()
sizeBuf := make([]byte, 4)
for {
if _, err := io.ReadFull(reader, sizeBuf); err != nil {
if err == io.EOF || err == io.ErrUnexpectedEOF {
return nil
}
return err
}
size := util.BytesToUint32(sizeBuf)
if size > maxLogEntrySize {
return fmt.Errorf("entry size %d exceeds %d", size, maxLogEntrySize)
}
entryData := make([]byte, size)
if _, err := io.ReadFull(reader, entryData); err != nil {
return err
}
logEntry := &filer_pb.LogEntry{}
if err := logEntry.UnmarshalVT(entryData); err != nil {
return err
}
if logEntry.TsNs <= startTsNs {
continue
}
if stopTsNs != 0 && logEntry.TsNs > stopTsNs {
return nil
}
if err := eachFn(logEntry); err != nil {
return err
}
}
}