package log_buffer import ( "testing" "time" "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" ) // TestEvictionWatermarkTracksSealedRing drives the watermark through the real // eviction path instead of writing the field: only copyToFlushInternal sees the // window about to be dropped, and it reads it one statement before SealBuffer // shifts it out of slot 0. The buffer here never flushes, matching the // aggregated meta ring, so the watermark is its only emptiness proof. func TestEvictionWatermarkTracksSealedRing(t *testing.T) { lb := NewLogBuffer("evict-watermark", time.Minute, nil, nil, nil) defer lb.ShutdownLogBuffer() // Each timestamp is further past the previous window's start than the flush // interval, so every append seals the window before it. base := time.Now().Add(-30 * time.Minute).Truncate(time.Second) step := 2 * time.Minute at := func(i int) time.Time { return base.Add(time.Duration(i) * step) } if got := lb.GetLastEvictedTsNs(); got != 0 { t.Fatalf("fresh buffer evicted through %v, want 0", time.Unix(0, got)) } // PreviousBufferCount seals only fill the ring; the next one drops window 0. for i := 0; i <= PreviousBufferCount; i++ { if err := lb.AddLogEntryToBuffer(&filer_pb.LogEntry{TsNs: at(i).UnixNano(), Data: []byte("x"), Key: []byte("k")}); err != nil { t.Fatalf("add %d: %v", i, err) } if got := lb.GetLastEvictedTsNs(); got != 0 { t.Fatalf("after %d appends evicted through %v, want nothing evicted yet", i+1, time.Unix(0, got)) } } if err := lb.AddLogEntryToBuffer(&filer_pb.LogEntry{TsNs: at(PreviousBufferCount + 1).UnixNano(), Data: []byte("x"), Key: []byte("k")}); err != nil { t.Fatalf("add evicting entry: %v", err) } // Window 0 held only the first entry, so its stopTime is that entry's ts. if got, want := lb.GetLastEvictedTsNs(), at(0).UnixNano(); got != want { t.Fatalf("evicted through %v, want the dropped window's stop %v", time.Unix(0, got), time.Unix(0, want)) } // One more eviction advances the watermark; it never regresses. if err := lb.AddLogEntryToBuffer(&filer_pb.LogEntry{TsNs: at(PreviousBufferCount + 2).UnixNano(), Data: []byte("x"), Key: []byte("k")}); err != nil { t.Fatalf("add second evicting entry: %v", err) } if got, want := lb.GetLastEvictedTsNs(), at(1).UnixNano(); got != want { t.Fatalf("evicted through %v, want %v", time.Unix(0, got), time.Unix(0, want)) } } // TestGapResumeCursorReadsSingleEntrySealedWindow pins the resume cursor // against the shape that actually breaks: a sealed window holding one entry, // where startTime == stopTime. The sealed-buffer lookup only enters a window // whose stopTime is strictly after the cursor, so a cursor sitting exactly on // earliest walks straight past such a window and its sole event is never // delivered. Low-volume metadata windows are routinely one entry. func TestGapResumeCursorReadsSingleEntrySealedWindow(t *testing.T) { lb := NewLogBuffer("gap-resume-sealed", time.Minute, nil, nil, nil) defer lb.ShutdownLogBuffer() // Timestamps far enough apart that every append seals the window before it, // so each sealed window holds exactly one entry. base := time.Now().Add(-time.Hour).Truncate(time.Second) for i := 0; i < 3; i++ { if err := lb.AddLogEntryToBuffer(&filer_pb.LogEntry{ TsNs: base.Add(time.Duration(i) * 2 * time.Minute).UnixNano(), Data: []byte("x"), Key: []byte("k"), }); err != nil { t.Fatalf("add %d: %v", i, err) } } earliest := lb.GetEarliestTime() if earliest.IsZero() { t.Fatal("expected in-memory data") } // firstTsFrom reports the timestamp of the first entry a read hands back. firstTsFrom := func(cursorTsNs int64) int64 { buf, _, pooled, err := lb.ReadFromBuffer(NewMessagePosition(cursorTsNs, gapResumeTestOffset)) if err != nil { t.Fatalf("read at %v: %v", time.Unix(0, cursorTsNs), err) } if buf == nil { t.Fatalf("read at %v returned no data", time.Unix(0, cursorTsNs)) } if pooled { defer lb.ReleaseMemory(buf) } _, ts, readErr := readTs(buf.Bytes(), 0) if readErr != nil { t.Fatalf("decode first entry: %v", readErr) } return ts } // The resume the resolvers issue must deliver the earliest entry itself. if got := firstTsFrom(earliest.UnixNano() - 1); got != earliest.UnixNano() { t.Fatalf("resume just below earliest starts at %v, want the earliest entry %v", time.Unix(0, got), earliest) } // Resuming exactly on earliest is the shape that loses it. if got := firstTsFrom(earliest.UnixNano()); got == earliest.UnixNano() { t.Fatal("expected a cursor exactly on earliest to skip the single-entry window (precondition)") } } // gapResumeTestOffset mirrors the sentinel the filer's gap resume carries. const gapResumeTestOffset = -2 // TestGapResumeCursorReadsFromMemory pins the contract the filer's gap resolver // depends on: the cursor it resumes with must be one this read actually serves. // ReadFromBuffer only falls through to memory for a cursor below the in-memory // window when the offset is a sentinel, and answers ResumeFromDiskError for // every positive one -- a resume carrying a positive offset would bounce // straight back to the resolver, which sees no progress and parks a subscriber // whose data is sitting in the ring. func TestGapResumeCursorReadsFromMemory(t *testing.T) { lb := NewLogBuffer("gap-resume", time.Minute, nil, nil, nil) defer lb.ShutdownLogBuffer() base := time.Now().Add(-time.Hour).Truncate(time.Second) for i := 0; i < 3; i++ { if err := lb.AddLogEntryToBuffer(&filer_pb.LogEntry{ TsNs: base.Add(time.Duration(i) * time.Second).UnixNano(), Data: []byte("x"), Key: []byte("k"), }); err != nil { t.Fatalf("add %d: %v", i, err) } } earliest := lb.GetEarliestTime() if earliest.IsZero() { t.Fatal("expected in-memory data") } // The resolver resumes at earliest itself, read inclusively. if buf, _, _, err := lb.ReadFromBuffer(NewMessagePosition(earliest.UnixNano(), -2)); err != nil || buf == nil { t.Fatalf("resume at earliest: buf=%v err=%v", buf != nil, err) } // A sentinel just below it is served too, so the exact boundary is not load-bearing. if buf, _, _, err := lb.ReadFromBuffer(NewMessagePosition(earliest.UnixNano()-1, -2)); err != nil || buf == nil { t.Fatalf("resume just below earliest: buf=%v err=%v", buf != nil, err) } // A positive offset below the window is refused: this is the shape a gap // resume must never take. if _, _, _, err := lb.ReadFromBuffer(NewMessagePosition(earliest.UnixNano()-1, 1)); err != ResumeFromDiskError { t.Fatalf("positive-offset cursor below the window: want ResumeFromDiskError, got %v", err) } } // TestFlushSubscriberContract pins what the filer's gap parks rest on: a flush // subscriber is woken when a flush lands and the flush watermark is already // visible at that moment - loopFlush stores lastFlushTsNs before notifying, so // a waiter that re-checks the watermark on wake-up cannot miss the flush that // woke it. Appends alone never signal this channel, and unregistering closes // it so an abandoned waiter is not stranded. func TestFlushSubscriberContract(t *testing.T) { flushed := make(chan struct{}, 16) // A flush interval far longer than the test, so appends never auto-seal a // window and ForceFlush is the only source of flushes: each round then has // exactly one flush, and the token received is known to belong to it. lb := NewLogBuffer("flush-sub", time.Hour, func(logBuffer *LogBuffer, startTime, stopTime time.Time, buf []byte, minOffset, maxOffset int64) { flushed <- struct{}{} }, nil, nil) defer lb.ShutdownLogBuffer() ch := lb.RegisterFlushSubscriber("s") // An append is not a flush: nothing may arrive on the channel. The probe // sits just below the first round timestamp: below, so no round entry gets // collision-bumped and its stored stopTime stays comparable to the local // value each round asserts against; just below, because a gap wider than // the flush interval would make round 0's own append auto-seal the probe // window - a second flush whose token throws every round off by one. if err := lb.AddLogEntryToBuffer(&filer_pb.LogEntry{TsNs: time.Now().Add(-time.Hour - time.Millisecond).UnixNano(), Data: []byte("x"), Key: []byte("k")}); err != nil { t.Fatalf("add: %v", err) } select { case <-ch: t.Fatal("an append must not wake a flush subscriber") case <-time.After(100 * time.Millisecond): } // A flush wakes it with the watermark already stored - the contract the gap // parks re-check on. The store-before-notify ordering itself is pinned by a // load-bearing comment at the loopFlush site (a reorder's window is // nanoseconds on that goroutine, untestable from here); these round-trips // verify the observable contract and catch a notify with no store at all. base := time.Now().Add(-time.Hour) for i := 0; i < 8; i++ { ts := base.Add(time.Duration(i) * time.Millisecond).UnixNano() // The round entry must be above the buffer head: a bumped timestamp // flushes a stopTime far above the local ts compared below, making the // round's assertion vacuously true. if last := lb.LastTsNs.Load(); last >= ts { t.Fatalf("round %d: ts not above buffer head; the assertion would be vacuous", i) } if err := lb.AddLogEntryToBuffer(&filer_pb.LogEntry{TsNs: ts, Data: []byte("x"), Key: []byte("k")}); err != nil { t.Fatalf("add round %d: %v", i, err) } go lb.ForceFlush() select { case <-ch: if got := lb.GetLastFlushTsNs(); got < ts { t.Fatalf("round %d: woken with watermark %v short of the flushed entry %v; a waiter re-checking it on wake-up misses the flush that woke it", i, time.Unix(0, got), time.Unix(0, ts)) } case <-time.After(5 * time.Second): t.Fatalf("round %d: a flush must wake the flush subscriber", i) } select { case <-flushed: case <-time.After(5 * time.Second): t.Fatalf("round %d: flushFn did not run", i) } } // Unregistering closes the channel so an abandoned waiter unblocks. lb.UnregisterFlushSubscriber("s") select { case _, ok := <-ch: if ok { t.Fatal("unregister must close the channel, not send on it") } case <-time.After(time.Second): t.Fatal("unregister must close the channel") } // Unknown ids and double unregisters are harmless. lb.UnregisterFlushSubscriber("s") lb.UnregisterFlushSubscriber("never-registered") } // TestEvictionGatedCursor pins the gated sentinel: below the eviction watermark // it is refused to disk under the read's own lock - the only place the check is // atomic with the serve decision - while the plain -2 sentinel keeps master's // serve-from-earliest behavior for the message queue's readers. func TestEvictionGatedCursor(t *testing.T) { lb := NewLogBuffer("gated-cursor", time.Minute, nil, nil, nil) defer lb.ShutdownLogBuffer() // Entries far enough apart that every append seals the window before it; // wrapping the ring moves the watermark for real. base := time.Now().Add(-time.Hour).Truncate(time.Second) for i := 0; i < PreviousBufferCount+2; i++ { if err := lb.AddLogEntryToBuffer(&filer_pb.LogEntry{ TsNs: base.Add(time.Duration(i) * 2 * time.Minute).UnixNano(), Data: []byte("x"), Key: []byte("k"), }); err != nil { t.Fatalf("add %d: %v", i, err) } } evicted := lb.GetLastEvictedTsNs() if evicted == 0 { t.Fatal("precondition: the ring evicted nothing") } // Below the watermark: gated goes to disk, -2 keeps serving (MQ contract). if _, _, _, err := lb.ReadFromBuffer(NewMessagePosition(evicted-1, EvictionGatedOffset)); err != ResumeFromDiskError { t.Fatalf("gated below watermark: want ResumeFromDiskError, got %v", err) } if buf, _, _, err := lb.ReadFromBuffer(NewMessagePosition(evicted-1, -2)); err != nil || buf == nil { t.Fatalf("plain sentinel below watermark: buf=%v err=%v, master behavior must hold", buf != nil, err) } // At and above the watermark the gated cursor serves normally. if buf, _, _, err := lb.ReadFromBuffer(NewMessagePosition(evicted, EvictionGatedOffset)); err != nil || buf == nil { t.Fatalf("gated at watermark: buf=%v err=%v", buf != nil, err) } if buf, _, _, err := lb.ReadFromBuffer(NewMessagePosition(evicted+1, EvictionGatedOffset)); err != nil || buf == nil { t.Fatalf("gated above watermark: buf=%v err=%v", buf != nil, err) } } // TestMarkEvictedThroughGatesYoungRing pins the young-ring gate: with the mark // set, a below-window gated cursor goes to disk before the first real eviction // instead of being served the earliest retained entry. func TestMarkEvictedThroughGatesYoungRing(t *testing.T) { lb := NewLogBuffer("young-ring", time.Minute, nil, nil, nil) defer lb.ShutdownLogBuffer() seed := time.Now().Add(-time.Minute).UnixNano() lb.MarkEvictedThrough(seed) if got := lb.GetLastEvictedTsNs(); got != seed { t.Fatalf("evicted through %v, want the mark %v", time.Unix(0, got), time.Unix(0, seed)) } if got := lb.GetLastEvictedOriginalTsNs(); got != seed { t.Fatalf("original watermark %v, want the mark %v", time.Unix(0, got), time.Unix(0, seed)) } // A lower mark must not regress an established boundary. lb.MarkEvictedThrough(seed - int64(time.Hour)) if got := lb.GetLastEvictedTsNs(); got != seed { t.Fatalf("lower mark regressed the watermark to %v", time.Unix(0, got)) } // One post-mark arrival: the ring's window starts well above the mark. if err := lb.AddLogEntryToBuffer(&filer_pb.LogEntry{ TsNs: time.Now().UnixNano(), Data: []byte("x"), Key: []byte("k"), }); err != nil { t.Fatal(err) } // Below the mark: gated goes to disk, -2 keeps serving (MQ contract). if _, _, _, err := lb.ReadFromBuffer(NewMessagePosition(seed-1, EvictionGatedOffset)); err != ResumeFromDiskError { t.Fatalf("gated below the mark: want ResumeFromDiskError, got %v", err) } if buf, _, _, err := lb.ReadFromBuffer(NewMessagePosition(seed-1, -2)); err != nil || buf == nil { t.Fatalf("plain sentinel below the mark: buf=%v err=%v", buf != nil, err) } if buf, _, _, err := lb.ReadFromBuffer(NewMessagePosition(seed, EvictionGatedOffset)); err != nil || buf == nil { t.Fatalf("gated at the mark: buf=%v err=%v", buf != nil, err) } } // TestEvictionOriginalWatermark pins the second timestamp space. The ring bumps // an out-of-order arrival past its head, so a bump-heavy interval (peer history // replay) leaves stopTimes above anything on any peer's disk; a gap gate // comparing disk cursors against those would park a subscriber that drained // every peer's log. The original watermark tracks what was actually received. func TestEvictionOriginalWatermark(t *testing.T) { lb := NewLogBuffer("orig-watermark", time.Minute, nil, nil, nil) defer lb.ShutdownLogBuffer() // Newest original first: it sets the ring head, so every later arrival is // bumped. Seals go by bumped time, which advances by nanoseconds, so each // window is sealed explicitly; wrapping the ring evicts. newest := time.Now().Add(-time.Hour).Truncate(time.Second).UnixNano() for i := 0; i < PreviousBufferCount+2; i++ { orig := newest - int64(i)*int64(time.Minute) if err := lb.AddLogEntryToBuffer(&filer_pb.LogEntry{TsNs: orig, Data: []byte("x"), Key: []byte("k")}); err != nil { t.Fatalf("add %d: %v", i, err) } lb.ForceFlush() } bumped := lb.GetLastEvictedTsNs() original := lb.GetLastEvictedOriginalTsNs() if bumped == 0 || original == 0 { t.Fatalf("precondition: nothing evicted (bumped=%d original=%d)", bumped, original) } if original > newest { t.Fatalf("original watermark %v exceeds the highest received timestamp %v", time.Unix(0, original), time.Unix(0, newest)) } if bumped <= newest { t.Fatalf("bumped watermark %v not past the ring head %v: the spaces did not diverge", time.Unix(0, bumped), time.Unix(0, newest)) } // The punchline: a disk cursor that drained every peer's log sits at the // highest received timestamp - clearing the original watermark while still // below the bumped one, which no disk timestamp can ever reach. diskHead := newest if diskHead < original { t.Fatalf("disk head %v short of the original watermark %v", time.Unix(0, diskHead), time.Unix(0, original)) } if diskHead >= bumped { t.Fatal("disk head reached the bumped watermark; the gate would not have parked and this test proves nothing") } } // TestOriginalWatermarkWindowAttribution pins which window an entry's received // timestamp is credited to. An append that rolls the window over seals the // previous one first; crediting the incoming timestamp before that hands it to // the sealed window (inflating its watermark: spurious parks) and loses it // from the new one (deflating: gaps proven empty that are not). func TestOriginalWatermarkWindowAttribution(t *testing.T) { lb := NewLogBuffer("orig-attribution", time.Minute, nil, nil, nil) defer lb.ShutdownLogBuffer() base := time.Now().Add(-time.Hour).Truncate(time.Second) t1 := base.UnixNano() t2 := base.Add(2 * time.Minute).UnixNano() // past the flush interval: seals window A if err := lb.AddLogEntryToBuffer(&filer_pb.LogEntry{TsNs: t1, Data: []byte("x"), Key: []byte("k")}); err != nil { t.Fatalf("add t1: %v", err) } if err := lb.AddLogEntryToBuffer(&filer_pb.LogEntry{TsNs: t2, Data: []byte("x"), Key: []byte("k")}); err != nil { t.Fatalf("add t2: %v", err) } sealed := lb.prevBuffers.buffers[len(lb.prevBuffers.buffers)-1] if sealed.size == 0 { t.Fatal("precondition: the second append did not seal the first window") } if got := sealed.maxOriginalTsNs; got != t1 { t.Fatalf("sealed window credited with %v, want its own entry %v (t2 belongs to the open window)", time.Unix(0, got), time.Unix(0, t1)) } if got := lb.curWindowMaxOriginalTsNs; got != t2 { t.Fatalf("open window holds %v, want the entry that rolled it over %v", time.Unix(0, got), time.Unix(0, t2)) } }