Files
upstream-seaweedfs/weed/mount/meta_cache/meta_cache_sections_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

975 lines
34 KiB
Go

package meta_cache
import (
"context"
"errors"
"fmt"
"net"
"sync"
"testing"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"github.com/seaweedfs/seaweedfs/weed/filer"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/util"
)
func TestSectionOf(t *testing.T) {
sl := newSectionTable([]string{"g", "p"})
for name, want := range map[string]int{
"a": 0, "f": 0,
"g": 1, "h": 1, "o": 1,
"p": 2, "z": 2,
} {
if got := sl.sectionOf(name); got != want {
t.Errorf("sectionOf(%q) = %d, want %d", name, got, want)
}
}
if lo, hi := sl.sectionRange(0); lo != "" || hi != "g" {
t.Errorf("sectionRange(0) = %q..%q, want ..g", lo, hi)
}
if lo, hi := sl.sectionRange(1); lo != "g" || hi != "p" {
t.Errorf("sectionRange(1) = %q..%q, want g..p", lo, hi)
}
if lo, hi := sl.sectionRange(2); lo != "p" || hi != "" {
t.Errorf("sectionRange(2) = %q..%q, want p..", lo, hi)
}
}
func buildSectionedDir(t *testing.T, mc *MetaCache, dir util.FullPath, snapshotTsNs int64, bounds []string) {
t.Helper()
if err := mc.BeginDirectoryBuild(context.Background(), dir); err != nil {
t.Fatalf("begin build: %v", err)
}
if err := mc.CompleteDirectoryBuild(context.Background(), dir, snapshotTsNs, bounds); err != nil {
t.Fatalf("complete build: %v", err)
}
}
func applyChurn(t *testing.T, mc *MetaCache, dir, prefix string, n int, baseTsNs int64, options MetadataResponseApplyOptions) {
t.Helper()
for i := 0; i < n; i++ {
resp := &filer_pb.SubscribeMetadataResponse{
Directory: dir,
EventNotification: &filer_pb.EventNotification{
NewEntry: &filer_pb.Entry{
Name: fmt.Sprintf("%s%03d", prefix, i),
Attributes: &filer_pb.FuseAttributes{
Crtime: 1,
Mtime: 1,
FileMode: 0100644,
FileSize: 1,
},
},
},
TsNs: baseTsNs + int64(i),
}
if err := mc.ApplyMetadataResponse(context.Background(), resp, options); err != nil {
t.Fatalf("apply churn %d: %v", i, err)
}
}
}
func TestSectionBurstInvalidatesOnlyItsSection(t *testing.T) {
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{
"/": true,
"/dir": true,
})
defer mc.Shutdown()
buildSectionedDir(t, mc, util.FullPath("/dir"), 1000, []string{"m"})
applyChurn(t, mc, "/dir", "a-", sectionHotThreshold, 2000, SubscriberMetadataResponseApplyOptions)
if mc.IsNameFresh(util.FullPath("/dir/a-000")) {
t.Fatal("burst section should be invalidated")
}
if !mc.IsNameFresh(util.FullPath("/dir/zzz")) {
t.Fatal("the other section must stay fresh")
}
}
func TestLocalChangesDoNotInvalidateSections(t *testing.T) {
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{
"/": true,
"/dir": true,
})
defer mc.Shutdown()
buildSectionedDir(t, mc, util.FullPath("/dir"), 1000, nil)
applyChurn(t, mc, "/dir", "a-", 2*sectionHotThreshold, 2000, LocalMetadataResponseApplyOptions)
if !mc.IsNameFresh(util.FullPath("/dir/a-000")) {
t.Fatal("this mount's own writes must not invalidate its cache")
}
}
func TestSectionRefreshReconciles(t *testing.T) {
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{
"/": true,
"/dir": true,
})
defer mc.Shutdown()
mc.SetPinnedChildFn(func(entry *filer.Entry) bool {
return entry.Name() == "b-pinned"
})
buildSectionedDir(t, mc, util.FullPath("/dir"), 1000, []string{"m"})
for _, name := range []string{"b-keep", "b-vanish", "b-pinned"} {
if err := mc.InsertEntry(context.Background(), &filer.Entry{
FullPath: util.FullPath("/dir/" + name),
Attr: filer.Attr{Crtime: time.Unix(1, 0), Mtime: time.Unix(1, 0), Mode: 0100644, FileSize: 1},
}, 0); err != nil {
t.Fatalf("seed %s: %v", name, err)
}
}
// applied ahead of the refresh snapshot; the listing must not roll it back
if err := mc.ApplyMetadataResponse(context.Background(), &filer_pb.SubscribeMetadataResponse{
Directory: "/dir",
EventNotification: &filer_pb.EventNotification{
NewEntry: &filer_pb.Entry{
Name: "b-newer",
Attributes: &filer_pb.FuseAttributes{Crtime: 1, Mtime: 9, FileMode: 0100644, FileSize: 9},
},
},
TsNs: 9000,
}, SubscriberMetadataResponseApplyOptions); err != nil {
t.Fatalf("apply newer event: %v", err)
}
applyChurn(t, mc, "/dir", "a-", sectionHotThreshold, 2000, SubscriberMetadataResponseApplyOptions)
if mc.IsNameFresh(util.FullPath("/dir/b-keep")) {
t.Fatal("section should be stale before the refresh")
}
if err := mc.enqueueAndWait(context.Background(), metadataApplyRequest{
kind: metadataSectionRefresh,
buildPath: util.FullPath("/dir"),
refresh: &sectionRefresh{
hi: "m",
entries: []*filer.Entry{
{
FullPath: util.FullPath("/dir/b-keep"),
Attr: filer.Attr{Crtime: time.Unix(1, 0), Mtime: time.Unix(2, 0), Mode: 0100644, FileSize: 7},
},
{
FullPath: util.FullPath("/dir/b-new"),
Attr: filer.Attr{Crtime: time.Unix(2, 0), Mtime: time.Unix(2, 0), Mode: 0100644, FileSize: 3},
},
},
snapshotTsNs: 5000,
},
}); err != nil {
t.Fatalf("refresh: %v", err)
}
if !mc.IsNameFresh(util.FullPath("/dir/b-keep")) {
t.Fatal("section should be fresh after the refresh")
}
entry, _, err := mc.FindEntry(context.Background(), util.FullPath("/dir/b-keep"))
if err != nil || entry.FileSize != 7 {
t.Fatalf("b-keep = %+v, %v; want size 7", entry, err)
}
if entry, _, err = mc.FindEntry(context.Background(), util.FullPath("/dir/b-new")); err != nil || entry.FileSize != 3 {
t.Fatalf("b-new = %+v, %v; want size 3", entry, err)
}
if _, _, err = mc.FindEntry(context.Background(), util.FullPath("/dir/b-vanish")); err != filer_pb.ErrNotFound {
t.Fatalf("b-vanish error = %v, want not found", err)
}
if _, _, err = mc.FindEntry(context.Background(), util.FullPath("/dir/a-000")); err != filer_pb.ErrNotFound {
t.Fatalf("churned a-000 error = %v, want not found", err)
}
if entry, _, err = mc.FindEntry(context.Background(), util.FullPath("/dir/b-newer")); err != nil || entry.FileSize != 9 {
t.Fatalf("b-newer = %+v, %v; want size 9 kept", entry, err)
}
if entry, _, err = mc.FindEntry(context.Background(), util.FullPath("/dir/b-pinned")); err != nil || entry == nil {
t.Fatalf("pinned local-only entry must survive a refresh: %v", err)
}
// a redelivered create older than the snapshot must not resurrect b-vanish
if err := mc.ApplyMetadataResponse(context.Background(), &filer_pb.SubscribeMetadataResponse{
Directory: "/dir",
EventNotification: &filer_pb.EventNotification{
NewEntry: &filer_pb.Entry{
Name: "b-vanish",
Attributes: &filer_pb.FuseAttributes{Crtime: 1, Mtime: 1, FileMode: 0100644, FileSize: 1},
},
},
TsNs: 4000,
}, SubscriberMetadataResponseApplyOptions); err != nil {
t.Fatalf("apply stale event: %v", err)
}
if _, _, err = mc.FindEntry(context.Background(), util.FullPath("/dir/b-vanish")); err != filer_pb.ErrNotFound {
t.Fatalf("b-vanish resurrected by a pre-snapshot event: %v", err)
}
}
func TestSectionRefreshSplitsOvergrownSection(t *testing.T) {
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{
"/": true,
"/dir": true,
})
defer mc.Shutdown()
buildSectionedDir(t, mc, util.FullPath("/dir"), 1000, nil)
count := 2*dirSectionSize + 1
entries := make([]*filer.Entry, 0, count)
for i := 0; i < count; i++ {
entries = append(entries, &filer.Entry{
FullPath: util.FullPath(fmt.Sprintf("/dir/f-%05d", i)),
Attr: filer.Attr{Crtime: time.Unix(1, 0), Mtime: time.Unix(1, 0), Mode: 0100644, FileSize: 1},
})
}
if err := mc.enqueueAndWait(context.Background(), metadataApplyRequest{
kind: metadataSectionRefresh,
buildPath: util.FullPath("/dir"),
refresh: &sectionRefresh{entries: entries, snapshotTsNs: 5000},
}); err != nil {
t.Fatalf("refresh: %v", err)
}
mc.RLock()
sl := mc.dirSections[util.FullPath("/dir")]
bounds := append([]string(nil), sl.bounds...)
sections := len(sl.sections)
mc.RUnlock()
want := []string{fmt.Sprintf("f-%05d", dirSectionSize), fmt.Sprintf("f-%05d", 2*dirSectionSize)}
if len(bounds) != len(want) || bounds[0] != want[0] || bounds[1] != want[1] {
t.Fatalf("bounds = %v, want %v", bounds, want)
}
if sections != len(bounds)+1 {
t.Fatalf("sections = %d, want %d", sections, len(bounds)+1)
}
}
type sectionFilerServer struct {
filer_pb.UnimplementedSeaweedFilerServer
mu sync.Mutex
names []string // sorted; all under one directory
snapshot int64
requests []*filer_pb.ListEntriesRequest
}
func (s *sectionFilerServer) ListEntries(req *filer_pb.ListEntriesRequest, stream filer_pb.SeaweedFiler_ListEntriesServer) error {
s.mu.Lock()
s.requests = append(s.requests, req)
names := s.names
snapshot := s.snapshot
s.mu.Unlock()
sent := uint32(0)
first := true
for _, name := range names {
if name < req.StartFromFileName || (name == req.StartFromFileName && !req.InclusiveStartFrom) {
continue
}
resp := &filer_pb.ListEntriesResponse{Entry: &filer_pb.Entry{
Name: name,
Attributes: &filer_pb.FuseAttributes{Crtime: 1, Mtime: 1, FileMode: 0100644, FileSize: 1},
}}
if first {
resp.SnapshotTsNs = snapshot
first = false
}
if err := stream.Send(resp); err != nil {
return err
}
sent++
if req.Limit > 0 && sent >= req.Limit {
break
}
}
return nil
}
func (s *sectionFilerServer) listRequests() []*filer_pb.ListEntriesRequest {
s.mu.Lock()
defer s.mu.Unlock()
return append([]*filer_pb.ListEntriesRequest(nil), s.requests...)
}
type sectionTestFilerClient struct {
addr string
}
func (c *sectionTestFilerClient) WithFilerClient(streamingMode bool, fn func(filer_pb.SeaweedFilerClient) error) error {
conn, err := grpc.NewClient(c.addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return err
}
defer conn.Close()
return fn(filer_pb.NewSeaweedFilerClient(conn))
}
func (c *sectionTestFilerClient) AdjustedUrl(location *filer_pb.Location) string { return location.Url }
func (c *sectionTestFilerClient) GetDataCenter() string { return "" }
func startSectionFilerServer(t *testing.T, s *sectionFilerServer) filer_pb.FilerClient {
t.Helper()
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen: %v", err)
}
srv := grpc.NewServer()
filer_pb.RegisterSeaweedFilerServer(srv, s)
go srv.Serve(listener)
t.Cleanup(srv.Stop)
return &sectionTestFilerClient{addr: listener.Addr().String()}
}
func TestEnsureListingFreshRefreshesFromFiler(t *testing.T) {
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{
"/": true,
"/dir": true,
})
defer mc.Shutdown()
server := &sectionFilerServer{snapshot: 5000}
for i := 0; i < 1500; i++ {
server.names = append(server.names, fmt.Sprintf("b-%04d", i))
}
// at and beyond the section's end, must not be applied
server.names = append(server.names, "m", "z-1")
client := startSectionFilerServer(t, server)
buildSectionedDir(t, mc, util.FullPath("/dir"), 1000, []string{"m"})
applyChurn(t, mc, "/dir", "a-", sectionHotThreshold, 2000, SubscriberMetadataResponseApplyOptions)
if mc.IsNameFresh(util.FullPath("/dir/a-000")) {
t.Fatal("section should be stale before the refresh")
}
if err := EnsureListingFresh(context.Background(), mc, client, util.FullPath("/dir"), ""); err != nil {
t.Fatalf("refresh: %v", err)
}
if !mc.IsNameFresh(util.FullPath("/dir/a-000")) {
t.Fatal("section should be fresh after the refresh")
}
for _, name := range []string{"b-0000", "b-1499"} {
if entry, _, err := mc.FindEntry(context.Background(), util.FullPath("/dir/"+name)); err != nil || entry == nil {
t.Fatalf("%s missing after refresh: %v", name, err)
}
}
if _, _, err := mc.FindEntry(context.Background(), util.FullPath("/dir/a-000")); err != filer_pb.ErrNotFound {
t.Fatalf("churned a-000 error = %v, want not found", err)
}
for _, name := range []string{"m", "z-1"} {
if _, _, err := mc.FindEntry(context.Background(), util.FullPath("/dir/"+name)); err != filer_pb.ErrNotFound {
t.Fatalf("%s at or beyond the section's end was applied: %v", name, err)
}
}
requests := server.listRequests()
if len(requests) < 2 {
t.Fatalf("multi-page refresh made %d requests, want at least 2", len(requests))
}
if requests[0].SnapshotTsNs != 0 || requests[1].SnapshotTsNs != 5000 {
t.Fatalf("snapshot not pinned across pages: %d then %d", requests[0].SnapshotTsNs, requests[1].SnapshotTsNs)
}
// a fresh section costs no filer calls
if err := EnsureListingFresh(context.Background(), mc, client, util.FullPath("/dir"), ""); err != nil {
t.Fatalf("second refresh: %v", err)
}
if got := len(server.listRequests()); got != len(requests) {
t.Fatalf("fresh section still listed the filer: %d requests, was %d", got, len(requests))
}
}
func TestEnsureListingFreshGivesUpOnOvergrownRange(t *testing.T) {
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{
"/": true,
"/dir": true,
})
defer mc.Shutdown()
server := &sectionFilerServer{snapshot: 5000}
for i := 0; i <= sectionRefreshMaxEntries; i++ {
server.names = append(server.names, fmt.Sprintf("c-%05d", i))
}
client := startSectionFilerServer(t, server)
buildSectionedDir(t, mc, util.FullPath("/dir"), 1000, nil)
applyChurn(t, mc, "/dir", "x-", sectionHotThreshold, 2000, SubscriberMetadataResponseApplyOptions)
err := EnsureListingFresh(context.Background(), mc, client, util.FullPath("/dir"), "")
if !errors.Is(err, ErrRefreshRangeTooLarge) {
t.Fatalf("refresh error = %v, want ErrRefreshRangeTooLarge", err)
}
if mc.IsNameFresh(util.FullPath("/dir/x-000")) {
t.Fatal("an aborted refresh must leave the section stale")
}
}
func TestSectionNoteChangeWindowExpiry(t *testing.T) {
sl := newSectionTable(nil)
t0 := time.Unix(100, 0)
for i := 0; i < sectionHotThreshold-1; i++ {
sl.noteChange(fmt.Sprintf("f-%03d", i), t0)
}
if !sl.isFresh("f-000") {
t.Fatal("below the threshold the section must stay fresh")
}
// the burst never completed inside one window, so the count restarts
t1 := t0.Add(sectionHotWindow + time.Second)
for i := 0; i < sectionHotThreshold-1; i++ {
sl.noteChange(fmt.Sprintf("f-%03d", i), t1)
}
if !sl.isFresh("f-000") {
t.Fatal("an expired window must not carry its count forward")
}
sl.noteChange("f-999", t1)
if sl.isFresh("f-000") {
t.Fatal("a full burst within one window must invalidate the section")
}
}
func TestSectionCompleteRefreshGuardsChangedTable(t *testing.T) {
sl := newSectionTable([]string{"g", "p"})
sl.sections[1].stale = true
if sl.completeRefresh("g", "q", []string{"h"}, 5000) {
t.Fatal("a range the table no longer has must be ignored")
}
if sl.isFresh("h") {
t.Fatal("an ignored refresh must not mark anything fresh")
}
if !sl.completeRefresh("g", "p", []string{"h"}, 5000) {
t.Fatal("the matching range must be accepted")
}
if !sl.isFresh("h") {
t.Fatal("section should be fresh after the refresh")
}
}
func TestSectionCompleteRefreshSplitsMiddleSection(t *testing.T) {
sl := newSectionTable([]string{"g", "p"})
sl.sections[1].stale = true
names := make([]string, 0, 2*dirSectionSize+1)
for i := 0; i <= 2*dirSectionSize; i++ {
names = append(names, fmt.Sprintf("g-%05d", i))
}
if !sl.completeRefresh("g", "p", names, 5000) {
t.Fatal("refresh of the middle section must be accepted")
}
wantBounds := []string{"g", names[dirSectionSize], names[2*dirSectionSize], "p"}
if len(sl.bounds) != len(wantBounds) {
t.Fatalf("bounds = %v, want %v", sl.bounds, wantBounds)
}
for i, b := range wantBounds {
if sl.bounds[i] != b {
t.Fatalf("bounds = %v, want %v", sl.bounds, wantBounds)
}
}
if len(sl.sections) != len(sl.bounds)+1 {
t.Fatalf("sections = %d, want %d", len(sl.sections), len(sl.bounds)+1)
}
for _, name := range []string{"a", "g-00000", names[dirSectionSize], "z"} {
if !sl.isFresh(name) {
t.Fatalf("%q should be fresh after the split", name)
}
}
if got := sl.sectionOf(names[dirSectionSize+1]); got != 2 {
t.Fatalf("sectionOf(%q) = %d, want 2", names[dirSectionSize+1], got)
}
}
func TestSectionFloorFencesAbsentNames(t *testing.T) {
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{
"/": true,
"/dir": true,
})
defer mc.Shutdown()
buildSectionedDir(t, mc, util.FullPath("/dir"), 1000, []string{"m"})
applyChurn(t, mc, "/dir", "a-", sectionHotThreshold, 2000, SubscriberMetadataResponseApplyOptions)
if err := mc.enqueueAndWait(context.Background(), metadataApplyRequest{
kind: metadataSectionRefresh,
buildPath: util.FullPath("/dir"),
refresh: &sectionRefresh{hi: "m", snapshotTsNs: 5000},
}); err != nil {
t.Fatalf("refresh: %v", err)
}
// a delayed create between the directory floor and the refresh snapshot,
// for a name neither the cache nor the listing ever held
ghost := func(tsNs int64) {
t.Helper()
if err := mc.ApplyMetadataResponse(context.Background(), &filer_pb.SubscribeMetadataResponse{
Directory: "/dir",
EventNotification: &filer_pb.EventNotification{
NewEntry: &filer_pb.Entry{
Name: "b-ghost",
Attributes: &filer_pb.FuseAttributes{Crtime: 1, Mtime: 1, FileMode: 0100644, FileSize: 1},
},
},
TsNs: tsNs,
}, SubscriberMetadataResponseApplyOptions); err != nil {
t.Fatalf("apply ghost event: %v", err)
}
}
ghost(4500)
if _, _, err := mc.FindEntry(context.Background(), util.FullPath("/dir/b-ghost")); err != filer_pb.ErrNotFound {
t.Fatalf("pre-snapshot event resurrected an absent name: %v", err)
}
ghost(6000)
if _, _, err := mc.FindEntry(context.Background(), util.FullPath("/dir/b-ghost")); err != nil {
t.Fatalf("post-snapshot event must apply: %v", err)
}
}
func TestUnversionedRefreshStaysStale(t *testing.T) {
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{
"/": true,
"/dir": true,
})
defer mc.Shutdown()
buildSectionedDir(t, mc, util.FullPath("/dir"), 1000, nil)
applyChurn(t, mc, "/dir", "a-", sectionHotThreshold, 2000, SubscriberMetadataResponseApplyOptions)
if err := mc.enqueueAndWait(context.Background(), metadataApplyRequest{
kind: metadataSectionRefresh,
buildPath: util.FullPath("/dir"),
refresh: &sectionRefresh{entries: []*filer.Entry{{
FullPath: util.FullPath("/dir/b-gap"),
Attr: filer.Attr{Crtime: time.Unix(1, 0), Mtime: time.Unix(1, 0), Mode: 0100644, FileSize: 1},
}}},
}); err != nil {
t.Fatalf("refresh: %v", err)
}
if mc.IsNameFresh(util.FullPath("/dir/a-000")) {
t.Fatal("an unversioned refresh vouches for nothing and must stay stale")
}
if _, _, err := mc.FindEntry(context.Background(), util.FullPath("/dir/b-gap")); err != nil {
t.Fatalf("gap fill should still land: %v", err)
}
if ranges := mc.staleRangesAhead(util.FullPath("/dir"), ""); len(ranges) != 0 {
t.Fatalf("an unverifiable section must not be re-listed, got %d ranges", len(ranges))
}
}
func TestEnsureListingFreshCoversAllStaleSectionsAhead(t *testing.T) {
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{
"/": true,
"/dir": true,
})
defer mc.Shutdown()
server := &sectionFilerServer{snapshot: 5000, names: []string{"b-1", "n-1"}}
client := startSectionFilerServer(t, server)
buildSectionedDir(t, mc, util.FullPath("/dir"), 1000, []string{"m"})
applyChurn(t, mc, "/dir", "a-", sectionHotThreshold, 2000, SubscriberMetadataResponseApplyOptions)
applyChurn(t, mc, "/dir", "n-", sectionHotThreshold, 3000, SubscriberMetadataResponseApplyOptions)
if err := EnsureListingFresh(context.Background(), mc, client, util.FullPath("/dir"), ""); err != nil {
t.Fatalf("refresh: %v", err)
}
if !mc.IsNameFresh(util.FullPath("/dir/a-000")) || !mc.IsNameFresh(util.FullPath("/dir/n-000")) {
t.Fatal("one call must re-validate every stale section ahead of the start")
}
}
func TestSectionCompleteRefreshSetsFloors(t *testing.T) {
sl := newSectionTable([]string{"g", "p"})
sl.sections[1].stale = true
names := make([]string, 0, 2*dirSectionSize+1)
for i := 0; i <= 2*dirSectionSize; i++ {
names = append(names, fmt.Sprintf("g-%05d", i))
}
if !sl.completeRefresh("g", "p", names, 7000) {
t.Fatal("refresh must be accepted")
}
for _, name := range []string{"g", "g-99999", names[dirSectionSize+1]} {
if got := sl.floorOf(name); got != 7000 {
t.Fatalf("floorOf(%q) = %d, want 7000", name, got)
}
}
if got := sl.floorOf("a"); got != 0 {
t.Fatalf("floorOf(a) = %d, want 0 for a never-refreshed section", got)
}
}
func TestSectionFloorOutranksOlderTombstone(t *testing.T) {
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{
"/": true,
"/dir": true,
})
defer mc.Shutdown()
buildSectionedDir(t, mc, util.FullPath("/dir"), 1000, nil)
event := func(tsNs int64, oldEntry, newEntry *filer_pb.Entry) {
t.Helper()
if err := mc.ApplyMetadataResponse(context.Background(), &filer_pb.SubscribeMetadataResponse{
Directory: "/dir",
EventNotification: &filer_pb.EventNotification{OldEntry: oldEntry, NewEntry: newEntry},
TsNs: tsNs,
}, SubscriberMetadataResponseApplyOptions); err != nil {
t.Fatalf("apply event at %d: %v", tsNs, err)
}
}
pbFile := &filer_pb.Entry{
Name: "b-x",
Attributes: &filer_pb.FuseAttributes{Crtime: 1, Mtime: 1, FileMode: 0100644, FileSize: 1},
}
event(1500, nil, pbFile) // create
event(2000, &filer_pb.Entry{Name: "b-x"}, nil) // delete, leaves a tombstone at 2000
applyChurn(t, mc, "/dir", "a-", sectionHotThreshold, 2500, SubscriberMetadataResponseApplyOptions)
if err := mc.enqueueAndWait(context.Background(), metadataApplyRequest{
kind: metadataSectionRefresh,
buildPath: util.FullPath("/dir"),
refresh: &sectionRefresh{snapshotTsNs: 5000},
}); err != nil {
t.Fatalf("refresh: %v", err)
}
// the floor at 5000 outranks the tombstone at 2000
event(4000, nil, pbFile)
if _, _, err := mc.FindEntry(context.Background(), util.FullPath("/dir/b-x")); err != filer_pb.ErrNotFound {
t.Fatalf("pre-floor event applied over an older tombstone: %v", err)
}
event(6000, nil, pbFile)
if _, _, err := mc.FindEntry(context.Background(), util.FullPath("/dir/b-x")); err != nil {
t.Fatalf("post-floor event must apply: %v", err)
}
}
func TestMismatchedRefreshLeavesStoreAlone(t *testing.T) {
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{
"/": true,
"/dir": true,
})
defer mc.Shutdown()
buildSectionedDir(t, mc, util.FullPath("/dir"), 1000, []string{"m"})
if err := mc.InsertEntry(context.Background(), &filer.Entry{
FullPath: util.FullPath("/dir/b-keep"),
Attr: filer.Attr{Crtime: time.Unix(1, 0), Mtime: time.Unix(1, 0), Mode: 0100644, FileSize: 1},
}, 0); err != nil {
t.Fatalf("seed: %v", err)
}
applyChurn(t, mc, "/dir", "a-", sectionHotThreshold, 2000, SubscriberMetadataResponseApplyOptions)
// a range the table does not have: the reconcile must not touch the store
if err := mc.enqueueAndWait(context.Background(), metadataApplyRequest{
kind: metadataSectionRefresh,
buildPath: util.FullPath("/dir"),
refresh: &sectionRefresh{hi: "q", snapshotTsNs: 5000, entries: []*filer.Entry{{
FullPath: util.FullPath("/dir/b-new"),
Attr: filer.Attr{Crtime: time.Unix(2, 0), Mtime: time.Unix(2, 0), Mode: 0100644, FileSize: 3},
}}},
}); err != nil {
t.Fatalf("refresh: %v", err)
}
if _, _, err := mc.FindEntry(context.Background(), util.FullPath("/dir/b-new")); err != filer_pb.ErrNotFound {
t.Fatalf("mismatched refresh inserted an entry: %v", err)
}
if _, _, err := mc.FindEntry(context.Background(), util.FullPath("/dir/b-keep")); err != nil {
t.Fatalf("mismatched refresh swept an entry: %v", err)
}
if mc.IsNameFresh(util.FullPath("/dir/a-000")) {
t.Fatal("mismatched refresh must not mark anything fresh")
}
}
func TestRefreshClearsUnversionedMarker(t *testing.T) {
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{
"/": true,
"/dir": true,
})
defer mc.Shutdown()
mc.SetPinnedChildFn(func(entry *filer.Entry) bool {
return entry.Name() == "b-pin"
})
buildSectionedDir(t, mc, util.FullPath("/dir"), 1000, nil)
for _, name := range []string{"b-local", "b-pin"} {
if err := mc.InsertEntry(context.Background(), &filer.Entry{
FullPath: util.FullPath("/dir/" + name),
Attr: filer.Attr{Crtime: time.Unix(1, 0), Mtime: time.Unix(1, 0), Mode: 0100644, FileSize: 1},
}, 0); err != nil { // versionTsNs 0 leaves the unversioned marker
t.Fatalf("seed %s: %v", name, err)
}
}
applyChurn(t, mc, "/dir", "a-", sectionHotThreshold, 2000, SubscriberMetadataResponseApplyOptions)
refreshed := func(name string) *filer.Entry {
return &filer.Entry{
FullPath: util.FullPath("/dir/" + name),
Attr: filer.Attr{Crtime: time.Unix(1, 0), Mtime: time.Unix(2, 0), Mode: 0100644, FileSize: 7},
}
}
if err := mc.enqueueAndWait(context.Background(), metadataApplyRequest{
kind: metadataSectionRefresh,
buildPath: util.FullPath("/dir"),
refresh: &sectionRefresh{snapshotTsNs: 5000, entries: []*filer.Entry{refreshed("b-local"), refreshed("b-pin")}},
}); err != nil {
t.Fatalf("refresh: %v", err)
}
entry, versionTsNs, err := mc.FindEntry(context.Background(), util.FullPath("/dir/b-local"))
if err != nil || entry.FileSize != 7 {
t.Fatalf("b-local = %+v, %v; want the snapshot's size 7", entry, err)
}
if versionTsNs != 5000 {
t.Fatalf("b-local version = %d, want the floor 5000 once the marker is cleared", versionTsNs)
}
// a delayed event below the floor must not roll the snapshot write back
if err := mc.ApplyMetadataResponse(context.Background(), &filer_pb.SubscribeMetadataResponse{
Directory: "/dir",
EventNotification: &filer_pb.EventNotification{
NewEntry: &filer_pb.Entry{
Name: "b-local",
Attributes: &filer_pb.FuseAttributes{Crtime: 1, Mtime: 3, FileMode: 0100644, FileSize: 9},
},
},
TsNs: 4000,
}, SubscriberMetadataResponseApplyOptions); err != nil {
t.Fatalf("apply delayed event: %v", err)
}
if entry, _, err = mc.FindEntry(context.Background(), util.FullPath("/dir/b-local")); err != nil || entry.FileSize != 7 {
t.Fatalf("b-local rolled back to %+v, %v", entry, err)
}
// pinned local-only state is neither replaced nor unmarked
entry, versionTsNs, err = mc.FindEntry(context.Background(), util.FullPath("/dir/b-pin"))
if err != nil || entry.FileSize != 1 {
t.Fatalf("b-pin = %+v, %v; want local size 1 kept", entry, err)
}
if versionTsNs != 0 {
t.Fatalf("b-pin version = %d, want 0 while pinned", versionTsNs)
}
}
func TestRefreshSkipsBuildingDirectory(t *testing.T) {
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{
"/": true,
"/dir": true,
})
defer mc.Shutdown()
buildSectionedDir(t, mc, util.FullPath("/dir"), 1000, nil)
applyChurn(t, mc, "/dir", "a-", sectionHotThreshold, 2000, SubscriberMetadataResponseApplyOptions)
// a rebuild is streaming: its inserts must survive a refresh's sweep
if err := mc.BeginDirectoryBuild(context.Background(), util.FullPath("/dir")); err != nil {
t.Fatalf("begin build: %v", err)
}
if err := mc.InsertEntry(context.Background(), &filer.Entry{
FullPath: util.FullPath("/dir/b-built"),
Attr: filer.Attr{Crtime: time.Unix(1, 0), Mtime: time.Unix(1, 0), Mode: 0100644, FileSize: 1},
}, 0); err != nil {
t.Fatalf("insert mid-build: %v", err)
}
if err := mc.enqueueAndWait(context.Background(), metadataApplyRequest{
kind: metadataSectionRefresh,
buildPath: util.FullPath("/dir"),
refresh: &sectionRefresh{snapshotTsNs: 5000},
}); err != nil {
t.Fatalf("refresh: %v", err)
}
if _, _, err := mc.FindEntry(context.Background(), util.FullPath("/dir/b-built")); err != nil {
t.Fatalf("refresh swept a mid-build insert: %v", err)
}
if mc.IsNameFresh(util.FullPath("/dir/a-000")) {
t.Fatal("a skipped refresh must not mark the section fresh")
}
if err := mc.AbortDirectoryBuild(context.Background(), util.FullPath("/dir")); err != nil {
t.Fatalf("abort build: %v", err)
}
}
// TestSectionBorderEntries pins the border semantics: a bound-named entry
// belongs to the section starting at the bound, a refresh sweeps [lo, hi)
// inclusive of lo and exclusive of hi, and the neighboring sections' entries
// come through untouched.
func TestSectionBorderEntries(t *testing.T) {
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{
"/": true,
"/dir": true,
})
defer mc.Shutdown()
buildSectionedDir(t, mc, util.FullPath("/dir"), 1000, []string{"g", "p"})
for _, name := range []string{"g", "h", "p"} {
if err := mc.InsertEntry(context.Background(), &filer.Entry{
FullPath: util.FullPath("/dir/" + name),
Attr: filer.Attr{Crtime: time.Unix(1, 0), Mtime: time.Unix(1, 0), Mode: 0100644, FileSize: 1},
}, 0); err != nil {
t.Fatalf("seed %s: %v", name, err)
}
}
applyChurn(t, mc, "/dir", "a-", sectionHotThreshold, 2000, SubscriberMetadataResponseApplyOptions)
applyChurn(t, mc, "/dir", "h-", sectionHotThreshold, 3000, SubscriberMetadataResponseApplyOptions)
// the bound name is part of the section starting at it
if mc.IsNameFresh(util.FullPath("/dir/a-000")) || mc.IsNameFresh(util.FullPath("/dir/g")) {
t.Fatal("both churned sections should be stale")
}
// refreshing [ , g) sweeps its churn but must not reach the bound entry
if err := mc.enqueueAndWait(context.Background(), metadataApplyRequest{
kind: metadataSectionRefresh,
buildPath: util.FullPath("/dir"),
refresh: &sectionRefresh{hi: "g", snapshotTsNs: 5000},
}); err != nil {
t.Fatalf("refresh section 0: %v", err)
}
if _, _, err := mc.FindEntry(context.Background(), util.FullPath("/dir/a-000")); err != filer_pb.ErrNotFound {
t.Fatalf("churned a-000 should be swept: %v", err)
}
for _, name := range []string{"g", "h", "p"} {
if _, _, err := mc.FindEntry(context.Background(), util.FullPath("/dir/"+name)); err != nil {
t.Fatalf("%s swept by the neighboring refresh: %v", name, err)
}
}
if !mc.IsNameFresh(util.FullPath("/dir/a-000")) || mc.IsNameFresh(util.FullPath("/dir/g")) {
t.Fatal("only the refreshed section should be fresh")
}
// refreshing [g, p) covers the bound entry itself and stops before p
if err := mc.enqueueAndWait(context.Background(), metadataApplyRequest{
kind: metadataSectionRefresh,
buildPath: util.FullPath("/dir"),
refresh: &sectionRefresh{lo: "g", hi: "p", snapshotTsNs: 5001, entries: []*filer.Entry{{
FullPath: util.FullPath("/dir/h"),
Attr: filer.Attr{Crtime: time.Unix(1, 0), Mtime: time.Unix(2, 0), Mode: 0100644, FileSize: 7},
}}},
}); err != nil {
t.Fatalf("refresh section 1: %v", err)
}
if _, _, err := mc.FindEntry(context.Background(), util.FullPath("/dir/g")); err != filer_pb.ErrNotFound {
t.Fatalf("vanished bound entry should be swept by its own section: %v", err)
}
if _, _, err := mc.FindEntry(context.Background(), util.FullPath("/dir/h-000")); err != filer_pb.ErrNotFound {
t.Fatalf("churned h-000 should be swept: %v", err)
}
if entry, _, err := mc.FindEntry(context.Background(), util.FullPath("/dir/h")); err != nil || entry.FileSize != 7 {
t.Fatalf("h = %+v, %v; want size 7", entry, err)
}
if _, _, err := mc.FindEntry(context.Background(), util.FullPath("/dir/p")); err != nil {
t.Fatalf("the next bound's entry is outside the range and must survive: %v", err)
}
}
func TestChurnBeyondBuiltEntriesLandsInEdgeSection(t *testing.T) {
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{
"/": true,
"/dir": true,
})
defer mc.Shutdown()
buildSectionedDir(t, mc, util.FullPath("/dir"), 1000, []string{"m"})
// names sorting past everything the build saw land in the last section
applyChurn(t, mc, "/dir", "z-", sectionHotThreshold, 2000, SubscriberMetadataResponseApplyOptions)
if mc.IsNameFresh(util.FullPath("/dir/z-000")) {
t.Fatal("the tail section should be invalidated")
}
if !mc.IsNameFresh(util.FullPath("/dir/a")) {
t.Fatal("the head section must stay fresh")
}
}
func TestRenameAcrossSectionsCountsBoth(t *testing.T) {
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{
"/": true,
"/dir": true,
})
defer mc.Shutdown()
buildSectionedDir(t, mc, util.FullPath("/dir"), 1000, []string{"m"})
for i := 0; i < sectionHotThreshold; i++ {
resp := &filer_pb.SubscribeMetadataResponse{
Directory: "/dir",
EventNotification: &filer_pb.EventNotification{
OldEntry: &filer_pb.Entry{Name: fmt.Sprintf("a-%03d", i)},
NewEntry: &filer_pb.Entry{
Name: fmt.Sprintf("n-%03d", i),
Attributes: &filer_pb.FuseAttributes{Crtime: 1, Mtime: 1, FileMode: 0100644, FileSize: 1},
},
NewParentPath: "/dir",
},
TsNs: 2000 + int64(i),
}
if err := mc.ApplyMetadataResponse(context.Background(), resp, SubscriberMetadataResponseApplyOptions); err != nil {
t.Fatalf("apply rename %d: %v", i, err)
}
}
if mc.IsNameFresh(util.FullPath("/dir/a-000")) {
t.Fatal("the vacated side's section should be invalidated")
}
if mc.IsNameFresh(util.FullPath("/dir/n-000")) {
t.Fatal("the landing side's section should be invalidated")
}
}
// A refresh sweeps the names its listing did not return. One recorded at
// exactly the snapshot has its write reflected but not its removal, so reading
// the gate as a write left it cached for good.
func TestSectionRefreshSweepsANameRecordedAtTheSnapshot(t *testing.T) {
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{"/": true, "/dir": true})
defer mc.Shutdown()
buildSectionedDir(t, mc, util.FullPath("/dir"), 1000, []string{"m"})
const snapshotTsNs = 5000
if err := mc.ApplyMetadataResponse(context.Background(), &filer_pb.SubscribeMetadataResponse{
Directory: "/dir",
EventNotification: &filer_pb.EventNotification{
NewEntry: &filer_pb.Entry{
Name: "b-vanish",
Attributes: &filer_pb.FuseAttributes{Crtime: 1, Mtime: 1, FileMode: 0100644, FileSize: 3},
},
},
TsNs: snapshotTsNs,
}, SubscriberMetadataResponseApplyOptions); err != nil {
t.Fatalf("seed at the snapshot version: %v", err)
}
if err := mc.enqueueAndWait(context.Background(), metadataApplyRequest{
kind: metadataSectionRefresh,
buildPath: util.FullPath("/dir"),
refresh: &sectionRefresh{
hi: "m",
entries: []*filer.Entry{
{
FullPath: util.FullPath("/dir/b-keep"),
Attr: filer.Attr{Crtime: time.Unix(1, 0), Mtime: time.Unix(2, 0), Mode: 0100644, FileSize: 7},
},
},
snapshotTsNs: snapshotTsNs,
},
}); err != nil {
t.Fatalf("refresh: %v", err)
}
if entry, _, err := mc.FindEntry(context.Background(), util.FullPath("/dir/b-vanish")); err == nil && entry != nil {
t.Error("a name the refresh did not list is still cached")
}
}