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,544 @@
package weed_server
import (
"fmt"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/seaweedfs/seaweedfs/weed/filer"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/util/log_buffer"
)
// slowStream simulates a gRPC stream with configurable per-Send latency.
// It counts individual events including those packed inside batches.
// Atomic counters use atomic.Int64 so they stay 8-byte aligned on 32-bit
// architectures (386, ARMv7, mips32) where a bare int64 struct field is
// only 4-byte aligned and panics under atomic.AddInt64.
type slowStream struct {
sends atomic.Int64 // number of stream.Send() calls
eventsSent atomic.Int64 // total events (1 + len(Events) per Send)
sendDelay time.Duration
}
func (s *slowStream) Send(msg *filer_pb.SubscribeMetadataResponse) error {
time.Sleep(s.sendDelay)
s.sends.Add(1)
s.eventsSent.Add(1 + int64(len(msg.Events)))
return nil
}
type collectingStream struct {
messages []*filer_pb.SubscribeMetadataResponse
err error
}
func (s *collectingStream) Send(msg *filer_pb.SubscribeMetadataResponse) error {
if s.err != nil {
return s.err
}
s.messages = append(s.messages, msg)
return nil
}
func makeEvent(dir, name string, tsNs int64) *filer_pb.SubscribeMetadataResponse {
return &filer_pb.SubscribeMetadataResponse{
Directory: dir,
TsNs: tsNs,
EventNotification: &filer_pb.EventNotification{
NewEntry: &filer_pb.Entry{
Name: name,
IsDirectory: false,
},
},
}
}
// makeOldEvents creates events with timestamps far in the past (triggers batch mode).
func makeOldEvents(n int) []*filer_pb.SubscribeMetadataResponse {
baseTs := time.Now().Add(-time.Hour).UnixNano() // 1 hour ago → well past batchBehindThreshold
events := make([]*filer_pb.SubscribeMetadataResponse, n)
for i := range events {
events[i] = makeEvent("/bucket/dir", fmt.Sprintf("file%06d.txt", i), baseTs+int64(i))
}
return events
}
// makeRecentEvents creates events with timestamps close to now (sends one-by-one).
func makeRecentEvents(n int) []*filer_pb.SubscribeMetadataResponse {
baseTs := time.Now().UnixNano()
events := make([]*filer_pb.SubscribeMetadataResponse, n)
for i := range events {
events[i] = makeEvent("/bucket/dir", fmt.Sprintf("file%06d.txt", i), baseTs+int64(i))
}
return events
}
// TestPipelinedSenderThroughput compares direct (blocking) stream.Send with
// the pipelinedSender with adaptive batching.
//
// Simulates realistic backlog catch-up: the reader loads one log file at a time
// from a volume server (fileReadDelay per file), producing a burst of ~300
// events. The sender has per-Send gRPC overhead (sendDelay).
//
// - Direct: serial — each event: send one-by-one between file reads
// - Pipelined+batched: file I/O overlaps with batched sending
func TestPipelinedSenderThroughput(t *testing.T) {
const (
eventsPerFile = 300 // events in one minute-log file
numFiles = 7 // files to process
totalEvents = eventsPerFile * numFiles // 2100
fileReadDelay = 5 * time.Millisecond // volume server read per log file
sendDelay = 50 * time.Microsecond // gRPC round-trip per Send()
)
// Partition old events into file-sized bursts
files := make([][]*filer_pb.SubscribeMetadataResponse, numFiles)
baseTs := time.Now().Add(-time.Hour).UnixNano()
for f := 0; f < numFiles; f++ {
files[f] = make([]*filer_pb.SubscribeMetadataResponse, eventsPerFile)
for i := 0; i < eventsPerFile; i++ {
idx := f*eventsPerFile + i
files[f][i] = makeEvent("/bucket/dir", fmt.Sprintf("file%06d.txt", idx), baseTs+int64(idx))
}
}
// --- Direct (old behavior): read file, send events one-by-one, repeat ---
var directRate float64
t.Run("direct_send", func(t *testing.T) {
stream := &slowStream{sendDelay: sendDelay}
start := time.Now()
for _, file := range files {
time.Sleep(fileReadDelay) // read log file from volume server
for _, ev := range file {
if err := stream.Send(ev); err != nil {
t.Fatalf("send error: %v", err)
}
}
}
elapsed := time.Since(start)
directRate = float64(stream.eventsSent.Load()) / elapsed.Seconds()
t.Logf("direct: %d events %4d sends %v %6.0f events/sec",
stream.eventsSent.Load(), stream.sends.Load(), elapsed.Round(time.Millisecond), directRate)
})
// --- Pipelined + batched (new behavior): file reads overlap with batched sends ---
var batchedRate float64
t.Run("pipelined_batched_send", func(t *testing.T) {
stream := &slowStream{sendDelay: sendDelay}
sender := newPipelinedSender(stream, 1024, true)
start := time.Now()
for _, file := range files {
time.Sleep(fileReadDelay) // read log file from volume server
for _, ev := range file {
if err := sender.Send(ev); err != nil {
t.Fatalf("send error: %v", err)
}
}
}
if err := sender.Close(); err != nil {
t.Fatalf("close error: %v", err)
}
elapsed := time.Since(start)
batchedRate = float64(stream.eventsSent.Load()) / elapsed.Seconds()
t.Logf("pipelined+batch: %d events %4d sends %v %6.0f events/sec",
stream.eventsSent.Load(), stream.sends.Load(), elapsed.Round(time.Millisecond), batchedRate)
})
if directRate > 0 {
t.Logf("Speedup: %.1fx (pipelined+batched vs direct)", batchedRate/directRate)
}
}
func TestEachEventNotificationFnMatchesRenameTargetsForAllWatchTypes(t *testing.T) {
fs := &FilerServer{
option: &FilerOption{Host: pb.ServerAddress("127.0.0.1:8888")},
filer: &filer.Filer{Signature: 123},
}
tests := []struct {
name string
req *filer_pb.SubscribeMetadataRequest
}{
{
name: "additional path prefix",
req: &filer_pb.SubscribeMetadataRequest{
ClientName: "test",
PathPrefix: "/data/",
PathPrefixes: []string{"/etc/remote"},
},
},
{
name: "directory watch",
req: &filer_pb.SubscribeMetadataRequest{
ClientName: "test",
PathPrefix: "/data/",
Directories: []string{"/etc/iam/identities"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
stream := &collectingStream{}
var unsyncedEvents int64
eachEventFn := fs.eachEventNotificationFn(tt.req, stream, "client", &unsyncedEvents)
newDir := "/etc/remote"
if len(tt.req.Directories) > 0 {
newDir = tt.req.Directories[0]
}
err := eachEventFn("/tmp", &filer_pb.EventNotification{
OldEntry: &filer_pb.Entry{Name: "old"},
NewEntry: &filer_pb.Entry{Name: "new"},
NewParentPath: newDir,
}, time.Now().UnixNano())
if err != nil {
t.Fatalf("eachEventFn: %v", err)
}
if len(stream.messages) != 1 {
t.Fatalf("messages sent = %d, want 1", len(stream.messages))
}
})
}
}
// TestBatchingAdaptive verifies the adaptive behavior: old events are batched,
// recent events are sent one-by-one.
func TestBatchingAdaptive(t *testing.T) {
const numEvents = 500
t.Run("old_events_are_batched", func(t *testing.T) {
stream := &slowStream{sendDelay: 10 * time.Microsecond}
sender := newPipelinedSender(stream, 1024, true)
// Push all events at once (no read delay) so the sender can batch aggressively
for _, ev := range makeOldEvents(numEvents) {
sender.Send(ev)
}
sender.Close()
sends := stream.sends.Load()
events := stream.eventsSent.Load()
t.Logf("old events: %d events in %d sends (avg batch size: %.1f)",
events, sends, float64(events)/float64(sends))
if sends >= int64(numEvents) {
t.Errorf("expected batching to reduce sends below %d, got %d", numEvents, sends)
}
})
t.Run("recent_events_sent_individually", func(t *testing.T) {
stream := &slowStream{sendDelay: 10 * time.Microsecond}
sender := newPipelinedSender(stream, 1024, true)
for _, ev := range makeRecentEvents(numEvents) {
sender.Send(ev)
}
sender.Close()
sends := stream.sends.Load()
events := stream.eventsSent.Load()
t.Logf("recent events: %d events in %d sends (avg batch size: %.1f)",
events, sends, float64(events)/float64(sends))
if sends != int64(numEvents) {
t.Errorf("expected 1:1 sends for recent events, got %d sends for %d events", sends, numEvents)
}
})
}
// errorStreamImpl is a metadataStreamSender that returns an error after N sends.
// count uses atomic.Int64 so it stays 8-byte aligned on 32-bit architectures
// (386, ARMv7, mips32) where a bare int64 struct field after smaller fields
// is only 4-byte aligned and panics under atomic.AddInt64.
type errorStreamImpl struct {
count atomic.Int64
failAfter int
err error
}
func (s *errorStreamImpl) Send(msg *filer_pb.SubscribeMetadataResponse) error {
n := s.count.Add(1)
if int(n) > s.failAfter {
return s.err
}
return nil
}
// TestPipelinedSenderErrorPropagation verifies that when stream.Send fails,
// the error propagates to pipelinedSender.Send callers and Close.
func TestPipelinedSenderErrorPropagation(t *testing.T) {
sendErr := fmt.Errorf("connection reset")
t.Run("send_returns_error", func(t *testing.T) {
// Stream fails after 5 successful sends
stream := &errorStreamImpl{failAfter: 5, err: sendErr}
sender := newPipelinedSender(stream, 4, true)
var lastErr error
for i := 0; i < 100; i++ {
ev := makeOldEvents(1)[0]
if err := sender.Send(ev); err != nil {
lastErr = err
break
}
}
if lastErr == nil {
t.Fatal("expected Send to return an error after stream failure")
}
t.Logf("Send returned error after stream failure: %v", lastErr)
})
t.Run("close_returns_error_if_not_consumed", func(t *testing.T) {
// Stream fails on the very first send — error surfaces via Close
// since Send may have already returned before the sender goroutine
// processes the message.
stream := &errorStreamImpl{failAfter: 0, err: sendErr}
sender := newPipelinedSender(stream, 1024, true)
ev := makeOldEvents(1)[0]
sender.Send(ev)
closeErr := sender.Close()
if closeErr == nil {
t.Log("Close returned nil (error was consumed by Send)")
} else {
t.Logf("Close returned error: %v", closeErr)
}
})
}
// TestPipelinedSingleVsParallelStreams shows 1 pipelined+batched stream vs
// N parallel pipelined+batched streams, using the realistic burst-read pattern.
func TestPipelinedSingleVsParallelStreams(t *testing.T) {
const (
numDirs = 10
filesPerDir = 7 // log files per directory
eventsPerFile = 300 // events per log file
totalEvents = numDirs * filesPerDir * eventsPerFile // 21000
fileReadDelay = 5 * time.Millisecond
sendDelay = 50 * time.Microsecond
)
// Generate partitioned OLD events grouped into file-sized bursts
baseTs := time.Now().Add(-time.Hour).UnixNano()
type logFile []*filer_pb.SubscribeMetadataResponse
// partitions[dir][file][event]
partitions := make([][]logFile, numDirs)
var allFiles []logFile
idx := 0
for d := 0; d < numDirs; d++ {
dir := fmt.Sprintf("/bucket/dir%03d", d)
for f := 0; f < filesPerDir; f++ {
file := make(logFile, eventsPerFile)
for i := 0; i < eventsPerFile; i++ {
file[i] = makeEvent(dir, fmt.Sprintf("file%06d.txt", idx), baseTs+int64(idx))
idx++
}
partitions[d] = append(partitions[d], file)
allFiles = append(allFiles, file)
}
}
// simulatePipeline: read files with I/O delay, push events, send via pipelinedSender
simulatePipeline := func(files []logFile) (eventsSent, sends int64, elapsed time.Duration, err error) {
stream := &slowStream{sendDelay: sendDelay}
sender := newPipelinedSender(stream, 1024, true)
start := time.Now()
outer:
for _, file := range files {
time.Sleep(fileReadDelay) // volume server read
for _, ev := range file {
if err = sender.Send(ev); err != nil {
break outer
}
}
}
if closeErr := sender.Close(); closeErr != nil && err == nil {
err = closeErr
}
elapsed = time.Since(start)
eventsSent = stream.eventsSent.Load()
sends = stream.sends.Load()
return
}
var singleRate float64
t.Run("1_pipelined_stream", func(t *testing.T) {
eventsSent, sends, elapsed, err := simulatePipeline(allFiles)
if err != nil {
t.Fatalf("pipeline error: %v", err)
}
singleRate = float64(eventsSent) / elapsed.Seconds()
t.Logf("1 stream: %5d events %4d sends %v %7.0f events/sec",
eventsSent, sends, elapsed.Round(time.Millisecond), singleRate)
})
var parallelRate float64
t.Run("10_pipelined_streams", func(t *testing.T) {
// atomic.Int64 guarantees 8-byte alignment on 32-bit architectures where
// a local int64 variable's address is only 4-byte aligned and atomic
// 64-bit operations panic with "unaligned 64-bit atomic operation".
var totalEventsSent, totalSends atomic.Int64
var wg sync.WaitGroup
start := time.Now()
for d := 0; d < numDirs; d++ {
wg.Add(1)
go func(files []logFile) {
defer wg.Done()
eventsSent, sends, _, _ := simulatePipeline(files)
totalEventsSent.Add(eventsSent)
totalSends.Add(sends)
}(partitions[d])
}
wg.Wait()
elapsed := time.Since(start)
totalEvents := totalEventsSent.Load()
parallelRate = float64(totalEvents) / elapsed.Seconds()
t.Logf("%d streams: %5d events %4d sends %v %7.0f events/sec",
numDirs, totalEvents, totalSends.Load(), elapsed.Round(time.Millisecond), parallelRate)
})
if singleRate > 0 && parallelRate > 0 {
t.Logf("Speedup: %.1fx (%d parallel pipelined streams vs 1)", parallelRate/singleRate, numDirs)
}
}
func TestMaybeSendIdleHeartbeat(t *testing.T) {
lb := log_buffer.NewLogBuffer("test", time.Minute, nil, nil, nil)
defer lb.ShutdownLogBuffer()
fs := &FilerServer{}
const recentEvent = int64(1_000_000)
t.Run("not opted in", func(t *testing.T) {
lb.LastTsNs.Store(recentEvent)
s := &collectingStream{}
req := &filer_pb.SubscribeMetadataRequest{ClientSupportsIdleHeartbeat: false}
got := fs.maybeSendIdleHeartbeat(req, s, lb, recentEvent, recentEvent, 0)
if got != 0 || len(s.messages) != 0 {
t.Fatalf("expected no heartbeat, got lastHeartbeat=%d msgs=%d", got, len(s.messages))
}
})
t.Run("behind buffer head", func(t *testing.T) {
lb.LastTsNs.Store(recentEvent)
s := &collectingStream{}
req := &filer_pb.SubscribeMetadataRequest{ClientSupportsIdleHeartbeat: true}
// startTs and lastSeen both below the buffer head: still replaying.
got := fs.maybeSendIdleHeartbeat(req, s, lb, 0, recentEvent-1, 0)
if got != 0 || len(s.messages) != 0 {
t.Fatalf("expected no heartbeat while behind, got lastHeartbeat=%d msgs=%d", got, len(s.messages))
}
})
t.Run("caught up via lastSeen", func(t *testing.T) {
lb.LastTsNs.Store(recentEvent)
s := &collectingStream{}
req := &filer_pb.SubscribeMetadataRequest{ClientSupportsIdleHeartbeat: true}
got := fs.maybeSendIdleHeartbeat(req, s, lb, 0, recentEvent, 0)
if len(s.messages) != 1 {
t.Fatalf("expected one heartbeat, got %d", len(s.messages))
}
hb := s.messages[0]
if hb.EventNotification != nil || len(hb.Events) != 0 || hb.TsNs <= 0 {
t.Fatalf("heartbeat should be an empty timestamped response, got %+v", hb)
}
if got != hb.TsNs {
t.Fatalf("expected returned lastHeartbeat %d to equal sent ts %d", got, hb.TsNs)
}
})
t.Run("caught up via read position floor", func(t *testing.T) {
// The read cursor has advanced past the buffer head while lastSeen stayed
// 0. This is the idle-source case (subscribed from "now", read nothing) and
// also metadata-chunks mode, where persisted entries replay as log file
// refs and never reach eachLogEntryFn.
lb.LastTsNs.Store(recentEvent)
s := &collectingStream{}
req := &filer_pb.SubscribeMetadataRequest{ClientSupportsIdleHeartbeat: true}
readPosition := time.Now().UnixNano()
got := fs.maybeSendIdleHeartbeat(req, s, lb, readPosition, 0, 0)
if len(s.messages) != 1 || got <= 0 {
t.Fatalf("expected heartbeat for caught-up subscriber, got msgs=%d lastHeartbeat=%d", len(s.messages), got)
}
})
t.Run("throttled within interval", func(t *testing.T) {
lb.LastTsNs.Store(recentEvent)
s := &collectingStream{}
req := &filer_pb.SubscribeMetadataRequest{ClientSupportsIdleHeartbeat: true}
justSent := time.Now().UnixNano()
got := fs.maybeSendIdleHeartbeat(req, s, lb, 0, recentEvent, justSent)
if got != justSent || len(s.messages) != 0 {
t.Fatalf("expected throttled (no send), got lastHeartbeat=%d msgs=%d", got, len(s.messages))
}
})
t.Run("send error keeps prior heartbeat time", func(t *testing.T) {
lb.LastTsNs.Store(recentEvent)
s := &collectingStream{err: fmt.Errorf("broken stream")}
req := &filer_pb.SubscribeMetadataRequest{ClientSupportsIdleHeartbeat: true}
got := fs.maybeSendIdleHeartbeat(req, s, lb, 0, recentEvent, 0)
if got != 0 {
t.Fatalf("expected lastHeartbeat unchanged on send error, got %d", got)
}
})
}
// TestFilteredEventsEmitMaxUnsyncedMarker pins the source-side shape the client
// keys off: after MaxUnsyncedEvents filtered events, eachEventNotificationFn
// emits a marker with a fresh timestamp and a non-nil but empty EventNotification.
// Consumed by TestFilerSyncOffsetStaysFreshOnFilteredMarker.
func TestFilteredEventsEmitMaxUnsyncedMarker(t *testing.T) {
fs := &FilerServer{
option: &FilerOption{Host: pb.ServerAddress("127.0.0.1:8888")},
filer: &filer.Filer{Signature: 123},
}
req := &filer_pb.SubscribeMetadataRequest{ClientName: "syncFrom_A_To_B", PathPrefix: "/watched/"}
stream := &collectingStream{}
var unsyncedEvents int64
eachEventFn := fs.eachEventNotificationFn(req, stream, "client", &unsyncedEvents)
base := time.Now().UnixNano()
var lastTsNs int64
// Feed MaxUnsyncedEvents+1 events on a NON-watched path so every one is filtered.
total := int(MaxUnsyncedEvents) + 1
for i := 0; i < total; i++ {
lastTsNs = base + int64(i)
err := eachEventFn("/other/dir", &filer_pb.EventNotification{
NewEntry: &filer_pb.Entry{Name: fmt.Sprintf("file%d", i)},
}, lastTsNs)
if err != nil {
t.Fatalf("eachEventFn: %v", err)
}
}
if len(stream.messages) != 1 {
t.Fatalf("expected exactly 1 MaxUnsyncedEvents marker, got %d", len(stream.messages))
}
marker := stream.messages[0]
if !filer_pb.IsEmpty(marker) {
t.Errorf("marker should have empty EventNotification (IsEmpty), got %+v", marker.EventNotification)
}
if marker.EventNotification == nil {
t.Error("marker EventNotification should be non-nil but empty (the shape the client keys off)")
}
if marker.TsNs != lastTsNs {
t.Errorf("marker TsNs = %d, want fresh source ts %d", marker.TsNs, lastTsNs)
}
t.Logf("source emits marker{EventNotification:&{}, TsNs:%d} after %d filtered events", marker.TsNs, total)
}