snapshot of https://github.com/seaweedfs/seaweedfs.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-16 03:23:42 +05:00
commit 89f5be005d
3997 changed files with 961970 additions and 0 deletions

View File

@@ -0,0 +1,390 @@
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))
}
}