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:
357
weed/pb/filer_pb/filer_client.go
Normal file
357
weed/pb/filer_pb/filer_client.go
Normal file
@@ -0,0 +1,357 @@
|
||||
package filer_pb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
)
|
||||
|
||||
var (
|
||||
OS_UID = uint32(os.Getuid())
|
||||
OS_GID = uint32(os.Getgid())
|
||||
)
|
||||
|
||||
type FilerClient interface {
|
||||
WithFilerClient(streamingMode bool, fn func(SeaweedFilerClient) error) error // 15 implementation
|
||||
AdjustedUrl(location *Location) string
|
||||
GetDataCenter() string
|
||||
}
|
||||
|
||||
// GetEntry returns the entry together with the log position the filer stamped
|
||||
// for it and the signature of the filer that stamped it — positions are only
|
||||
// comparable within one filer's clock. Both are zero against a filer that does
|
||||
// not stamp them.
|
||||
func GetEntry(ctx context.Context, filerClient FilerClient, fullFilePath util.FullPath) (entry *Entry, logTsNs int64, logSignature int32, err error) {
|
||||
|
||||
dir, name := fullFilePath.DirAndName()
|
||||
|
||||
err = filerClient.WithFilerClient(false, func(client SeaweedFilerClient) error {
|
||||
|
||||
request := &LookupDirectoryEntryRequest{
|
||||
Directory: dir,
|
||||
Name: name,
|
||||
}
|
||||
|
||||
// glog.V(3).Infof("read %s request: %v", fullFilePath, request)
|
||||
resp, err := LookupEntry(ctx, client, request)
|
||||
if err != nil {
|
||||
glog.V(3).InfofCtx(ctx, "read %s %v: %v", fullFilePath, resp, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.Entry == nil {
|
||||
// glog.V(3).Infof("read %s entry: %v", fullFilePath, entry)
|
||||
return nil
|
||||
}
|
||||
|
||||
entry = resp.Entry
|
||||
logTsNs = resp.LogTsNs
|
||||
logSignature = resp.LogSignature
|
||||
return nil
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ListSnapshotTsNsTrailerKey carries the listing snapshot in the ListEntries
|
||||
// stream trailer, so empty listings can convey it without a response older
|
||||
// consumers would mistake for an entry.
|
||||
const ListSnapshotTsNsTrailerKey = "sw-list-snapshot-ts-ns"
|
||||
|
||||
type EachEntryFunction func(entry *Entry, isLast bool) error
|
||||
|
||||
func ReadDirAllEntries(ctx context.Context, filerClient FilerClient, fullDirPath util.FullPath, prefix string, fn EachEntryFunction) (err error) {
|
||||
_, err = ReadDirAllEntriesWithSnapshot(ctx, filerClient, fullDirPath, prefix, fn)
|
||||
return err
|
||||
}
|
||||
|
||||
func ReadDirAllEntriesWithSnapshot(ctx context.Context, filerClient FilerClient, fullDirPath util.FullPath, prefix string, fn EachEntryFunction) (snapshotTsNs int64, err error) {
|
||||
var counter uint32
|
||||
var startFrom string
|
||||
var counterFunc = func(entry *Entry, isLast bool) error {
|
||||
counter++
|
||||
startFrom = entry.Name
|
||||
return fn(entry, isLast)
|
||||
}
|
||||
|
||||
var paginationLimit uint32 = 10000
|
||||
|
||||
if snapshotTsNs, err = doListWithSnapshot(ctx, filerClient, fullDirPath, prefix, counterFunc, "", false, paginationLimit, 0); err != nil {
|
||||
return snapshotTsNs, err
|
||||
}
|
||||
|
||||
for counter == paginationLimit {
|
||||
counter = 0
|
||||
lastStartFrom := startFrom
|
||||
if _, err = doListWithSnapshot(ctx, filerClient, fullDirPath, prefix, counterFunc, startFrom, false, paginationLimit, snapshotTsNs); err != nil {
|
||||
return snapshotTsNs, err
|
||||
}
|
||||
// A full page that ends on the name it started from would loop forever;
|
||||
// a store whose ordering does not advance past the cursor causes this.
|
||||
if counter == paginationLimit && startFrom == lastStartFrom {
|
||||
return snapshotTsNs, fmt.Errorf("list %s: pagination stuck at %q", fullDirPath, startFrom)
|
||||
}
|
||||
}
|
||||
|
||||
return snapshotTsNs, nil
|
||||
}
|
||||
|
||||
func List(ctx context.Context, filerClient FilerClient, parentDirectoryPath, prefix string, fn EachEntryFunction, startFrom string, inclusive bool, limit uint32) (err error) {
|
||||
return filerClient.WithFilerClient(false, func(client SeaweedFilerClient) error {
|
||||
return doSeaweedList(ctx, client, util.FullPath(parentDirectoryPath), prefix, fn, startFrom, inclusive, limit)
|
||||
})
|
||||
}
|
||||
|
||||
func doListWithSnapshot(ctx context.Context, filerClient FilerClient, fullDirPath util.FullPath, prefix string, fn EachEntryFunction, startFrom string, inclusive bool, limit uint32, snapshotTsNs int64) (actualSnapshotTsNs int64, err error) {
|
||||
err = filerClient.WithFilerClient(false, func(client SeaweedFilerClient) error {
|
||||
actualSnapshotTsNs, err = DoSeaweedListWithSnapshot(ctx, client, fullDirPath, prefix, fn, startFrom, inclusive, limit, snapshotTsNs)
|
||||
return err
|
||||
})
|
||||
return actualSnapshotTsNs, err
|
||||
}
|
||||
|
||||
func SeaweedList(ctx context.Context, client SeaweedFilerClient, parentDirectoryPath, prefix string, fn EachEntryFunction, startFrom string, inclusive bool, limit uint32) (err error) {
|
||||
return doSeaweedList(ctx, client, util.FullPath(parentDirectoryPath), prefix, fn, startFrom, inclusive, limit)
|
||||
}
|
||||
|
||||
func doSeaweedList(ctx context.Context, client SeaweedFilerClient, fullDirPath util.FullPath, prefix string, fn EachEntryFunction, startFrom string, inclusive bool, limit uint32) (err error) {
|
||||
_, err = DoSeaweedListWithSnapshot(ctx, client, fullDirPath, prefix, fn, startFrom, inclusive, limit, 0)
|
||||
return err
|
||||
}
|
||||
|
||||
func DoSeaweedListWithSnapshot(ctx context.Context, client SeaweedFilerClient, fullDirPath util.FullPath, prefix string, fn EachEntryFunction, startFrom string, inclusive bool, limit uint32, snapshotTsNs int64) (actualSnapshotTsNs int64, err error) {
|
||||
// Redundancy limit to make it correctly judge whether it is the last file.
|
||||
redLimit := limit
|
||||
|
||||
if limit < math.MaxInt32 && limit != 0 {
|
||||
redLimit = limit + 1
|
||||
}
|
||||
if redLimit > math.MaxInt32 {
|
||||
redLimit = math.MaxInt32
|
||||
}
|
||||
request := &ListEntriesRequest{
|
||||
Directory: string(fullDirPath),
|
||||
Prefix: prefix,
|
||||
StartFromFileName: startFrom,
|
||||
Limit: redLimit,
|
||||
InclusiveStartFrom: inclusive,
|
||||
SnapshotTsNs: snapshotTsNs,
|
||||
OmitChunks: ChunksOmitted(ctx),
|
||||
}
|
||||
|
||||
// Preserve the caller-requested snapshot so pagination uses the same
|
||||
// boundary across pages. For first requests (snapshotTsNs==0) we do NOT
|
||||
// synthesize a client-side timestamp — if the server returns no entries,
|
||||
// we return 0 so callers like CompleteDirectoryBuild know no server
|
||||
// snapshot was received and can replay all buffered events without
|
||||
// clock-skew-sensitive filtering.
|
||||
actualSnapshotTsNs = snapshotTsNs
|
||||
|
||||
glog.V(4).InfofCtx(ctx, "read directory: %v", request)
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
stream, err := client.ListEntries(ctx, request)
|
||||
if err != nil {
|
||||
// fullDirPath is the caller's bucket and prefix; keep the status the filer
|
||||
// sent so a retry is decided on that rather than on this message
|
||||
return actualSnapshotTsNs, fmt.Errorf("list %s: %w", fullDirPath, err)
|
||||
}
|
||||
|
||||
var prevEntry *Entry
|
||||
count := 0
|
||||
for {
|
||||
resp, recvErr := stream.Recv()
|
||||
if recvErr != nil {
|
||||
if recvErr == io.EOF {
|
||||
if prevEntry != nil {
|
||||
if err := fn(prevEntry, true); err != nil {
|
||||
return actualSnapshotTsNs, err
|
||||
}
|
||||
}
|
||||
// An empty listing carries no in-band snapshot (a snapshot-only
|
||||
// response would be read as an entry by older consumers); newer
|
||||
// filers send it in the stream trailer instead.
|
||||
if actualSnapshotTsNs == 0 {
|
||||
if values := stream.Trailer().Get(ListSnapshotTsNsTrailerKey); len(values) > 0 {
|
||||
if trailerTsNs, parseErr := strconv.ParseInt(values[0], 10, 64); parseErr == nil {
|
||||
actualSnapshotTsNs = trailerTsNs
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
} else {
|
||||
return actualSnapshotTsNs, recvErr
|
||||
}
|
||||
}
|
||||
if resp.SnapshotTsNs != 0 {
|
||||
actualSnapshotTsNs = resp.SnapshotTsNs
|
||||
}
|
||||
if prevEntry != nil {
|
||||
if err := fn(prevEntry, false); err != nil {
|
||||
return actualSnapshotTsNs, err
|
||||
}
|
||||
}
|
||||
prevEntry = resp.Entry
|
||||
count++
|
||||
if count > int(limit) && limit != 0 {
|
||||
prevEntry = nil
|
||||
}
|
||||
}
|
||||
|
||||
return actualSnapshotTsNs, nil
|
||||
}
|
||||
|
||||
func Exists(ctx context.Context, filerClient FilerClient, parentDirectoryPath string, entryName string, isDirectory bool) (exists bool, err error) {
|
||||
|
||||
err = filerClient.WithFilerClient(false, func(client SeaweedFilerClient) error {
|
||||
|
||||
request := &LookupDirectoryEntryRequest{
|
||||
Directory: parentDirectoryPath,
|
||||
Name: entryName,
|
||||
}
|
||||
|
||||
glog.V(4).InfofCtx(ctx, "exists entry %v/%v: %v", parentDirectoryPath, entryName, request)
|
||||
resp, err := LookupEntry(ctx, client, request)
|
||||
if err != nil {
|
||||
if err == ErrNotFound {
|
||||
exists = false
|
||||
return nil
|
||||
}
|
||||
glog.V(0).InfofCtx(ctx, "exists entry %v: %v", request, err)
|
||||
return fmt.Errorf("exists entry %s/%s: %v", parentDirectoryPath, entryName, err)
|
||||
}
|
||||
|
||||
exists = resp.Entry.IsDirectory == isDirectory
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func Mkdir(ctx context.Context, filerClient FilerClient, parentDirectoryPath string, dirName string, fn func(entry *Entry)) error {
|
||||
return filerClient.WithFilerClient(false, func(client SeaweedFilerClient) error {
|
||||
return DoMkdir(ctx, client, parentDirectoryPath, dirName, fn)
|
||||
})
|
||||
}
|
||||
|
||||
func DoMkdir(ctx context.Context, client SeaweedFilerClient, parentDirectoryPath string, dirName string, fn func(entry *Entry)) error {
|
||||
entry := &Entry{
|
||||
Name: dirName,
|
||||
IsDirectory: true,
|
||||
Attributes: &FuseAttributes{
|
||||
Mtime: time.Now().Unix(),
|
||||
Crtime: time.Now().Unix(),
|
||||
FileMode: uint32(0777 | os.ModeDir),
|
||||
Uid: OS_UID,
|
||||
Gid: OS_GID,
|
||||
},
|
||||
}
|
||||
|
||||
if fn != nil {
|
||||
fn(entry)
|
||||
}
|
||||
|
||||
request := &CreateEntryRequest{
|
||||
Directory: parentDirectoryPath,
|
||||
Entry: entry,
|
||||
}
|
||||
|
||||
glog.V(1).InfofCtx(ctx, "mkdir: %v", request)
|
||||
if err := CreateEntry(ctx, client, request); err != nil {
|
||||
glog.V(0).InfofCtx(ctx, "mkdir %v: %v", request, err)
|
||||
return fmt.Errorf("mkdir %s/%s: %w", parentDirectoryPath, dirName, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func MkFile(ctx context.Context, filerClient FilerClient, parentDirectoryPath string, fileName string, chunks []*FileChunk, fn func(entry *Entry)) error {
|
||||
return filerClient.WithFilerClient(false, func(client SeaweedFilerClient) error {
|
||||
|
||||
entry := &Entry{
|
||||
Name: fileName,
|
||||
IsDirectory: false,
|
||||
Attributes: &FuseAttributes{
|
||||
Mtime: time.Now().Unix(),
|
||||
Crtime: time.Now().Unix(),
|
||||
FileMode: uint32(0770),
|
||||
Uid: OS_UID,
|
||||
Gid: OS_GID,
|
||||
},
|
||||
Chunks: chunks,
|
||||
}
|
||||
|
||||
if fn != nil {
|
||||
fn(entry)
|
||||
}
|
||||
|
||||
request := &CreateEntryRequest{
|
||||
Directory: parentDirectoryPath,
|
||||
Entry: entry,
|
||||
}
|
||||
|
||||
glog.V(1).InfofCtx(ctx, "create file: %s/%s", parentDirectoryPath, fileName)
|
||||
if err := CreateEntry(ctx, client, request); err != nil {
|
||||
glog.V(0).InfofCtx(ctx, "create file %v:%v", request, err)
|
||||
return fmt.Errorf("create file %s/%s: %w", parentDirectoryPath, fileName, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func Remove(ctx context.Context, filerClient FilerClient, parentDirectoryPath, name string, isDeleteData, isRecursive, ignoreRecursiveErr, isFromOtherCluster bool, signatures []int32) error {
|
||||
_, err := RemoveWithResponse(ctx, filerClient, parentDirectoryPath, name, isDeleteData, isRecursive, ignoreRecursiveErr, isFromOtherCluster, signatures)
|
||||
return err
|
||||
}
|
||||
|
||||
func DoRemove(ctx context.Context, client SeaweedFilerClient, parentDirectoryPath string, name string, isDeleteData bool, isRecursive bool, ignoreRecursiveErr bool, isFromOtherCluster bool, signatures []int32) error {
|
||||
_, err := DoRemoveWithResponse(ctx, client, parentDirectoryPath, name, isDeleteData, isRecursive, ignoreRecursiveErr, isFromOtherCluster, signatures)
|
||||
return err
|
||||
}
|
||||
|
||||
func RemoveWithResponse(ctx context.Context, filerClient FilerClient, parentDirectoryPath, name string, isDeleteData, isRecursive, ignoreRecursiveErr, isFromOtherCluster bool, signatures []int32) (*DeleteEntryResponse, error) {
|
||||
var resp *DeleteEntryResponse
|
||||
err := filerClient.WithFilerClient(false, func(client SeaweedFilerClient) error {
|
||||
var innerErr error
|
||||
resp, innerErr = DoRemoveWithResponse(ctx, client, parentDirectoryPath, name, isDeleteData, isRecursive, ignoreRecursiveErr, isFromOtherCluster, signatures)
|
||||
return innerErr
|
||||
})
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func DoRemoveWithResponse(ctx context.Context, client SeaweedFilerClient, parentDirectoryPath string, name string, isDeleteData bool, isRecursive bool, ignoreRecursiveErr bool, isFromOtherCluster bool, signatures []int32) (*DeleteEntryResponse, error) {
|
||||
deleteEntryRequest := &DeleteEntryRequest{
|
||||
Directory: parentDirectoryPath,
|
||||
Name: name,
|
||||
IsDeleteData: isDeleteData,
|
||||
IsRecursive: isRecursive,
|
||||
IgnoreRecursiveError: ignoreRecursiveErr,
|
||||
IsFromOtherCluster: isFromOtherCluster,
|
||||
Signatures: signatures,
|
||||
}
|
||||
if resp, err := client.DeleteEntry(ctx, deleteEntryRequest); err != nil {
|
||||
if strings.Contains(err.Error(), ErrNotFound.Error()) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
} else {
|
||||
if resp.Error != "" {
|
||||
if strings.Contains(resp.Error, ErrNotFound.Error()) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, errors.New(resp.Error)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
}
|
||||
141
weed/pb/filer_pb/filer_client_bfs.go
Normal file
141
weed/pb/filer_pb/filer_client_bfs.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package filer_pb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
)
|
||||
|
||||
func TraverseBfs(ctx context.Context, filerClient FilerClient, parentPath util.FullPath, fn func(parentPath util.FullPath, entry *Entry) error) (err error) {
|
||||
K := 5
|
||||
|
||||
// callers hand in user-typed paths, and every entry is reported relative
|
||||
// to this one, so a trailing slash must not reach the callback
|
||||
parentPath = util.NormalizePath(string(parentPath))
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
queue := util.NewQueue[util.FullPath]()
|
||||
var pending sync.WaitGroup
|
||||
pending.Add(1)
|
||||
queue.Enqueue(parentPath)
|
||||
|
||||
var once sync.Once
|
||||
var firstErr error
|
||||
|
||||
// A directory delivered twice (a page-boundary race with concurrent
|
||||
// renames, or a store whose listing order misbehaves) must not be walked
|
||||
// twice: the second walk re-lists the same subtree and can keep the
|
||||
// traversal from ever terminating.
|
||||
var visited sync.Map
|
||||
visited.Store(string(parentPath), struct{}{})
|
||||
|
||||
enqueue := func(p util.FullPath) bool {
|
||||
// Stop expanding traversal once canceled (e.g. first error encountered).
|
||||
if ctx.Err() != nil {
|
||||
return false
|
||||
}
|
||||
if _, seen := visited.LoadOrStore(string(p), struct{}{}); seen {
|
||||
return true
|
||||
}
|
||||
pending.Add(1)
|
||||
queue.Enqueue(p)
|
||||
return true
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
var workers sync.WaitGroup
|
||||
for i := 0; i < K; i++ {
|
||||
workers.Add(1)
|
||||
go func() {
|
||||
defer workers.Done()
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
dir := queue.Dequeue()
|
||||
if dir == "" {
|
||||
// queue is empty for now
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Always mark the directory as done so the closer can finish.
|
||||
if ctx.Err() == nil {
|
||||
processErr := processOneDirectory(ctx, filerClient, dir, enqueue, fn)
|
||||
if processErr != nil {
|
||||
once.Do(func() {
|
||||
firstErr = processErr
|
||||
cancel()
|
||||
})
|
||||
}
|
||||
}
|
||||
pending.Done()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
pending.Wait()
|
||||
close(done)
|
||||
|
||||
workers.Wait()
|
||||
|
||||
return firstErr
|
||||
}
|
||||
|
||||
func processOneDirectory(ctx context.Context, filerClient FilerClient, parentPath util.FullPath, enqueue func(p util.FullPath) bool, fn func(parentPath util.FullPath, entry *Entry) error) (err error) {
|
||||
|
||||
return ReadDirAllEntries(ctx, filerClient, parentPath, "", func(entry *Entry, isLast bool) error {
|
||||
|
||||
if err := fn(parentPath, entry); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if entry.IsDirectory {
|
||||
if !enqueue(parentPath.Child(entry.Name)) {
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func StreamBfs(client SeaweedFilerClient, dir util.FullPath, olderThanTsNs int64, fn func(parentPath util.FullPath, entry *Entry) error) (err error) {
|
||||
glog.V(0).Infof("TraverseBfsMetadata %v if before %v", dir, time.Unix(0, olderThanTsNs))
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
stream, err := client.TraverseBfsMetadata(ctx, &TraverseBfsMetadataRequest{
|
||||
Directory: string(dir),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("traverse bfs metadata: %w", err)
|
||||
}
|
||||
for {
|
||||
resp, err := stream.Recv()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return fmt.Errorf("traverse bfs metadata: %w", err)
|
||||
}
|
||||
if err := fn(util.FullPath(resp.Directory), resp.Entry); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
191
weed/pb/filer_pb/filer_client_bfs_test.go
Normal file
191
weed/pb/filer_pb/filer_client_bfs_test.go
Normal file
@@ -0,0 +1,191 @@
|
||||
package filer_pb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
// stubFiler serves canned listings and counts how often each directory is listed.
|
||||
type stubFiler struct {
|
||||
UnimplementedSeaweedFilerServer
|
||||
mu sync.Mutex
|
||||
listed map[string]int
|
||||
listings func(req *ListEntriesRequest, send func(*Entry) error) error
|
||||
}
|
||||
|
||||
func (s *stubFiler) ListEntries(req *ListEntriesRequest, stream SeaweedFiler_ListEntriesServer) error {
|
||||
s.mu.Lock()
|
||||
if s.listed == nil {
|
||||
s.listed = make(map[string]int)
|
||||
}
|
||||
s.listed[req.Directory]++
|
||||
s.mu.Unlock()
|
||||
return s.listings(req, func(entry *Entry) error {
|
||||
return stream.Send(&ListEntriesResponse{Entry: entry})
|
||||
})
|
||||
}
|
||||
|
||||
func (s *stubFiler) listedCount(dir string) int {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.listed[dir]
|
||||
}
|
||||
|
||||
type stubFilerClient struct {
|
||||
conn *grpc.ClientConn
|
||||
}
|
||||
|
||||
func (c *stubFilerClient) WithFilerClient(streamingMode bool, fn func(SeaweedFilerClient) error) error {
|
||||
return fn(NewSeaweedFilerClient(c.conn))
|
||||
}
|
||||
|
||||
func (c *stubFilerClient) AdjustedUrl(location *Location) string { return location.Url }
|
||||
|
||||
func (c *stubFilerClient) GetDataCenter() string { return "" }
|
||||
|
||||
func startStubFiler(t *testing.T, s *stubFiler) *stubFilerClient {
|
||||
t.Helper()
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
grpcServer := grpc.NewServer()
|
||||
RegisterSeaweedFilerServer(grpcServer, s)
|
||||
go grpcServer.Serve(listener)
|
||||
t.Cleanup(grpcServer.Stop)
|
||||
conn, err := grpc.NewClient(listener.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { conn.Close() })
|
||||
return &stubFilerClient{conn: conn}
|
||||
}
|
||||
|
||||
// A directory delivered twice by a misbehaving listing must still be walked once.
|
||||
func TestTraverseBfsVisitsDuplicateDirectoryOnce(t *testing.T) {
|
||||
server := &stubFiler{}
|
||||
server.listings = func(req *ListEntriesRequest, send func(*Entry) error) error {
|
||||
switch req.Directory {
|
||||
case "/":
|
||||
if req.StartFromFileName != "" {
|
||||
return nil
|
||||
}
|
||||
for _, name := range []string{"dup", "dup"} {
|
||||
if err := send(&Entry{Name: name, IsDirectory: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case "/dup":
|
||||
if req.StartFromFileName != "" {
|
||||
return nil
|
||||
}
|
||||
for _, name := range []string{"a.txt", "b.txt"} {
|
||||
if err := send(&Entry{Name: name}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
filerClient := startStubFiler(t, server)
|
||||
|
||||
var mu sync.Mutex
|
||||
seen := make(map[string]int)
|
||||
err := TraverseBfs(context.Background(), filerClient, "/", func(parentPath util.FullPath, entry *Entry) error {
|
||||
mu.Lock()
|
||||
seen[string(parentPath.Child(entry.Name))]++
|
||||
mu.Unlock()
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if got := server.listedCount("/dup"); got != 1 {
|
||||
t.Fatalf("listed /dup %d times, want 1", got)
|
||||
}
|
||||
for _, path := range []string{"/dup/a.txt", "/dup/b.txt"} {
|
||||
if seen[path] != 1 {
|
||||
t.Fatalf("visited %s %d times, want 1", path, seen[path])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A store whose pagination never advances past the cursor must error out
|
||||
// instead of re-listing the same page forever.
|
||||
func TestReadDirAllEntriesStuckPagination(t *testing.T) {
|
||||
server := &stubFiler{}
|
||||
server.listings = func(req *ListEntriesRequest, send func(*Entry) error) error {
|
||||
for i := uint32(0); i < req.Limit; i++ {
|
||||
if err := send(&Entry{Name: fmt.Sprintf("f%05d", i)}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
filerClient := startStubFiler(t, server)
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- ReadDirAllEntries(context.Background(), filerClient, "/", "", func(entry *Entry, isLast bool) error {
|
||||
return nil
|
||||
})
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
if err == nil || !strings.Contains(err.Error(), "pagination stuck") {
|
||||
t.Fatalf("want pagination stuck error, got %v", err)
|
||||
}
|
||||
case <-time.After(30 * time.Second):
|
||||
t.Fatal("listing loops forever on a non-advancing store")
|
||||
}
|
||||
}
|
||||
|
||||
// A start path with a trailing slash must still descend into subdirectories.
|
||||
func TestTraverseBfsTrailingSlashRoot(t *testing.T) {
|
||||
server := &stubFiler{}
|
||||
server.listings = func(req *ListEntriesRequest, send func(*Entry) error) error {
|
||||
if req.StartFromFileName != "" {
|
||||
return nil
|
||||
}
|
||||
// the filer trims a single trailing slash, nothing more
|
||||
switch strings.TrimSuffix(req.Directory, "/") {
|
||||
case "/buckets/data":
|
||||
return send(&Entry{Name: "sub", IsDirectory: true})
|
||||
case "/buckets/data/sub":
|
||||
return send(&Entry{Name: "a.txt"})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
filerClient := startStubFiler(t, server)
|
||||
|
||||
var mu sync.Mutex
|
||||
var seen []string
|
||||
err := TraverseBfs(context.Background(), filerClient, "/buckets/data/", func(parentPath util.FullPath, entry *Entry) error {
|
||||
mu.Lock()
|
||||
seen = append(seen, fmt.Sprintf("%s -> %s", parentPath, entry.Name))
|
||||
mu.Unlock()
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
sort.Strings(seen)
|
||||
want := []string{"/buckets/data -> sub", "/buckets/data/sub -> a.txt"}
|
||||
if !reflect.DeepEqual(seen, want) {
|
||||
t.Fatalf("visited %v, want %v", seen, want)
|
||||
}
|
||||
}
|
||||
165
weed/pb/filer_pb/filer_client_snapshot_test.go
Normal file
165
weed/pb/filer_pb/filer_client_snapshot_test.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package filer_pb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type snapshotListStream struct {
|
||||
responses []*ListEntriesResponse
|
||||
index int
|
||||
}
|
||||
|
||||
func (s *snapshotListStream) Recv() (*ListEntriesResponse, error) {
|
||||
if s.index >= len(s.responses) {
|
||||
return nil, io.EOF
|
||||
}
|
||||
resp := s.responses[s.index]
|
||||
s.index++
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *snapshotListStream) Header() (metadata.MD, error) { return metadata.MD{}, nil }
|
||||
func (s *snapshotListStream) Trailer() metadata.MD { return metadata.MD{} }
|
||||
func (s *snapshotListStream) CloseSend() error { return nil }
|
||||
func (s *snapshotListStream) Context() context.Context { return context.Background() }
|
||||
func (s *snapshotListStream) SendMsg(any) error { return nil }
|
||||
func (s *snapshotListStream) RecvMsg(any) error { return nil }
|
||||
|
||||
type snapshotListClient struct {
|
||||
SeaweedFilerClient
|
||||
entries []*Entry
|
||||
requests []*ListEntriesRequest
|
||||
snapshotTs int64
|
||||
listCalled bool
|
||||
}
|
||||
|
||||
func (c *snapshotListClient) ListEntries(ctx context.Context, in *ListEntriesRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ListEntriesResponse], error) {
|
||||
c.listCalled = true
|
||||
c.requests = append(c.requests, proto.Clone(in).(*ListEntriesRequest))
|
||||
|
||||
start := 0
|
||||
if in.StartFromFileName != "" {
|
||||
start = len(c.entries)
|
||||
for i, entry := range c.entries {
|
||||
if entry.Name == in.StartFromFileName {
|
||||
start = i
|
||||
if !in.InclusiveStartFrom {
|
||||
start++
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
end := len(c.entries)
|
||||
if in.Limit > 0 && start+int(in.Limit) < end {
|
||||
end = start + int(in.Limit)
|
||||
}
|
||||
|
||||
snapshotTs := in.SnapshotTsNs
|
||||
if snapshotTs == 0 {
|
||||
snapshotTs = c.snapshotTs
|
||||
}
|
||||
|
||||
responses := make([]*ListEntriesResponse, 0, end-start)
|
||||
for i, entry := range c.entries[start:end] {
|
||||
resp := &ListEntriesResponse{
|
||||
Entry: entry,
|
||||
}
|
||||
if i == 0 {
|
||||
resp.SnapshotTsNs = snapshotTs
|
||||
}
|
||||
responses = append(responses, resp)
|
||||
}
|
||||
|
||||
return &snapshotListStream{responses: responses}, nil
|
||||
}
|
||||
|
||||
type snapshotFilerAccessor struct {
|
||||
client SeaweedFilerClient
|
||||
}
|
||||
|
||||
func (a *snapshotFilerAccessor) WithFilerClient(_ bool, fn func(SeaweedFilerClient) error) error {
|
||||
return fn(a.client)
|
||||
}
|
||||
|
||||
func (a *snapshotFilerAccessor) AdjustedUrl(*Location) string { return "" }
|
||||
func (a *snapshotFilerAccessor) GetDataCenter() string { return "" }
|
||||
|
||||
func TestReadDirAllEntriesWithSnapshotCarriesSnapshotAcrossPages(t *testing.T) {
|
||||
entries := make([]*Entry, 0, 10001)
|
||||
for i := 0; i < 10001; i++ {
|
||||
entries = append(entries, &Entry{Name: fmt.Sprintf("entry-%05d", i), Attributes: &FuseAttributes{}})
|
||||
}
|
||||
|
||||
client := &snapshotListClient{
|
||||
entries: entries,
|
||||
snapshotTs: 123456789,
|
||||
}
|
||||
accessor := &snapshotFilerAccessor{client: client}
|
||||
|
||||
var listed []string
|
||||
snapshotTs, err := ReadDirAllEntriesWithSnapshot(context.Background(), accessor, util.FullPath("/dir"), "", func(entry *Entry, isLast bool) error {
|
||||
listed = append(listed, entry.Name)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ReadDirAllEntriesWithSnapshot: %v", err)
|
||||
}
|
||||
|
||||
if got := len(listed); got != len(entries) {
|
||||
t.Fatalf("listed %d entries, want %d", got, len(entries))
|
||||
}
|
||||
if snapshotTs != client.snapshotTs {
|
||||
t.Fatalf("snapshotTs = %d, want %d", snapshotTs, client.snapshotTs)
|
||||
}
|
||||
if got := len(client.requests); got != 2 {
|
||||
t.Fatalf("request count = %d, want 2", got)
|
||||
}
|
||||
if client.requests[0].SnapshotTsNs != 0 {
|
||||
t.Fatalf("first request snapshot = %d, want 0", client.requests[0].SnapshotTsNs)
|
||||
}
|
||||
if client.requests[1].SnapshotTsNs != client.snapshotTs {
|
||||
t.Fatalf("second request snapshot = %d, want %d", client.requests[1].SnapshotTsNs, client.snapshotTs)
|
||||
}
|
||||
if client.requests[1].StartFromFileName != entries[9999].Name {
|
||||
t.Fatalf("second request marker = %q, want %q", client.requests[1].StartFromFileName, entries[9999].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadDirAllEntriesWithSnapshotEmptyDirectory(t *testing.T) {
|
||||
client := &snapshotListClient{
|
||||
entries: nil, // empty directory
|
||||
snapshotTs: 999888777,
|
||||
}
|
||||
accessor := &snapshotFilerAccessor{client: client}
|
||||
|
||||
var listed []string
|
||||
snapshotTs, err := ReadDirAllEntriesWithSnapshot(context.Background(), accessor, util.FullPath("/empty"), "", func(entry *Entry, isLast bool) error {
|
||||
listed = append(listed, entry.Name)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ReadDirAllEntriesWithSnapshot: %v", err)
|
||||
}
|
||||
if len(listed) != 0 {
|
||||
t.Fatalf("listed %d entries, want 0", len(listed))
|
||||
}
|
||||
// When the server sends no entries (empty directory), no snapshot is
|
||||
// received. The client returns 0 so callers like CompleteDirectoryBuild
|
||||
// know to replay all buffered events without clock-skew filtering.
|
||||
if snapshotTs != 0 {
|
||||
t.Fatalf("snapshotTs = %d, want 0 for empty directory", snapshotTs)
|
||||
}
|
||||
if !client.listCalled {
|
||||
t.Fatal("ListEntries was not invoked for the empty directory")
|
||||
}
|
||||
}
|
||||
1433
weed/pb/filer_pb/filer_grpc.pb.go
Normal file
1433
weed/pb/filer_pb/filer_grpc.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
142
weed/pb/filer_pb/filer_pb_event_scan.go
Normal file
142
weed/pb/filer_pb/filer_pb_event_scan.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package filer_pb
|
||||
|
||||
import (
|
||||
"google.golang.org/protobuf/encoding/protowire"
|
||||
)
|
||||
|
||||
// ScanMetadataEventSkeleton extracts just the fields the subscription matcher
|
||||
// reads, without materializing the entries' chunk lists. ok=false means the
|
||||
// caller must fall back to a full unmarshal.
|
||||
func ScanMetadataEventSkeleton(data []byte) (skeleton *SubscribeMetadataResponse, ok bool) {
|
||||
skeleton = &SubscribeMetadataResponse{}
|
||||
rest := data
|
||||
for len(rest) > 0 {
|
||||
num, typ, n := protowire.ConsumeTag(rest)
|
||||
if n < 0 {
|
||||
return nil, false
|
||||
}
|
||||
rest = rest[n:]
|
||||
switch {
|
||||
case num == 1 && typ == protowire.BytesType: // directory
|
||||
v, m := protowire.ConsumeBytes(rest)
|
||||
if m < 0 {
|
||||
return nil, false
|
||||
}
|
||||
skeleton.Directory = string(v)
|
||||
rest = rest[m:]
|
||||
case num == 2 && typ == protowire.BytesType: // event_notification
|
||||
if skeleton.EventNotification != nil {
|
||||
// repeated occurrences of a message field merge; punt to a full decode
|
||||
return nil, false
|
||||
}
|
||||
v, m := protowire.ConsumeBytes(rest)
|
||||
if m < 0 {
|
||||
return nil, false
|
||||
}
|
||||
notification, scanOk := scanEventNotificationSkeleton(v)
|
||||
if !scanOk {
|
||||
return nil, false
|
||||
}
|
||||
skeleton.EventNotification = notification
|
||||
rest = rest[m:]
|
||||
case num == 3 && typ == protowire.VarintType: // ts_ns
|
||||
v, m := protowire.ConsumeVarint(rest)
|
||||
if m < 0 {
|
||||
return nil, false
|
||||
}
|
||||
skeleton.TsNs = int64(v)
|
||||
rest = rest[m:]
|
||||
default:
|
||||
m := protowire.ConsumeFieldValue(num, typ, rest)
|
||||
if m < 0 {
|
||||
return nil, false
|
||||
}
|
||||
rest = rest[m:]
|
||||
}
|
||||
}
|
||||
return skeleton, true
|
||||
}
|
||||
|
||||
func scanEventNotificationSkeleton(data []byte) (*EventNotification, bool) {
|
||||
notification := &EventNotification{}
|
||||
rest := data
|
||||
for len(rest) > 0 {
|
||||
num, typ, n := protowire.ConsumeTag(rest)
|
||||
if n < 0 {
|
||||
return nil, false
|
||||
}
|
||||
rest = rest[n:]
|
||||
switch {
|
||||
case num == 1 && typ == protowire.BytesType: // old_entry
|
||||
if notification.OldEntry != nil {
|
||||
return nil, false
|
||||
}
|
||||
v, m := protowire.ConsumeBytes(rest)
|
||||
if m < 0 {
|
||||
return nil, false
|
||||
}
|
||||
name, scanOk := scanEntryName(v)
|
||||
if !scanOk {
|
||||
return nil, false
|
||||
}
|
||||
notification.OldEntry = &Entry{Name: name}
|
||||
rest = rest[m:]
|
||||
case num == 2 && typ == protowire.BytesType: // new_entry
|
||||
if notification.NewEntry != nil {
|
||||
return nil, false
|
||||
}
|
||||
v, m := protowire.ConsumeBytes(rest)
|
||||
if m < 0 {
|
||||
return nil, false
|
||||
}
|
||||
name, scanOk := scanEntryName(v)
|
||||
if !scanOk {
|
||||
return nil, false
|
||||
}
|
||||
notification.NewEntry = &Entry{Name: name}
|
||||
rest = rest[m:]
|
||||
case num == 4 && typ == protowire.BytesType: // new_parent_path
|
||||
v, m := protowire.ConsumeBytes(rest)
|
||||
if m < 0 {
|
||||
return nil, false
|
||||
}
|
||||
notification.NewParentPath = string(v)
|
||||
rest = rest[m:]
|
||||
default:
|
||||
m := protowire.ConsumeFieldValue(num, typ, rest)
|
||||
if m < 0 {
|
||||
return nil, false
|
||||
}
|
||||
rest = rest[m:]
|
||||
}
|
||||
}
|
||||
return notification, true
|
||||
}
|
||||
|
||||
// scanEntryName walks the whole entry so field order does not matter.
|
||||
func scanEntryName(data []byte) (string, bool) {
|
||||
name := ""
|
||||
rest := data
|
||||
for len(rest) > 0 {
|
||||
num, typ, n := protowire.ConsumeTag(rest)
|
||||
if n < 0 {
|
||||
return "", false
|
||||
}
|
||||
rest = rest[n:]
|
||||
if num == 1 && typ == protowire.BytesType { // name; last occurrence wins like proto merge
|
||||
v, m := protowire.ConsumeBytes(rest)
|
||||
if m < 0 {
|
||||
return "", false
|
||||
}
|
||||
name = string(v)
|
||||
rest = rest[m:]
|
||||
continue
|
||||
}
|
||||
m := protowire.ConsumeFieldValue(num, typ, rest)
|
||||
if m < 0 {
|
||||
return "", false
|
||||
}
|
||||
rest = rest[m:]
|
||||
}
|
||||
return name, true
|
||||
}
|
||||
60
weed/pb/filer_pb/filer_pb_event_scan_bench_test.go
Normal file
60
weed/pb/filer_pb/filer_pb_event_scan_bench_test.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package filer_pb
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func benchmarkEventBytes(b *testing.B, chunkCount int) []byte {
|
||||
b.Helper()
|
||||
chunks := make([]*FileChunk, chunkCount)
|
||||
for i := range chunks {
|
||||
chunks[i] = &FileChunk{FileId: "3,1234567890ab", Offset: int64(i) * 4096, Size: 4096, ModifiedTsNs: int64(i)}
|
||||
}
|
||||
data, err := proto.Marshal(&SubscribeMetadataResponse{
|
||||
Directory: "/data/pvc-42",
|
||||
TsNs: 123456789,
|
||||
EventNotification: &EventNotification{
|
||||
OldEntry: &Entry{Name: "0.log", Chunks: chunks},
|
||||
NewEntry: &Entry{Name: "0.log", Chunks: chunks},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// the non-matching subscriber case, which is what a per-path fleet hits for
|
||||
// almost every entry
|
||||
|
||||
func BenchmarkSubscriptionMatchFullDecode(b *testing.B) {
|
||||
data := benchmarkEventBytes(b, 1000)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
event := &SubscribeMetadataResponse{}
|
||||
if err := proto.Unmarshal(data, event); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if MetadataEventMatchesSubscription(event, "/data/pvc-7/", nil, nil) {
|
||||
b.Fatal("unexpected match")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSubscriptionMatchSkeleton(b *testing.B) {
|
||||
data := benchmarkEventBytes(b, 1000)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
skeleton, ok := ScanMetadataEventSkeleton(data)
|
||||
if !ok {
|
||||
b.Fatal("scan failed")
|
||||
}
|
||||
if MetadataEventMatchesSubscription(skeleton, "/data/pvc-7/", nil, nil) {
|
||||
b.Fatal("unexpected match")
|
||||
}
|
||||
}
|
||||
}
|
||||
112
weed/pb/filer_pb/filer_pb_event_scan_test.go
Normal file
112
weed/pb/filer_pb/filer_pb_event_scan_test.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package filer_pb
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"google.golang.org/protobuf/encoding/protowire"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func marshalEvent(t *testing.T, event *SubscribeMetadataResponse) []byte {
|
||||
t.Helper()
|
||||
data, err := proto.Marshal(event)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func bigEntry(name string) *Entry {
|
||||
chunks := make([]*FileChunk, 50)
|
||||
for i := range chunks {
|
||||
chunks[i] = &FileChunk{FileId: "3,1234567890ab", Offset: int64(i) * 100, Size: 100}
|
||||
}
|
||||
return &Entry{Name: name, Chunks: chunks, Attributes: &FuseAttributes{FileSize: 5000, Mtime: 1234567890}}
|
||||
}
|
||||
|
||||
func TestScanMetadataEventSkeletonMatchesFullDecode(t *testing.T) {
|
||||
events := map[string]*SubscribeMetadataResponse{
|
||||
"create": {Directory: "/data/pvc-7", TsNs: 100,
|
||||
EventNotification: &EventNotification{NewEntry: bigEntry("a.log")}},
|
||||
"update": {Directory: "/data/pvc-7", TsNs: 200,
|
||||
EventNotification: &EventNotification{OldEntry: bigEntry("a.log"), NewEntry: bigEntry("a.log")}},
|
||||
"delete": {Directory: "/data/pvc-7", TsNs: 300,
|
||||
EventNotification: &EventNotification{OldEntry: bigEntry("a.log"), DeleteChunks: true}},
|
||||
"rename within dir": {Directory: "/data/pvc-7", TsNs: 400,
|
||||
EventNotification: &EventNotification{OldEntry: bigEntry("a.log"), NewEntry: bigEntry("b.log")}},
|
||||
"rename across dirs": {Directory: "/data/pvc-7", TsNs: 500,
|
||||
EventNotification: &EventNotification{OldEntry: bigEntry("a.log"), NewEntry: bigEntry("a.log"), NewParentPath: "/data/pvc-9"}},
|
||||
"empty notification": {Directory: "/data/pvc-7", TsNs: 600,
|
||||
EventNotification: &EventNotification{}},
|
||||
"no notification": {Directory: "/data/pvc-7", TsNs: 700},
|
||||
}
|
||||
filters := []struct {
|
||||
prefix string
|
||||
prefixes []string
|
||||
dirs []string
|
||||
}{
|
||||
{prefix: "/data/pvc-7/"},
|
||||
{prefix: "/data/pvc-9/"},
|
||||
{prefix: "/data/pvc-1/"},
|
||||
{prefix: "/"},
|
||||
{prefixes: []string{"/other/", "/data/pvc-9/"}},
|
||||
{dirs: []string{"/data/pvc-7"}},
|
||||
{dirs: []string{"/data/pvc-9"}},
|
||||
}
|
||||
|
||||
for name, event := range events {
|
||||
skeleton, ok := ScanMetadataEventSkeleton(marshalEvent(t, event))
|
||||
if !ok {
|
||||
t.Fatalf("%s: scan failed", name)
|
||||
}
|
||||
if skeleton.TsNs != event.TsNs {
|
||||
t.Errorf("%s: ts %d != %d", name, skeleton.TsNs, event.TsNs)
|
||||
}
|
||||
for _, f := range filters {
|
||||
want := MetadataEventMatchesSubscription(event, f.prefix, f.prefixes, f.dirs)
|
||||
got := MetadataEventMatchesSubscription(skeleton, f.prefix, f.prefixes, f.dirs)
|
||||
if got != want {
|
||||
t.Errorf("%s with filter %+v: skeleton match %v, full match %v", name, f, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanMetadataEventSkeletonFallsBack(t *testing.T) {
|
||||
if _, ok := ScanMetadataEventSkeleton([]byte{0xff, 0xff, 0xff}); ok {
|
||||
t.Error("malformed payload must not scan")
|
||||
}
|
||||
|
||||
// two event_notification occurrences merge under proto semantics; the
|
||||
// scanner must punt rather than guess
|
||||
one := marshalEvent(t, &SubscribeMetadataResponse{
|
||||
EventNotification: &EventNotification{OldEntry: &Entry{Name: "a"}}})
|
||||
two := append(append([]byte{}, one...), one...)
|
||||
if _, ok := ScanMetadataEventSkeleton(two); ok {
|
||||
t.Error("duplicated message field must fall back to full decode")
|
||||
}
|
||||
|
||||
// same for a duplicated entry inside the notification
|
||||
entry, err := proto.Marshal(&Entry{Name: "a"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var notification []byte
|
||||
for i := 0; i < 2; i++ {
|
||||
notification = protowire.AppendTag(notification, 1, protowire.BytesType) // old_entry
|
||||
notification = protowire.AppendBytes(notification, entry)
|
||||
}
|
||||
var response []byte
|
||||
response = protowire.AppendTag(response, 2, protowire.BytesType) // event_notification
|
||||
response = protowire.AppendBytes(response, notification)
|
||||
if _, ok := ScanMetadataEventSkeleton(response); ok {
|
||||
t.Error("duplicated nested entry field must fall back to full decode")
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanMetadataEventSkeletonEmptyPayload(t *testing.T) {
|
||||
skeleton, ok := ScanMetadataEventSkeleton(nil)
|
||||
if !ok || skeleton.Directory != "" || skeleton.EventNotification != nil {
|
||||
t.Fatalf("empty payload: %+v ok=%v", skeleton, ok)
|
||||
}
|
||||
}
|
||||
376
weed/pb/filer_pb/filer_pb_helper.go
Normal file
376
weed/pb/filer_pb/filer_pb_helper.go
Normal file
@@ -0,0 +1,376 @@
|
||||
package filer_pb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
||||
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
"github.com/viant/ptrie"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const cutoffTimeNewEmptyDir = 3
|
||||
|
||||
func (entry *Entry) IsInRemoteOnly() bool {
|
||||
return len(entry.GetChunks()) == 0 && entry.RemoteEntry != nil && entry.RemoteEntry.RemoteSize > 0
|
||||
}
|
||||
|
||||
func (entry *Entry) IsDirectoryKeyObject() bool {
|
||||
// Also true for a file promoted to a directory by a child write, which keeps its
|
||||
// chunks/content, or its remote entry when the file was tiered to remote storage.
|
||||
return entry.IsDirectory &&
|
||||
((entry.Attributes != nil && entry.Attributes.Mime != "") || len(entry.GetChunks()) > 0 || len(entry.GetContent()) > 0 || entry.IsInRemoteOnly() || entry.IsPrefixObject())
|
||||
}
|
||||
|
||||
// IsPrefixObject reports whether the directory entry also holds the object named by its
|
||||
// path without a trailing slash, which an empty object leaves no other trace of.
|
||||
func (entry *Entry) IsPrefixObject() bool {
|
||||
if entry == nil || !entry.IsDirectory {
|
||||
return false
|
||||
}
|
||||
_, marked := entry.Extended[s3_constants.SeaweedFSPrefixObject]
|
||||
return marked
|
||||
}
|
||||
|
||||
// MarkPrefixObject turns a file entry into the directory entry that stores it, so keys
|
||||
// nested under its key can be created beside it.
|
||||
func (entry *Entry) MarkPrefixObject() {
|
||||
entry.IsDirectory = true
|
||||
if entry.Attributes != nil {
|
||||
entry.Attributes.FileMode |= uint32(os.ModeDir) | 0111
|
||||
}
|
||||
if entry.Extended == nil {
|
||||
entry.Extended = make(map[string][]byte)
|
||||
}
|
||||
entry.Extended[s3_constants.SeaweedFSPrefixObject] = []byte("true")
|
||||
}
|
||||
|
||||
func (entry *Entry) GetExpiryTime() (expiryTime int64) {
|
||||
// For S3 objects with lifecycle expiration, use Mtime (modification time)
|
||||
// For regular TTL entries, use Crtime (creation time) for backward compatibility
|
||||
if entry.Extended != nil {
|
||||
if _, hasS3Expiry := entry.Extended[s3_constants.SeaweedFSExpiresS3]; hasS3Expiry {
|
||||
// S3 lifecycle expiration: base TTL on modification time
|
||||
expiryTime = entry.Attributes.Mtime
|
||||
if expiryTime == 0 {
|
||||
expiryTime = entry.Attributes.Crtime
|
||||
}
|
||||
expiryTime += int64(entry.Attributes.TtlSec)
|
||||
return expiryTime
|
||||
}
|
||||
}
|
||||
|
||||
// Regular TTL expiration: base on creation time only
|
||||
expiryTime = entry.Attributes.Crtime + int64(entry.Attributes.TtlSec)
|
||||
return expiryTime
|
||||
}
|
||||
|
||||
func (entry *Entry) IsExpired() bool {
|
||||
return entry != nil && entry.Attributes != nil && entry.Attributes.TtlSec > 0 &&
|
||||
time.Now().Unix() >= entry.GetExpiryTime()
|
||||
}
|
||||
|
||||
func (entry *Entry) FileMode() (fileMode os.FileMode) {
|
||||
if entry != nil && entry.Attributes != nil {
|
||||
fileMode = os.FileMode(entry.Attributes.FileMode)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (entry *Entry) IsOlderDir() bool {
|
||||
return entry.IsDirectory && entry.Attributes != nil && entry.Attributes.Mime == "" && entry.Attributes.GetCrtime() <= time.Now().Unix()-cutoffTimeNewEmptyDir
|
||||
}
|
||||
|
||||
func ToFileIdObject(fileIdStr string) (*FileId, error) {
|
||||
t, err := needle.ParseFileIdFromString(fileIdStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &FileId{
|
||||
VolumeId: uint32(t.VolumeId),
|
||||
Cookie: uint32(t.Cookie),
|
||||
FileKey: uint64(t.Key),
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func (fid *FileId) toFileIdString() string {
|
||||
return needle.NewFileId(needle.VolumeId(fid.VolumeId), fid.FileKey, fid.Cookie).String()
|
||||
}
|
||||
|
||||
func (c *FileChunk) GetFileIdString() string {
|
||||
if c.FileId != "" {
|
||||
return c.FileId
|
||||
}
|
||||
if c.Fid != nil {
|
||||
c.FileId = c.Fid.toFileIdString()
|
||||
return c.FileId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func BeforeEntrySerialization(chunks []*FileChunk) {
|
||||
|
||||
for _, chunk := range chunks {
|
||||
|
||||
if chunk.FileId != "" {
|
||||
if fid, err := ToFileIdObject(chunk.FileId); err == nil {
|
||||
chunk.Fid = fid
|
||||
chunk.FileId = ""
|
||||
}
|
||||
}
|
||||
|
||||
if chunk.SourceFileId != "" {
|
||||
if fid, err := ToFileIdObject(chunk.SourceFileId); err == nil {
|
||||
chunk.SourceFid = fid
|
||||
chunk.SourceFileId = ""
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func AfterEntryDeserialization(chunks []*FileChunk) {
|
||||
|
||||
for _, chunk := range chunks {
|
||||
|
||||
if chunk.Fid != nil && chunk.FileId == "" {
|
||||
chunk.FileId = chunk.Fid.toFileIdString()
|
||||
}
|
||||
|
||||
if chunk.SourceFid != nil && chunk.SourceFileId == "" {
|
||||
chunk.SourceFileId = chunk.SourceFid.toFileIdString()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func CreateEntry(ctx context.Context, client SeaweedFilerClient, request *CreateEntryRequest) error {
|
||||
_, err := CreateEntryWithResponse(ctx, client, request)
|
||||
return err
|
||||
}
|
||||
|
||||
func CreateEntryWithResponse(ctx context.Context, client SeaweedFilerClient, request *CreateEntryRequest) (*CreateEntryResponse, error) {
|
||||
resp, err := client.CreateEntry(ctx, request)
|
||||
if err != nil {
|
||||
glog.V(1).InfofCtx(ctx, "create entry %s/%s %v: %v", request.Directory, request.Entry.Name, request.OExcl, err)
|
||||
return nil, fmt.Errorf("CreateEntry: %w", err)
|
||||
}
|
||||
if resp.ErrorCode != FilerError_OK {
|
||||
glog.V(1).InfofCtx(ctx, "create entry %s/%s %v: %v (code %v)", request.Directory, request.Entry.Name, request.OExcl, resp.Error, resp.ErrorCode)
|
||||
if sentinel := FilerErrorToSentinel(resp.ErrorCode); sentinel != nil {
|
||||
return nil, fmt.Errorf("CreateEntry %s/%s: %w", request.Directory, request.Entry.Name, sentinel)
|
||||
}
|
||||
return nil, fmt.Errorf("CreateEntry: %w", errors.New(resp.Error))
|
||||
}
|
||||
if resp.Error != "" {
|
||||
glog.V(1).InfofCtx(ctx, "create entry %s/%s %v: %v", request.Directory, request.Entry.Name, request.OExcl, resp.Error)
|
||||
return nil, fmt.Errorf("CreateEntry: %w", errors.New(resp.Error))
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func UpdateEntry(ctx context.Context, client SeaweedFilerClient, request *UpdateEntryRequest) error {
|
||||
_, err := UpdateEntryWithResponse(ctx, client, request)
|
||||
return err
|
||||
}
|
||||
|
||||
func UpdateEntryWithResponse(ctx context.Context, client SeaweedFilerClient, request *UpdateEntryRequest) (*UpdateEntryResponse, error) {
|
||||
resp, err := client.UpdateEntry(ctx, request)
|
||||
if err != nil {
|
||||
glog.V(1).InfofCtx(ctx, "update entry %s/%s :%v", request.Directory, request.Entry.Name, err)
|
||||
return nil, fmt.Errorf("UpdateEntry: %w", err)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func LookupEntry(ctx context.Context, client SeaweedFilerClient, request *LookupDirectoryEntryRequest) (*LookupDirectoryEntryResponse, error) {
|
||||
resp, err := client.LookupDirectoryEntry(ctx, request)
|
||||
if err != nil {
|
||||
if err == ErrNotFound || strings.Contains(err.Error(), ErrNotFound.Error()) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
glog.V(3).InfofCtx(ctx, "read %s/%v: %v", request.Directory, request.Name, err)
|
||||
return nil, fmt.Errorf("LookupEntry1: %w", err)
|
||||
}
|
||||
if resp.Entry == nil {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
var ErrNotFound = errors.New("filer: no entry is found in filer store")
|
||||
|
||||
// Sentinel errors for filer entry operations.
|
||||
// These are set by the filer and reconstructed from FilerError codes after
|
||||
// crossing the gRPC boundary, so consumers can use errors.Is() instead of
|
||||
// parsing error strings.
|
||||
var (
|
||||
ErrEntryNameTooLong = errors.New("entry name too long")
|
||||
ErrParentIsFile = errors.New("parent path is a file")
|
||||
ErrExistingIsDirectory = errors.New("existing entry is a directory")
|
||||
ErrExistingIsFile = errors.New("existing entry is a file")
|
||||
ErrEntryAlreadyExists = errors.New("entry already exists")
|
||||
)
|
||||
|
||||
// FilerErrorToSentinel maps a proto FilerError code to its sentinel error.
|
||||
// Returns nil for OK or unknown codes.
|
||||
func FilerErrorToSentinel(code FilerError) error {
|
||||
switch code {
|
||||
case FilerError_ENTRY_NAME_TOO_LONG:
|
||||
return ErrEntryNameTooLong
|
||||
case FilerError_PARENT_IS_FILE:
|
||||
return ErrParentIsFile
|
||||
case FilerError_EXISTING_IS_DIRECTORY:
|
||||
return ErrExistingIsDirectory
|
||||
case FilerError_EXISTING_IS_FILE:
|
||||
return ErrExistingIsFile
|
||||
case FilerError_ENTRY_ALREADY_EXISTS:
|
||||
return ErrEntryAlreadyExists
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func IsEmpty(event *SubscribeMetadataResponse) bool {
|
||||
return event.EventNotification.NewEntry == nil && event.EventNotification.OldEntry == nil
|
||||
}
|
||||
|
||||
func IsCreate(event *SubscribeMetadataResponse) bool {
|
||||
return event.EventNotification.NewEntry != nil && event.EventNotification.OldEntry == nil
|
||||
}
|
||||
|
||||
func IsUpdate(event *SubscribeMetadataResponse) bool {
|
||||
return event.EventNotification.NewEntry != nil &&
|
||||
event.EventNotification.OldEntry != nil &&
|
||||
event.Directory == event.EventNotification.NewParentPath &&
|
||||
event.EventNotification.NewEntry.Name == event.EventNotification.OldEntry.Name
|
||||
}
|
||||
|
||||
func IsDelete(event *SubscribeMetadataResponse) bool {
|
||||
return event.EventNotification.NewEntry == nil && event.EventNotification.OldEntry != nil
|
||||
}
|
||||
|
||||
func IsRename(event *SubscribeMetadataResponse) bool {
|
||||
return event.EventNotification.NewEntry != nil &&
|
||||
event.EventNotification.OldEntry != nil &&
|
||||
(event.Directory != event.EventNotification.NewParentPath ||
|
||||
event.EventNotification.NewEntry.Name != event.EventNotification.OldEntry.Name)
|
||||
}
|
||||
|
||||
func MetadataEventSourceDirectory(event *SubscribeMetadataResponse) string {
|
||||
if event == nil {
|
||||
return ""
|
||||
}
|
||||
return event.Directory
|
||||
}
|
||||
|
||||
func MetadataEventTargetDirectory(event *SubscribeMetadataResponse) string {
|
||||
if event == nil {
|
||||
return ""
|
||||
}
|
||||
if event.EventNotification != nil && event.EventNotification.NewParentPath != "" {
|
||||
return event.EventNotification.NewParentPath
|
||||
}
|
||||
return event.Directory
|
||||
}
|
||||
|
||||
func metadataEventSourceEntryName(event *SubscribeMetadataResponse) string {
|
||||
if event == nil || event.EventNotification == nil {
|
||||
return ""
|
||||
}
|
||||
if event.EventNotification.OldEntry != nil {
|
||||
return event.EventNotification.OldEntry.Name
|
||||
}
|
||||
if event.EventNotification.NewEntry != nil {
|
||||
return event.EventNotification.NewEntry.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func metadataEventTargetEntryName(event *SubscribeMetadataResponse) string {
|
||||
if event == nil || event.EventNotification == nil {
|
||||
return ""
|
||||
}
|
||||
if event.EventNotification.NewEntry != nil {
|
||||
return event.EventNotification.NewEntry.Name
|
||||
}
|
||||
if event.EventNotification.OldEntry != nil {
|
||||
return event.EventNotification.OldEntry.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func MetadataEventSourceFullPath(event *SubscribeMetadataResponse) string {
|
||||
return util.Join(MetadataEventSourceDirectory(event), metadataEventSourceEntryName(event))
|
||||
}
|
||||
|
||||
func MetadataEventTargetFullPath(event *SubscribeMetadataResponse) string {
|
||||
return util.Join(MetadataEventTargetDirectory(event), metadataEventTargetEntryName(event))
|
||||
}
|
||||
|
||||
func MetadataEventTouchesDirectory(event *SubscribeMetadataResponse, dir string) bool {
|
||||
if MetadataEventSourceDirectory(event) == dir {
|
||||
return true
|
||||
}
|
||||
return event != nil &&
|
||||
event.EventNotification != nil &&
|
||||
event.EventNotification.NewEntry != nil &&
|
||||
MetadataEventTargetDirectory(event) == dir
|
||||
}
|
||||
|
||||
func MetadataEventMatchesSubscription(event *SubscribeMetadataResponse, pathPrefix string, pathPrefixes []string, directories []string) bool {
|
||||
if event == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if metadataEventMatchesPath(MetadataEventSourceFullPath(event), MetadataEventSourceDirectory(event), pathPrefix, pathPrefixes, directories) {
|
||||
return true
|
||||
}
|
||||
|
||||
return event.EventNotification != nil &&
|
||||
event.EventNotification.NewEntry != nil &&
|
||||
metadataEventMatchesPath(MetadataEventTargetFullPath(event), MetadataEventTargetDirectory(event), pathPrefix, pathPrefixes, directories)
|
||||
}
|
||||
|
||||
func metadataEventMatchesPath(fullPath, dirPath, pathPrefix string, pathPrefixes []string, directories []string) bool {
|
||||
if hasPrefixIn(fullPath, pathPrefixes) {
|
||||
return true
|
||||
}
|
||||
if matchByDirectory(dirPath, directories) {
|
||||
return true
|
||||
}
|
||||
return strings.HasPrefix(fullPath, pathPrefix)
|
||||
}
|
||||
|
||||
func hasPrefixIn(text string, prefixes []string) bool {
|
||||
for _, p := range prefixes {
|
||||
if strings.HasPrefix(text, p) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func matchByDirectory(dirPath string, directories []string) bool {
|
||||
for _, dir := range directories {
|
||||
if dirPath == dir {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var _ = ptrie.KeyProvider(&FilerConf_PathConf{})
|
||||
|
||||
func (fp *FilerConf_PathConf) Key() interface{} {
|
||||
key, _ := proto.Marshal(fp)
|
||||
return string(key)
|
||||
}
|
||||
61
weed/pb/filer_pb/filer_pb_helper_test.go
Normal file
61
weed/pb/filer_pb/filer_pb_helper_test.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package filer_pb
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
||||
)
|
||||
|
||||
func TestIsDirectoryKeyObject(t *testing.T) {
|
||||
chunk := []*FileChunk{{FileId: "1,01", Size: 75}}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
e *Entry
|
||||
want bool
|
||||
}{
|
||||
{"plain directory", &Entry{IsDirectory: true, Attributes: &FuseAttributes{}}, false},
|
||||
{"directory marker with mime", &Entry{IsDirectory: true, Attributes: &FuseAttributes{Mime: "application/octet-stream"}}, true},
|
||||
{"directory promoted from file keeps chunks", &Entry{IsDirectory: true, Attributes: &FuseAttributes{}, Chunks: chunk}, true},
|
||||
{"directory promoted from small file keeps content", &Entry{IsDirectory: true, Attributes: &FuseAttributes{}, Content: []byte("abc")}, true},
|
||||
{"directory promoted from remote-tiered file", &Entry{IsDirectory: true, Attributes: &FuseAttributes{}, RemoteEntry: &RemoteEntry{RemoteSize: 100}}, true},
|
||||
{"directory with chunks and nil attributes", &Entry{IsDirectory: true, Chunks: chunk}, true},
|
||||
{"regular file with chunks", &Entry{IsDirectory: false, Attributes: &FuseAttributes{}, Chunks: chunk}, false},
|
||||
{"remote mount directory has no remote size", &Entry{IsDirectory: true, Attributes: &FuseAttributes{}, RemoteEntry: &RemoteEntry{StorageName: "s3"}}, false},
|
||||
{"empty prefix object has only the mark", &Entry{IsDirectory: true, Attributes: &FuseAttributes{}, Extended: map[string][]byte{s3_constants.SeaweedFSPrefixObject: []byte("true")}}, true},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
if got := c.e.IsDirectoryKeyObject(); got != c.want {
|
||||
t.Errorf("IsDirectoryKeyObject() = %v, want %v", got, c.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkPrefixObject(t *testing.T) {
|
||||
entry := &Entry{Name: "foo", Attributes: &FuseAttributes{FileMode: 0644}}
|
||||
|
||||
if entry.IsPrefixObject() {
|
||||
t.Fatal("a file is not a prefix object")
|
||||
}
|
||||
|
||||
entry.MarkPrefixObject()
|
||||
|
||||
if !entry.IsDirectory || !entry.IsPrefixObject() {
|
||||
t.Errorf("MarkPrefixObject() left %+v", entry)
|
||||
}
|
||||
// The filer derives the entry type from the mode, so the directory bit has to be
|
||||
// on it for the store to keep this as a directory.
|
||||
if mode := os.FileMode(entry.Attributes.FileMode); mode&os.ModeDir == 0 || mode.Perm()&0111 != 0111 {
|
||||
t.Errorf("FileMode = %v, want a traversable directory", mode)
|
||||
}
|
||||
|
||||
// A directory the mark was stripped from is a plain directory again.
|
||||
delete(entry.Extended, s3_constants.SeaweedFSPrefixObject)
|
||||
if entry.IsPrefixObject() || entry.IsDirectoryKeyObject() {
|
||||
t.Error("a demoted directory names no key")
|
||||
}
|
||||
}
|
||||
306
weed/pb/filer_pb/filer_vtproto_test.go
Normal file
306
weed/pb/filer_pb/filer_vtproto_test.go
Normal file
@@ -0,0 +1,306 @@
|
||||
package filer_pb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// sampleResponse builds a metadata event exercising the fields touched on the
|
||||
// hot subscribe path: nested entries, a repeated chunk list, a map, and byte
|
||||
// fields.
|
||||
func sampleResponse() *SubscribeMetadataResponse {
|
||||
entry := func(name string) *Entry {
|
||||
return &Entry{
|
||||
Name: name,
|
||||
IsDirectory: false,
|
||||
Chunks: []*FileChunk{
|
||||
{
|
||||
FileId: "3,01637037d6",
|
||||
Offset: 0,
|
||||
Size: 1234,
|
||||
ModifiedTsNs: 111,
|
||||
ETag: "abc",
|
||||
Fid: &FileId{VolumeId: 3, FileKey: 0x1637037, Cookie: 0xd6},
|
||||
CipherKey: []byte{1, 2, 3, 4},
|
||||
},
|
||||
},
|
||||
Attributes: &FuseAttributes{
|
||||
FileSize: 1234, Mtime: 222, FileMode: 0644, Uid: 1000, Gid: 1000,
|
||||
GroupName: []string{"staff", "wheel"}, Md5: []byte{9, 8, 7},
|
||||
},
|
||||
Extended: map[string][]byte{
|
||||
"foo": []byte("bar"),
|
||||
"baz": []byte("qux"),
|
||||
"k3": []byte("v3"),
|
||||
},
|
||||
Content: []byte("hello"),
|
||||
}
|
||||
}
|
||||
return &SubscribeMetadataResponse{
|
||||
Directory: "/buckets/test/dir",
|
||||
EventNotification: &EventNotification{
|
||||
OldEntry: entry("old.txt"),
|
||||
NewEntry: entry("new.txt"),
|
||||
DeleteChunks: true,
|
||||
NewParentPath: "/buckets/test/dir2",
|
||||
IsFromOtherCluster: true,
|
||||
Signatures: []int32{101, 202, 303},
|
||||
},
|
||||
TsNs: 1234567890,
|
||||
}
|
||||
}
|
||||
|
||||
// eventWithChunks models a real create/update event: a file split into n chunks,
|
||||
// each a nested FileChunk+FileId message. n scales the number of nested messages
|
||||
// the decoder must allocate, which is where vtproto's edge over reflection grows.
|
||||
func eventWithChunks(n int) *SubscribeMetadataResponse {
|
||||
chunks := make([]*FileChunk, n)
|
||||
for i := 0; i < n; i++ {
|
||||
chunks[i] = &FileChunk{
|
||||
FileId: fmt.Sprintf("%d,%08x%04x", i%64, i*7919, i%251),
|
||||
Offset: int64(i) * 8 << 20,
|
||||
Size: 8 << 20,
|
||||
ModifiedTsNs: 1700000000000000000 + int64(i),
|
||||
ETag: fmt.Sprintf("etag-%d", i),
|
||||
Fid: &FileId{VolumeId: uint32(i%64 + 1), FileKey: uint64(i * 7919), Cookie: uint32(i % 251)},
|
||||
}
|
||||
}
|
||||
return &SubscribeMetadataResponse{
|
||||
Directory: "/buckets/data/2026/07/15",
|
||||
EventNotification: &EventNotification{
|
||||
NewEntry: &Entry{
|
||||
Name: "object.bin",
|
||||
Chunks: chunks,
|
||||
Attributes: &FuseAttributes{FileSize: uint64(n) * 8 << 20, Mtime: 1700000000, FileMode: 0644, Uid: 1000, Gid: 1000},
|
||||
},
|
||||
Signatures: []int32{12345},
|
||||
},
|
||||
TsNs: 1700000000000000000,
|
||||
}
|
||||
}
|
||||
|
||||
// TestVTWireCompatibility guards that the vtproto-generated MarshalVT/UnmarshalVT
|
||||
// stay wire-compatible with the reflection-based proto path in both directions,
|
||||
// so adopting VT on the metadata hot paths cannot silently corrupt events.
|
||||
func TestVTWireCompatibility(t *testing.T) {
|
||||
orig := sampleResponse()
|
||||
|
||||
// vt marshal -> proto unmarshal
|
||||
vtBytes, err := orig.MarshalVT()
|
||||
if err != nil {
|
||||
t.Fatalf("MarshalVT: %v", err)
|
||||
}
|
||||
var fromVT SubscribeMetadataResponse
|
||||
if err := proto.Unmarshal(vtBytes, &fromVT); err != nil {
|
||||
t.Fatalf("proto.Unmarshal(vtBytes): %v", err)
|
||||
}
|
||||
if !proto.Equal(orig, &fromVT) {
|
||||
t.Fatalf("proto.Unmarshal(MarshalVT) diverged from original")
|
||||
}
|
||||
|
||||
// proto marshal -> vt unmarshal
|
||||
protoBytes, err := proto.Marshal(orig)
|
||||
if err != nil {
|
||||
t.Fatalf("proto.Marshal: %v", err)
|
||||
}
|
||||
var fromProto SubscribeMetadataResponse
|
||||
if err := fromProto.UnmarshalVT(protoBytes); err != nil {
|
||||
t.Fatalf("UnmarshalVT(protoBytes): %v", err)
|
||||
}
|
||||
if !proto.Equal(orig, &fromProto) {
|
||||
t.Fatalf("UnmarshalVT(proto.Marshal) diverged from original")
|
||||
}
|
||||
|
||||
// MarshalToSizedBufferVT into a pre-sized slice matches MarshalVT, the write
|
||||
// path used by the log buffer.
|
||||
sized := make([]byte, orig.SizeVT())
|
||||
if _, err := orig.MarshalToSizedBufferVT(sized); err != nil {
|
||||
t.Fatalf("MarshalToSizedBufferVT: %v", err)
|
||||
}
|
||||
var fromSized SubscribeMetadataResponse
|
||||
if err := fromSized.UnmarshalVT(sized); err != nil {
|
||||
t.Fatalf("UnmarshalVT(sized): %v", err)
|
||||
}
|
||||
if !proto.Equal(orig, &fromSized) {
|
||||
t.Fatalf("MarshalToSizedBufferVT diverged from original")
|
||||
}
|
||||
|
||||
// LogEntry carrying a marshaled event, mirroring the log buffer write path.
|
||||
le := &LogEntry{TsNs: 7, PartitionKeyHash: 9, Data: vtBytes, Key: []byte("k"), Offset: 42}
|
||||
leBytes, err := le.MarshalVT()
|
||||
if err != nil {
|
||||
t.Fatalf("LogEntry.MarshalVT: %v", err)
|
||||
}
|
||||
var leDecoded LogEntry
|
||||
if err := proto.Unmarshal(leBytes, &leDecoded); err != nil {
|
||||
t.Fatalf("proto.Unmarshal(LogEntry): %v", err)
|
||||
}
|
||||
if !proto.Equal(le, &leDecoded) {
|
||||
t.Fatalf("LogEntry vt/proto roundtrip diverged")
|
||||
}
|
||||
}
|
||||
|
||||
// TestUTF8Validation documents the deliberate UTF-8 difference between the two
|
||||
// decoders. UnmarshalVT skips proto3's UTF-8 validation for string fields, so
|
||||
// the metadata delivery paths keep decoding SubscribeMetadataResponse with
|
||||
// proto.Unmarshal; only LogEntry (bytes-only) decodes use UnmarshalVT.
|
||||
func TestUTF8Validation(t *testing.T) {
|
||||
// SubscribeMetadataResponse{Directory: "\xff"}: field 1 (string), wire type 2,
|
||||
// length 1, byte 0xff.
|
||||
malformed := []byte{0x0a, 0x01, 0xff}
|
||||
|
||||
var viaProto SubscribeMetadataResponse
|
||||
if err := proto.Unmarshal(malformed, &viaProto); err == nil {
|
||||
t.Fatal("proto.Unmarshal accepted invalid UTF-8 in a string field; expected rejection")
|
||||
}
|
||||
|
||||
var viaVT SubscribeMetadataResponse
|
||||
if err := viaVT.UnmarshalVT(malformed); err != nil {
|
||||
t.Fatalf("UnmarshalVT rejected invalid UTF-8: %v", err)
|
||||
}
|
||||
if viaVT.Directory != "\xff" {
|
||||
t.Fatalf("UnmarshalVT Directory = %q, want \\xff", viaVT.Directory)
|
||||
}
|
||||
|
||||
// LogEntry carries only bytes fields, so the decoders agree on arbitrary bytes;
|
||||
// that is why the log entry decode paths can use UnmarshalVT.
|
||||
// LogEntry{Data: "\xff"}: field 3 (bytes), wire type 2, length 1, byte 0xff.
|
||||
leBytes := []byte{0x1a, 0x01, 0xff}
|
||||
var leProto LogEntry
|
||||
if err := proto.Unmarshal(leBytes, &leProto); err != nil {
|
||||
t.Fatalf("proto.Unmarshal LogEntry: %v", err)
|
||||
}
|
||||
var leVT LogEntry
|
||||
if err := leVT.UnmarshalVT(leBytes); err != nil {
|
||||
t.Fatalf("UnmarshalVT LogEntry: %v", err)
|
||||
}
|
||||
if !proto.Equal(&leProto, &leVT) {
|
||||
t.Fatal("LogEntry proto/VT decode of a bytes field diverged")
|
||||
}
|
||||
}
|
||||
|
||||
var chunkCounts = []int{1, 8, 32, 128}
|
||||
|
||||
// BenchmarkUnmarshal is the decode hot path: each subscriber decodes every
|
||||
// delivered event, so this cost is paid once per (event, subscriber).
|
||||
func BenchmarkUnmarshal(b *testing.B) {
|
||||
for _, n := range chunkCounts {
|
||||
data, _ := proto.Marshal(eventWithChunks(n))
|
||||
b.Run(fmt.Sprintf("proto/chunks=%d", n), func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
var m SubscribeMetadataResponse
|
||||
if err := proto.Unmarshal(data, &m); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
b.Run(fmt.Sprintf("vt/chunks=%d", n), func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
var m SubscribeMetadataResponse
|
||||
if err := m.UnmarshalVT(data); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkMarshal is the encode path: each metadata write marshals one LogEntry
|
||||
// payload.
|
||||
func BenchmarkMarshal(b *testing.B) {
|
||||
for _, n := range chunkCounts {
|
||||
ev := eventWithChunks(n)
|
||||
b.Run(fmt.Sprintf("proto/chunks=%d", n), func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err := proto.Marshal(ev); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
b.Run(fmt.Sprintf("vt/chunks=%d", n), func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err := ev.MarshalVT(); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkMarshalIntoBuffer mirrors the log buffer write path: marshal into a
|
||||
// reused destination buffer. proto.Marshal must allocate a fresh slice and copy;
|
||||
// SizeVT + MarshalToSizedBufferVT writes straight into the destination.
|
||||
func BenchmarkMarshalIntoBuffer(b *testing.B) {
|
||||
for _, n := range chunkCounts {
|
||||
ev := eventWithChunks(n)
|
||||
dst := make([]byte, 0, ev.SizeVT()*2)
|
||||
b.Run(fmt.Sprintf("proto+copy/chunks=%d", n), func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
data, err := proto.Marshal(ev)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
dst = dst[:len(data)]
|
||||
copy(dst, data)
|
||||
}
|
||||
})
|
||||
b.Run(fmt.Sprintf("vt-sized/chunks=%d", n), func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
size := ev.SizeVT()
|
||||
dst = dst[:size]
|
||||
if _, err := ev.MarshalToSizedBufferVT(dst); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The log position fields must survive VT round-trips, or a VT-marshaled
|
||||
// response silently loses the version the mount fences handles with.
|
||||
func TestVtProtoPreservesLogTsNs(t *testing.T) {
|
||||
messages := []interface {
|
||||
MarshalVT() ([]byte, error)
|
||||
}{
|
||||
&LookupDirectoryEntryResponse{LogTsNs: 42},
|
||||
&CacheRemoteObjectToLocalClusterResponse{LogTsNs: 43},
|
||||
&CreateEntryResponse{LogTsNs: 44},
|
||||
&UpdateEntryResponse{LogTsNs: 45},
|
||||
}
|
||||
for i, m := range messages {
|
||||
data, err := m.MarshalVT()
|
||||
if err != nil {
|
||||
t.Fatalf("MarshalVT %d: %v", i, err)
|
||||
}
|
||||
switch v := m.(type) {
|
||||
case *LookupDirectoryEntryResponse:
|
||||
out := &LookupDirectoryEntryResponse{}
|
||||
if err := out.UnmarshalVT(data); err != nil || out.LogTsNs != v.LogTsNs {
|
||||
t.Fatalf("LookupDirectoryEntryResponse round-trip LogTsNs = %d, %v; want %d", out.LogTsNs, err, v.LogTsNs)
|
||||
}
|
||||
case *CacheRemoteObjectToLocalClusterResponse:
|
||||
out := &CacheRemoteObjectToLocalClusterResponse{}
|
||||
if err := out.UnmarshalVT(data); err != nil || out.LogTsNs != v.LogTsNs {
|
||||
t.Fatalf("CacheRemoteObjectToLocalClusterResponse round-trip LogTsNs = %d, %v; want %d", out.LogTsNs, err, v.LogTsNs)
|
||||
}
|
||||
case *CreateEntryResponse:
|
||||
out := &CreateEntryResponse{}
|
||||
if err := out.UnmarshalVT(data); err != nil || out.LogTsNs != v.LogTsNs {
|
||||
t.Fatalf("CreateEntryResponse round-trip LogTsNs = %d, %v; want %d", out.LogTsNs, err, v.LogTsNs)
|
||||
}
|
||||
case *UpdateEntryResponse:
|
||||
out := &UpdateEntryResponse{}
|
||||
if err := out.UnmarshalVT(data); err != nil || out.LogTsNs != v.LogTsNs {
|
||||
t.Fatalf("UpdateEntryResponse round-trip LogTsNs = %d, %v; want %d", out.LogTsNs, err, v.LogTsNs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
weed/pb/filer_pb/listing_options.go
Normal file
22
weed/pb/filer_pb/listing_options.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package filer_pb
|
||||
|
||||
import "context"
|
||||
|
||||
type omitChunksKey struct{}
|
||||
|
||||
// WithChunksOmitted marks a listing as wanting attributes only. A listing over
|
||||
// gRPC then asks the filer to leave the chunk lists out, and a listing served
|
||||
// from a local store skips building them. The file size is carried in the
|
||||
// attributes either way.
|
||||
//
|
||||
// Only a caller that reads attributes and nothing else may use this. In
|
||||
// particular a listing that populates a cache, or one whose entries can be
|
||||
// written back or deleted, needs the chunks.
|
||||
func WithChunksOmitted(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, omitChunksKey{}, true)
|
||||
}
|
||||
|
||||
// ChunksOmitted reports whether the listing wants attributes only.
|
||||
func ChunksOmitted(ctx context.Context) bool {
|
||||
return ctx.Value(omitChunksKey{}) != nil
|
||||
}
|
||||
Reference in New Issue
Block a user