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

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

View File

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

View 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
}

View 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)
}
}

View 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")
}
}

File diff suppressed because it is too large Load Diff

View 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
}

View 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")
}
}
}

View 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)
}
}

View 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)
}

View 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")
}
}

View 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)
}
}
}
}

View 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
}