Files
upstream-seaweedfs/weed/filer/filer_notify_read_minute_test.go
upstream-mirror 89f5be005d
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
snapshot of https://github.com/seaweedfs/seaweedfs.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 03:23:42 +05:00

271 lines
12 KiB
Go

package filer
import (
"bytes"
"fmt"
"io"
"testing"
"time"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/util"
"github.com/seaweedfs/seaweedfs/weed/wdclient"
)
// TestPersistedLogScanStartCoversSpanningWindow pins the file-selection window.
// A log file is named for the start of the window it holds, and a window spans
// up to LogFlushInterval, so a cursor inside the window's later minutes must
// still open the file named for its earlier one -- otherwise the read reports
// "nothing on disk" for entries that are sitting in it, and the gap resolvers
// take that miss as proof the range is empty.
func TestPersistedLogScanStartCoversSpanningWindow(t *testing.T) {
// A window sealed at 12:30:59 ending 12:31:58 is written to "12-30".
sealed := time.Date(2026, 6, 29, 12, 30, 59, 0, time.UTC)
fileMinute := sealed.Format("15-04")
// A subscriber resuming mid-window must not sort past that file.
for _, cursor := range []time.Time{
sealed.Add(11 * time.Second), // 12:31:10, the reported case
sealed.Add(59 * time.Second), // 12:31:58, the window's last entry
sealed, // exactly the window start
} {
scanMinute := persistedLogScanStart(cursor).Format("15-04")
if scanMinute > fileMinute {
t.Fatalf("cursor %v scans from %q, which sorts past the file %q holding it",
cursor.Format("15:04:05"), scanMinute, fileMinute)
}
}
}
// A cursor just after midnight has to reach back into the previous day.
func TestPersistedLogScanStartCrossesMidnight(t *testing.T) {
cursor := time.Date(2026, 6, 29, 0, 0, 30, 0, time.UTC)
scanFrom := persistedLogScanStart(cursor)
if got, want := scanFrom.Format("2006-01-02"), "2026-06-28"; got != want {
t.Fatalf("scan date = %q, want %q so the previous day's last file is listed", got, want)
}
}
// TestSpanningLogFileIsNotSkipped pins the other half of the minute-boundary
// fix, against the predicate the iterator actually calls. Widening which files
// get listed is useless if the iterator then drops the spanning file, which it
// did by treating the following file's name as an upper bound on this file's
// contents -- it is not.
func TestSpanningLogFileIsNotSkipped(t *testing.T) {
// Window sealed at 12:30:59, ending 12:31:58, written to "12-30".
spanning := time.Date(2026, 6, 29, 12, 30, 0, 0, time.UTC).UnixNano()
// The next window starts at 12:31:59 and is written to "12-31".
following := time.Date(2026, 6, 29, 12, 31, 0, 0, time.UTC).UnixNano()
cursor := time.Date(2026, 6, 29, 12, 31, 10, 0, time.UTC).UnixNano()
if following > cursor {
t.Fatal("precondition: the following file's name sorts at or before the cursor")
}
if !logFileMayContainAfter(spanning, cursor) {
t.Fatal("the spanning file holds entries past the cursor and must be read")
}
// A file that genuinely cannot reach the cursor is still skipped, so the
// widening does not turn into reading the whole day.
old := time.Date(2026, 6, 29, 12, 0, 0, 0, time.UTC).UnixNano()
if logFileMayContainAfter(old, cursor) {
t.Fatal("a file a full interval behind the cursor should still be skipped")
}
}
// TestClampLogRefsCursorNeverRewinds pins that a chunk-ref read cannot move the
// subscriber backwards. The refs carry minute-level names, so the last one
// normally sorts before the requested position, and the caller assigns that
// value straight to the read cursor.
func TestClampLogRefsCursorNeverRewinds(t *testing.T) {
cursor := time.Date(2026, 6, 29, 12, 31, 10, 0, time.UTC).UnixNano()
spanningFile := time.Date(2026, 6, 29, 12, 30, 0, 0, time.UTC).UnixNano()
if got := clampLogRefsCursor(spanningFile, cursor); got != cursor {
t.Fatalf("cursor moved to %v, want it held at %v", time.Unix(0, got), time.Unix(0, cursor))
}
// A ref genuinely ahead of the cursor still advances it.
ahead := time.Date(2026, 6, 29, 12, 32, 0, 0, time.UTC).UnixNano()
if got := clampLogRefsCursor(ahead, cursor); got != ahead {
t.Fatalf("cursor = %v, want it to advance to %v", time.Unix(0, got), time.Unix(0, ahead))
}
}
// TestPersistedLogScanStartTsNsMinuteAligned pins the prune bound against the
// collector's minute-granular file comparison: a cursor at 12:31:20 still
// collects the 12-30 file, so the bound must not sit past 12:30:00 - an
// exact-ns bound disowns the file's sent state and the next pass reships it
// whole, re-creating the duplicate-refs class the state exists to prevent.
func TestPersistedLogScanStartTsNsMinuteAligned(t *testing.T) {
cursor := time.Date(2026, 6, 29, 12, 31, 20, 0, time.UTC)
fileTsNs := time.Date(2026, 6, 29, 12, 30, 0, 0, time.UTC).UnixNano()
bound := PersistedLogScanStartTsNs(cursor)
if fileTsNs < bound {
t.Fatalf("bound %v disowns the 12-30 file the collector still lists", time.Unix(0, bound).UTC())
}
// A file a full interval plus a minute behind is genuinely out of scan range.
old := time.Date(2026, 6, 29, 12, 28, 0, 0, time.UTC).UnixNano()
if old >= bound {
t.Fatalf("bound %v keeps state for files the scan can no longer list", time.Unix(0, bound).UTC())
}
}
// TestLastShippedLogEntryTsNs pins the chunk-mode cursor source against the
// client's reader, which it must mirror: per file the readable prefix - the
// client stops at the first unreadable chunk and never resumes within the file
// - and per filer the newest file with content, because the client keeps
// reading later files after skipping a bad one. A marker past what the client
// applies loses events; one short only re-ships.
func TestLastShippedLogEntryTsNs(t *testing.T) {
f := &Filer{persistedLogCache: newPersistedLogCache(1 << 20)}
entry := func(ts int64) *filer_pb.LogEntry { return &filer_pb.LogEntry{TsNs: ts} }
chunk := func(id string) *filer_pb.FileChunk { return &filer_pb.FileChunk{FileId: id, Size: 10} }
ref := func(fileTsNs int64, chunks ...*filer_pb.FileChunk) *filer_pb.LogFileChunkRef {
return &filer_pb.LogFileChunkRef{FilerId: "a", FileTsNs: fileTsNs, Chunks: chunks}
}
byId := map[string]struct {
entries []*filer_pb.LogEntry
err error
}{
"good-early": {entries: []*filer_pb.LogEntry{entry(100), entry(200)}},
"good-late": {entries: []*filer_pb.LogEntry{entry(300), entry(400)}},
"missing": {err: fmt.Errorf("volume 7 not found")},
"missing2": {err: fmt.Errorf("volume 8 not found")},
"empty": {},
}
origLoad := loadLogFileEntriesFn
loadLogFileEntriesFn = func(masterClient *wdclient.MasterClient, c *filer_pb.FileChunk) ([]*filer_pb.LogEntry, bool, error) {
r := byId[c.FileId]
return r.entries, true, r.err
}
origLookup := lookupLogChunkFn
deadVolumes := map[string]bool{}
lookupLogChunkFn = func(f *Filer, fileId string) error {
if deadVolumes[fileId] {
return fmt.Errorf("volume 9 not found")
}
return nil
}
defer func() { loadLogFileEntriesFn = origLoad; lookupLogChunkFn = origLookup }()
t.Run("a fully readable file answers with its tail, complete", func(t *testing.T) {
ts, ok, complete := f.lastShippedLogEntryTsNs([]*filer_pb.FileChunk{chunk("good-early"), chunk("good-late")})
if !ok || ts != 400 || !complete {
t.Fatalf("ts=%d ok=%v complete=%v, want 400 and complete", ts, ok, complete)
}
})
t.Run("the prefix ends at the first missing chunk", func(t *testing.T) {
// The client's reader stops there and never resumes within the file:
// answering from the readable suffix would put the marker past entries
// the client never applied, losing them permanently.
ts, ok, complete := f.lastShippedLogEntryTsNs([]*filer_pb.FileChunk{chunk("good-early"), chunk("missing"), chunk("good-late")})
if !ok || ts != 200 {
t.Fatalf("ts=%d ok=%v, want the prefix end 200, never the suffix 400", ts, ok)
}
if complete {
t.Fatal("a prefix-limited read must report incomplete, or the unread suffix is never re-shipped")
}
})
t.Run("nothing readable is a clean no-answer", func(t *testing.T) {
if _, ok, _ := f.lastShippedLogEntryTsNs([]*filer_pb.FileChunk{chunk("missing"), chunk("empty")}); ok {
t.Fatal("want no answer")
}
})
t.Run("an entirely missing last file falls back to earlier files", func(t *testing.T) {
// The client skips the bad file but has applied the earlier ones;
// discarding their progress would rewind the marker to the start
// cursor and stall or replay.
ts, answeredFileTsNs, ok, _ := f.LastShippedLogEntryTsNsForFiler([]*filer_pb.LogFileChunkRef{
ref(1000, chunk("good-late")),
ref(2000, chunk("missing"), chunk("missing2")),
})
if !ok || ts != 400 {
t.Fatalf("ts=%d ok=%v, want the previous file's 400", ts, ok)
}
if answeredFileTsNs != 1000 {
t.Fatalf("answered file %d, want 1000: the caller rolls back sent state above it", answeredFileTsNs)
}
})
t.Run("a cached chunk on a dead volume ends the prefix", func(t *testing.T) {
// Warm the cache, then kill the middle chunk's volume. The client
// reads directly and stops there; a probe trusting its own decoded
// cache would sail past to the tail and put the marker beyond entries
// the client never applied.
if ts, ok, _ := f.lastShippedLogEntryTsNs([]*filer_pb.FileChunk{chunk("good-early"), chunk("good-late")}); !ok || ts != 400 {
t.Fatalf("warmup: ts=%d ok=%v, want 400", ts, ok)
}
deadVolumes["good-early"] = true
defer delete(deadVolumes, "good-early")
ts, ok, complete := f.lastShippedLogEntryTsNs([]*filer_pb.FileChunk{chunk("good-early"), chunk("good-late")})
if ok || ts != 0 {
t.Fatalf("ts=%d ok=%v, want no answer: the first chunk's volume is gone however warm the cache", ts, ok)
}
if complete {
t.Fatal("a lookup-limited read must report incomplete so the ref re-ships")
}
})
t.Run("a dead needle behind a warm cache is the accepted residual", func(t *testing.T) {
// Production liveness is a volume lookup: it cannot see a needle 404
// or a stale location inside a resolvable volume, so a warm cache can
// answer past a chunk the client fails to read. Accepted because
// metadata log chunks die volume-at-a-time (TTL'd log volumes) and the
// alternative is a real read per probe, which is what the probe exists
// to avoid; the client logs the skip at V(0). This test pins the
// boundary so a change to it is a decision, not an accident.
if ts, ok, _ := f.lastShippedLogEntryTsNs([]*filer_pb.FileChunk{chunk("good-early"), chunk("good-late")}); !ok || ts != 400 {
t.Fatalf("warmup: ts=%d ok=%v, want 400", ts, ok)
}
// The needle dies: loads fail, but the volume still resolves.
prev := byId["good-early"]
byId["good-early"] = struct {
entries []*filer_pb.LogEntry
err error
}{err: fmt.Errorf("read: 404 Not Found: not found")}
defer func() { byId["good-early"] = prev }()
ts, ok, complete := f.lastShippedLogEntryTsNs([]*filer_pb.FileChunk{chunk("good-early"), chunk("good-late")})
if !ok || ts != 400 || !complete {
t.Fatalf("ts=%d ok=%v complete=%v: the warm cache masks a dead needle - if this now fails, the residual was closed and this test should assert the new behavior", ts, ok, complete)
}
})
t.Run("spanning records stream the shipped list", func(t *testing.T) {
origLoad2 := loadLogFileEntriesFn
loadLogFileEntriesFn = func(masterClient *wdclient.MasterClient, c *filer_pb.FileChunk) ([]*filer_pb.LogEntry, bool, error) {
return nil, false, errLogChunkIncomplete
}
origStream := newLogFileStreamReader
newLogFileStreamReader = func(masterClient *wdclient.MasterClient, chunks []*filer_pb.FileChunk) io.Reader {
var buf bytes.Buffer
for _, ts := range []int64{500, 600} {
data, _ := (&filer_pb.LogEntry{TsNs: ts}).MarshalVT()
var sizeBuf [4]byte
util.Uint32toBytes(sizeBuf[:], uint32(len(data)))
buf.Write(sizeBuf[:])
buf.Write(data)
}
// A torn trailing size prefix, as a crashed writer leaves it: the
// client reads it as a clean end, so the probe must too - failing
// here would block the marker forever on data the client accepts.
buf.Write([]byte{0x01, 0x02})
return &buf
}
defer func() { loadLogFileEntriesFn = origLoad2; newLogFileStreamReader = origStream }()
ts, ok, complete := f.lastShippedLogEntryTsNs([]*filer_pb.FileChunk{chunk("incomplete")})
if !ok || ts != 600 {
t.Fatalf("ts=%d ok=%v, want the streamed tail 600 past the torn prefix", ts, ok)
}
if !complete {
t.Fatal("a torn tail is where the client's read ends too: complete, nothing to re-ship")
}
})
}