mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-16 12:16:36 +00:00
A bounded LoopProcessLogData (stopTsNs set) on a buffer that never took a write since process start fell into the ResumeFromDiskError branch, which never checks stopTsNs when ReadFromDiskFn is nil and HasData() is false. The read parked on the notification loop forever while the subscription's idle heartbeats kept the stream looking alive, so a bounded SubscribeMetadata pass on a freshly restarted idle filer never completed. Terminate like the caught-up path does, returning a nil error: leaking the pending ResumeFromDiskError would latch the filer's outer loop into its gap machinery, which parks the bounded subscriber all over again.
795 lines
25 KiB
Go
795 lines
25 KiB
Go
package weed_server
|
|
|
|
// End-to-end tests for the metadata subscribe loops. The unit tests in this
|
|
// package pin individual helpers; every escaped bug across this PR's review
|
|
// rounds lived in the interactions - the loop state machine, the disk/memory
|
|
// handoff, and the server/client contract. These tests run the real
|
|
// SubscribeLocalMetadata loop against a real filer store, with only the volume
|
|
// layer faked, and assert the delivered stream itself: exactly the written
|
|
// events, in order, no duplicates from the entry path, and the chunk-mode
|
|
// marker never claiming more than the real client code applies.
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"google.golang.org/grpc/metadata"
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/filer"
|
|
"github.com/seaweedfs/seaweedfs/weed/filer/leveldb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
"github.com/seaweedfs/seaweedfs/weed/util/log_buffer"
|
|
)
|
|
|
|
// ---- store configuration ----
|
|
|
|
type testConfig map[string]string
|
|
|
|
func (c testConfig) GetString(key string) string { return c[key] }
|
|
func (c testConfig) GetBool(key string) bool { return false }
|
|
func (c testConfig) GetInt(key string) int { return 0 }
|
|
func (c testConfig) GetStringSlice(key string) []string { return nil }
|
|
func (c testConfig) SetDefault(key string, v interface{}) {}
|
|
|
|
// ---- fake gRPC stream ----
|
|
|
|
type fakeSubscribeStream struct {
|
|
ctx context.Context
|
|
mu sync.Mutex
|
|
msgs []*filer_pb.SubscribeMetadataResponse
|
|
}
|
|
|
|
func (s *fakeSubscribeStream) Send(resp *filer_pb.SubscribeMetadataResponse) error {
|
|
select {
|
|
case <-s.ctx.Done():
|
|
return s.ctx.Err()
|
|
default:
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.msgs = append(s.msgs, resp)
|
|
return nil
|
|
}
|
|
|
|
func (s *fakeSubscribeStream) snapshot() []*filer_pb.SubscribeMetadataResponse {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return append([]*filer_pb.SubscribeMetadataResponse(nil), s.msgs...)
|
|
}
|
|
|
|
func (s *fakeSubscribeStream) Context() context.Context { return s.ctx }
|
|
func (s *fakeSubscribeStream) SetHeader(metadata.MD) error { return nil }
|
|
func (s *fakeSubscribeStream) SendHeader(metadata.MD) error { return nil }
|
|
func (s *fakeSubscribeStream) SetTrailer(metadata.MD) {}
|
|
func (s *fakeSubscribeStream) SendMsg(m interface{}) error { return nil }
|
|
func (s *fakeSubscribeStream) RecvMsg(m interface{}) error { return nil }
|
|
|
|
// ---- fake volume layer ----
|
|
|
|
type fakeLogVolumes struct {
|
|
mu sync.Mutex
|
|
bytes map[string][]byte // chunk fileId -> raw log bytes (size-prefixed entries)
|
|
dead map[string]bool
|
|
nextId int
|
|
}
|
|
|
|
func newFakeLogVolumes() *fakeLogVolumes {
|
|
return &fakeLogVolumes{bytes: make(map[string][]byte), dead: make(map[string]bool)}
|
|
}
|
|
|
|
func (v *fakeLogVolumes) put(data []byte) string {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
v.nextId++
|
|
id := fmt.Sprintf("t,%d", v.nextId)
|
|
v.bytes[id] = data
|
|
return id
|
|
}
|
|
|
|
func (v *fakeLogVolumes) kill(fileId string) {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
v.dead[fileId] = true
|
|
}
|
|
|
|
// notFoundErr matches both the server's and the client's missing-chunk
|
|
// predicates, like a real dead log volume does.
|
|
func notFoundErr(fileId string) error { return fmt.Errorf("read %s: volume 42 not found", fileId) }
|
|
|
|
func (v *fakeLogVolumes) get(fileId string) ([]byte, error) {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
if v.dead[fileId] {
|
|
return nil, notFoundErr(fileId)
|
|
}
|
|
data, found := v.bytes[fileId]
|
|
if !found {
|
|
return nil, notFoundErr(fileId)
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
func decodeLogBytes(data []byte) ([]*filer_pb.LogEntry, error) {
|
|
var entries []*filer_pb.LogEntry
|
|
for pos := 0; pos+4 <= len(data); {
|
|
size := int(util.BytesToUint32(data[pos : pos+4]))
|
|
if pos+4+size > len(data) {
|
|
break // torn tail
|
|
}
|
|
entry := &filer_pb.LogEntry{}
|
|
if err := entry.UnmarshalVT(data[pos+4 : pos+4+size]); err != nil {
|
|
return nil, err
|
|
}
|
|
entries = append(entries, entry)
|
|
pos += 4 + size
|
|
}
|
|
return entries, nil
|
|
}
|
|
|
|
// chunkStreamReader mimics the client's whole-file byte stream: sequential,
|
|
// erroring at the first dead chunk.
|
|
type chunkStreamReader struct {
|
|
vol *fakeLogVolumes
|
|
chunks []*filer_pb.FileChunk
|
|
buf []byte
|
|
idx int
|
|
}
|
|
|
|
func (r *chunkStreamReader) Read(p []byte) (int, error) {
|
|
for len(r.buf) == 0 {
|
|
if r.idx >= len(r.chunks) {
|
|
return 0, io.EOF
|
|
}
|
|
data, err := r.vol.get(r.chunks[r.idx].GetFileIdString())
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
r.buf = data
|
|
r.idx++
|
|
}
|
|
n := copy(p, r.buf)
|
|
r.buf = r.buf[n:]
|
|
return n, nil
|
|
}
|
|
|
|
func (r *chunkStreamReader) Close() error { return nil }
|
|
|
|
// ---- the harness ----
|
|
|
|
type subscribeHarness struct {
|
|
t *testing.T
|
|
f *filer.Filer
|
|
fs *FilerServer
|
|
vol *fakeLogVolumes
|
|
base int64 // fixed timestamp origin; a per-call time.Now() shifts across second boundaries mid-test
|
|
|
|
// flushGate, when non-nil, blocks the flush function - the "volume outage
|
|
// stalls the metadata log flush" state the whole PR exists to handle.
|
|
gateMu sync.Mutex
|
|
flushGate chan struct{}
|
|
|
|
// flushMu guards stopped against the flushes still in flight when the test
|
|
// ends, so none of them writes through a store the cleanup has closed.
|
|
flushMu sync.RWMutex
|
|
stopped bool
|
|
}
|
|
|
|
const testFilerIdSuffix = "0000abcd"
|
|
|
|
func newSubscribeHarness(t *testing.T) *subscribeHarness {
|
|
// Shrink the timing knobs so parks and retries run at test speed.
|
|
prevRetry, prevWarn, prevStall := unflushedGapRetryInterval, gapStallWarnInterval, maxGapStall
|
|
unflushedGapRetryInterval, gapStallWarnInterval, maxGapStall = 30*time.Millisecond, 200*time.Millisecond, time.Hour
|
|
t.Cleanup(func() {
|
|
unflushedGapRetryInterval, gapStallWarnInterval, maxGapStall = prevRetry, prevWarn, prevStall
|
|
})
|
|
|
|
f := filer.NewFiler(pb.ServerDiscovery{}, nil, "", "", "", "", "", 255, nil)
|
|
store := &leveldb.LevelDBStore{}
|
|
if err := store.Initialize(testConfig{"test.dir": t.TempDir()}, "test."); err != nil {
|
|
t.Fatalf("init store: %v", err)
|
|
}
|
|
f.SetStore(store)
|
|
|
|
vol := newFakeLogVolumes()
|
|
restore := filer.SetLogReadHooksForTesting(
|
|
func(chunk *filer_pb.FileChunk) ([]*filer_pb.LogEntry, error) {
|
|
data, err := vol.get(chunk.GetFileIdString())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return decodeLogBytes(data)
|
|
},
|
|
func(chunks []*filer_pb.FileChunk) io.Reader {
|
|
return &chunkStreamReader{vol: vol, chunks: chunks}
|
|
},
|
|
func(fileId string) error {
|
|
_, err := vol.get(fileId)
|
|
return err
|
|
},
|
|
)
|
|
t.Cleanup(restore)
|
|
|
|
h := &subscribeHarness{t: t, f: f, vol: vol,
|
|
base: time.Now().Add(-time.Hour).Truncate(time.Second).UnixNano()}
|
|
|
|
// The real buffer, with the flush function writing through the fake volume
|
|
// layer the way logFlushFunc writes through real volumes.
|
|
f.LocalMetaLogBuffer.ShutdownLogBuffer()
|
|
f.LocalMetaLogBuffer = log_buffer.NewLogBuffer("local", time.Minute, h.flushToStore, nil, nil)
|
|
// Shutting the buffer down is not enough: NewFiler's deletion loop keeps the
|
|
// filer - and so the replacement buffer's tens of megabytes - reachable for
|
|
// the rest of the run. Quiesce the flush path first, since Filer.Shutdown
|
|
// closes the store a flush still in flight would write through.
|
|
t.Cleanup(f.Shutdown)
|
|
t.Cleanup(h.stopFlushes)
|
|
|
|
h.fs = &FilerServer{
|
|
filer: f,
|
|
option: &FilerOption{Host: pb.ServerAddress("test:8888")},
|
|
knownListeners: make(map[int32]int32),
|
|
}
|
|
return h
|
|
}
|
|
|
|
func (h *subscribeHarness) blockFlushes() {
|
|
h.gateMu.Lock()
|
|
defer h.gateMu.Unlock()
|
|
if h.flushGate == nil {
|
|
h.flushGate = make(chan struct{})
|
|
}
|
|
}
|
|
|
|
func (h *subscribeHarness) releaseFlushes() {
|
|
h.gateMu.Lock()
|
|
defer h.gateMu.Unlock()
|
|
if h.flushGate != nil {
|
|
close(h.flushGate)
|
|
h.flushGate = nil
|
|
}
|
|
}
|
|
|
|
// stopFlushes lets go of any gated flush and waits for the ones in flight to
|
|
// finish, then refuses the rest, so the store stays untouched from here on.
|
|
func (h *subscribeHarness) stopFlushes() {
|
|
h.releaseFlushes()
|
|
h.flushMu.Lock()
|
|
defer h.flushMu.Unlock()
|
|
h.stopped = true
|
|
}
|
|
|
|
func (h *subscribeHarness) flushToStore(lb *log_buffer.LogBuffer, startTime, stopTime time.Time, buf []byte, minOffset, maxOffset int64) {
|
|
h.gateMu.Lock()
|
|
gate := h.flushGate
|
|
h.gateMu.Unlock()
|
|
if gate != nil {
|
|
<-gate
|
|
}
|
|
|
|
h.flushMu.RLock()
|
|
defer h.flushMu.RUnlock()
|
|
if h.stopped {
|
|
return
|
|
}
|
|
|
|
// The same file naming and append shape as logFlushFunc, against the fake
|
|
// volumes: one chunk per flushed window, named for the window start minute.
|
|
startTime, stopTime = startTime.UTC(), stopTime.UTC()
|
|
targetFile := fmt.Sprintf("%s/%04d-%02d-%02d/%02d-%02d.%s", filer.SystemLogDir,
|
|
startTime.Year(), startTime.Month(), startTime.Day(), startTime.Hour(), startTime.Minute(), testFilerIdSuffix)
|
|
data := append([]byte(nil), buf...)
|
|
fileId := h.vol.put(data)
|
|
|
|
ctx := context.Background()
|
|
fullpath := util.FullPath(targetFile)
|
|
entry, err := h.f.FindEntry(ctx, fullpath)
|
|
var offset int64
|
|
if err == filer_pb.ErrNotFound {
|
|
entry = &filer.Entry{
|
|
FullPath: fullpath,
|
|
Attr: filer.Attr{Crtime: time.Now(), Mtime: time.Now(), Mode: 0644},
|
|
}
|
|
} else if err != nil {
|
|
h.t.Errorf("find %s: %v", targetFile, err)
|
|
return
|
|
} else {
|
|
offset = int64(filer.TotalSize(entry.GetChunks()))
|
|
}
|
|
entry.Chunks = append(entry.GetChunks(), &filer_pb.FileChunk{
|
|
FileId: fileId,
|
|
Offset: offset,
|
|
Size: uint64(len(data)),
|
|
ModifiedTsNs: time.Now().UnixNano(),
|
|
})
|
|
if err := h.f.CreateEntry(ctx, entry, nil, false, false, nil, false, 255); err != nil {
|
|
h.t.Errorf("write log file %s: %v", targetFile, err)
|
|
}
|
|
}
|
|
|
|
// event builds a metadata event log entry the way the filer's notification
|
|
// path does, so the loop's real decode and filter code runs.
|
|
func testEvent(tsNs int64, name string) *filer_pb.LogEntry {
|
|
data, err := proto.Marshal(&filer_pb.SubscribeMetadataResponse{
|
|
Directory: "/t",
|
|
EventNotification: &filer_pb.EventNotification{NewEntry: &filer_pb.Entry{Name: name}},
|
|
TsNs: tsNs,
|
|
})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return &filer_pb.LogEntry{TsNs: tsNs, Data: data, Key: []byte("/t/" + name)}
|
|
}
|
|
|
|
func (h *subscribeHarness) append(tsNs int64) {
|
|
if err := h.f.LocalMetaLogBuffer.AddLogEntryToBuffer(testEvent(tsNs, fmt.Sprintf("f-%d", tsNs))); err != nil {
|
|
h.t.Fatalf("append: %v", err)
|
|
}
|
|
}
|
|
|
|
type runningSubscribe struct {
|
|
stream *fakeSubscribeStream
|
|
cancel context.CancelFunc
|
|
done chan error
|
|
finished chan struct{} // closed after done is populated; safe to wait repeatedly
|
|
}
|
|
|
|
func (h *subscribeHarness) subscribe(sinceNs int64, mutate func(*filer_pb.SubscribeMetadataRequest)) *runningSubscribe {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
stream := &fakeSubscribeStream{ctx: ctx}
|
|
req := &filer_pb.SubscribeMetadataRequest{
|
|
ClientName: "loop-test",
|
|
ClientId: 7,
|
|
ClientEpoch: 1,
|
|
SinceNs: sinceNs,
|
|
}
|
|
if mutate != nil {
|
|
mutate(req)
|
|
}
|
|
r := &runningSubscribe{stream: stream, cancel: cancel, done: make(chan error, 1), finished: make(chan struct{})}
|
|
go func() {
|
|
r.done <- h.fs.SubscribeLocalMetadata(req, stream)
|
|
close(r.finished)
|
|
}()
|
|
h.t.Cleanup(func() {
|
|
cancel()
|
|
select {
|
|
case <-r.finished:
|
|
case <-time.After(5 * time.Second):
|
|
h.t.Error("subscribe loop did not exit on cancel")
|
|
}
|
|
})
|
|
return r
|
|
}
|
|
|
|
// eventTimestamps extracts delivered metadata events (markers, heartbeats and
|
|
// refs excluded).
|
|
func eventTimestamps(msgs []*filer_pb.SubscribeMetadataResponse) []int64 {
|
|
var out []int64
|
|
for _, m := range msgs {
|
|
if len(m.LogFileRefs) > 0 || m.EventNotification == nil || m.EventNotification.NewEntry == nil {
|
|
continue
|
|
}
|
|
out = append(out, m.TsNs)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func waitForEvents(t *testing.T, r *runningSubscribe, want []int64, timeout time.Duration) []int64 {
|
|
t.Helper()
|
|
deadline := time.Now().Add(timeout)
|
|
var got []int64
|
|
for time.Now().Before(deadline) {
|
|
got = eventTimestamps(r.stream.snapshot())
|
|
if len(got) >= len(want) {
|
|
break
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
if fmt.Sprint(got) != fmt.Sprint(want) {
|
|
t.Fatalf("delivered %v, want %v", got, want)
|
|
}
|
|
return got
|
|
}
|
|
|
|
func assertNoEventsFor(t *testing.T, r *runningSubscribe, d time.Duration) {
|
|
t.Helper()
|
|
time.Sleep(d)
|
|
if got := eventTimestamps(r.stream.snapshot()); len(got) > 0 {
|
|
t.Fatalf("delivered %v while the gap was unproven; these events must wait", got)
|
|
}
|
|
}
|
|
|
|
// tsAt returns test timestamps from the harness's fixed base: old enough that
|
|
// windows seal on the jump between them, entries 1ms apart within a window.
|
|
func (h *subscribeHarness) tsAt(window, i int) int64 {
|
|
return h.base + int64(window)*int64(2*time.Minute) + int64(i)*int64(time.Millisecond)
|
|
}
|
|
|
|
// ---- scenarios ----
|
|
|
|
// The headline behavior of the whole PR: events evicted from the ring before
|
|
// their flush landed must not be skipped. The subscriber parks while the gap
|
|
// is unproven and delivers everything once the stalled flush lands.
|
|
func TestSubscribeLoop_EvictedUnflushedGapWaitsThenDelivers(t *testing.T) {
|
|
h := newSubscribeHarness(t)
|
|
h.blockFlushes()
|
|
|
|
var want []int64
|
|
for w := 0; w < log_buffer.PreviousBufferCount+3; w++ {
|
|
ts := h.tsAt(w, 0)
|
|
want = append(want, ts)
|
|
h.append(ts)
|
|
}
|
|
if h.f.LocalMetaLogBuffer.GetLastEvictedTsNs() == 0 {
|
|
t.Fatal("precondition: the ring evicted nothing")
|
|
}
|
|
|
|
r := h.subscribe(0, nil)
|
|
// The evicted windows are nowhere: not in memory, not on disk. Master
|
|
// silently skipped them here; the loop must park instead.
|
|
assertNoEventsFor(t, r, 300*time.Millisecond)
|
|
|
|
h.releaseFlushes()
|
|
waitForEvents(t, r, want, 5*time.Second)
|
|
}
|
|
|
|
// A gap that never existed is proven empty and served from memory promptly -
|
|
// the guard must not park subscribers on rings that evicted nothing.
|
|
func TestSubscribeLoop_NothingEvictedServesFromMemory(t *testing.T) {
|
|
h := newSubscribeHarness(t)
|
|
h.blockFlushes() // no disk at all; memory alone must serve
|
|
|
|
want := []int64{h.tsAt(0, 0), h.tsAt(0, 1), h.tsAt(0, 2)}
|
|
for _, ts := range want {
|
|
h.append(ts)
|
|
}
|
|
r := h.subscribe(0, nil)
|
|
waitForEvents(t, r, want, 3*time.Second)
|
|
}
|
|
|
|
// Disk backlog then live tail: the handoff must deliver every event exactly
|
|
// once, in order. Timestamps are 1ms-adjacent ACROSS every boundary - window
|
|
// to window on disk, and disk to retained memory - so a cursor error of even
|
|
// one entry at any handoff shows up as a hole or a duplicate. Flushed windows
|
|
// exist on disk AND in the retained ring, which is exactly where
|
|
// inclusive/exclusive mistakes on either side used to hide.
|
|
func TestSubscribeLoop_BacklogThenLiveExactlyOnce(t *testing.T) {
|
|
h := newSubscribeHarness(t)
|
|
|
|
ts := func(i int) int64 { return h.base + int64(i)*int64(time.Millisecond) }
|
|
|
|
var want []int64
|
|
n := 0
|
|
for w := 0; w < 3; w++ {
|
|
for i := 0; i < 3; i++ {
|
|
want = append(want, ts(n))
|
|
h.append(ts(n))
|
|
n++
|
|
}
|
|
h.f.LocalMetaLogBuffer.ForceFlush() // adjacent-timestamp window boundary on disk
|
|
}
|
|
// A retained, unflushed tail starting 1ms after the flushed content ends.
|
|
for i := 0; i < 3; i++ {
|
|
want = append(want, ts(n))
|
|
h.append(ts(n))
|
|
n++
|
|
}
|
|
|
|
r := h.subscribe(0, nil)
|
|
waitForEvents(t, r, want, 5*time.Second)
|
|
|
|
// Live tail on top.
|
|
live := []int64{ts(n), ts(n + 1)}
|
|
for _, l := range live {
|
|
h.append(l)
|
|
}
|
|
waitForEvents(t, r, append(append([]int64(nil), want...), live...), 5*time.Second)
|
|
}
|
|
|
|
// A bounded subscription delivers through its bound and terminates - it must
|
|
// not park forever on the gap machinery with its window already served.
|
|
func TestSubscribeLoop_BoundedSubscriptionTerminates(t *testing.T) {
|
|
h := newSubscribeHarness(t)
|
|
|
|
all := []int64{h.tsAt(0, 0), h.tsAt(0, 1), h.tsAt(1, 0), h.tsAt(1, 1)}
|
|
for _, ts := range all {
|
|
h.append(ts)
|
|
}
|
|
h.f.LocalMetaLogBuffer.ForceFlush()
|
|
|
|
until := all[1]
|
|
r := h.subscribe(0, func(req *filer_pb.SubscribeMetadataRequest) { req.UntilNs = until })
|
|
|
|
select {
|
|
case err := <-r.done:
|
|
if err != nil {
|
|
t.Fatalf("bounded subscription failed: %v", err)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("bounded subscription did not terminate")
|
|
}
|
|
for _, ts := range eventTimestamps(r.stream.snapshot()) {
|
|
if ts > until {
|
|
t.Fatalf("delivered %d past the bound %d", ts, until)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A bounded subscription against a filer whose meta-log buffer has never taken
|
|
// a write - fresh restart, zero metadata traffic since - must still terminate.
|
|
// The empty ring makes every memory read return ResumeFromDiskError, and with
|
|
// no gap to resolve the loop used to park inside LoopProcessLogData forever,
|
|
// its idle heartbeats keeping the stream looking healthy to the client.
|
|
func TestSubscribeLoop_BoundedEmptyFilerTerminates(t *testing.T) {
|
|
h := newSubscribeHarness(t)
|
|
|
|
r := h.subscribe(h.base, func(req *filer_pb.SubscribeMetadataRequest) {
|
|
req.UntilNs = time.Now().UnixNano()
|
|
})
|
|
|
|
select {
|
|
case err := <-r.done:
|
|
if err != nil {
|
|
t.Fatalf("bounded subscription failed: %v", err)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("bounded subscription on an idle filer did not terminate")
|
|
}
|
|
if got := eventTimestamps(r.stream.snapshot()); len(got) > 0 {
|
|
t.Fatalf("delivered %v from an empty filer", got)
|
|
}
|
|
}
|
|
|
|
// Vacuumed logs: the flush watermark proves a gap empty even though the files
|
|
// are gone - the resolver must skip to the retained ring and deliver its
|
|
// earliest window intact, including a single-entry window whose start and stop
|
|
// coincide. This is the one path where resolveGapResume itself advances the
|
|
// stream, and the resume-below-earliest arithmetic is load-bearing.
|
|
func TestSubscribeLoop_FlushProvenGapSkipsToRetained(t *testing.T) {
|
|
h := newSubscribeHarness(t)
|
|
|
|
windows := log_buffer.PreviousBufferCount + 2
|
|
for w := 0; w < windows; w++ {
|
|
h.append(h.tsAt(w, 0))
|
|
}
|
|
// The last append seals the window before it, so every window but the
|
|
// current one is queued for flush. Wait for all of them, not just the one
|
|
// the eviction watermark names: a flush still in flight writes its file
|
|
// back after the delete below, and the subscriber then serves that window
|
|
// from disk instead of taking the gap path this test is about.
|
|
waitForFlushedFiles(t, h, h.tsAt(windows-2, 0))
|
|
if h.f.LocalMetaLogBuffer.GetLastEvictedTsNs() == 0 {
|
|
t.Fatal("precondition: nothing evicted")
|
|
}
|
|
deleteAllLogFiles(t, h)
|
|
|
|
earliest := h.f.LocalMetaLogBuffer.GetEarliestTime().UnixNano()
|
|
var retained []int64
|
|
for w := 0; w < windows; w++ {
|
|
if ts := h.tsAt(w, 0); ts >= earliest {
|
|
retained = append(retained, ts)
|
|
}
|
|
}
|
|
|
|
r := h.subscribe(0, nil)
|
|
waitForEvents(t, r, retained, 5*time.Second)
|
|
}
|
|
|
|
// waitForFlushedFiles waits until the flush of the window ending at
|
|
// throughTsNs has landed. Flushes run in queue order on one goroutine, so that
|
|
// also proves every window sealed before it is on disk and none is in flight.
|
|
func waitForFlushedFiles(t *testing.T, h *subscribeHarness, throughTsNs int64) {
|
|
t.Helper()
|
|
deadline := time.Now().Add(3 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
if h.f.LocalMetaLogBuffer.GetLastFlushTsNs() >= throughTsNs {
|
|
return
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
t.Fatal("flushes did not land")
|
|
}
|
|
|
|
func deleteAllLogFiles(t *testing.T, h *subscribeHarness) {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
days, _, err := h.f.ListDirectoryEntries(ctx, filer.SystemLogDir, "", true, 1000, "", "", "")
|
|
if err != nil {
|
|
t.Fatalf("list log days: %v", err)
|
|
}
|
|
store := h.f.GetStore()
|
|
for _, day := range days {
|
|
if err := store.DeleteFolderChildren(ctx, day.FullPath); err != nil {
|
|
t.Fatalf("delete %s children: %v", day.FullPath, err)
|
|
}
|
|
if err := store.DeleteEntry(ctx, day.FullPath); err != nil {
|
|
t.Fatalf("delete %s: %v", day.FullPath, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A permanently wedged flush ends in the loud give-up skip: the loss is
|
|
// bounded to the unprovable range, counted, and the stream keeps delivering
|
|
// what memory still holds - it must not stay silent forever and must not fail.
|
|
func TestSubscribeLoop_GiveUpSkipsAndKeepsStreaming(t *testing.T) {
|
|
h := newSubscribeHarness(t)
|
|
prevStall := maxGapStall
|
|
maxGapStall = 250 * time.Millisecond
|
|
t.Cleanup(func() { maxGapStall = prevStall })
|
|
|
|
h.blockFlushes()
|
|
var retained []int64
|
|
for w := 0; w < log_buffer.PreviousBufferCount+3; w++ {
|
|
h.append(h.tsAt(w, 0))
|
|
}
|
|
// What the ring still holds after eviction is what must arrive post-skip.
|
|
earliest := h.f.LocalMetaLogBuffer.GetEarliestTime().UnixNano()
|
|
for w := 0; w < log_buffer.PreviousBufferCount+3; w++ {
|
|
if ts := h.tsAt(w, 0); ts >= earliest {
|
|
retained = append(retained, ts)
|
|
}
|
|
}
|
|
|
|
r := h.subscribe(0, nil)
|
|
waitForEvents(t, r, retained, 5*time.Second)
|
|
select {
|
|
case err := <-r.done:
|
|
t.Fatalf("stream ended (%v); the give-up must keep it alive", err)
|
|
default:
|
|
}
|
|
}
|
|
|
|
// Chunk mode, checked against the real client code: everything the marker
|
|
// claims must be applied by pb.ReadLogFileRefs over the shipped refs, and the
|
|
// inline stream must start strictly after the marker - the contract whose two
|
|
// sides drifted in round after round of review.
|
|
func TestSubscribeLoop_ChunkModeMarkerMatchesClientReplay(t *testing.T) {
|
|
h := newSubscribeHarness(t)
|
|
|
|
var want []int64
|
|
for w := 0; w < 3; w++ {
|
|
for i := 0; i < 3; i++ {
|
|
ts := h.tsAt(w, i)
|
|
want = append(want, ts)
|
|
h.append(ts)
|
|
}
|
|
}
|
|
h.f.LocalMetaLogBuffer.ForceFlush()
|
|
|
|
r := h.subscribe(0, func(req *filer_pb.SubscribeMetadataRequest) { req.ClientSupportsMetadataChunks = true })
|
|
|
|
// Wait for refs plus their transition marker.
|
|
var refs []*filer_pb.LogFileChunkRef
|
|
var markerTsNs int64
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
refs = refs[:0]
|
|
markerTsNs = 0
|
|
for _, m := range r.stream.snapshot() {
|
|
if len(m.LogFileRefs) > 0 {
|
|
refs = append(refs, m.LogFileRefs...)
|
|
if markerTsNs != 0 {
|
|
t.Fatal("refs arrived after their batch's marker")
|
|
}
|
|
continue
|
|
}
|
|
if m.EventNotification != nil && m.EventNotification.NewEntry == nil && m.TsNs > 0 && markerTsNs == 0 {
|
|
markerTsNs = m.TsNs
|
|
}
|
|
}
|
|
if markerTsNs != 0 {
|
|
break
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
if markerTsNs == 0 {
|
|
t.Fatal("no transition marker followed the refs; the client would buffer them forever")
|
|
}
|
|
|
|
// Apply the refs exactly the way the real client does.
|
|
var applied []int64
|
|
clientLastTs, err := pb.ReadLogFileRefs(refs,
|
|
func(chunks []*filer_pb.FileChunk) (io.ReadCloser, error) {
|
|
return &chunkStreamReader{vol: h.vol, chunks: chunks}, nil
|
|
},
|
|
0, 0, pb.PathFilter{},
|
|
func(resp *filer_pb.SubscribeMetadataResponse) error {
|
|
applied = append(applied, resp.TsNs)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("client replay: %v", err)
|
|
}
|
|
if markerTsNs > clientLastTs {
|
|
t.Fatalf("marker %d claims more than the client applied through %d; the difference is silently lost", markerTsNs, clientLastTs)
|
|
}
|
|
if fmt.Sprint(applied) != fmt.Sprint(want) {
|
|
t.Fatalf("client applied %v, want %v", applied, want)
|
|
}
|
|
|
|
// The inline stream must not re-deliver ref-covered content.
|
|
for _, ts := range eventTimestamps(r.stream.snapshot()) {
|
|
if ts <= markerTsNs {
|
|
t.Fatalf("inline event %d at or below the marker %d duplicates the client's chunk replay", ts, markerTsNs)
|
|
}
|
|
}
|
|
|
|
// And a live tail still arrives inline, after the marker.
|
|
live := h.tsAt(4, 0)
|
|
h.append(live)
|
|
waitForEvents(t, r, []int64{live}, 5*time.Second)
|
|
}
|
|
|
|
// Chunk mode with a dead volume mid-file: the marker must stop where the
|
|
// client's read stops, the stream must keep working, and the events after the
|
|
// dead chunk's file must still arrive.
|
|
func TestSubscribeLoop_ChunkModeDeadVolumeAgreesWithClient(t *testing.T) {
|
|
h := newSubscribeHarness(t)
|
|
|
|
// Three flushed windows -> three files; kill the middle file's chunk.
|
|
var written []int64
|
|
for w := 0; w < 3; w++ {
|
|
ts := h.tsAt(w, 0)
|
|
written = append(written, ts)
|
|
h.append(ts)
|
|
}
|
|
h.f.LocalMetaLogBuffer.ForceFlush()
|
|
h.vol.kill("t,2")
|
|
|
|
r := h.subscribe(0, func(req *filer_pb.SubscribeMetadataRequest) { req.ClientSupportsMetadataChunks = true })
|
|
|
|
var refs []*filer_pb.LogFileChunkRef
|
|
var markerTsNs int64
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
refs = refs[:0]
|
|
markerTsNs = 0
|
|
for _, m := range r.stream.snapshot() {
|
|
if len(m.LogFileRefs) > 0 {
|
|
refs = append(refs, m.LogFileRefs...)
|
|
} else if m.EventNotification != nil && m.EventNotification.NewEntry == nil && m.TsNs > markerTsNs {
|
|
markerTsNs = m.TsNs
|
|
}
|
|
}
|
|
if markerTsNs != 0 {
|
|
break
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
if markerTsNs == 0 {
|
|
t.Fatal("no transition marker; a dead volume must not block it")
|
|
}
|
|
|
|
var applied []int64
|
|
clientLastTs, err := pb.ReadLogFileRefs(refs,
|
|
func(chunks []*filer_pb.FileChunk) (io.ReadCloser, error) {
|
|
return &chunkStreamReader{vol: h.vol, chunks: chunks}, nil
|
|
},
|
|
0, 0, pb.PathFilter{},
|
|
func(resp *filer_pb.SubscribeMetadataResponse) error {
|
|
applied = append(applied, resp.TsNs)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("client replay: %v", err)
|
|
}
|
|
if markerTsNs > clientLastTs {
|
|
t.Fatalf("marker %d ahead of the client's %d with a dead chunk in between; the suffix is silently lost", markerTsNs, clientLastTs)
|
|
}
|
|
// The client skips the dead file but applies the later one.
|
|
sort.Slice(applied, func(i, j int) bool { return applied[i] < applied[j] })
|
|
appliedStr := fmt.Sprint(applied)
|
|
if !strings.Contains(appliedStr, fmt.Sprint(written[2])) || strings.Contains(appliedStr, fmt.Sprint(written[1])) {
|
|
t.Fatalf("client applied %v; want the dead file %d skipped and the later file %d applied", applied, written[1], written[2])
|
|
}
|
|
}
|