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
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:
32
weed/mount/meta_cache/cache_config.go
Normal file
32
weed/mount/meta_cache/cache_config.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package meta_cache
|
||||
|
||||
import "github.com/seaweedfs/seaweedfs/weed/util"
|
||||
|
||||
var (
|
||||
_ = util.Configuration(&cacheConfig{})
|
||||
)
|
||||
|
||||
// implementing util.Configuration
|
||||
type cacheConfig struct {
|
||||
dir string
|
||||
}
|
||||
|
||||
func (c cacheConfig) GetString(key string) string {
|
||||
return c.dir
|
||||
}
|
||||
|
||||
func (c cacheConfig) GetBool(key string) bool {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (c cacheConfig) GetInt(key string) int {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (c cacheConfig) GetStringSlice(key string) []string {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (c cacheConfig) SetDefault(key string, value interface{}) {
|
||||
panic("implement me")
|
||||
}
|
||||
101
weed/mount/meta_cache/id_mapper.go
Normal file
101
weed/mount/meta_cache/id_mapper.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package meta_cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type UidGidMapper struct {
|
||||
uidMapper *IdMapper
|
||||
gidMapper *IdMapper
|
||||
}
|
||||
|
||||
type IdMapper struct {
|
||||
localToFiler map[uint32]uint32
|
||||
filerToLocal map[uint32]uint32
|
||||
}
|
||||
|
||||
// UidGidMapper translates local uid/gid to filer uid/gid
|
||||
// The local storage always persists the same as the filer.
|
||||
// The local->filer translation happens when updating the filer first and later saving to meta_cache.
|
||||
// And filer->local happens when reading from the meta_cache.
|
||||
func NewUidGidMapper(uidPairsStr, gidPairStr string) (*UidGidMapper, error) {
|
||||
uidMapper, err := newIdMapper(uidPairsStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gidMapper, err := newIdMapper(gidPairStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &UidGidMapper{
|
||||
uidMapper: uidMapper,
|
||||
gidMapper: gidMapper,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *UidGidMapper) LocalToFiler(uid, gid uint32) (uint32, uint32) {
|
||||
return m.uidMapper.LocalToFiler(uid), m.gidMapper.LocalToFiler(gid)
|
||||
}
|
||||
func (m *UidGidMapper) FilerToLocal(uid, gid uint32) (uint32, uint32) {
|
||||
return m.uidMapper.FilerToLocal(uid), m.gidMapper.FilerToLocal(gid)
|
||||
}
|
||||
|
||||
func (m *IdMapper) LocalToFiler(id uint32) uint32 {
|
||||
value, found := m.localToFiler[id]
|
||||
if found {
|
||||
return value
|
||||
}
|
||||
return id
|
||||
}
|
||||
func (m *IdMapper) FilerToLocal(id uint32) uint32 {
|
||||
value, found := m.filerToLocal[id]
|
||||
if found {
|
||||
return value
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func newIdMapper(pairsStr string) (*IdMapper, error) {
|
||||
|
||||
localToFiler, filerToLocal, err := parseUint32Pairs(pairsStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &IdMapper{
|
||||
localToFiler: localToFiler,
|
||||
filerToLocal: filerToLocal,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func parseUint32Pairs(pairsStr string) (localToFiler, filerToLocal map[uint32]uint32, err error) {
|
||||
|
||||
if pairsStr == "" {
|
||||
return
|
||||
}
|
||||
|
||||
localToFiler = make(map[uint32]uint32)
|
||||
filerToLocal = make(map[uint32]uint32)
|
||||
for _, pairStr := range strings.Split(pairsStr, ",") {
|
||||
pair := strings.Split(pairStr, ":")
|
||||
localUidStr, filerUidStr := pair[0], pair[1]
|
||||
localUid, localUidErr := strconv.Atoi(localUidStr)
|
||||
if localUidErr != nil {
|
||||
err = fmt.Errorf("failed to parse local %s: %v", localUidStr, localUidErr)
|
||||
return
|
||||
}
|
||||
filerUid, filerUidErr := strconv.Atoi(filerUidStr)
|
||||
if filerUidErr != nil {
|
||||
err = fmt.Errorf("failed to parse remote %s: %v", filerUidStr, filerUidErr)
|
||||
return
|
||||
}
|
||||
localToFiler[uint32(localUid)] = uint32(filerUid)
|
||||
filerToLocal[uint32(filerUid)] = uint32(localUid)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
1341
weed/mount/meta_cache/meta_cache.go
Normal file
1341
weed/mount/meta_cache/meta_cache.go
Normal file
File diff suppressed because it is too large
Load Diff
931
weed/mount/meta_cache/meta_cache_apply_test.go
Normal file
931
weed/mount/meta_cache/meta_cache_apply_test.go
Normal file
@@ -0,0 +1,931 @@
|
||||
package meta_cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/filer"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
)
|
||||
|
||||
func TestApplyMetadataResponseAppliesEventsInOrder(t *testing.T) {
|
||||
mc, _, notifications, invalidations := newTestMetaCache(t, map[util.FullPath]bool{
|
||||
"/": true,
|
||||
"/dir": true,
|
||||
})
|
||||
defer mc.Shutdown()
|
||||
|
||||
createResp := &filer_pb.SubscribeMetadataResponse{
|
||||
Directory: "/dir",
|
||||
EventNotification: &filer_pb.EventNotification{
|
||||
NewEntry: &filer_pb.Entry{
|
||||
Name: "file.txt",
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
Crtime: 1,
|
||||
Mtime: 1,
|
||||
FileMode: 0100644,
|
||||
FileSize: 11,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
updateResp := &filer_pb.SubscribeMetadataResponse{
|
||||
Directory: "/dir",
|
||||
EventNotification: &filer_pb.EventNotification{
|
||||
OldEntry: &filer_pb.Entry{
|
||||
Name: "file.txt",
|
||||
},
|
||||
NewEntry: &filer_pb.Entry{
|
||||
Name: "file.txt",
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
Crtime: 1,
|
||||
Mtime: 2,
|
||||
FileMode: 0100644,
|
||||
FileSize: 29,
|
||||
},
|
||||
},
|
||||
NewParentPath: "/dir",
|
||||
},
|
||||
}
|
||||
deleteResp := &filer_pb.SubscribeMetadataResponse{
|
||||
Directory: "/dir",
|
||||
EventNotification: &filer_pb.EventNotification{
|
||||
OldEntry: &filer_pb.Entry{
|
||||
Name: "file.txt",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := mc.ApplyMetadataResponse(context.Background(), createResp, SubscriberMetadataResponseApplyOptions); err != nil {
|
||||
t.Fatalf("apply create: %v", err)
|
||||
}
|
||||
|
||||
entry, _, err := mc.FindEntry(context.Background(), util.FullPath("/dir/file.txt"))
|
||||
if err != nil {
|
||||
t.Fatalf("find created entry: %v", err)
|
||||
}
|
||||
if entry.FileSize != 11 {
|
||||
t.Fatalf("created file size = %d, want 11", entry.FileSize)
|
||||
}
|
||||
|
||||
if err := mc.ApplyMetadataResponse(context.Background(), updateResp, SubscriberMetadataResponseApplyOptions); err != nil {
|
||||
t.Fatalf("apply update: %v", err)
|
||||
}
|
||||
|
||||
entry, _, err = mc.FindEntry(context.Background(), util.FullPath("/dir/file.txt"))
|
||||
if err != nil {
|
||||
t.Fatalf("find updated entry: %v", err)
|
||||
}
|
||||
if entry.FileSize != 29 {
|
||||
t.Fatalf("updated file size = %d, want 29", entry.FileSize)
|
||||
}
|
||||
|
||||
if err := mc.ApplyMetadataResponse(context.Background(), deleteResp, SubscriberMetadataResponseApplyOptions); err != nil {
|
||||
t.Fatalf("apply delete: %v", err)
|
||||
}
|
||||
|
||||
entry, _, err = mc.FindEntry(context.Background(), util.FullPath("/dir/file.txt"))
|
||||
if err != filer_pb.ErrNotFound {
|
||||
t.Fatalf("find deleted entry error = %v, want %v", err, filer_pb.ErrNotFound)
|
||||
}
|
||||
if entry != nil {
|
||||
t.Fatalf("deleted entry still cached: %+v", entry)
|
||||
}
|
||||
|
||||
if got := countPath(notifications.paths(), util.FullPath("/dir")); got != 3 {
|
||||
t.Fatalf("directory notifications for /dir = %d, want 3", got)
|
||||
}
|
||||
mc.WaitForEntryInvalidations()
|
||||
if got := countPath(invalidations.paths(), util.FullPath("/dir/file.txt")); got != 3 {
|
||||
t.Fatalf("invalidations for /dir/file.txt = %d, want 3 (create + update + delete)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyMetadataResponseRenamesAcrossCachedDirectories(t *testing.T) {
|
||||
mc, _, notifications, invalidations := newTestMetaCache(t, map[util.FullPath]bool{
|
||||
"/": true,
|
||||
"/src": true,
|
||||
"/dst": true,
|
||||
})
|
||||
defer mc.Shutdown()
|
||||
|
||||
if err := mc.InsertEntry(context.Background(), &filer.Entry{
|
||||
FullPath: "/src/file.tmp",
|
||||
Attr: filer.Attr{
|
||||
Crtime: time.Unix(1, 0),
|
||||
Mtime: time.Unix(1, 0),
|
||||
Mode: 0100644,
|
||||
FileSize: 7,
|
||||
},
|
||||
}, 0); err != nil {
|
||||
t.Fatalf("insert source entry: %v", err)
|
||||
}
|
||||
|
||||
renameResp := &filer_pb.SubscribeMetadataResponse{
|
||||
Directory: "/src",
|
||||
EventNotification: &filer_pb.EventNotification{
|
||||
OldEntry: &filer_pb.Entry{
|
||||
Name: "file.tmp",
|
||||
},
|
||||
NewEntry: &filer_pb.Entry{
|
||||
Name: "file.txt",
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
Crtime: 1,
|
||||
Mtime: 2,
|
||||
FileMode: 0100644,
|
||||
FileSize: 41,
|
||||
},
|
||||
},
|
||||
NewParentPath: "/dst",
|
||||
},
|
||||
}
|
||||
|
||||
if err := mc.ApplyMetadataResponse(context.Background(), renameResp, SubscriberMetadataResponseApplyOptions); err != nil {
|
||||
t.Fatalf("apply rename: %v", err)
|
||||
}
|
||||
|
||||
oldEntry, _, err := mc.FindEntry(context.Background(), util.FullPath("/src/file.tmp"))
|
||||
if err != filer_pb.ErrNotFound {
|
||||
t.Fatalf("find old path error = %v, want %v", err, filer_pb.ErrNotFound)
|
||||
}
|
||||
if oldEntry != nil {
|
||||
t.Fatalf("old path still cached: %+v", oldEntry)
|
||||
}
|
||||
|
||||
newEntry, _, err := mc.FindEntry(context.Background(), util.FullPath("/dst/file.txt"))
|
||||
if err != nil {
|
||||
t.Fatalf("find new path: %v", err)
|
||||
}
|
||||
if newEntry.FileSize != 41 {
|
||||
t.Fatalf("renamed file size = %d, want 41", newEntry.FileSize)
|
||||
}
|
||||
|
||||
if got := countPath(notifications.paths(), util.FullPath("/src")); got != 1 {
|
||||
t.Fatalf("directory notifications for /src = %d, want 1", got)
|
||||
}
|
||||
if got := countPath(notifications.paths(), util.FullPath("/dst")); got != 1 {
|
||||
t.Fatalf("directory notifications for /dst = %d, want 1", got)
|
||||
}
|
||||
mc.WaitForEntryInvalidations()
|
||||
if got := countPath(invalidations.paths(), util.FullPath("/src/file.tmp")); got != 1 {
|
||||
t.Fatalf("invalidations for /src/file.tmp = %d, want 1", got)
|
||||
}
|
||||
if got := countPath(invalidations.paths(), util.FullPath("/dst/file.txt")); got != 1 {
|
||||
t.Fatalf("invalidations for /dst/file.txt = %d, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyMetadataResponseLocalOptionsSkipInvalidations(t *testing.T) {
|
||||
mc, _, notifications, invalidations := newTestMetaCache(t, map[util.FullPath]bool{
|
||||
"/": true,
|
||||
"/dir": true,
|
||||
})
|
||||
defer mc.Shutdown()
|
||||
|
||||
if err := mc.InsertEntry(context.Background(), &filer.Entry{
|
||||
FullPath: "/dir/file.txt",
|
||||
Attr: filer.Attr{
|
||||
Crtime: time.Unix(1, 0),
|
||||
Mtime: time.Unix(1, 0),
|
||||
Mode: 0100644,
|
||||
FileSize: 7,
|
||||
},
|
||||
}, 0); err != nil {
|
||||
t.Fatalf("insert source entry: %v", err)
|
||||
}
|
||||
|
||||
updateResp := &filer_pb.SubscribeMetadataResponse{
|
||||
Directory: "/dir",
|
||||
EventNotification: &filer_pb.EventNotification{
|
||||
OldEntry: &filer_pb.Entry{
|
||||
Name: "file.txt",
|
||||
},
|
||||
NewEntry: &filer_pb.Entry{
|
||||
Name: "file.txt",
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
Crtime: 1,
|
||||
Mtime: 2,
|
||||
FileMode: 0100644,
|
||||
FileSize: 17,
|
||||
},
|
||||
},
|
||||
NewParentPath: "/dir",
|
||||
},
|
||||
}
|
||||
|
||||
if err := mc.ApplyMetadataResponse(context.Background(), updateResp, LocalMetadataResponseApplyOptions); err != nil {
|
||||
t.Fatalf("apply local update: %v", err)
|
||||
}
|
||||
|
||||
entry, _, err := mc.FindEntry(context.Background(), util.FullPath("/dir/file.txt"))
|
||||
if err != nil {
|
||||
t.Fatalf("find updated entry: %v", err)
|
||||
}
|
||||
if entry.FileSize != 17 {
|
||||
t.Fatalf("updated file size = %d, want 17", entry.FileSize)
|
||||
}
|
||||
if got := countPath(notifications.paths(), util.FullPath("/dir")); got != 1 {
|
||||
t.Fatalf("directory notifications for /dir = %d, want 1", got)
|
||||
}
|
||||
mc.WaitForEntryInvalidations()
|
||||
if got := len(invalidations.paths()); got != 0 {
|
||||
t.Fatalf("invalidations = %d, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyMetadataResponseDeduplicatesRepeatedFilerEvent(t *testing.T) {
|
||||
mc, _, notifications, invalidations := newTestMetaCache(t, map[util.FullPath]bool{
|
||||
"/": true,
|
||||
"/dir": true,
|
||||
})
|
||||
defer mc.Shutdown()
|
||||
|
||||
if err := mc.InsertEntry(context.Background(), &filer.Entry{
|
||||
FullPath: "/dir/file.txt",
|
||||
Attr: filer.Attr{
|
||||
Crtime: time.Unix(1, 0),
|
||||
Mtime: time.Unix(1, 0),
|
||||
Mode: 0100644,
|
||||
FileSize: 5,
|
||||
},
|
||||
}, 0); err != nil {
|
||||
t.Fatalf("insert source entry: %v", err)
|
||||
}
|
||||
|
||||
updateResp := &filer_pb.SubscribeMetadataResponse{
|
||||
Directory: "/dir",
|
||||
EventNotification: &filer_pb.EventNotification{
|
||||
OldEntry: &filer_pb.Entry{
|
||||
Name: "file.txt",
|
||||
},
|
||||
NewEntry: &filer_pb.Entry{
|
||||
Name: "file.txt",
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
Crtime: 1,
|
||||
Mtime: 2,
|
||||
FileMode: 0100644,
|
||||
FileSize: 15,
|
||||
},
|
||||
},
|
||||
NewParentPath: "/dir",
|
||||
Signatures: []int32{7},
|
||||
},
|
||||
TsNs: 99,
|
||||
}
|
||||
|
||||
if err := mc.ApplyMetadataResponse(context.Background(), updateResp, SubscriberMetadataResponseApplyOptions); err != nil {
|
||||
t.Fatalf("first apply: %v", err)
|
||||
}
|
||||
if err := mc.ApplyMetadataResponse(context.Background(), updateResp, SubscriberMetadataResponseApplyOptions); err != nil {
|
||||
t.Fatalf("second apply: %v", err)
|
||||
}
|
||||
|
||||
entry, _, err := mc.FindEntry(context.Background(), util.FullPath("/dir/file.txt"))
|
||||
if err != nil {
|
||||
t.Fatalf("find updated entry: %v", err)
|
||||
}
|
||||
if entry.FileSize != 15 {
|
||||
t.Fatalf("updated file size = %d, want 15", entry.FileSize)
|
||||
}
|
||||
if got := countPath(notifications.paths(), util.FullPath("/dir")); got != 1 {
|
||||
t.Fatalf("directory notifications for /dir = %d, want 1", got)
|
||||
}
|
||||
mc.WaitForEntryInvalidations()
|
||||
if got := countPath(invalidations.paths(), util.FullPath("/dir/file.txt")); got != 1 {
|
||||
t.Fatalf("invalidations for /dir/file.txt = %d, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyMetadataResponseSkipsHiddenSystemEntryWhenDisabled(t *testing.T) {
|
||||
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{
|
||||
"/": true,
|
||||
})
|
||||
defer mc.Shutdown()
|
||||
|
||||
createResp := &filer_pb.SubscribeMetadataResponse{
|
||||
Directory: "/",
|
||||
EventNotification: &filer_pb.EventNotification{
|
||||
NewEntry: &filer_pb.Entry{
|
||||
Name: "topics",
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
Crtime: 1,
|
||||
Mtime: 1,
|
||||
FileMode: uint32(os.ModeDir | 0o755),
|
||||
},
|
||||
IsDirectory: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := mc.ApplyMetadataResponse(context.Background(), createResp, SubscriberMetadataResponseApplyOptions); err != nil {
|
||||
t.Fatalf("apply create: %v", err)
|
||||
}
|
||||
|
||||
entry, _, err := mc.FindEntry(context.Background(), util.FullPath("/topics"))
|
||||
if err != filer_pb.ErrNotFound {
|
||||
t.Fatalf("find hidden entry error = %v, want %v", err, filer_pb.ErrNotFound)
|
||||
}
|
||||
if entry != nil {
|
||||
t.Fatalf("hidden entry still cached: %+v", entry)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyMetadataResponsePurgesHiddenDestinationPath(t *testing.T) {
|
||||
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{
|
||||
"/": true,
|
||||
"/src": true,
|
||||
})
|
||||
defer mc.Shutdown()
|
||||
|
||||
if err := mc.InsertEntry(context.Background(), &filer.Entry{
|
||||
FullPath: "/topics",
|
||||
Attr: filer.Attr{
|
||||
Crtime: time.Unix(1, 0),
|
||||
Mtime: time.Unix(1, 0),
|
||||
Mode: os.ModeDir | 0o755,
|
||||
},
|
||||
}, 0); err != nil {
|
||||
t.Fatalf("insert stale hidden dir: %v", err)
|
||||
}
|
||||
if err := mc.InsertEntry(context.Background(), &filer.Entry{
|
||||
FullPath: "/topics/leaked.txt",
|
||||
Attr: filer.Attr{
|
||||
Crtime: time.Unix(1, 0),
|
||||
Mtime: time.Unix(1, 0),
|
||||
Mode: 0o644,
|
||||
FileSize: 7,
|
||||
},
|
||||
}, 0); err != nil {
|
||||
t.Fatalf("insert leaked hidden child: %v", err)
|
||||
}
|
||||
if err := mc.InsertEntry(context.Background(), &filer.Entry{
|
||||
FullPath: "/src/visible",
|
||||
Attr: filer.Attr{
|
||||
Crtime: time.Unix(1, 0),
|
||||
Mtime: time.Unix(1, 0),
|
||||
Mode: os.ModeDir | 0o755,
|
||||
},
|
||||
}, 0); err != nil {
|
||||
t.Fatalf("insert source dir: %v", err)
|
||||
}
|
||||
|
||||
renameResp := &filer_pb.SubscribeMetadataResponse{
|
||||
Directory: "/src",
|
||||
EventNotification: &filer_pb.EventNotification{
|
||||
OldEntry: &filer_pb.Entry{
|
||||
Name: "visible",
|
||||
IsDirectory: true,
|
||||
},
|
||||
NewEntry: &filer_pb.Entry{
|
||||
Name: "topics",
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
Crtime: 2,
|
||||
Mtime: 2,
|
||||
FileMode: uint32(os.ModeDir | 0o755),
|
||||
},
|
||||
IsDirectory: true,
|
||||
},
|
||||
NewParentPath: "/",
|
||||
},
|
||||
}
|
||||
|
||||
if err := mc.ApplyMetadataResponse(context.Background(), renameResp, SubscriberMetadataResponseApplyOptions); err != nil {
|
||||
t.Fatalf("apply rename: %v", err)
|
||||
}
|
||||
|
||||
if entry, _, err := mc.FindEntry(context.Background(), util.FullPath("/src/visible")); err != filer_pb.ErrNotFound || entry != nil {
|
||||
t.Fatalf("source dir after rename = %+v, %v; want nil, %v", entry, err, filer_pb.ErrNotFound)
|
||||
}
|
||||
if entry, _, err := mc.FindEntry(context.Background(), util.FullPath("/topics")); err != filer_pb.ErrNotFound || entry != nil {
|
||||
t.Fatalf("hidden destination after rename = %+v, %v; want nil, %v", entry, err, filer_pb.ErrNotFound)
|
||||
}
|
||||
if entry, _, err := mc.FindEntry(context.Background(), util.FullPath("/topics/leaked.txt")); err != filer_pb.ErrNotFound || entry != nil {
|
||||
t.Fatalf("hidden child after rename = %+v, %v; want nil, %v", entry, err, filer_pb.ErrNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
// The entry attached to each invalidation is what an open file handle gets
|
||||
// refreshed with, so it must be the entry now at that path — or nil when the
|
||||
// path was vacated and the handle should keep its last entry — versioned by
|
||||
// the event's filer log timestamp.
|
||||
func TestCollectEntryInvalidationsCarryAuthoritativeEntries(t *testing.T) {
|
||||
newEntry := &filer_pb.Entry{
|
||||
Name: "file.txt",
|
||||
Attributes: &filer_pb.FuseAttributes{FileSize: 42},
|
||||
}
|
||||
|
||||
update := &filer_pb.SubscribeMetadataResponse{
|
||||
Directory: "/dir",
|
||||
TsNs: 77,
|
||||
EventNotification: &filer_pb.EventNotification{
|
||||
OldEntry: &filer_pb.Entry{Name: "file.txt"},
|
||||
NewEntry: newEntry,
|
||||
NewParentPath: "/dir",
|
||||
},
|
||||
}
|
||||
got := collectEntryInvalidations(update)
|
||||
if len(got) != 1 || got[0].Path != "/dir/file.txt" || got[0].Entry != newEntry || got[0].TsNs != 77 {
|
||||
t.Fatalf("in-place update invalidations = %+v, want [{/dir/file.txt NewEntry ts 77}]", got)
|
||||
}
|
||||
|
||||
rename := &filer_pb.SubscribeMetadataResponse{
|
||||
Directory: "/src",
|
||||
TsNs: 78,
|
||||
EventNotification: &filer_pb.EventNotification{
|
||||
OldEntry: &filer_pb.Entry{Name: "file.tmp"},
|
||||
NewEntry: newEntry,
|
||||
NewParentPath: "/dst",
|
||||
},
|
||||
}
|
||||
got = collectEntryInvalidations(rename)
|
||||
if len(got) != 2 || got[0].Path != "/src/file.tmp" || got[0].Entry != nil || got[0].TsNs != 78 ||
|
||||
got[1].Path != "/dst/file.txt" || got[1].Entry != newEntry || got[1].TsNs != 78 {
|
||||
t.Fatalf("rename invalidations = %+v, want [{/src/file.tmp nil} {/dst/file.txt NewEntry}]", got)
|
||||
}
|
||||
|
||||
create := &filer_pb.SubscribeMetadataResponse{
|
||||
Directory: "/dir",
|
||||
TsNs: 79,
|
||||
EventNotification: &filer_pb.EventNotification{
|
||||
NewEntry: newEntry,
|
||||
},
|
||||
}
|
||||
got = collectEntryInvalidations(create)
|
||||
if len(got) != 1 || got[0].Path != "/dir/file.txt" || got[0].Entry != newEntry || got[0].TsNs != 79 {
|
||||
t.Fatalf("create invalidations = %+v, want [{/dir/file.txt NewEntry ts 79}]", got)
|
||||
}
|
||||
|
||||
deleteResp := &filer_pb.SubscribeMetadataResponse{
|
||||
Directory: "/dir",
|
||||
TsNs: 80,
|
||||
EventNotification: &filer_pb.EventNotification{
|
||||
OldEntry: &filer_pb.Entry{Name: "file.txt"},
|
||||
},
|
||||
}
|
||||
got = collectEntryInvalidations(deleteResp)
|
||||
if len(got) != 1 || got[0].Path != "/dir/file.txt" || got[0].Entry != nil || got[0].TsNs != 80 {
|
||||
t.Fatalf("delete invalidations = %+v, want [{/dir/file.txt nil ts 80}]", got)
|
||||
}
|
||||
}
|
||||
|
||||
func newTestMetaCache(t *testing.T, cached map[util.FullPath]bool) (*MetaCache, map[util.FullPath]bool, *recordedPaths, *recordedPaths) {
|
||||
t.Helper()
|
||||
|
||||
mapper, err := NewUidGidMapper("", "")
|
||||
if err != nil {
|
||||
t.Fatalf("uid/gid mapper: %v", err)
|
||||
}
|
||||
|
||||
var cachedMu sync.Mutex
|
||||
notifications := &recordedPaths{}
|
||||
invalidations := &recordedPaths{}
|
||||
|
||||
mc := NewMetaCache(
|
||||
filepath.Join(t.TempDir(), "meta"),
|
||||
mapper,
|
||||
util.FullPath("/"),
|
||||
false,
|
||||
func(path util.FullPath) {
|
||||
cachedMu.Lock()
|
||||
defer cachedMu.Unlock()
|
||||
cached[path] = true
|
||||
},
|
||||
func(path util.FullPath) bool {
|
||||
cachedMu.Lock()
|
||||
defer cachedMu.Unlock()
|
||||
return cached[path]
|
||||
},
|
||||
func(inv EntryInvalidation) {
|
||||
invalidations.record(inv.Path)
|
||||
},
|
||||
func(dir util.FullPath) {
|
||||
notifications.record(dir)
|
||||
},
|
||||
)
|
||||
|
||||
return mc, cached, notifications, invalidations
|
||||
}
|
||||
|
||||
type recordedPaths struct {
|
||||
mu sync.Mutex
|
||||
items []util.FullPath
|
||||
}
|
||||
|
||||
func (r *recordedPaths) record(path util.FullPath) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.items = append(r.items, path)
|
||||
}
|
||||
|
||||
func (r *recordedPaths) paths() []util.FullPath {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return append([]util.FullPath(nil), r.items...)
|
||||
}
|
||||
|
||||
func countPath(paths []util.FullPath, target util.FullPath) int {
|
||||
count := 0
|
||||
for _, path := range paths {
|
||||
if path == target {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// An unversioned local write replaces the content, so the previous version
|
||||
// claim no longer describes it and must be cleared — keeping it would fence
|
||||
// out events correcting the unversioned state.
|
||||
func TestUnversionedWriteClearsEntryVersion(t *testing.T) {
|
||||
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{
|
||||
"/": true,
|
||||
"/dir": true,
|
||||
})
|
||||
defer mc.Shutdown()
|
||||
|
||||
versioned := &filer_pb.SubscribeMetadataResponse{
|
||||
Directory: "/dir",
|
||||
TsNs: 1000,
|
||||
EventNotification: &filer_pb.EventNotification{
|
||||
NewEntry: &filer_pb.Entry{
|
||||
Name: "file.txt",
|
||||
Attributes: &filer_pb.FuseAttributes{FileSize: 11, FileMode: 0100644},
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := mc.ApplyMetadataResponse(context.Background(), versioned, SubscriberMetadataResponseApplyOptions); err != nil {
|
||||
t.Fatalf("apply versioned event: %v", err)
|
||||
}
|
||||
if _, versionTsNs, err := mc.FindEntry(context.Background(), util.FullPath("/dir/file.txt")); err != nil || versionTsNs != 1000 {
|
||||
t.Fatalf("versioned entry = ts %d, %v; want 1000", versionTsNs, err)
|
||||
}
|
||||
|
||||
if err := mc.UpdateEntry(context.Background(), &filer.Entry{
|
||||
FullPath: "/dir/file.txt",
|
||||
Attr: filer.Attr{
|
||||
Crtime: time.Unix(1, 0),
|
||||
Mtime: time.Unix(2, 0),
|
||||
Mode: 0100644,
|
||||
FileSize: 22,
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("local update: %v", err)
|
||||
}
|
||||
if _, versionTsNs, err := mc.FindEntry(context.Background(), util.FullPath("/dir/file.txt")); err != nil || versionTsNs != 0 {
|
||||
t.Fatalf("after unversioned write = ts %d, %v; want 0", versionTsNs, err)
|
||||
}
|
||||
mc.WaitForEntryInvalidations()
|
||||
}
|
||||
|
||||
// A rebuild against a pre-upgrade filer returns no snapshot; the reinserted
|
||||
// entries must not inherit the version records their pre-rebuild
|
||||
// incarnations left behind, or valid events below the stale claim are
|
||||
// rejected.
|
||||
func TestUnversionedRebuildClearsStaleVersions(t *testing.T) {
|
||||
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{
|
||||
"/": true,
|
||||
"/dir": true,
|
||||
})
|
||||
defer mc.Shutdown()
|
||||
|
||||
versioned := &filer_pb.SubscribeMetadataResponse{
|
||||
Directory: "/dir",
|
||||
TsNs: 3000,
|
||||
EventNotification: &filer_pb.EventNotification{
|
||||
NewEntry: &filer_pb.Entry{
|
||||
Name: "file.txt",
|
||||
Attributes: &filer_pb.FuseAttributes{FileSize: 11, FileMode: 0100644},
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := mc.ApplyMetadataResponse(context.Background(), versioned, SubscriberMetadataResponseApplyOptions); err != nil {
|
||||
t.Fatalf("apply versioned event: %v", err)
|
||||
}
|
||||
|
||||
if err := mc.BeginDirectoryBuild(context.Background(), util.FullPath("/dir")); err != nil {
|
||||
t.Fatalf("begin build: %v", err)
|
||||
}
|
||||
if err := mc.doBatchInsertEntries(context.Background(), []*filer.Entry{{
|
||||
FullPath: "/dir/file.txt",
|
||||
Attr: filer.Attr{
|
||||
Crtime: time.Unix(1, 0),
|
||||
Mtime: time.Unix(1, 0),
|
||||
Mode: 0100644,
|
||||
FileSize: 22,
|
||||
},
|
||||
}}); err != nil {
|
||||
t.Fatalf("batch insert: %v", err)
|
||||
}
|
||||
if err := mc.CompleteDirectoryBuild(context.Background(), util.FullPath("/dir"), 0, nil); err != nil {
|
||||
t.Fatalf("complete unversioned build: %v", err)
|
||||
}
|
||||
|
||||
if _, versionTsNs, err := mc.FindEntry(context.Background(), util.FullPath("/dir/file.txt")); err != nil || versionTsNs != 0 {
|
||||
t.Fatalf("rebuilt entry version = %d, %v; want 0 (stale claim must not survive an unversioned rebuild)", versionTsNs, err)
|
||||
}
|
||||
mc.WaitForEntryInvalidations()
|
||||
}
|
||||
|
||||
// Tombstones are scoped to directories whose cached state the fence
|
||||
// protects; an uncached parent never serves from the store nor applies the
|
||||
// resurrecting insert, so a tombstone there would only accumulate.
|
||||
func TestTombstonesScopedToCachedDirectories(t *testing.T) {
|
||||
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{
|
||||
"/": true,
|
||||
"/cached": true,
|
||||
})
|
||||
defer mc.Shutdown()
|
||||
|
||||
deleteEventFor := func(dir, name string, tsNs int64) *filer_pb.SubscribeMetadataResponse {
|
||||
return &filer_pb.SubscribeMetadataResponse{
|
||||
Directory: dir,
|
||||
TsNs: tsNs,
|
||||
EventNotification: &filer_pb.EventNotification{
|
||||
OldEntry: &filer_pb.Entry{Name: name},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if err := mc.ApplyMetadataResponse(context.Background(), deleteEventFor("/cached", "file", 2000), SubscriberMetadataResponseApplyOptions); err != nil {
|
||||
t.Fatalf("apply cached delete: %v", err)
|
||||
}
|
||||
if tsNs, tombstone := mc.getEntryVersionRecordLocked(context.Background(), util.FullPath("/cached/file")); tsNs != 2000 || !tombstone {
|
||||
t.Fatalf("cached-dir delete record = (%d, %v), want tombstone at 2000", tsNs, tombstone)
|
||||
}
|
||||
|
||||
if err := mc.ApplyMetadataResponse(context.Background(), deleteEventFor("/uncached", "file", 2000), SubscriberMetadataResponseApplyOptions); err != nil {
|
||||
t.Fatalf("apply uncached delete: %v", err)
|
||||
}
|
||||
if tsNs, _ := mc.getEntryVersionRecordLocked(context.Background(), util.FullPath("/uncached/file")); tsNs != 0 {
|
||||
t.Fatalf("uncached-dir delete record = %d, want none", tsNs)
|
||||
}
|
||||
mc.WaitForEntryInvalidations()
|
||||
}
|
||||
|
||||
// A completed listing's absence floor supersedes the direct-child tombstones
|
||||
// at or below its snapshot; newer tombstones survive.
|
||||
func TestBuildCompletionPrunesSupersededTombstones(t *testing.T) {
|
||||
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{
|
||||
"/": true,
|
||||
"/dir": true,
|
||||
})
|
||||
defer mc.Shutdown()
|
||||
|
||||
apply := func(resp *filer_pb.SubscribeMetadataResponse) {
|
||||
t.Helper()
|
||||
if err := mc.ApplyMetadataResponse(context.Background(), resp, SubscriberMetadataResponseApplyOptions); err != nil {
|
||||
t.Fatalf("apply: %v", err)
|
||||
}
|
||||
}
|
||||
createEvent := func(name string, tsNs int64) *filer_pb.SubscribeMetadataResponse {
|
||||
return &filer_pb.SubscribeMetadataResponse{
|
||||
Directory: "/dir",
|
||||
TsNs: tsNs,
|
||||
EventNotification: &filer_pb.EventNotification{
|
||||
NewEntry: &filer_pb.Entry{
|
||||
Name: name,
|
||||
Attributes: &filer_pb.FuseAttributes{FileSize: 1, FileMode: 0100644},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
deleteEvent := func(name string, tsNs int64) *filer_pb.SubscribeMetadataResponse {
|
||||
return &filer_pb.SubscribeMetadataResponse{
|
||||
Directory: "/dir",
|
||||
TsNs: tsNs,
|
||||
EventNotification: &filer_pb.EventNotification{
|
||||
OldEntry: &filer_pb.Entry{Name: name},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
apply(createEvent("a", 900))
|
||||
apply(deleteEvent("a", 1000))
|
||||
apply(createEvent("b", 3900))
|
||||
apply(deleteEvent("b", 4000))
|
||||
|
||||
// A deeper descendant's tombstone belongs to its own directory's floor.
|
||||
mc.Lock()
|
||||
mc.setEntryTombstoneLocked(context.Background(), util.FullPath("/dir/sub/x"), 1000)
|
||||
mc.Unlock()
|
||||
|
||||
if err := mc.BeginDirectoryBuild(context.Background(), util.FullPath("/dir")); err != nil {
|
||||
t.Fatalf("begin build: %v", err)
|
||||
}
|
||||
if err := mc.CompleteDirectoryBuild(context.Background(), util.FullPath("/dir"), 3000, nil); err != nil {
|
||||
t.Fatalf("complete build: %v", err)
|
||||
}
|
||||
|
||||
if tsNs, _ := mc.getEntryVersionRecordLocked(context.Background(), util.FullPath("/dir/a")); tsNs != 0 {
|
||||
t.Fatalf("tombstone at 1000 should be pruned by the floor at 3000, got %d", tsNs)
|
||||
}
|
||||
if tsNs, tombstone := mc.getEntryVersionRecordLocked(context.Background(), util.FullPath("/dir/b")); tsNs != 4000 || !tombstone {
|
||||
t.Fatalf("tombstone at 4000 must survive the floor at 3000, got (%d, %v)", tsNs, tombstone)
|
||||
}
|
||||
if tsNs, tombstone := mc.getEntryVersionRecordLocked(context.Background(), util.FullPath("/dir/sub/x")); tsNs != 1000 || !tombstone {
|
||||
t.Fatalf("descendant tombstone must survive the parent's prune, got (%d, %v)", tsNs, tombstone)
|
||||
}
|
||||
mc.WaitForEntryInvalidations()
|
||||
}
|
||||
|
||||
// Evicting a directory clears its children's version records (plain and
|
||||
// tombstone) so they cannot accumulate for the lifetime of a long-running
|
||||
// mount.
|
||||
func TestDirectoryEvictionClearsVersionRecords(t *testing.T) {
|
||||
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{
|
||||
"/": true,
|
||||
"/dir": true,
|
||||
})
|
||||
defer mc.Shutdown()
|
||||
|
||||
create := &filer_pb.SubscribeMetadataResponse{
|
||||
Directory: "/dir",
|
||||
TsNs: 1000,
|
||||
EventNotification: &filer_pb.EventNotification{
|
||||
NewEntry: &filer_pb.Entry{Name: "keep", Attributes: &filer_pb.FuseAttributes{FileSize: 1, FileMode: 0100644}},
|
||||
},
|
||||
}
|
||||
if err := mc.ApplyMetadataResponse(context.Background(), create, SubscriberMetadataResponseApplyOptions); err != nil {
|
||||
t.Fatalf("apply create: %v", err)
|
||||
}
|
||||
del := &filer_pb.SubscribeMetadataResponse{
|
||||
Directory: "/dir",
|
||||
TsNs: 1100,
|
||||
EventNotification: &filer_pb.EventNotification{OldEntry: &filer_pb.Entry{Name: "gone"}},
|
||||
}
|
||||
if err := mc.ApplyMetadataResponse(context.Background(), del, SubscriberMetadataResponseApplyOptions); err != nil {
|
||||
t.Fatalf("apply delete: %v", err)
|
||||
}
|
||||
|
||||
if tsNs, _ := mc.getEntryVersionRecordLocked(context.Background(), util.FullPath("/dir/keep")); tsNs != 1000 {
|
||||
t.Fatalf("version record for /dir/keep = %d, want 1000 before eviction", tsNs)
|
||||
}
|
||||
if tsNs, tombstone := mc.getEntryVersionRecordLocked(context.Background(), util.FullPath("/dir/gone")); tsNs != 1100 || !tombstone {
|
||||
t.Fatalf("tombstone for /dir/gone = (%d,%v), want (1100,true) before eviction", tsNs, tombstone)
|
||||
}
|
||||
|
||||
if err := mc.DeleteFolderChildren(context.Background(), util.FullPath("/dir")); err != nil {
|
||||
t.Fatalf("evict directory: %v", err)
|
||||
}
|
||||
|
||||
if tsNs, _ := mc.getEntryVersionRecordLocked(context.Background(), util.FullPath("/dir/keep")); tsNs != 0 {
|
||||
t.Fatalf("version record for /dir/keep survived eviction: %d", tsNs)
|
||||
}
|
||||
if tsNs, _ := mc.getEntryVersionRecordLocked(context.Background(), util.FullPath("/dir/gone")); tsNs != 0 {
|
||||
t.Fatalf("tombstone for /dir/gone survived eviction: %d", tsNs)
|
||||
}
|
||||
mc.WaitForEntryInvalidations()
|
||||
}
|
||||
|
||||
// A completed build versions its children through one directory floor rather
|
||||
// than a version record per child, and the floor fences exactly as per-child
|
||||
// records did.
|
||||
func TestBuildFloorVersionsChildrenWithoutPerChildRecords(t *testing.T) {
|
||||
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{
|
||||
"/": true,
|
||||
"/dir": true,
|
||||
})
|
||||
defer mc.Shutdown()
|
||||
|
||||
if err := mc.BeginDirectoryBuild(context.Background(), util.FullPath("/dir")); err != nil {
|
||||
t.Fatalf("begin build: %v", err)
|
||||
}
|
||||
if err := mc.doBatchInsertEntries(context.Background(), []*filer.Entry{{
|
||||
FullPath: "/dir/file",
|
||||
Attr: filer.Attr{
|
||||
Crtime: time.Unix(1, 0),
|
||||
Mtime: time.Unix(1, 0),
|
||||
Mode: 0100644,
|
||||
FileSize: 300,
|
||||
},
|
||||
}}); err != nil {
|
||||
t.Fatalf("batch insert: %v", err)
|
||||
}
|
||||
if err := mc.CompleteDirectoryBuild(context.Background(), util.FullPath("/dir"), 2000, nil); err != nil {
|
||||
t.Fatalf("complete build: %v", err)
|
||||
}
|
||||
|
||||
// No per-child record was written...
|
||||
if tsNs, _ := mc.getEntryVersionRecordLocked(context.Background(), util.FullPath("/dir/file")); tsNs != 0 {
|
||||
t.Fatalf("per-child version record = %d, want none (the floor versions the child)", tsNs)
|
||||
}
|
||||
// ...yet the child reads back at the listing snapshot.
|
||||
if _, versionTsNs, err := mc.FindEntry(context.Background(), util.FullPath("/dir/file")); err != nil || versionTsNs != 2000 {
|
||||
t.Fatalf("child version = %d, %v; want 2000 from the directory floor", versionTsNs, err)
|
||||
}
|
||||
|
||||
// And an event the snapshot already covered is still fenced out.
|
||||
covered := &filer_pb.SubscribeMetadataResponse{
|
||||
Directory: "/dir",
|
||||
TsNs: 1500,
|
||||
EventNotification: &filer_pb.EventNotification{
|
||||
OldEntry: &filer_pb.Entry{Name: "file"},
|
||||
NewEntry: &filer_pb.Entry{Name: "file", Attributes: &filer_pb.FuseAttributes{FileSize: 100, FileMode: 0100644}},
|
||||
NewParentPath: "/dir",
|
||||
},
|
||||
}
|
||||
if err := mc.ApplyMetadataResponse(context.Background(), covered, SubscriberMetadataResponseApplyOptions); err != nil {
|
||||
t.Fatalf("apply covered event: %v", err)
|
||||
}
|
||||
entry, _, err := mc.FindEntry(context.Background(), util.FullPath("/dir/file"))
|
||||
if err != nil || entry.FileSize != 300 {
|
||||
t.Fatalf("entry = %+v, %v; want size 300 (floor must fence the covered event)", entry, err)
|
||||
}
|
||||
mc.WaitForEntryInvalidations()
|
||||
}
|
||||
|
||||
// The presence probe applies the same TTL expiry the read path does, so a
|
||||
// logically-expired entry is judged by its directory's floor rather than by
|
||||
// the stale version record it still carries.
|
||||
func TestExpiredEntryIsJudgedByDirectoryFloor(t *testing.T) {
|
||||
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{
|
||||
"/": true,
|
||||
"/dir": true,
|
||||
})
|
||||
defer mc.Shutdown()
|
||||
|
||||
if err := mc.InsertEntry(context.Background(), &filer.Entry{
|
||||
FullPath: "/dir/file",
|
||||
Attr: filer.Attr{
|
||||
Crtime: time.Now().Add(-2 * time.Hour),
|
||||
Mtime: time.Now().Add(-2 * time.Hour),
|
||||
Mode: 0100644,
|
||||
FileSize: 7,
|
||||
TtlSec: 1,
|
||||
},
|
||||
}, 0); err != nil {
|
||||
t.Fatalf("insert expiring entry: %v", err)
|
||||
}
|
||||
|
||||
mc.Lock()
|
||||
// A record newer than the directory floor: it only governs while the entry
|
||||
// it describes still exists. Once the entry has logically expired the floor
|
||||
// is the accurate answer, and an event above the floor must apply.
|
||||
mc.setEntryVersionLocked(context.Background(), util.FullPath("/dir/file"), 5000)
|
||||
mc.dirVersionFloors[util.FullPath("/dir")] = 3000
|
||||
blocks := mc.entryVersionBlocksLocked(context.Background(), util.FullPath("/dir/file"), 4000, false)
|
||||
mc.Unlock()
|
||||
|
||||
if blocks {
|
||||
t.Fatal("event at 4000 fenced by an expired entry's stale record at 5000; the floor at 3000 should govern")
|
||||
}
|
||||
}
|
||||
|
||||
// An unversioned local write leaves content no log position describes, so it
|
||||
// must not inherit the directory's listing floor — that would fence the very
|
||||
// events needed to correct it.
|
||||
func TestUnversionedWriteDoesNotInheritDirectoryFloor(t *testing.T) {
|
||||
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{
|
||||
"/": true,
|
||||
"/dir": true,
|
||||
})
|
||||
defer mc.Shutdown()
|
||||
|
||||
if err := mc.BeginDirectoryBuild(context.Background(), util.FullPath("/dir")); err != nil {
|
||||
t.Fatalf("begin build: %v", err)
|
||||
}
|
||||
if err := mc.CompleteDirectoryBuild(context.Background(), util.FullPath("/dir"), 2000, nil); err != nil {
|
||||
t.Fatalf("complete build: %v", err)
|
||||
}
|
||||
|
||||
// A local write with no log position behind it.
|
||||
if err := mc.InsertEntry(context.Background(), &filer.Entry{
|
||||
FullPath: "/dir/file",
|
||||
Attr: filer.Attr{
|
||||
Crtime: time.Unix(1, 0),
|
||||
Mtime: time.Unix(1, 0),
|
||||
Mode: 0100644,
|
||||
FileSize: 7,
|
||||
},
|
||||
}, 0); err != nil {
|
||||
t.Fatalf("local insert: %v", err)
|
||||
}
|
||||
|
||||
if _, versionTsNs, err := mc.FindEntry(context.Background(), util.FullPath("/dir/file")); err != nil || versionTsNs != 0 {
|
||||
t.Fatalf("version = %d, %v; want 0 (unversioned content must not inherit the floor at 2000)", versionTsNs, err)
|
||||
}
|
||||
|
||||
// And an event below the floor can still correct it.
|
||||
correcting := &filer_pb.SubscribeMetadataResponse{
|
||||
Directory: "/dir",
|
||||
TsNs: 1500,
|
||||
EventNotification: &filer_pb.EventNotification{
|
||||
OldEntry: &filer_pb.Entry{Name: "file"},
|
||||
NewEntry: &filer_pb.Entry{Name: "file", Attributes: &filer_pb.FuseAttributes{FileSize: 42, FileMode: 0100644}},
|
||||
NewParentPath: "/dir",
|
||||
},
|
||||
}
|
||||
if err := mc.ApplyMetadataResponse(context.Background(), correcting, SubscriberMetadataResponseApplyOptions); err != nil {
|
||||
t.Fatalf("apply correcting event: %v", err)
|
||||
}
|
||||
entry, _, err := mc.FindEntry(context.Background(), util.FullPath("/dir/file"))
|
||||
if err != nil || entry.FileSize != 42 {
|
||||
t.Fatalf("entry = %+v, %v; want size 42 (the floor must not fence a correction of unversioned content)", entry, err)
|
||||
}
|
||||
mc.WaitForEntryInvalidations()
|
||||
}
|
||||
635
weed/mount/meta_cache/meta_cache_build_test.go
Normal file
635
weed/mount/meta_cache/meta_cache_build_test.go
Normal file
@@ -0,0 +1,635 @@
|
||||
package meta_cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/filer"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
type buildListStream struct {
|
||||
responses []*filer_pb.ListEntriesResponse
|
||||
onFirstRecv func()
|
||||
once sync.Once
|
||||
index int
|
||||
}
|
||||
|
||||
func (s *buildListStream) Recv() (*filer_pb.ListEntriesResponse, error) {
|
||||
s.once.Do(func() {
|
||||
if s.onFirstRecv != nil {
|
||||
s.onFirstRecv()
|
||||
}
|
||||
})
|
||||
if s.index >= len(s.responses) {
|
||||
return nil, io.EOF
|
||||
}
|
||||
resp := s.responses[s.index]
|
||||
s.index++
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *buildListStream) Header() (metadata.MD, error) { return metadata.MD{}, nil }
|
||||
func (s *buildListStream) Trailer() metadata.MD { return metadata.MD{} }
|
||||
func (s *buildListStream) CloseSend() error { return nil }
|
||||
func (s *buildListStream) Context() context.Context { return context.Background() }
|
||||
func (s *buildListStream) SendMsg(any) error { return nil }
|
||||
func (s *buildListStream) RecvMsg(any) error { return nil }
|
||||
|
||||
type buildListClient struct {
|
||||
filer_pb.SeaweedFilerClient
|
||||
responses []*filer_pb.ListEntriesResponse
|
||||
onFirstRecv func()
|
||||
}
|
||||
|
||||
func (c *buildListClient) ListEntries(ctx context.Context, in *filer_pb.ListEntriesRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[filer_pb.ListEntriesResponse], error) {
|
||||
return &buildListStream{
|
||||
responses: c.responses,
|
||||
onFirstRecv: c.onFirstRecv,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type buildFilerAccessor struct {
|
||||
client filer_pb.SeaweedFilerClient
|
||||
}
|
||||
|
||||
func (a *buildFilerAccessor) WithFilerClient(_ bool, fn func(filer_pb.SeaweedFilerClient) error) error {
|
||||
return fn(a.client)
|
||||
}
|
||||
|
||||
func (a *buildFilerAccessor) AdjustedUrl(*filer_pb.Location) string { return "" }
|
||||
func (a *buildFilerAccessor) GetDataCenter() string { return "" }
|
||||
|
||||
func TestEnsureVisitedReplaysBufferedEventsAfterSnapshot(t *testing.T) {
|
||||
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{
|
||||
"/": true,
|
||||
})
|
||||
defer mc.Shutdown()
|
||||
|
||||
var applyErr error
|
||||
accessor := &buildFilerAccessor{
|
||||
client: &buildListClient{
|
||||
responses: []*filer_pb.ListEntriesResponse{
|
||||
{
|
||||
Entry: &filer_pb.Entry{
|
||||
Name: "base.txt",
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
Crtime: 1,
|
||||
Mtime: 1,
|
||||
FileMode: 0100644,
|
||||
FileSize: 3,
|
||||
},
|
||||
},
|
||||
SnapshotTsNs: 100,
|
||||
},
|
||||
},
|
||||
onFirstRecv: func() {
|
||||
applyErr = mc.ApplyMetadataResponse(context.Background(), &filer_pb.SubscribeMetadataResponse{
|
||||
Directory: "/dir",
|
||||
EventNotification: &filer_pb.EventNotification{
|
||||
NewEntry: &filer_pb.Entry{
|
||||
Name: "after.txt",
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
Crtime: 2,
|
||||
Mtime: 2,
|
||||
FileMode: 0100644,
|
||||
FileSize: 9,
|
||||
},
|
||||
},
|
||||
},
|
||||
TsNs: 101,
|
||||
}, SubscriberMetadataResponseApplyOptions)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := EnsureVisited(mc, accessor, util.FullPath("/dir"), 0); err != nil {
|
||||
t.Fatalf("ensure visited: %v", err)
|
||||
}
|
||||
if applyErr != nil {
|
||||
t.Fatalf("apply buffered event: %v", applyErr)
|
||||
}
|
||||
if !mc.IsDirectoryCached(util.FullPath("/dir")) {
|
||||
t.Fatal("directory /dir should be cached after build completes")
|
||||
}
|
||||
|
||||
baseEntry, _, err := mc.FindEntry(context.Background(), util.FullPath("/dir/base.txt"))
|
||||
if err != nil {
|
||||
t.Fatalf("find base entry: %v", err)
|
||||
}
|
||||
if baseEntry.FileSize != 3 {
|
||||
t.Fatalf("base entry size = %d, want 3", baseEntry.FileSize)
|
||||
}
|
||||
|
||||
afterEntry, _, err := mc.FindEntry(context.Background(), util.FullPath("/dir/after.txt"))
|
||||
if err != nil {
|
||||
t.Fatalf("find replayed entry: %v", err)
|
||||
}
|
||||
if afterEntry.FileSize != 9 {
|
||||
t.Fatalf("replayed entry size = %d, want 9", afterEntry.FileSize)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDirectoryNotificationsSuppressedDuringBuild verifies that metadata events
|
||||
// targeting a directory under active build do NOT fire onDirectoryUpdate for
|
||||
// that directory. In production, onDirectoryUpdate can trigger
|
||||
// markDirectoryReadThrough → DeleteFolderChildren, which would wipe entries
|
||||
// that EnsureVisited already inserted mid-build.
|
||||
func TestDirectoryNotificationsSuppressedDuringBuild(t *testing.T) {
|
||||
mc, _, notifications, _ := newTestMetaCache(t, map[util.FullPath]bool{
|
||||
"/": true,
|
||||
})
|
||||
defer mc.Shutdown()
|
||||
|
||||
// Start building /dir (simulates the beginning of EnsureVisited)
|
||||
if err := mc.BeginDirectoryBuild(context.Background(), util.FullPath("/dir")); err != nil {
|
||||
t.Fatalf("begin build: %v", err)
|
||||
}
|
||||
|
||||
// Insert an entry as EnsureVisited would during the filer listing
|
||||
if err := mc.InsertEntry(context.Background(), &filer.Entry{
|
||||
FullPath: "/dir/existing.txt",
|
||||
Attr: filer.Attr{
|
||||
Crtime: time.Unix(1, 0),
|
||||
Mtime: time.Unix(1, 0),
|
||||
Mode: 0100644,
|
||||
FileSize: 100,
|
||||
},
|
||||
}, 0); err != nil {
|
||||
t.Fatalf("insert entry during build: %v", err)
|
||||
}
|
||||
|
||||
// Simulate multiple metadata events arriving for /dir while the build
|
||||
// is in progress. Each event would normally call noteDirectoryUpdate,
|
||||
// which in production can trigger markDirectoryReadThrough and wipe entries.
|
||||
for i := 0; i < 5; i++ {
|
||||
resp := &filer_pb.SubscribeMetadataResponse{
|
||||
Directory: "/dir",
|
||||
EventNotification: &filer_pb.EventNotification{
|
||||
NewEntry: &filer_pb.Entry{
|
||||
Name: fmt.Sprintf("new-%d.txt", i),
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
Crtime: int64(10 + i),
|
||||
Mtime: int64(10 + i),
|
||||
FileMode: 0100644,
|
||||
FileSize: uint64(i + 1),
|
||||
},
|
||||
},
|
||||
},
|
||||
TsNs: int64(200 + i),
|
||||
}
|
||||
if err := mc.ApplyMetadataResponse(context.Background(), resp, SubscriberMetadataResponseApplyOptions); err != nil {
|
||||
t.Fatalf("apply event %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// The building directory /dir must NOT have received any notifications.
|
||||
// If it did, markDirectoryReadThrough would wipe the cache mid-build.
|
||||
for _, p := range notifications.paths() {
|
||||
if p == util.FullPath("/dir") {
|
||||
t.Fatal("onDirectoryUpdate was called for /dir during build; this would cause markDirectoryReadThrough to wipe entries mid-build")
|
||||
}
|
||||
}
|
||||
|
||||
// The entry inserted during the build must still be present
|
||||
entry, _, err := mc.FindEntry(context.Background(), util.FullPath("/dir/existing.txt"))
|
||||
if err != nil {
|
||||
t.Fatalf("entry wiped during build: %v", err)
|
||||
}
|
||||
if entry.FileSize != 100 {
|
||||
t.Fatalf("entry size = %d, want 100", entry.FileSize)
|
||||
}
|
||||
|
||||
// Complete the build — buffered events should be replayed
|
||||
if err := mc.CompleteDirectoryBuild(context.Background(), util.FullPath("/dir"), 150, nil); err != nil {
|
||||
t.Fatalf("complete build: %v", err)
|
||||
}
|
||||
|
||||
// After build completes, the entry from the listing should still exist
|
||||
entry, _, err = mc.FindEntry(context.Background(), util.FullPath("/dir/existing.txt"))
|
||||
if err != nil {
|
||||
t.Fatalf("entry lost after build completion: %v", err)
|
||||
}
|
||||
if entry.FileSize != 100 {
|
||||
t.Fatalf("entry size after build = %d, want 100", entry.FileSize)
|
||||
}
|
||||
|
||||
// Buffered events with TsNs > snapshotTsNs (150) should have been replayed
|
||||
for i := 0; i < 5; i++ {
|
||||
name := fmt.Sprintf("new-%d.txt", i)
|
||||
e, _, err := mc.FindEntry(context.Background(), util.FullPath("/dir/"+name))
|
||||
if err != nil {
|
||||
t.Fatalf("replayed entry %s not found: %v", name, err)
|
||||
}
|
||||
if e.FileSize != uint64(i+1) {
|
||||
t.Fatalf("replayed entry %s size = %d, want %d", name, e.FileSize, i+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestEmptyDirectoryBuildReplaysAllBufferedEvents verifies that when a
|
||||
// directory build completes with snapshotTsNs=0 (empty directory — server
|
||||
// returned no entries and no snapshot), ALL buffered events are replayed
|
||||
// without any TsNs filtering. This prevents clock-skew between client and
|
||||
// filer from dropping legitimate mutations.
|
||||
func TestEmptyDirectoryBuildReplaysAllBufferedEvents(t *testing.T) {
|
||||
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{
|
||||
"/": true,
|
||||
})
|
||||
defer mc.Shutdown()
|
||||
|
||||
if err := mc.BeginDirectoryBuild(context.Background(), util.FullPath("/empty")); err != nil {
|
||||
t.Fatalf("begin build: %v", err)
|
||||
}
|
||||
|
||||
// Buffer events with a range of TsNs values — some very old, some recent.
|
||||
// With a client-synthesized snapshot, old events could be incorrectly filtered.
|
||||
tsValues := []int64{1, 50, 500, 5000, 50000}
|
||||
for i, ts := range tsValues {
|
||||
resp := &filer_pb.SubscribeMetadataResponse{
|
||||
Directory: "/empty",
|
||||
EventNotification: &filer_pb.EventNotification{
|
||||
NewEntry: &filer_pb.Entry{
|
||||
Name: fmt.Sprintf("file-%d.txt", i),
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
Crtime: ts,
|
||||
Mtime: ts,
|
||||
FileMode: 0100644,
|
||||
FileSize: uint64(i + 10),
|
||||
},
|
||||
},
|
||||
},
|
||||
TsNs: ts,
|
||||
}
|
||||
if err := mc.ApplyMetadataResponse(context.Background(), resp, SubscriberMetadataResponseApplyOptions); err != nil {
|
||||
t.Fatalf("apply event %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Complete with snapshotTsNs=0 — simulates empty directory listing
|
||||
if err := mc.CompleteDirectoryBuild(context.Background(), util.FullPath("/empty"), 0, nil); err != nil {
|
||||
t.Fatalf("complete build: %v", err)
|
||||
}
|
||||
|
||||
// Every buffered event must have been replayed, regardless of TsNs
|
||||
for i := range tsValues {
|
||||
name := fmt.Sprintf("file-%d.txt", i)
|
||||
e, _, err := mc.FindEntry(context.Background(), util.FullPath("/empty/"+name))
|
||||
if err != nil {
|
||||
t.Fatalf("replayed entry %s not found: %v", name, err)
|
||||
}
|
||||
if e.FileSize != uint64(i+10) {
|
||||
t.Fatalf("replayed entry %s size = %d, want %d", name, e.FileSize, i+10)
|
||||
}
|
||||
}
|
||||
|
||||
if !mc.IsDirectoryCached(util.FullPath("/empty")) {
|
||||
t.Fatal("/empty should be marked cached after build completes")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildCompletionSurvivesCallerCancellation verifies that once
|
||||
// CompleteDirectoryBuild is enqueued, a cancelled caller context does not
|
||||
// prevent the build from completing. The apply loop uses context.Background()
|
||||
// internally, so the operation finishes even if the caller gives up waiting.
|
||||
func TestBuildCompletionSurvivesCallerCancellation(t *testing.T) {
|
||||
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{
|
||||
"/": true,
|
||||
})
|
||||
defer mc.Shutdown()
|
||||
|
||||
if err := mc.BeginDirectoryBuild(context.Background(), util.FullPath("/dir")); err != nil {
|
||||
t.Fatalf("begin build: %v", err)
|
||||
}
|
||||
|
||||
// Insert an entry during the build (as EnsureVisited would)
|
||||
if err := mc.InsertEntry(context.Background(), &filer.Entry{
|
||||
FullPath: "/dir/kept.txt",
|
||||
Attr: filer.Attr{
|
||||
Crtime: time.Unix(1, 0),
|
||||
Mtime: time.Unix(1, 0),
|
||||
Mode: 0100644,
|
||||
FileSize: 42,
|
||||
},
|
||||
}, 0); err != nil {
|
||||
t.Fatalf("insert entry: %v", err)
|
||||
}
|
||||
|
||||
// Buffer an event that should be replayed
|
||||
if err := mc.ApplyMetadataResponse(context.Background(), &filer_pb.SubscribeMetadataResponse{
|
||||
Directory: "/dir",
|
||||
EventNotification: &filer_pb.EventNotification{
|
||||
NewEntry: &filer_pb.Entry{
|
||||
Name: "buffered.txt",
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
Crtime: 5,
|
||||
Mtime: 5,
|
||||
FileMode: 0100644,
|
||||
FileSize: 77,
|
||||
},
|
||||
},
|
||||
},
|
||||
TsNs: 200,
|
||||
}, SubscriberMetadataResponseApplyOptions); err != nil {
|
||||
t.Fatalf("apply event: %v", err)
|
||||
}
|
||||
|
||||
// Complete with an already-cancelled context. The operation should still
|
||||
// succeed because enqueueAndWait sets req.ctx = context.Background().
|
||||
cancelledCtx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // cancel immediately
|
||||
|
||||
// CompleteDirectoryBuild may return ctx.Err() if the select picks
|
||||
// ctx.Done() first, but the operation itself still completes in the
|
||||
// apply loop. Poll for the observable side effect instead of using
|
||||
// a fixed sleep.
|
||||
_ = mc.CompleteDirectoryBuild(cancelledCtx, util.FullPath("/dir"), 100, nil)
|
||||
|
||||
// Poll until the build completes or a deadline elapses.
|
||||
deadline := time.After(2 * time.Second)
|
||||
for !mc.IsDirectoryCached(util.FullPath("/dir")) {
|
||||
select {
|
||||
case <-deadline:
|
||||
t.Fatal("/dir should be cached — CompleteDirectoryBuild must have executed despite cancelled context")
|
||||
default:
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
// The pre-existing entry must survive
|
||||
entry, _, findErr := mc.FindEntry(context.Background(), util.FullPath("/dir/kept.txt"))
|
||||
if findErr != nil {
|
||||
t.Fatalf("find kept entry: %v", findErr)
|
||||
}
|
||||
if entry.FileSize != 42 {
|
||||
t.Fatalf("kept entry size = %d, want 42", entry.FileSize)
|
||||
}
|
||||
|
||||
// The buffered event (TsNs 200 > snapshot 100) must have been replayed
|
||||
buffered, _, findErr := mc.FindEntry(context.Background(), util.FullPath("/dir/buffered.txt"))
|
||||
if findErr != nil {
|
||||
t.Fatalf("find buffered entry: %v", findErr)
|
||||
}
|
||||
if buffered.FileSize != 77 {
|
||||
t.Fatalf("buffered entry size = %d, want 77", buffered.FileSize)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBufferedRenameUpdatesOtherDirectoryBeforeBuildCompletes(t *testing.T) {
|
||||
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{
|
||||
"/": true,
|
||||
"/src": true,
|
||||
})
|
||||
defer mc.Shutdown()
|
||||
|
||||
if err := mc.InsertEntry(context.Background(), &filer.Entry{
|
||||
FullPath: "/src/from.txt",
|
||||
Attr: filer.Attr{
|
||||
Crtime: time.Unix(1, 0),
|
||||
Mtime: time.Unix(1, 0),
|
||||
Mode: 0100644,
|
||||
FileSize: 7,
|
||||
},
|
||||
}, 0); err != nil {
|
||||
t.Fatalf("insert source entry: %v", err)
|
||||
}
|
||||
|
||||
if err := mc.BeginDirectoryBuild(context.Background(), util.FullPath("/dst")); err != nil {
|
||||
t.Fatalf("begin build: %v", err)
|
||||
}
|
||||
|
||||
renameResp := &filer_pb.SubscribeMetadataResponse{
|
||||
Directory: "/src",
|
||||
EventNotification: &filer_pb.EventNotification{
|
||||
OldEntry: &filer_pb.Entry{
|
||||
Name: "from.txt",
|
||||
},
|
||||
NewEntry: &filer_pb.Entry{
|
||||
Name: "to.txt",
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
Crtime: 2,
|
||||
Mtime: 2,
|
||||
FileMode: 0100644,
|
||||
FileSize: 12,
|
||||
},
|
||||
},
|
||||
NewParentPath: "/dst",
|
||||
},
|
||||
TsNs: 101,
|
||||
}
|
||||
|
||||
if err := mc.ApplyMetadataResponse(context.Background(), renameResp, SubscriberMetadataResponseApplyOptions); err != nil {
|
||||
t.Fatalf("apply rename: %v", err)
|
||||
}
|
||||
|
||||
oldEntry, _, err := mc.FindEntry(context.Background(), util.FullPath("/src/from.txt"))
|
||||
if err != filer_pb.ErrNotFound {
|
||||
t.Fatalf("find old path error = %v, want %v", err, filer_pb.ErrNotFound)
|
||||
}
|
||||
if oldEntry != nil {
|
||||
t.Fatalf("old path should be removed before build completes: %+v", oldEntry)
|
||||
}
|
||||
|
||||
newEntry, _, err := mc.FindEntry(context.Background(), util.FullPath("/dst/to.txt"))
|
||||
if err != filer_pb.ErrNotFound {
|
||||
t.Fatalf("find buffered new path error = %v, want %v", err, filer_pb.ErrNotFound)
|
||||
}
|
||||
if newEntry != nil {
|
||||
t.Fatalf("new path should stay hidden until build completes: %+v", newEntry)
|
||||
}
|
||||
|
||||
if err := mc.CompleteDirectoryBuild(context.Background(), util.FullPath("/dst"), 100, nil); err != nil {
|
||||
t.Fatalf("complete build: %v", err)
|
||||
}
|
||||
|
||||
newEntry, _, err = mc.FindEntry(context.Background(), util.FullPath("/dst/to.txt"))
|
||||
if err != nil {
|
||||
t.Fatalf("find replayed new path: %v", err)
|
||||
}
|
||||
if newEntry.FileSize != 12 {
|
||||
t.Fatalf("replayed new path size = %d, want 12", newEntry.FileSize)
|
||||
}
|
||||
}
|
||||
|
||||
// TestEnsureVisitedPreservesLocalOnlyEntry reproduces the residual coherence
|
||||
// gap behind the FUSE ConcurrentReadWrite ENOENT flake.
|
||||
//
|
||||
// A FUSE create on the writeback/deferFilerCreate path inserts the entry into
|
||||
// the local store directly (weedfs_file_mkrm.go createFile), off the metaCache
|
||||
// apply loop, before the filer holds it. A concurrent rebuild of the parent —
|
||||
// triggered when the directory falls out of cache (idle evict, hot-dir
|
||||
// read-through) — wipes the store and refills it from a filer listing that does
|
||||
// not yet include the un-flushed local create, then publishes the directory
|
||||
// authoritatively cached (markCachedFn). The local entry vanishes although the
|
||||
// client created it: lookupEntry then returns an authoritative ENOENT for it.
|
||||
func TestEnsureVisitedPreservesLocalOnlyEntry(t *testing.T) {
|
||||
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{"/": true})
|
||||
defer mc.Shutdown()
|
||||
|
||||
// The mount pins the un-flushed create (open dirty handle / pending flush),
|
||||
// keyed off the inode the entry carries so a kernel Forget that dropped the
|
||||
// path→inode mapping cannot unpin an in-flight create.
|
||||
mc.SetPinnedChildFn(func(e *filer.Entry) bool { return e.Attr.Inode == 42 })
|
||||
|
||||
// A deferred local create lands before the rebuild; /dir is not yet cached.
|
||||
// It carries its allocated inode, as createFile's placeholder does.
|
||||
if err := mc.InsertEntry(context.Background(), &filer.Entry{
|
||||
FullPath: "/dir/pending.txt",
|
||||
Attr: filer.Attr{Crtime: time.Unix(1, 0), Mtime: time.Unix(1, 0), Mode: 0100644, FileSize: 1, Inode: 42},
|
||||
}, 0); err != nil {
|
||||
t.Fatalf("insert pending entry: %v", err)
|
||||
}
|
||||
|
||||
// A concurrent rebuild lists the filer, whose snapshot pre-dates the
|
||||
// un-flushed create, so it returns only the already-persisted sibling.
|
||||
accessor := &buildFilerAccessor{client: &buildListClient{
|
||||
responses: []*filer_pb.ListEntriesResponse{{
|
||||
Entry: &filer_pb.Entry{
|
||||
Name: "base.txt",
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
Crtime: 1,
|
||||
Mtime: 1,
|
||||
FileMode: 0100644,
|
||||
FileSize: 3,
|
||||
},
|
||||
},
|
||||
SnapshotTsNs: 100,
|
||||
}},
|
||||
}}
|
||||
|
||||
if err := EnsureVisited(mc, accessor, util.FullPath("/dir"), 0); err != nil {
|
||||
t.Fatalf("ensure visited: %v", err)
|
||||
}
|
||||
if !mc.IsDirectoryCached(util.FullPath("/dir")) {
|
||||
t.Fatal("/dir should be cached after build completes")
|
||||
}
|
||||
|
||||
// base.txt from the listing is present.
|
||||
if _, _, err := mc.FindEntry(context.Background(), util.FullPath("/dir/base.txt")); err != nil {
|
||||
t.Fatalf("listed entry missing after build: %v", err)
|
||||
}
|
||||
|
||||
// The un-flushed local create must survive the rebuild. With /dir now
|
||||
// authoritatively cached, losing it is the file-vanishes flake.
|
||||
if _, _, err := mc.FindEntry(context.Background(), util.FullPath("/dir/pending.txt")); err != nil {
|
||||
t.Fatalf("local-only entry lost across concurrent rebuild: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestEnsureVisitedDropsUnpinnedStaleEntry guards the preservation's selectivity:
|
||||
// a cached child the filer listing no longer returns and that is NOT pinned must
|
||||
// still be wiped, so the rebuild can't resurrect a deleted/renamed entry.
|
||||
func TestEnsureVisitedDropsUnpinnedStaleEntry(t *testing.T) {
|
||||
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{"/": true})
|
||||
defer mc.Shutdown()
|
||||
|
||||
mc.SetPinnedChildFn(func(*filer.Entry) bool { return false })
|
||||
|
||||
// A stale child sits in the cache; the filer no longer has it.
|
||||
insertCacheEntry(t, mc, "/dir/stale.txt")
|
||||
|
||||
accessor := &buildFilerAccessor{client: &buildListClient{
|
||||
responses: []*filer_pb.ListEntriesResponse{{
|
||||
Entry: &filer_pb.Entry{
|
||||
Name: "base.txt",
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
Crtime: 1,
|
||||
Mtime: 1,
|
||||
FileMode: 0100644,
|
||||
FileSize: 3,
|
||||
},
|
||||
},
|
||||
SnapshotTsNs: 100,
|
||||
}},
|
||||
}}
|
||||
|
||||
if err := EnsureVisited(mc, accessor, util.FullPath("/dir"), 0); err != nil {
|
||||
t.Fatalf("ensure visited: %v", err)
|
||||
}
|
||||
if entry, _, err := mc.FindEntry(context.Background(), util.FullPath("/dir/stale.txt")); err != filer_pb.ErrNotFound || entry != nil {
|
||||
t.Fatalf("unpinned stale entry survived rebuild = %+v, %v; want nil, %v", entry, err, filer_pb.ErrNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
// sequencedListClient returns a different ListEntries result per call (repeating
|
||||
// the last), modelling a filer that lists empty transiently then the real entries.
|
||||
type sequencedListClient struct {
|
||||
filer_pb.SeaweedFilerClient
|
||||
mu sync.Mutex
|
||||
perCall [][]*filer_pb.ListEntriesResponse
|
||||
calls int
|
||||
}
|
||||
|
||||
func (c *sequencedListClient) ListEntries(ctx context.Context, in *filer_pb.ListEntriesRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[filer_pb.ListEntriesResponse], error) {
|
||||
c.mu.Lock()
|
||||
idx := c.calls
|
||||
if idx >= len(c.perCall) {
|
||||
idx = len(c.perCall) - 1
|
||||
}
|
||||
c.calls++
|
||||
resp := c.perCall[idx]
|
||||
c.mu.Unlock()
|
||||
return &buildListStream{responses: resp}, nil
|
||||
}
|
||||
|
||||
// TestEnsureVisitedConfirmsTransientEmptyListing: a rebuild whose first filer
|
||||
// listing comes back empty must re-read and cache the real entries, not strand
|
||||
// the directory cached over an empty store (the ConcurrentReadWrite ENOENT flake).
|
||||
func TestEnsureVisitedConfirmsTransientEmptyListing(t *testing.T) {
|
||||
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{"/": true})
|
||||
defer mc.Shutdown()
|
||||
|
||||
entry := &filer_pb.Entry{
|
||||
Name: "keep.txt",
|
||||
Attributes: &filer_pb.FuseAttributes{Crtime: 1, Mtime: 1, FileMode: 0100644, FileSize: 7},
|
||||
}
|
||||
accessor := &buildFilerAccessor{client: &sequencedListClient{
|
||||
perCall: [][]*filer_pb.ListEntriesResponse{
|
||||
{}, // first read: transient empty
|
||||
{{Entry: entry, SnapshotTsNs: 100}}, // confirm read: the real entry
|
||||
},
|
||||
}}
|
||||
|
||||
if err := EnsureVisited(mc, accessor, util.FullPath("/dir"), 0); err != nil {
|
||||
t.Fatalf("ensure visited: %v", err)
|
||||
}
|
||||
if !mc.IsDirectoryCached(util.FullPath("/dir")) {
|
||||
t.Fatal("/dir should be cached after build completes")
|
||||
}
|
||||
if _, _, err := mc.FindEntry(context.Background(), util.FullPath("/dir/keep.txt")); err != nil {
|
||||
t.Fatalf("/dir/keep.txt stranded after transient empty listing: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestEnsureVisitedCachesGenuinelyEmptyDirectory: a really-empty directory lists
|
||||
// empty on every confirm and must still end up cached.
|
||||
func TestEnsureVisitedCachesGenuinelyEmptyDirectory(t *testing.T) {
|
||||
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{"/": true})
|
||||
defer mc.Shutdown()
|
||||
|
||||
client := &sequencedListClient{
|
||||
perCall: [][]*filer_pb.ListEntriesResponse{{}}, // always empty
|
||||
}
|
||||
accessor := &buildFilerAccessor{client: client}
|
||||
|
||||
if err := EnsureVisited(mc, accessor, util.FullPath("/empty"), 0); err != nil {
|
||||
t.Fatalf("ensure visited: %v", err)
|
||||
}
|
||||
if !mc.IsDirectoryCached(util.FullPath("/empty")) {
|
||||
t.Fatal("/empty should be cached even though it has no entries")
|
||||
}
|
||||
// The empty result must have been confirmed, not trusted on the first read.
|
||||
client.mu.Lock()
|
||||
calls := client.calls
|
||||
client.mu.Unlock()
|
||||
if calls != emptyRebuildConfirmations+1 {
|
||||
t.Fatalf("list calls = %d, want %d (initial + confirmations)", calls, emptyRebuildConfirmations+1)
|
||||
}
|
||||
}
|
||||
161
weed/mount/meta_cache/meta_cache_deadlock_test.go
Normal file
161
weed/mount/meta_cache/meta_cache_deadlock_test.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package meta_cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
)
|
||||
|
||||
// TestApplyLoopInvalidateDoesNotDeadlockWithLockHoldingEnqueuer reproduces the
|
||||
// mount hang seen in production (all readdirs stuck, FUSE waiting>0):
|
||||
//
|
||||
// - A FUSE flush handler (flushMetadataToFiler) holds the file handle's
|
||||
// exclusive lock in wfs.fhLockTable and then enqueues a local metadata
|
||||
// event, waiting for the apply loop to process it.
|
||||
// - Concurrently the apply loop processes a subscriber event whose
|
||||
// invalidateFunc (weedfs.go) acquires the same file handle lock.
|
||||
//
|
||||
// The single-goroutine apply loop blocks on the fh lock while the fh lock
|
||||
// holder blocks on the apply loop: a permanent ABBA deadlock. Every later
|
||||
// EnsureVisited/BeginDirectoryBuild then backs up behind the dead loop, FUSE
|
||||
// handler goroutines exhaust, and the kernel queue (waiting) grows.
|
||||
//
|
||||
// The contract under test: the apply loop must never block on locks that can
|
||||
// be held by goroutines waiting on the apply loop.
|
||||
func TestApplyLoopInvalidateDoesNotDeadlockWithLockHoldingEnqueuer(t *testing.T) {
|
||||
mapper, err := NewUidGidMapper("", "")
|
||||
if err != nil {
|
||||
t.Fatalf("uid/gid mapper: %v", err)
|
||||
}
|
||||
|
||||
// Stand-in for wfs.fhLockTable, keyed like FileHandleId.
|
||||
fhLockTable := util.NewLockTable[uint64]()
|
||||
const fhKey uint64 = 42
|
||||
|
||||
invalidateEntered := make(chan struct{})
|
||||
var enteredOnce sync.Once
|
||||
|
||||
var cachedMu sync.Mutex
|
||||
cached := map[util.FullPath]bool{"/": true, "/dir": true}
|
||||
|
||||
mc := NewMetaCache(
|
||||
filepath.Join(t.TempDir(), "meta"),
|
||||
mapper,
|
||||
util.FullPath("/"),
|
||||
false,
|
||||
func(path util.FullPath) {
|
||||
cachedMu.Lock()
|
||||
defer cachedMu.Unlock()
|
||||
cached[path] = true
|
||||
},
|
||||
func(path util.FullPath) bool {
|
||||
cachedMu.Lock()
|
||||
defer cachedMu.Unlock()
|
||||
return cached[path]
|
||||
},
|
||||
func(inv EntryInvalidation) {
|
||||
// Mirrors the wfs invalidateFunc: it takes the open file
|
||||
// handle's exclusive lock before refreshing the handle entry.
|
||||
enteredOnce.Do(func() { close(invalidateEntered) })
|
||||
lock := fhLockTable.AcquireLock("invalidateFunc", fhKey, util.ExclusiveLock)
|
||||
fhLockTable.ReleaseLock(fhKey, lock)
|
||||
},
|
||||
func(dir util.FullPath) {},
|
||||
)
|
||||
defer func() {
|
||||
// Only safe to shut down once the apply loop is unwedged.
|
||||
mc.Shutdown()
|
||||
}()
|
||||
|
||||
subscriberEvent := &filer_pb.SubscribeMetadataResponse{
|
||||
Directory: "/dir",
|
||||
EventNotification: &filer_pb.EventNotification{
|
||||
NewEntry: &filer_pb.Entry{
|
||||
Name: "file.txt",
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
Crtime: 1,
|
||||
Mtime: 1,
|
||||
FileMode: 0100644,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
localEvent := &filer_pb.SubscribeMetadataResponse{
|
||||
Directory: "/dir",
|
||||
EventNotification: &filer_pb.EventNotification{
|
||||
OldEntry: &filer_pb.Entry{Name: "file.txt"},
|
||||
NewEntry: &filer_pb.Entry{
|
||||
Name: "file.txt",
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
Crtime: 1,
|
||||
Mtime: 2,
|
||||
FileMode: 0100644,
|
||||
},
|
||||
},
|
||||
NewParentPath: "/dir",
|
||||
},
|
||||
}
|
||||
|
||||
// Step 1: the "flush handler" takes the fh exclusive lock
|
||||
// (flushMetadataToFiler does this before talking to the filer).
|
||||
flushLock := fhLockTable.AcquireLock("doFlush", fhKey, util.ExclusiveLock)
|
||||
flushLockReleased := false
|
||||
releaseFlushLock := func() {
|
||||
if !flushLockReleased {
|
||||
flushLockReleased = true
|
||||
fhLockTable.ReleaseLock(fhKey, flushLock)
|
||||
}
|
||||
}
|
||||
defer releaseFlushLock()
|
||||
|
||||
// Step 2: a subscriber event arrives; the apply loop will run
|
||||
// invalidateFunc, which wants the fh lock held above.
|
||||
subscriberDone := make(chan error, 1)
|
||||
go func() {
|
||||
subscriberDone <- mc.ApplyMetadataResponse(context.Background(), subscriberEvent, SubscriberMetadataResponseApplyOptions)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-invalidateEntered:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("invalidateFunc was never invoked for the subscriber event")
|
||||
}
|
||||
// Give the invalidate goroutine a moment to actually block on AcquireLock.
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
// Step 3: still holding the fh lock, the flush handler applies its local
|
||||
// metadata event and waits for the apply loop — exactly what
|
||||
// flushMetadataToFiler does after streamCreateEntry succeeds.
|
||||
flushApplyDone := make(chan error, 1)
|
||||
go func() {
|
||||
flushApplyDone <- mc.ApplyMetadataResponseOwned(context.Background(), localEvent, LocalMetadataResponseApplyOptions)
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-flushApplyDone:
|
||||
if err != nil {
|
||||
t.Fatalf("local metadata apply: %v", err)
|
||||
}
|
||||
case <-time.After(3 * time.Second):
|
||||
// Unwedge the apply loop so Shutdown in the deferred cleanup
|
||||
// doesn't hang the whole test binary.
|
||||
releaseFlushLock()
|
||||
t.Fatal("deadlock: apply loop blocked on fh lock while fh lock holder waits on apply loop")
|
||||
}
|
||||
|
||||
releaseFlushLock()
|
||||
|
||||
select {
|
||||
case err := <-subscriberDone:
|
||||
if err != nil {
|
||||
t.Fatalf("subscriber apply: %v", err)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("subscriber apply never completed")
|
||||
}
|
||||
}
|
||||
230
weed/mount/meta_cache/meta_cache_init.go
Normal file
230
weed/mount/meta_cache/meta_cache_init.go
Normal file
@@ -0,0 +1,230 @@
|
||||
package meta_cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/filer"
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
)
|
||||
|
||||
// DirectoryTooLargeError reports a directory the mount refuses to cache
|
||||
// locally. Its listings read through to the filer instead.
|
||||
type DirectoryTooLargeError struct {
|
||||
Path util.FullPath
|
||||
}
|
||||
|
||||
func (e *DirectoryTooLargeError) Error() string {
|
||||
return fmt.Sprintf("directory %s is too large to cache locally", e.Path)
|
||||
}
|
||||
|
||||
// maxCacheableEntries is the directory size above which a build gives up, or 0
|
||||
// to cache everything.
|
||||
func EnsureVisited(mc *MetaCache, client filer_pb.FilerClient, dirPath util.FullPath, maxCacheableEntries int) error {
|
||||
// Collect all uncached paths from target directory up to root
|
||||
var uncachedPaths []util.FullPath
|
||||
currentPath := dirPath
|
||||
|
||||
for {
|
||||
// If this path is cached, all ancestors are also cached
|
||||
if mc.isCachedFn(currentPath) {
|
||||
break
|
||||
}
|
||||
if mc.isOversized(currentPath) {
|
||||
// The directory itself reads through; an ancestor is stepped over,
|
||||
// or it would wedge every listing beneath it forever.
|
||||
if currentPath == dirPath {
|
||||
return &DirectoryTooLargeError{Path: currentPath}
|
||||
}
|
||||
} else {
|
||||
uncachedPaths = append(uncachedPaths, currentPath)
|
||||
}
|
||||
|
||||
// Continue to parent directory
|
||||
if currentPath != mc.root {
|
||||
parent, _ := currentPath.DirAndName()
|
||||
currentPath = util.FullPath(parent)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(uncachedPaths) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fetch all uncached directories in parallel with context for cancellation
|
||||
// If one fetch fails, cancel the others to avoid unnecessary work
|
||||
g, ctx := errgroup.WithContext(context.Background())
|
||||
for _, p := range uncachedPaths {
|
||||
path := p // capture for closure
|
||||
g.Go(func() error {
|
||||
err := doEnsureVisited(ctx, mc, client, path, maxCacheableEntries)
|
||||
var tooLarge *DirectoryTooLargeError
|
||||
if errors.As(err, &tooLarge) && path != dirPath {
|
||||
// An ancestor found oversized just reads through; failing the
|
||||
// group here would cancel the builds of its cacheable
|
||||
// descendants, and the caller would treat the refusal as the
|
||||
// listed directory's own.
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
})
|
||||
}
|
||||
return g.Wait()
|
||||
}
|
||||
|
||||
// batchInsertSize is the number of entries to accumulate before flushing to LevelDB.
|
||||
// 100 provides a balance between memory usage (~100 Entry pointers) and write efficiency
|
||||
// (fewer disk syncs). Larger values reduce I/O overhead but increase memory and latency.
|
||||
const batchInsertSize = 100
|
||||
|
||||
const (
|
||||
emptyRebuildConfirmations = 2
|
||||
emptyRebuildConfirmDelay = 50 * time.Millisecond
|
||||
)
|
||||
|
||||
func doEnsureVisited(ctx context.Context, mc *MetaCache, client filer_pb.FilerClient, path util.FullPath, maxCacheableEntries int) error {
|
||||
// Use singleflight to deduplicate concurrent requests for the same path
|
||||
_, err, _ := mc.visitGroup.Do(string(path), func() (interface{}, error) {
|
||||
// Check for cancellation before starting
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
// Double-check if already cached (another goroutine may have completed)
|
||||
if mc.isCachedFn(path) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
glog.V(4).Infof("ReadDirAllEntries %s ...", path)
|
||||
|
||||
// Use context.Background() for build lifecycle calls so that
|
||||
// errgroup cancellation of ctx doesn't cause enqueueAndWait to
|
||||
// return early, which would trigger cleanupBuild while the
|
||||
// operation is still queued.
|
||||
if err := mc.BeginDirectoryBuild(context.Background(), path); err != nil {
|
||||
return nil, fmt.Errorf("begin build %s: %w", path, err)
|
||||
}
|
||||
cleanupDone := false
|
||||
cleanupBuild := func(reason string) {
|
||||
if cleanupDone {
|
||||
return
|
||||
}
|
||||
cleanupDone = true
|
||||
if deleteErr := mc.deleteFolderChildrenForRebuild(context.Background(), path); deleteErr != nil {
|
||||
glog.V(2).Infof("clear %s build %s: %v", reason, path, deleteErr)
|
||||
}
|
||||
if abortErr := mc.AbortDirectoryBuild(context.Background(), path); abortErr != nil {
|
||||
glog.V(2).Infof("abort %s build %s: %v", reason, path, abortErr)
|
||||
}
|
||||
}
|
||||
defer func() {
|
||||
if !cleanupDone && ctx.Err() != nil {
|
||||
cleanupBuild("canceled")
|
||||
}
|
||||
}()
|
||||
|
||||
// reloadFromFiler wipes the cached children and reloads them from the filer.
|
||||
reloadFromFiler := func() (entryCount int, snapshotTsNs int64, sections sectionBoundsCollector, err error) {
|
||||
err = util.Retry("ReadDirAllEntries", func() error {
|
||||
entryCount = 0
|
||||
sections = sectionBoundsCollector{}
|
||||
var batch []*filer.Entry // reset on retry, allow GC of previous entries
|
||||
if err := mc.deleteFolderChildrenForRebuild(ctx, path); err != nil {
|
||||
return fmt.Errorf("clear existing entries for %s: %w", path, err)
|
||||
}
|
||||
var listErr error
|
||||
snapshotTsNs, listErr = filer_pb.ReadDirAllEntriesWithSnapshot(ctx, client, path, "", func(pbEntry *filer_pb.Entry, isLast bool) error {
|
||||
entry := filer.FromPbEntry(string(path), pbEntry)
|
||||
if !mc.includeSystemEntries && IsHiddenSystemEntry(string(path), entry.Name()) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if maxCacheableEntries > 0 && entryCount >= maxCacheableEntries {
|
||||
return &DirectoryTooLargeError{Path: path}
|
||||
}
|
||||
sections.note(entry.Name())
|
||||
batch = append(batch, entry)
|
||||
entryCount++
|
||||
|
||||
// flush by size, not isLast: hidden entries can return early
|
||||
if len(batch) >= batchInsertSize {
|
||||
if err := mc.doBatchInsertEntries(ctx, batch); err != nil {
|
||||
return fmt.Errorf("batch insert for %s: %w", path, err)
|
||||
}
|
||||
batch = make([]*filer.Entry, 0, batchInsertSize)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if listErr != nil {
|
||||
return listErr
|
||||
}
|
||||
if len(batch) > 0 {
|
||||
if err := mc.doBatchInsertEntries(ctx, batch); err != nil {
|
||||
return fmt.Errorf("batch insert remaining for %s: %w", path, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return entryCount, snapshotTsNs, sections, err
|
||||
}
|
||||
|
||||
entryCount, snapshotTsNs, sections, fetchErr := reloadFromFiler()
|
||||
if fetchErr != nil {
|
||||
var tooLarge *DirectoryTooLargeError
|
||||
if errors.As(fetchErr, &tooLarge) {
|
||||
// Remember the refusal so the next visit fails fast instead of
|
||||
// streaming up to the limit again to rediscover it.
|
||||
mc.markOversized(path)
|
||||
glog.V(0).Infof("directory %s exceeds %d entries, reading it through instead of caching", path, maxCacheableEntries)
|
||||
cleanupBuild("oversized")
|
||||
return nil, fetchErr
|
||||
}
|
||||
cleanupBuild("failed")
|
||||
return nil, fmt.Errorf("list %s: %w", path, fetchErr)
|
||||
}
|
||||
|
||||
// A transient empty listing would strand a populated directory cached over
|
||||
// an empty store; re-read to confirm before trusting it. First re-read is
|
||||
// immediate (a clean-EOF stream glitch clears at once), later ones space out.
|
||||
// On cancellation the deferred cleanup aborts the build.
|
||||
for attempt := 0; entryCount == 0 && attempt < emptyRebuildConfirmations; attempt++ {
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
if attempt > 0 {
|
||||
select {
|
||||
case <-time.After(emptyRebuildConfirmDelay):
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
if entryCount, snapshotTsNs, sections, fetchErr = reloadFromFiler(); fetchErr != nil {
|
||||
cleanupBuild("failed")
|
||||
return nil, fmt.Errorf("confirm empty list %s: %w", path, fetchErr)
|
||||
}
|
||||
if entryCount > 0 {
|
||||
glog.Warningf("rebuild of %s saw a transient empty listing, recovered %d entries on confirmation", path, entryCount)
|
||||
}
|
||||
}
|
||||
|
||||
if err := mc.CompleteDirectoryBuild(context.Background(), path, snapshotTsNs, sections.bounds); err != nil {
|
||||
cleanupBuild("unreplayed")
|
||||
return nil, fmt.Errorf("complete build for %s: %w", path, err)
|
||||
}
|
||||
cleanupDone = true // Prevent deferred cleanup after successful publish
|
||||
return nil, nil
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func IsHiddenSystemEntry(dir, name string) bool {
|
||||
return dir == "/" && (name == "topics" || name == "etc")
|
||||
}
|
||||
98
weed/mount/meta_cache/meta_cache_purge_test.go
Normal file
98
weed/mount/meta_cache/meta_cache_purge_test.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package meta_cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/filer"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
)
|
||||
|
||||
func insertCacheEntry(t *testing.T, mc *MetaCache, path util.FullPath) {
|
||||
t.Helper()
|
||||
if err := mc.InsertEntry(context.Background(), &filer.Entry{
|
||||
FullPath: path,
|
||||
Attr: filer.Attr{
|
||||
Crtime: time.Unix(1, 0),
|
||||
Mtime: time.Unix(1, 0),
|
||||
Mode: 0100644,
|
||||
FileSize: 1,
|
||||
},
|
||||
}, 0); err != nil {
|
||||
t.Fatalf("insert %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
// barrier flushes the apply loop: enqueued before it, PurgeDirectoryChildren is
|
||||
// asynchronous, so a synchronous apply-loop call afterward guarantees the purge
|
||||
// has been processed (the loop is FIFO and single-threaded).
|
||||
func barrier(t *testing.T, mc *MetaCache) {
|
||||
t.Helper()
|
||||
if err := mc.AbortDirectoryBuild(context.Background(), util.FullPath("/__barrier__")); err != nil {
|
||||
t.Fatalf("barrier: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPurgeSkippedWhileDirectoryBuilding is the core regression guard: a purge
|
||||
// (idle eviction / kernel Forget / copy-range fallback) that lands while a
|
||||
// directory is being rebuilt must NOT wipe the entries the build just inserted.
|
||||
// Otherwise CompleteDirectoryBuild marks the directory cached over an empty
|
||||
// store, and every file in it vanishes from the mount though it is safe on the
|
||||
// filer.
|
||||
func TestPurgeSkippedWhileDirectoryBuilding(t *testing.T) {
|
||||
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{"/": true})
|
||||
defer mc.Shutdown()
|
||||
|
||||
dir := util.FullPath("/dir")
|
||||
if err := mc.BeginDirectoryBuild(context.Background(), dir); err != nil {
|
||||
t.Fatalf("begin build: %v", err)
|
||||
}
|
||||
insertCacheEntry(t, mc, "/dir/a.txt")
|
||||
insertCacheEntry(t, mc, "/dir/b.txt")
|
||||
|
||||
// A concurrent eviction tries to purge the directory mid-build. It is
|
||||
// enqueued before CompleteDirectoryBuild, so the apply loop processes it
|
||||
// first — while /dir is still building.
|
||||
var resetCalls int32
|
||||
mc.PurgeDirectoryChildren(dir, func() { atomic.AddInt32(&resetCalls, 1) })
|
||||
|
||||
if err := mc.CompleteDirectoryBuild(context.Background(), dir, 0, nil); err != nil {
|
||||
t.Fatalf("complete build: %v", err)
|
||||
}
|
||||
|
||||
if got := atomic.LoadInt32(&resetCalls); got != 0 {
|
||||
t.Fatalf("resetFn ran %d times during build; purge must be skipped", got)
|
||||
}
|
||||
if !mc.IsDirectoryCached(dir) {
|
||||
t.Fatal("/dir should be cached after build completes")
|
||||
}
|
||||
for _, name := range []string{"/dir/a.txt", "/dir/b.txt"} {
|
||||
if _, _, err := mc.FindEntry(context.Background(), util.FullPath(name)); err != nil {
|
||||
t.Fatalf("%s missing after mid-build purge: %v", name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestPurgeClearsWhenNotBuilding verifies the eviction path still works off the
|
||||
// apply loop: with no build in flight, the purge resets the cached flag and
|
||||
// wipes the store.
|
||||
func TestPurgeClearsWhenNotBuilding(t *testing.T) {
|
||||
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{"/": true, "/dir": true})
|
||||
defer mc.Shutdown()
|
||||
|
||||
dir := util.FullPath("/dir")
|
||||
insertCacheEntry(t, mc, "/dir/a.txt")
|
||||
|
||||
var resetCalls int32
|
||||
mc.PurgeDirectoryChildren(dir, func() { atomic.AddInt32(&resetCalls, 1) })
|
||||
barrier(t, mc)
|
||||
|
||||
if got := atomic.LoadInt32(&resetCalls); got != 1 {
|
||||
t.Fatalf("resetFn ran %d times; want 1", got)
|
||||
}
|
||||
if _, _, err := mc.FindEntry(context.Background(), util.FullPath("/dir/a.txt")); err == nil {
|
||||
t.Fatal("/dir/a.txt should have been purged")
|
||||
}
|
||||
}
|
||||
95
weed/mount/meta_cache/meta_cache_rename_gate_test.go
Normal file
95
weed/mount/meta_cache/meta_cache_rename_gate_test.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package meta_cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
)
|
||||
|
||||
// A rename stamps its source and takes the name away at the same log position,
|
||||
// so the removal arrives at exactly the version the source already records.
|
||||
// Read as a write that is already reflected, it was dropped and the source
|
||||
// stayed visible next to the destination.
|
||||
func TestRenameRemovesSourceAtItsOwnVersion(t *testing.T) {
|
||||
const version = 1787761708173717200
|
||||
|
||||
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{"/": true, "/dir": true})
|
||||
defer mc.Shutdown()
|
||||
ctx := context.Background()
|
||||
|
||||
write := func(name string, tsNs int64) *filer_pb.SubscribeMetadataResponse {
|
||||
return &filer_pb.SubscribeMetadataResponse{
|
||||
Directory: "/dir",
|
||||
EventNotification: &filer_pb.EventNotification{
|
||||
NewEntry: &filer_pb.Entry{
|
||||
Name: name,
|
||||
Attributes: &filer_pb.FuseAttributes{Crtime: 1, Mtime: 1, FileMode: 0100644, FileSize: 3},
|
||||
},
|
||||
},
|
||||
TsNs: tsNs,
|
||||
}
|
||||
}
|
||||
for _, event := range []*filer_pb.SubscribeMetadataResponse{write("src", version), write("dst", version-1000)} {
|
||||
if err := mc.ApplyMetadataResponseOwned(ctx, event, LocalMetadataResponseApplyOptions); err != nil {
|
||||
t.Fatalf("seed %s: %v", event.EventNotification.NewEntry.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
rename := &filer_pb.SubscribeMetadataResponse{
|
||||
Directory: "/dir",
|
||||
EventNotification: &filer_pb.EventNotification{
|
||||
OldEntry: &filer_pb.Entry{Name: "src"},
|
||||
NewEntry: &filer_pb.Entry{
|
||||
Name: "dst",
|
||||
Attributes: &filer_pb.FuseAttributes{Crtime: 1, Mtime: 1, FileMode: 0100644, FileSize: 3},
|
||||
},
|
||||
NewParentPath: "/dir",
|
||||
},
|
||||
TsNs: version,
|
||||
}
|
||||
if err := mc.ApplyMetadataResponseOwned(ctx, rename, LocalMetadataResponseApplyOptions); err != nil {
|
||||
t.Fatalf("rename: %v", err)
|
||||
}
|
||||
|
||||
if entry, _, err := mc.FindEntry(ctx, "/dir/src"); err == nil && entry != nil {
|
||||
t.Error("the renamed-away source is still cached")
|
||||
}
|
||||
if entry, _, err := mc.FindEntry(ctx, "/dir/dst"); err != nil || entry == nil {
|
||||
t.Errorf("the rename destination is missing: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// A removal older than the entry's record is still stale and must not apply:
|
||||
// the record describes a write the removal never saw.
|
||||
func TestRemovalOlderThanTheRecordStillFences(t *testing.T) {
|
||||
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{"/": true, "/dir": true})
|
||||
defer mc.Shutdown()
|
||||
ctx := context.Background()
|
||||
|
||||
if err := mc.ApplyMetadataResponseOwned(ctx, &filer_pb.SubscribeMetadataResponse{
|
||||
Directory: "/dir",
|
||||
EventNotification: &filer_pb.EventNotification{
|
||||
NewEntry: &filer_pb.Entry{
|
||||
Name: "file",
|
||||
Attributes: &filer_pb.FuseAttributes{Crtime: 1, Mtime: 1, FileMode: 0100644, FileSize: 3},
|
||||
},
|
||||
},
|
||||
TsNs: 2000,
|
||||
}, LocalMetadataResponseApplyOptions); err != nil {
|
||||
t.Fatalf("seed: %v", err)
|
||||
}
|
||||
|
||||
if err := mc.ApplyMetadataResponseOwned(ctx, &filer_pb.SubscribeMetadataResponse{
|
||||
Directory: "/dir",
|
||||
EventNotification: &filer_pb.EventNotification{OldEntry: &filer_pb.Entry{Name: "file"}},
|
||||
TsNs: 1000,
|
||||
}, LocalMetadataResponseApplyOptions); err != nil {
|
||||
t.Fatalf("stale removal: %v", err)
|
||||
}
|
||||
|
||||
if entry, _, err := mc.FindEntry(ctx, "/dir/file"); err != nil || entry == nil {
|
||||
t.Errorf("a removal older than the entry's record deleted it: %v", err)
|
||||
}
|
||||
}
|
||||
422
weed/mount/meta_cache/meta_cache_sections.go
Normal file
422
weed/mount/meta_cache/meta_cache_sections.go
Normal file
@@ -0,0 +1,422 @@
|
||||
package meta_cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"math"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/filer"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
)
|
||||
|
||||
// A cached directory's listing is split into contiguous name-range sections so
|
||||
// a burst of remote changes invalidates one section, not the whole listing.
|
||||
// Events keep applying to a stale section; staleness only means the next
|
||||
// listing re-validates that range against the filer before serving it, and
|
||||
// lookups in it read through until then.
|
||||
const (
|
||||
// dirSectionSize is the target entries per section, fixed when a listing
|
||||
// is built and re-derived when a re-listed section has outgrown it.
|
||||
dirSectionSize = 1024
|
||||
// sectionHotThreshold remote changes within sectionHotWindow invalidate
|
||||
// the section they land in.
|
||||
sectionHotThreshold = 64
|
||||
sectionHotWindow = 2 * time.Second
|
||||
// sectionRefreshTimeout bounds how long a readdir waits on re-validating
|
||||
// a section before serving the maintained-but-unverified cache instead.
|
||||
sectionRefreshTimeout = 5 * time.Second
|
||||
// sectionRefreshMaxEntries is the most one refresh will carry; a section
|
||||
// grown past it is cheaper to re-tile with a full directory rebuild.
|
||||
sectionRefreshMaxEntries = 4 * dirSectionSize
|
||||
)
|
||||
|
||||
// ErrRefreshRangeTooLarge reports a section that outgrew one refresh; the
|
||||
// caller should drop the directory cache so a full rebuild re-tiles it.
|
||||
var ErrRefreshRangeTooLarge = errors.New("section outgrew one refresh")
|
||||
|
||||
// sectionList: bounds[i] is the first name of section i+1; section 0 starts at
|
||||
// the beginning of the namespace, the last section runs to the end. It is a
|
||||
// plain state machine — no locking, no store; MetaCache drives it under its
|
||||
// own mutex.
|
||||
type sectionList struct {
|
||||
bounds []string
|
||||
sections []sectionState
|
||||
}
|
||||
|
||||
type sectionState struct {
|
||||
stale bool
|
||||
updateCount int
|
||||
windowStart time.Time
|
||||
// unverifiable marks a stale section whose filer stamps no listing
|
||||
// snapshots: re-listing it can never vouch for it, so stop trying and
|
||||
// leave its lookups reading through.
|
||||
unverifiable bool
|
||||
// floorTsNs is the section's own listing snapshot: a refresh at it covered
|
||||
// every name in the range, present or absent, so it fences like the
|
||||
// directory floor but for this range alone.
|
||||
floorTsNs int64
|
||||
}
|
||||
|
||||
func newSectionTable(bounds []string) *sectionList {
|
||||
return §ionList{bounds: bounds, sections: make([]sectionState, len(bounds)+1)}
|
||||
}
|
||||
|
||||
// sectionBoundsCollector derives section boundaries from an ordered listing:
|
||||
// every dirSectionSize-th name starts a new section.
|
||||
type sectionBoundsCollector struct {
|
||||
count int
|
||||
bounds []string
|
||||
}
|
||||
|
||||
func (c *sectionBoundsCollector) note(name string) {
|
||||
if c.count > 0 && c.count%dirSectionSize == 0 {
|
||||
c.bounds = append(c.bounds, name)
|
||||
}
|
||||
c.count++
|
||||
}
|
||||
|
||||
// sectionRefresh carries one section's re-listing to the apply loop.
|
||||
type sectionRefresh struct {
|
||||
lo, hi string
|
||||
entries []*filer.Entry
|
||||
snapshotTsNs int64
|
||||
}
|
||||
|
||||
func (sl *sectionList) sectionOf(name string) int {
|
||||
idx := sort.SearchStrings(sl.bounds, name)
|
||||
if idx < len(sl.bounds) && sl.bounds[idx] == name {
|
||||
idx++
|
||||
}
|
||||
return idx
|
||||
}
|
||||
|
||||
// sectionRange returns the [lo, hi) name range of section idx; "" is unbounded.
|
||||
func (sl *sectionList) sectionRange(idx int) (lo, hi string) {
|
||||
if idx > 0 {
|
||||
lo = sl.bounds[idx-1]
|
||||
}
|
||||
if idx < len(sl.bounds) {
|
||||
hi = sl.bounds[idx]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// noteChange counts one change against the section it lands in, marking the
|
||||
// section stale when a burst crosses the threshold.
|
||||
func (sl *sectionList) noteChange(name string, now time.Time) {
|
||||
s := &sl.sections[sl.sectionOf(name)]
|
||||
if s.stale {
|
||||
return
|
||||
}
|
||||
if s.windowStart.IsZero() || now.Sub(s.windowStart) > sectionHotWindow {
|
||||
s.windowStart = now
|
||||
s.updateCount = 0
|
||||
}
|
||||
s.updateCount++
|
||||
if s.updateCount >= sectionHotThreshold {
|
||||
s.stale = true
|
||||
}
|
||||
}
|
||||
|
||||
func (sl *sectionList) isFresh(name string) bool {
|
||||
return !sl.sections[sl.sectionOf(name)].stale
|
||||
}
|
||||
|
||||
// floorOf returns the refresh snapshot covering this name, or zero when its
|
||||
// section has never been re-listed. A stale section keeps fencing: what its
|
||||
// last listing established stays established.
|
||||
func (sl *sectionList) floorOf(name string) int64 {
|
||||
return sl.sections[sl.sectionOf(name)].floorTsNs
|
||||
}
|
||||
|
||||
type nameRange struct {
|
||||
lo, hi string
|
||||
}
|
||||
|
||||
// staleRangesAhead returns the invalidated ranges worth re-listing from the
|
||||
// section holding startName to the end of the directory.
|
||||
func (sl *sectionList) staleRangesAhead(startName string) (ranges []nameRange) {
|
||||
for i := sl.sectionOf(startName); i < len(sl.sections); i++ {
|
||||
if sl.sections[i].stale && !sl.sections[i].unverifiable {
|
||||
lo, hi := sl.sectionRange(i)
|
||||
ranges = append(ranges, nameRange{lo: lo, hi: hi})
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// hasRange reports whether the table still has a section covering exactly
|
||||
// [lo, hi); a rebuild or re-split since a listing was taken retires the range
|
||||
// it described.
|
||||
func (sl *sectionList) hasRange(lo, hi string) bool {
|
||||
curLo, curHi := sl.sectionRange(sl.sectionOf(lo))
|
||||
return curLo == lo && curHi == hi
|
||||
}
|
||||
|
||||
// completeRefresh marks the section covering exactly [lo, hi) fresh after a
|
||||
// re-listing that fetched names at snapshotTsNs, re-splitting a section that
|
||||
// outgrew twice its target size. The snapshot becomes the section's floor. A
|
||||
// range the table no longer has is ignored — splicing bounds from a stale
|
||||
// range could leave the table unsorted — and an unversioned listing vouches
|
||||
// for nothing: the section stays stale, remembered as not worth re-listing.
|
||||
func (sl *sectionList) completeRefresh(lo, hi string, names []string, snapshotTsNs int64) bool {
|
||||
idx := sl.sectionOf(lo)
|
||||
if curLo, curHi := sl.sectionRange(idx); curLo != lo || curHi != hi {
|
||||
return false
|
||||
}
|
||||
if snapshotTsNs == 0 {
|
||||
sl.sections[idx].unverifiable = true
|
||||
return false
|
||||
}
|
||||
if len(names) > 2*dirSectionSize {
|
||||
var newBounds []string
|
||||
for i := dirSectionSize; i < len(names); i += dirSectionSize {
|
||||
newBounds = append(newBounds, names[i])
|
||||
}
|
||||
bounds := make([]string, 0, len(sl.bounds)+len(newBounds))
|
||||
bounds = append(bounds, sl.bounds[:idx]...)
|
||||
bounds = append(bounds, newBounds...)
|
||||
bounds = append(bounds, sl.bounds[idx:]...)
|
||||
sections := make([]sectionState, 0, len(bounds)+1)
|
||||
sections = append(sections, sl.sections[:idx]...)
|
||||
for i := 0; i <= len(newBounds); i++ {
|
||||
sections = append(sections, sectionState{floorTsNs: snapshotTsNs})
|
||||
}
|
||||
sections = append(sections, sl.sections[idx+1:]...)
|
||||
sl.bounds, sl.sections = bounds, sections
|
||||
} else {
|
||||
sl.sections[idx] = sectionState{floorTsNs: snapshotTsNs}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// noteSectionChangeLocked counts one remote change against the section of the
|
||||
// directory it lands in.
|
||||
func (mc *MetaCache) noteSectionChangeLocked(fp util.FullPath, now time.Time) {
|
||||
dir, name := fp.DirAndName()
|
||||
if sl := mc.dirSections[util.FullPath(dir)]; sl != nil {
|
||||
sl.noteChange(name, now)
|
||||
}
|
||||
}
|
||||
|
||||
// IsNameFresh reports whether the cached listing still vouches for this name.
|
||||
// A directory without section state vouches for all of it.
|
||||
func (mc *MetaCache) IsNameFresh(fp util.FullPath) bool {
|
||||
dir, name := fp.DirAndName()
|
||||
mc.RLock()
|
||||
defer mc.RUnlock()
|
||||
sl := mc.dirSections[util.FullPath(dir)]
|
||||
return sl == nil || sl.isFresh(name)
|
||||
}
|
||||
|
||||
func (mc *MetaCache) staleRangesAhead(dirPath util.FullPath, startName string) []nameRange {
|
||||
mc.RLock()
|
||||
defer mc.RUnlock()
|
||||
sl := mc.dirSections[dirPath]
|
||||
if sl == nil {
|
||||
return nil
|
||||
}
|
||||
return sl.staleRangesAhead(startName)
|
||||
}
|
||||
|
||||
func (mc *MetaCache) rangeStale(dirPath util.FullPath, lo string) bool {
|
||||
mc.RLock()
|
||||
defer mc.RUnlock()
|
||||
sl := mc.dirSections[dirPath]
|
||||
return sl != nil && !sl.isFresh(lo)
|
||||
}
|
||||
|
||||
// EnsureListingFresh re-validates every invalidated section from startName to
|
||||
// the end of the directory before a listing pages through them. A listing's
|
||||
// reach is unknowable up front — a resumed handle can skip far ahead, and
|
||||
// shrunken sections let one batch span many — so all of them are covered.
|
||||
func EnsureListingFresh(ctx context.Context, mc *MetaCache, client filer_pb.FilerClient, dirPath util.FullPath, startName string) error {
|
||||
ranges := mc.staleRangesAhead(dirPath, startName)
|
||||
if len(ranges) == 0 {
|
||||
return nil
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, sectionRefreshTimeout)
|
||||
defer cancel()
|
||||
for _, r := range ranges {
|
||||
if err := mc.refreshSection(ctx, client, dirPath, r.lo, r.hi); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mc *MetaCache) refreshSection(ctx context.Context, client filer_pb.FilerClient, dirPath util.FullPath, lo, hi string) error {
|
||||
_, err, _ := mc.visitGroup.Do(string(dirPath)+"\x00section\x00"+lo, func() (interface{}, error) {
|
||||
if !mc.rangeStale(dirPath, lo) {
|
||||
return nil, nil
|
||||
}
|
||||
entries, snapshotTsNs, err := mc.listFilerRange(ctx, client, dirPath, lo, hi)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, mc.enqueueAndWait(ctx, metadataApplyRequest{
|
||||
kind: metadataSectionRefresh,
|
||||
buildPath: dirPath,
|
||||
refresh: §ionRefresh{lo: lo, hi: hi, entries: entries, snapshotTsNs: snapshotTsNs},
|
||||
})
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// listFilerRange reads [lo, hi) from the filer at one snapshot, paging by
|
||||
// section-sized batches.
|
||||
func (mc *MetaCache) listFilerRange(ctx context.Context, client filer_pb.FilerClient, dirPath util.FullPath, lo, hi string) (entries []*filer.Entry, snapshotTsNs int64, err error) {
|
||||
startFrom, includeStart := lo, lo != ""
|
||||
for {
|
||||
var page []*filer.Entry
|
||||
var pageCount int
|
||||
var last string
|
||||
done := false
|
||||
err = client.WithFilerClient(false, func(sc filer_pb.SeaweedFilerClient) error {
|
||||
// reset in case a failover retry re-runs a partly streamed page
|
||||
page, pageCount, last, done = nil, 0, "", false
|
||||
ts, listErr := filer_pb.DoSeaweedListWithSnapshot(ctx, sc, dirPath, "", func(pbEntry *filer_pb.Entry, isLast bool) error {
|
||||
pageCount++
|
||||
last = pbEntry.Name
|
||||
if hi != "" && pbEntry.Name >= hi {
|
||||
done = true
|
||||
}
|
||||
if done {
|
||||
return nil
|
||||
}
|
||||
if !mc.includeSystemEntries && IsHiddenSystemEntry(string(dirPath), pbEntry.Name) {
|
||||
return nil
|
||||
}
|
||||
page = append(page, filer.FromPbEntry(string(dirPath), pbEntry))
|
||||
return nil
|
||||
}, startFrom, includeStart, dirSectionSize, snapshotTsNs)
|
||||
if listErr != nil {
|
||||
return listErr
|
||||
}
|
||||
if snapshotTsNs == 0 {
|
||||
snapshotTsNs = ts
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
entries = append(entries, page...)
|
||||
if len(entries) > sectionRefreshMaxEntries {
|
||||
return nil, 0, ErrRefreshRangeTooLarge
|
||||
}
|
||||
if done || pageCount < dirSectionSize {
|
||||
return entries, snapshotTsNs, nil
|
||||
}
|
||||
startFrom, includeStart = last, false
|
||||
}
|
||||
}
|
||||
|
||||
// applySectionRefreshNow reconciles one section against a filer listing of its
|
||||
// range, then marks it fresh. Runs on the apply loop; mutations go through the
|
||||
// version gate so the listing cannot roll back a newer applied event, and
|
||||
// pinned local-only entries (deferred creates not yet on the filer) survive.
|
||||
func (mc *MetaCache) applySectionRefreshNow(ctx context.Context, dirPath util.FullPath, r *sectionRefresh) error {
|
||||
lo, hi, snapshotTsNs := r.lo, r.hi, r.snapshotTsNs
|
||||
|
||||
// A build wipes and repopulates the store off-loop; reconciling against
|
||||
// it would sweep children the build already inserted and publish the
|
||||
// directory incomplete. The staleness dies with the build's fresh table.
|
||||
if mc.isBuildingDir(dirPath) {
|
||||
return nil
|
||||
}
|
||||
|
||||
mc.Lock()
|
||||
defer mc.Unlock()
|
||||
|
||||
// With no per-entry versions, only the section floor fences this work; a
|
||||
// range the rebuilt or re-split table no longer has gets no floor, so it
|
||||
// must not touch the store either. The lock is held through the floor
|
||||
// install below, so the check cannot go stale.
|
||||
sl := mc.dirSections[dirPath]
|
||||
if sl == nil || !sl.hasRange(lo, hi) {
|
||||
return nil
|
||||
}
|
||||
|
||||
fetchedNames := make([]string, 0, len(r.entries))
|
||||
fetched := make(map[string]struct{}, len(r.entries))
|
||||
for _, entry := range r.entries {
|
||||
fetchedNames = append(fetchedNames, entry.Name())
|
||||
fetched[entry.Name()] = struct{}{}
|
||||
if snapshotTsNs == 0 {
|
||||
// A pre-upgrade filer stamps no snapshot, leaving nothing to
|
||||
// order against: only fill gaps, so a concurrently applied event
|
||||
// can never be rolled back.
|
||||
if mc.entryExistsLocked(ctx, entry.FullPath) {
|
||||
continue
|
||||
}
|
||||
if _, tombstone := mc.getEntryVersionRecordLocked(ctx, entry.FullPath); tombstone {
|
||||
continue
|
||||
}
|
||||
if err := mc.localStore.InsertEntry(ctx, entry); err != nil {
|
||||
return err
|
||||
}
|
||||
mc.setEntryVersionLocked(ctx, entry.FullPath, 0)
|
||||
continue
|
||||
}
|
||||
if mc.entryVersionBlocksLocked(ctx, entry.FullPath, snapshotTsNs, false) {
|
||||
continue
|
||||
}
|
||||
// An unversioned marker would bypass the section floor, so it cannot
|
||||
// outlive the snapshot write that replaces its content — but pinned
|
||||
// local-only state stays authoritative and is not replaced at all.
|
||||
_, _, unversioned := mc.entryVersionRecordLocked(ctx, entry.FullPath)
|
||||
if unversioned {
|
||||
if existing, findErr := mc.localStore.FindEntry(ctx, entry.FullPath); findErr == nil && existing != nil && mc.pinnedChildFn != nil && mc.pinnedChildFn(existing) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
// no per-entry version: the section floor set below covers the range
|
||||
if err := mc.localStore.InsertEntry(ctx, entry); err != nil {
|
||||
return err
|
||||
}
|
||||
if unversioned {
|
||||
// only once the write landed: if it fails, the old content keeps
|
||||
// the marker, and with it the right to be corrected by any event
|
||||
mc.clearEntryVersionLocked(ctx, entry.FullPath)
|
||||
}
|
||||
}
|
||||
|
||||
// Deletions need the snapshot as an ordering reference; without one a
|
||||
// name created after the listing would be swept away.
|
||||
if snapshotTsNs != 0 {
|
||||
var vanished []*filer.Entry
|
||||
if _, err := mc.localStore.ListDirectoryEntries(ctx, dirPath, lo, true, math.MaxInt64, func(entry *filer.Entry) (bool, error) {
|
||||
if hi != "" && entry.Name() >= hi {
|
||||
return false, nil
|
||||
}
|
||||
if _, found := fetched[entry.Name()]; !found {
|
||||
vanished = append(vanished, entry)
|
||||
}
|
||||
return true, nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, entry := range vanished {
|
||||
if mc.pinnedChildFn != nil && mc.pinnedChildFn(entry) {
|
||||
continue
|
||||
}
|
||||
if mc.entryVersionBlocksLocked(ctx, entry.FullPath, snapshotTsNs, true) {
|
||||
continue
|
||||
}
|
||||
if err := mc.localStore.DeleteEntry(ctx, entry.FullPath); err != nil {
|
||||
return err
|
||||
}
|
||||
mc.clearEntryVersionLocked(ctx, entry.FullPath)
|
||||
}
|
||||
}
|
||||
|
||||
// The floor fences the whole range, absent names included; an unversioned
|
||||
// listing sets none and the section stays stale, its lookups reading
|
||||
// through, with no further re-listing attempts.
|
||||
sl.completeRefresh(lo, hi, fetchedNames, snapshotTsNs)
|
||||
return nil
|
||||
}
|
||||
974
weed/mount/meta_cache/meta_cache_sections_test.go
Normal file
974
weed/mount/meta_cache/meta_cache_sections_test.go
Normal file
@@ -0,0 +1,974 @@
|
||||
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: §ionRefresh{
|
||||
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: §ionRefresh{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 §ionTestFilerClient{addr: listener.Addr().String()}
|
||||
}
|
||||
|
||||
func TestEnsureListingFreshRefreshesFromFiler(t *testing.T) {
|
||||
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{
|
||||
"/": true,
|
||||
"/dir": true,
|
||||
})
|
||||
defer mc.Shutdown()
|
||||
|
||||
server := §ionFilerServer{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 := §ionFilerServer{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: §ionRefresh{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: §ionRefresh{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 := §ionFilerServer{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: §ionRefresh{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: §ionRefresh{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: §ionRefresh{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: §ionRefresh{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: §ionRefresh{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: §ionRefresh{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: §ionRefresh{
|
||||
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")
|
||||
}
|
||||
}
|
||||
102
weed/mount/meta_cache/meta_cache_subscribe.go
Normal file
102
weed/mount/meta_cache/meta_cache_subscribe.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package meta_cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/filer"
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
"github.com/seaweedfs/seaweedfs/weed/wdclient"
|
||||
)
|
||||
|
||||
type MetadataFollower struct {
|
||||
PathPrefixToWatch string
|
||||
ProcessEventFn func(resp *filer_pb.SubscribeMetadataResponse) error
|
||||
}
|
||||
|
||||
func mergeProcessors(mainProcessor func(resp *filer_pb.SubscribeMetadataResponse) error, followers ...*MetadataFollower) func(resp *filer_pb.SubscribeMetadataResponse) error {
|
||||
return func(resp *filer_pb.SubscribeMetadataResponse) error {
|
||||
|
||||
// build the full path
|
||||
entry := resp.EventNotification.NewEntry
|
||||
if entry == nil {
|
||||
entry = resp.EventNotification.OldEntry
|
||||
}
|
||||
if entry != nil {
|
||||
dir := resp.Directory
|
||||
if resp.EventNotification.NewParentPath != "" {
|
||||
dir = resp.EventNotification.NewParentPath
|
||||
}
|
||||
fp := util.NewFullPath(dir, entry.Name)
|
||||
|
||||
for _, follower := range followers {
|
||||
if strings.HasPrefix(string(fp), follower.PathPrefixToWatch) {
|
||||
if err := follower.ProcessEventFn(resp); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return mainProcessor(resp)
|
||||
}
|
||||
}
|
||||
|
||||
func SubscribeMetaEvents(mc *MetaCache, selfSignature int32, client filer_pb.FilerClient, lookupFn wdclient.LookupFileIdFunctionType, dir string, lastTsNs int64, skipSelfEvents bool, onRetry func(lastTsNs int64, err error), followers ...*MetadataFollower) error {
|
||||
|
||||
var prefixes []string
|
||||
for _, follower := range followers {
|
||||
prefixes = append(prefixes, follower.PathPrefixToWatch)
|
||||
}
|
||||
|
||||
processEventFn := func(resp *filer_pb.SubscribeMetadataResponse) error {
|
||||
if skipSelfEvents && resp.EventNotification != nil {
|
||||
for _, sig := range resp.EventNotification.Signatures {
|
||||
if sig == selfSignature {
|
||||
glog.V(4).Infof("skip self-originated event %s", resp.Directory)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return mc.ApplyMetadataResponse(context.Background(), resp, SubscriberMetadataResponseApplyOptions)
|
||||
}
|
||||
|
||||
prefix := dir
|
||||
if !strings.HasSuffix(prefix, "/") {
|
||||
prefix = prefix + "/"
|
||||
}
|
||||
|
||||
// Replaying the persisted log chunks here keeps the cost off the filer's heap.
|
||||
// The caller's lookup says where to read them from, so a mount that cannot
|
||||
// reach volume servers directly still replays through its filer.
|
||||
metadataFollowOption := &pb.MetadataFollowOption{
|
||||
ClientName: "mount",
|
||||
ClientId: selfSignature,
|
||||
ClientEpoch: 1,
|
||||
SelfSignature: selfSignature,
|
||||
PathPrefix: prefix,
|
||||
AdditionalPathPrefixes: prefixes,
|
||||
DirectoriesToWatch: nil,
|
||||
StartTsNs: lastTsNs,
|
||||
StopTsNs: 0,
|
||||
EventErrorType: pb.FatalOnError,
|
||||
LogFileReaderFn: func(chunks []*filer_pb.FileChunk) (io.ReadCloser, error) {
|
||||
return filer.NewChunkStreamReaderFromLookup(context.Background(), lookupFn, chunks), nil
|
||||
},
|
||||
}
|
||||
util.RetryUntil("followMetaUpdates", func() error {
|
||||
metadataFollowOption.ClientEpoch++
|
||||
return pb.WithFilerClientFollowMetadata(client, metadataFollowOption, mergeProcessors(processEventFn, followers...))
|
||||
}, func(err error) bool {
|
||||
if onRetry != nil {
|
||||
onRetry(metadataFollowOption.StartTsNs, err)
|
||||
}
|
||||
glog.Errorf("follow metadata updates: %v", err)
|
||||
return true
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
138
weed/mount/meta_cache/oversized_dir_test.go
Normal file
138
weed/mount/meta_cache/oversized_dir_test.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package meta_cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/filer"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func listResponses(n int) []*filer_pb.ListEntriesResponse {
|
||||
responses := make([]*filer_pb.ListEntriesResponse, 0, n)
|
||||
for i := 0; i < n; i++ {
|
||||
responses = append(responses, &filer_pb.ListEntriesResponse{
|
||||
Entry: &filer_pb.Entry{
|
||||
Name: fmt.Sprintf("f%05d", i),
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
Crtime: 1, Mtime: 1, FileMode: 0o644, FileSize: 3,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
return responses
|
||||
}
|
||||
|
||||
// TestEnsureVisitedRefusesOversizedDirectory checks that a directory past the
|
||||
// limit is not cached, that the refusal is remembered, and that the partial
|
||||
// build leaves nothing behind in the local store.
|
||||
func TestEnsureVisitedRefusesOversizedDirectory(t *testing.T) {
|
||||
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{"/": true})
|
||||
defer mc.Shutdown()
|
||||
|
||||
accessor := &buildFilerAccessor{client: &buildListClient{responses: listResponses(10)}}
|
||||
|
||||
err := EnsureVisited(mc, accessor, util.FullPath("/dir"), 5)
|
||||
var tooLarge *DirectoryTooLargeError
|
||||
if !errors.As(err, &tooLarge) {
|
||||
t.Fatalf("EnsureVisited = %v, want DirectoryTooLargeError", err)
|
||||
}
|
||||
if tooLarge.Path != util.FullPath("/dir") {
|
||||
t.Fatalf("oversized path = %s, want /dir", tooLarge.Path)
|
||||
}
|
||||
if mc.IsDirectoryCached(util.FullPath("/dir")) {
|
||||
t.Error("oversized directory reported cached")
|
||||
}
|
||||
// The aborted build must leave no partial children to be served later.
|
||||
count := 0
|
||||
if _, err := mc.ListDirectoryEntries(context.Background(), util.FullPath("/dir"), "", false, 100, func(e *filer.Entry) (bool, error) {
|
||||
count++
|
||||
return true, nil
|
||||
}); err != nil {
|
||||
t.Fatalf("list: %v", err)
|
||||
}
|
||||
if count != 0 {
|
||||
t.Errorf("local store still holds %d children of the aborted build", count)
|
||||
}
|
||||
|
||||
// A second visit fails fast without streaming to the limit again.
|
||||
if err := EnsureVisited(mc, accessor, util.FullPath("/dir"), 5); !errors.As(err, &tooLarge) {
|
||||
t.Fatalf("second EnsureVisited = %v, want DirectoryTooLargeError", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestEnsureVisitedStepsOverOversizedAncestor checks a huge ancestor does not
|
||||
// wedge the caching of its subdirectories.
|
||||
func TestEnsureVisitedStepsOverOversizedAncestor(t *testing.T) {
|
||||
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{"/": true})
|
||||
defer mc.Shutdown()
|
||||
mc.markOversized(util.FullPath("/huge"))
|
||||
|
||||
accessor := &buildFilerAccessor{client: &buildListClient{responses: listResponses(3)}}
|
||||
if err := EnsureVisited(mc, accessor, util.FullPath("/huge/sub"), 5); err != nil {
|
||||
t.Fatalf("EnsureVisited under an oversized ancestor: %v", err)
|
||||
}
|
||||
if !mc.IsDirectoryCached(util.FullPath("/huge/sub")) {
|
||||
t.Error("subdirectory of an oversized ancestor was not cached")
|
||||
}
|
||||
if mc.IsDirectoryCached(util.FullPath("/huge")) {
|
||||
t.Error("oversized ancestor became cached")
|
||||
}
|
||||
}
|
||||
|
||||
// TestEnsureVisitedUnderTheLimitStillCaches pins that the gate does not change
|
||||
// behaviour for ordinary directories.
|
||||
func TestEnsureVisitedUnderTheLimitStillCaches(t *testing.T) {
|
||||
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{"/": true})
|
||||
defer mc.Shutdown()
|
||||
|
||||
accessor := &buildFilerAccessor{client: &buildListClient{responses: listResponses(5)}}
|
||||
if err := EnsureVisited(mc, accessor, util.FullPath("/dir"), 5); err != nil {
|
||||
t.Fatalf("EnsureVisited: %v", err)
|
||||
}
|
||||
if !mc.IsDirectoryCached(util.FullPath("/dir")) {
|
||||
t.Error("directory at the limit was not cached")
|
||||
}
|
||||
}
|
||||
|
||||
// pathListClient serves canned listings per directory, so one visit can see
|
||||
// directories of different sizes.
|
||||
type pathListClient struct {
|
||||
filer_pb.SeaweedFilerClient
|
||||
perDir map[string][]*filer_pb.ListEntriesResponse
|
||||
}
|
||||
|
||||
func (c *pathListClient) ListEntries(ctx context.Context, in *filer_pb.ListEntriesRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[filer_pb.ListEntriesResponse], error) {
|
||||
return &buildListStream{responses: c.perDir[in.Directory]}, nil
|
||||
}
|
||||
|
||||
// TestEnsureVisitedAncestorFoundOversizedMidVisit covers the first discovery:
|
||||
// the ancestor's refusal must neither cancel the descendant's build nor be
|
||||
// reported as the descendant's own.
|
||||
func TestEnsureVisitedAncestorFoundOversizedMidVisit(t *testing.T) {
|
||||
mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{"/": true})
|
||||
defer mc.Shutdown()
|
||||
|
||||
accessor := &buildFilerAccessor{client: &pathListClient{perDir: map[string][]*filer_pb.ListEntriesResponse{
|
||||
"/huge": listResponses(10),
|
||||
"/huge/sub": listResponses(3),
|
||||
}}}
|
||||
|
||||
if err := EnsureVisited(mc, accessor, util.FullPath("/huge/sub"), 5); err != nil {
|
||||
t.Fatalf("EnsureVisited: %v", err)
|
||||
}
|
||||
if !mc.IsDirectoryCached(util.FullPath("/huge/sub")) {
|
||||
t.Error("descendant of a just-discovered oversized ancestor was not cached")
|
||||
}
|
||||
if mc.IsDirectoryCached(util.FullPath("/huge")) {
|
||||
t.Error("oversized ancestor became cached")
|
||||
}
|
||||
if !mc.isOversized(util.FullPath("/huge")) {
|
||||
t.Error("oversized ancestor was not remembered")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user