snapshot of https://github.com/seaweedfs/seaweedfs.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
This commit is contained in:
794
weed/server/filer_subscribe_loop_test.go
Normal file
794
weed/server/filer_subscribe_loop_test.go
Normal file
@@ -0,0 +1,794 @@
|
||||
package weed_server
|
||||
|
||||
// End-to-end tests for the metadata subscribe loops. The unit tests in this
|
||||
// package pin individual helpers; every escaped bug across this PR's review
|
||||
// rounds lived in the interactions - the loop state machine, the disk/memory
|
||||
// handoff, and the server/client contract. These tests run the real
|
||||
// SubscribeLocalMetadata loop against a real filer store, with only the volume
|
||||
// layer faked, and assert the delivered stream itself: exactly the written
|
||||
// events, in order, no duplicates from the entry path, and the chunk-mode
|
||||
// marker never claiming more than the real client code applies.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/filer"
|
||||
"github.com/seaweedfs/seaweedfs/weed/filer/leveldb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util/log_buffer"
|
||||
)
|
||||
|
||||
// ---- store configuration ----
|
||||
|
||||
type testConfig map[string]string
|
||||
|
||||
func (c testConfig) GetString(key string) string { return c[key] }
|
||||
func (c testConfig) GetBool(key string) bool { return false }
|
||||
func (c testConfig) GetInt(key string) int { return 0 }
|
||||
func (c testConfig) GetStringSlice(key string) []string { return nil }
|
||||
func (c testConfig) SetDefault(key string, v interface{}) {}
|
||||
|
||||
// ---- fake gRPC stream ----
|
||||
|
||||
type fakeSubscribeStream struct {
|
||||
ctx context.Context
|
||||
mu sync.Mutex
|
||||
msgs []*filer_pb.SubscribeMetadataResponse
|
||||
}
|
||||
|
||||
func (s *fakeSubscribeStream) Send(resp *filer_pb.SubscribeMetadataResponse) error {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return s.ctx.Err()
|
||||
default:
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.msgs = append(s.msgs, resp)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *fakeSubscribeStream) snapshot() []*filer_pb.SubscribeMetadataResponse {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return append([]*filer_pb.SubscribeMetadataResponse(nil), s.msgs...)
|
||||
}
|
||||
|
||||
func (s *fakeSubscribeStream) Context() context.Context { return s.ctx }
|
||||
func (s *fakeSubscribeStream) SetHeader(metadata.MD) error { return nil }
|
||||
func (s *fakeSubscribeStream) SendHeader(metadata.MD) error { return nil }
|
||||
func (s *fakeSubscribeStream) SetTrailer(metadata.MD) {}
|
||||
func (s *fakeSubscribeStream) SendMsg(m interface{}) error { return nil }
|
||||
func (s *fakeSubscribeStream) RecvMsg(m interface{}) error { return nil }
|
||||
|
||||
// ---- fake volume layer ----
|
||||
|
||||
type fakeLogVolumes struct {
|
||||
mu sync.Mutex
|
||||
bytes map[string][]byte // chunk fileId -> raw log bytes (size-prefixed entries)
|
||||
dead map[string]bool
|
||||
nextId int
|
||||
}
|
||||
|
||||
func newFakeLogVolumes() *fakeLogVolumes {
|
||||
return &fakeLogVolumes{bytes: make(map[string][]byte), dead: make(map[string]bool)}
|
||||
}
|
||||
|
||||
func (v *fakeLogVolumes) put(data []byte) string {
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
v.nextId++
|
||||
id := fmt.Sprintf("t,%d", v.nextId)
|
||||
v.bytes[id] = data
|
||||
return id
|
||||
}
|
||||
|
||||
func (v *fakeLogVolumes) kill(fileId string) {
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
v.dead[fileId] = true
|
||||
}
|
||||
|
||||
// notFoundErr matches both the server's and the client's missing-chunk
|
||||
// predicates, like a real dead log volume does.
|
||||
func notFoundErr(fileId string) error { return fmt.Errorf("read %s: volume 42 not found", fileId) }
|
||||
|
||||
func (v *fakeLogVolumes) get(fileId string) ([]byte, error) {
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
if v.dead[fileId] {
|
||||
return nil, notFoundErr(fileId)
|
||||
}
|
||||
data, found := v.bytes[fileId]
|
||||
if !found {
|
||||
return nil, notFoundErr(fileId)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func decodeLogBytes(data []byte) ([]*filer_pb.LogEntry, error) {
|
||||
var entries []*filer_pb.LogEntry
|
||||
for pos := 0; pos+4 <= len(data); {
|
||||
size := int(util.BytesToUint32(data[pos : pos+4]))
|
||||
if pos+4+size > len(data) {
|
||||
break // torn tail
|
||||
}
|
||||
entry := &filer_pb.LogEntry{}
|
||||
if err := entry.UnmarshalVT(data[pos+4 : pos+4+size]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
pos += 4 + size
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// chunkStreamReader mimics the client's whole-file byte stream: sequential,
|
||||
// erroring at the first dead chunk.
|
||||
type chunkStreamReader struct {
|
||||
vol *fakeLogVolumes
|
||||
chunks []*filer_pb.FileChunk
|
||||
buf []byte
|
||||
idx int
|
||||
}
|
||||
|
||||
func (r *chunkStreamReader) Read(p []byte) (int, error) {
|
||||
for len(r.buf) == 0 {
|
||||
if r.idx >= len(r.chunks) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
data, err := r.vol.get(r.chunks[r.idx].GetFileIdString())
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
r.buf = data
|
||||
r.idx++
|
||||
}
|
||||
n := copy(p, r.buf)
|
||||
r.buf = r.buf[n:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (r *chunkStreamReader) Close() error { return nil }
|
||||
|
||||
// ---- the harness ----
|
||||
|
||||
type subscribeHarness struct {
|
||||
t *testing.T
|
||||
f *filer.Filer
|
||||
fs *FilerServer
|
||||
vol *fakeLogVolumes
|
||||
base int64 // fixed timestamp origin; a per-call time.Now() shifts across second boundaries mid-test
|
||||
|
||||
// flushGate, when non-nil, blocks the flush function - the "volume outage
|
||||
// stalls the metadata log flush" state the whole PR exists to handle.
|
||||
gateMu sync.Mutex
|
||||
flushGate chan struct{}
|
||||
|
||||
// flushMu guards stopped against the flushes still in flight when the test
|
||||
// ends, so none of them writes through a store the cleanup has closed.
|
||||
flushMu sync.RWMutex
|
||||
stopped bool
|
||||
}
|
||||
|
||||
const testFilerIdSuffix = "0000abcd"
|
||||
|
||||
func newSubscribeHarness(t *testing.T) *subscribeHarness {
|
||||
// Shrink the timing knobs so parks and retries run at test speed.
|
||||
prevRetry, prevWarn, prevStall := unflushedGapRetryInterval, gapStallWarnInterval, maxGapStall
|
||||
unflushedGapRetryInterval, gapStallWarnInterval, maxGapStall = 30*time.Millisecond, 200*time.Millisecond, time.Hour
|
||||
t.Cleanup(func() {
|
||||
unflushedGapRetryInterval, gapStallWarnInterval, maxGapStall = prevRetry, prevWarn, prevStall
|
||||
})
|
||||
|
||||
f := filer.NewFiler(pb.ServerDiscovery{}, nil, "", "", "", "", "", 255, nil)
|
||||
store := &leveldb.LevelDBStore{}
|
||||
if err := store.Initialize(testConfig{"test.dir": t.TempDir()}, "test."); err != nil {
|
||||
t.Fatalf("init store: %v", err)
|
||||
}
|
||||
f.SetStore(store)
|
||||
|
||||
vol := newFakeLogVolumes()
|
||||
restore := filer.SetLogReadHooksForTesting(
|
||||
func(chunk *filer_pb.FileChunk) ([]*filer_pb.LogEntry, error) {
|
||||
data, err := vol.get(chunk.GetFileIdString())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return decodeLogBytes(data)
|
||||
},
|
||||
func(chunks []*filer_pb.FileChunk) io.Reader {
|
||||
return &chunkStreamReader{vol: vol, chunks: chunks}
|
||||
},
|
||||
func(fileId string) error {
|
||||
_, err := vol.get(fileId)
|
||||
return err
|
||||
},
|
||||
)
|
||||
t.Cleanup(restore)
|
||||
|
||||
h := &subscribeHarness{t: t, f: f, vol: vol,
|
||||
base: time.Now().Add(-time.Hour).Truncate(time.Second).UnixNano()}
|
||||
|
||||
// The real buffer, with the flush function writing through the fake volume
|
||||
// layer the way logFlushFunc writes through real volumes.
|
||||
f.LocalMetaLogBuffer.ShutdownLogBuffer()
|
||||
f.LocalMetaLogBuffer = log_buffer.NewLogBuffer("local", time.Minute, h.flushToStore, nil, nil)
|
||||
// Shutting the buffer down is not enough: NewFiler's deletion loop keeps the
|
||||
// filer - and so the replacement buffer's tens of megabytes - reachable for
|
||||
// the rest of the run. Quiesce the flush path first, since Filer.Shutdown
|
||||
// closes the store a flush still in flight would write through.
|
||||
t.Cleanup(f.Shutdown)
|
||||
t.Cleanup(h.stopFlushes)
|
||||
|
||||
h.fs = &FilerServer{
|
||||
filer: f,
|
||||
option: &FilerOption{Host: pb.ServerAddress("test:8888")},
|
||||
knownListeners: make(map[int32]int32),
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *subscribeHarness) blockFlushes() {
|
||||
h.gateMu.Lock()
|
||||
defer h.gateMu.Unlock()
|
||||
if h.flushGate == nil {
|
||||
h.flushGate = make(chan struct{})
|
||||
}
|
||||
}
|
||||
|
||||
func (h *subscribeHarness) releaseFlushes() {
|
||||
h.gateMu.Lock()
|
||||
defer h.gateMu.Unlock()
|
||||
if h.flushGate != nil {
|
||||
close(h.flushGate)
|
||||
h.flushGate = nil
|
||||
}
|
||||
}
|
||||
|
||||
// stopFlushes lets go of any gated flush and waits for the ones in flight to
|
||||
// finish, then refuses the rest, so the store stays untouched from here on.
|
||||
func (h *subscribeHarness) stopFlushes() {
|
||||
h.releaseFlushes()
|
||||
h.flushMu.Lock()
|
||||
defer h.flushMu.Unlock()
|
||||
h.stopped = true
|
||||
}
|
||||
|
||||
func (h *subscribeHarness) flushToStore(lb *log_buffer.LogBuffer, startTime, stopTime time.Time, buf []byte, minOffset, maxOffset int64) {
|
||||
h.gateMu.Lock()
|
||||
gate := h.flushGate
|
||||
h.gateMu.Unlock()
|
||||
if gate != nil {
|
||||
<-gate
|
||||
}
|
||||
|
||||
h.flushMu.RLock()
|
||||
defer h.flushMu.RUnlock()
|
||||
if h.stopped {
|
||||
return
|
||||
}
|
||||
|
||||
// The same file naming and append shape as logFlushFunc, against the fake
|
||||
// volumes: one chunk per flushed window, named for the window start minute.
|
||||
startTime, stopTime = startTime.UTC(), stopTime.UTC()
|
||||
targetFile := fmt.Sprintf("%s/%04d-%02d-%02d/%02d-%02d.%s", filer.SystemLogDir,
|
||||
startTime.Year(), startTime.Month(), startTime.Day(), startTime.Hour(), startTime.Minute(), testFilerIdSuffix)
|
||||
data := append([]byte(nil), buf...)
|
||||
fileId := h.vol.put(data)
|
||||
|
||||
ctx := context.Background()
|
||||
fullpath := util.FullPath(targetFile)
|
||||
entry, err := h.f.FindEntry(ctx, fullpath)
|
||||
var offset int64
|
||||
if err == filer_pb.ErrNotFound {
|
||||
entry = &filer.Entry{
|
||||
FullPath: fullpath,
|
||||
Attr: filer.Attr{Crtime: time.Now(), Mtime: time.Now(), Mode: 0644},
|
||||
}
|
||||
} else if err != nil {
|
||||
h.t.Errorf("find %s: %v", targetFile, err)
|
||||
return
|
||||
} else {
|
||||
offset = int64(filer.TotalSize(entry.GetChunks()))
|
||||
}
|
||||
entry.Chunks = append(entry.GetChunks(), &filer_pb.FileChunk{
|
||||
FileId: fileId,
|
||||
Offset: offset,
|
||||
Size: uint64(len(data)),
|
||||
ModifiedTsNs: time.Now().UnixNano(),
|
||||
})
|
||||
if err := h.f.CreateEntry(ctx, entry, nil, false, false, nil, false, 255); err != nil {
|
||||
h.t.Errorf("write log file %s: %v", targetFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
// event builds a metadata event log entry the way the filer's notification
|
||||
// path does, so the loop's real decode and filter code runs.
|
||||
func testEvent(tsNs int64, name string) *filer_pb.LogEntry {
|
||||
data, err := proto.Marshal(&filer_pb.SubscribeMetadataResponse{
|
||||
Directory: "/t",
|
||||
EventNotification: &filer_pb.EventNotification{NewEntry: &filer_pb.Entry{Name: name}},
|
||||
TsNs: tsNs,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &filer_pb.LogEntry{TsNs: tsNs, Data: data, Key: []byte("/t/" + name)}
|
||||
}
|
||||
|
||||
func (h *subscribeHarness) append(tsNs int64) {
|
||||
if err := h.f.LocalMetaLogBuffer.AddLogEntryToBuffer(testEvent(tsNs, fmt.Sprintf("f-%d", tsNs))); err != nil {
|
||||
h.t.Fatalf("append: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type runningSubscribe struct {
|
||||
stream *fakeSubscribeStream
|
||||
cancel context.CancelFunc
|
||||
done chan error
|
||||
finished chan struct{} // closed after done is populated; safe to wait repeatedly
|
||||
}
|
||||
|
||||
func (h *subscribeHarness) subscribe(sinceNs int64, mutate func(*filer_pb.SubscribeMetadataRequest)) *runningSubscribe {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
stream := &fakeSubscribeStream{ctx: ctx}
|
||||
req := &filer_pb.SubscribeMetadataRequest{
|
||||
ClientName: "loop-test",
|
||||
ClientId: 7,
|
||||
ClientEpoch: 1,
|
||||
SinceNs: sinceNs,
|
||||
}
|
||||
if mutate != nil {
|
||||
mutate(req)
|
||||
}
|
||||
r := &runningSubscribe{stream: stream, cancel: cancel, done: make(chan error, 1), finished: make(chan struct{})}
|
||||
go func() {
|
||||
r.done <- h.fs.SubscribeLocalMetadata(req, stream)
|
||||
close(r.finished)
|
||||
}()
|
||||
h.t.Cleanup(func() {
|
||||
cancel()
|
||||
select {
|
||||
case <-r.finished:
|
||||
case <-time.After(5 * time.Second):
|
||||
h.t.Error("subscribe loop did not exit on cancel")
|
||||
}
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
// eventTimestamps extracts delivered metadata events (markers, heartbeats and
|
||||
// refs excluded).
|
||||
func eventTimestamps(msgs []*filer_pb.SubscribeMetadataResponse) []int64 {
|
||||
var out []int64
|
||||
for _, m := range msgs {
|
||||
if len(m.LogFileRefs) > 0 || m.EventNotification == nil || m.EventNotification.NewEntry == nil {
|
||||
continue
|
||||
}
|
||||
out = append(out, m.TsNs)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func waitForEvents(t *testing.T, r *runningSubscribe, want []int64, timeout time.Duration) []int64 {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(timeout)
|
||||
var got []int64
|
||||
for time.Now().Before(deadline) {
|
||||
got = eventTimestamps(r.stream.snapshot())
|
||||
if len(got) >= len(want) {
|
||||
break
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
if fmt.Sprint(got) != fmt.Sprint(want) {
|
||||
t.Fatalf("delivered %v, want %v", got, want)
|
||||
}
|
||||
return got
|
||||
}
|
||||
|
||||
func assertNoEventsFor(t *testing.T, r *runningSubscribe, d time.Duration) {
|
||||
t.Helper()
|
||||
time.Sleep(d)
|
||||
if got := eventTimestamps(r.stream.snapshot()); len(got) > 0 {
|
||||
t.Fatalf("delivered %v while the gap was unproven; these events must wait", got)
|
||||
}
|
||||
}
|
||||
|
||||
// tsAt returns test timestamps from the harness's fixed base: old enough that
|
||||
// windows seal on the jump between them, entries 1ms apart within a window.
|
||||
func (h *subscribeHarness) tsAt(window, i int) int64 {
|
||||
return h.base + int64(window)*int64(2*time.Minute) + int64(i)*int64(time.Millisecond)
|
||||
}
|
||||
|
||||
// ---- scenarios ----
|
||||
|
||||
// The headline behavior of the whole PR: events evicted from the ring before
|
||||
// their flush landed must not be skipped. The subscriber parks while the gap
|
||||
// is unproven and delivers everything once the stalled flush lands.
|
||||
func TestSubscribeLoop_EvictedUnflushedGapWaitsThenDelivers(t *testing.T) {
|
||||
h := newSubscribeHarness(t)
|
||||
h.blockFlushes()
|
||||
|
||||
var want []int64
|
||||
for w := 0; w < log_buffer.PreviousBufferCount+3; w++ {
|
||||
ts := h.tsAt(w, 0)
|
||||
want = append(want, ts)
|
||||
h.append(ts)
|
||||
}
|
||||
if h.f.LocalMetaLogBuffer.GetLastEvictedTsNs() == 0 {
|
||||
t.Fatal("precondition: the ring evicted nothing")
|
||||
}
|
||||
|
||||
r := h.subscribe(0, nil)
|
||||
// The evicted windows are nowhere: not in memory, not on disk. Master
|
||||
// silently skipped them here; the loop must park instead.
|
||||
assertNoEventsFor(t, r, 300*time.Millisecond)
|
||||
|
||||
h.releaseFlushes()
|
||||
waitForEvents(t, r, want, 5*time.Second)
|
||||
}
|
||||
|
||||
// A gap that never existed is proven empty and served from memory promptly -
|
||||
// the guard must not park subscribers on rings that evicted nothing.
|
||||
func TestSubscribeLoop_NothingEvictedServesFromMemory(t *testing.T) {
|
||||
h := newSubscribeHarness(t)
|
||||
h.blockFlushes() // no disk at all; memory alone must serve
|
||||
|
||||
want := []int64{h.tsAt(0, 0), h.tsAt(0, 1), h.tsAt(0, 2)}
|
||||
for _, ts := range want {
|
||||
h.append(ts)
|
||||
}
|
||||
r := h.subscribe(0, nil)
|
||||
waitForEvents(t, r, want, 3*time.Second)
|
||||
}
|
||||
|
||||
// Disk backlog then live tail: the handoff must deliver every event exactly
|
||||
// once, in order. Timestamps are 1ms-adjacent ACROSS every boundary - window
|
||||
// to window on disk, and disk to retained memory - so a cursor error of even
|
||||
// one entry at any handoff shows up as a hole or a duplicate. Flushed windows
|
||||
// exist on disk AND in the retained ring, which is exactly where
|
||||
// inclusive/exclusive mistakes on either side used to hide.
|
||||
func TestSubscribeLoop_BacklogThenLiveExactlyOnce(t *testing.T) {
|
||||
h := newSubscribeHarness(t)
|
||||
|
||||
ts := func(i int) int64 { return h.base + int64(i)*int64(time.Millisecond) }
|
||||
|
||||
var want []int64
|
||||
n := 0
|
||||
for w := 0; w < 3; w++ {
|
||||
for i := 0; i < 3; i++ {
|
||||
want = append(want, ts(n))
|
||||
h.append(ts(n))
|
||||
n++
|
||||
}
|
||||
h.f.LocalMetaLogBuffer.ForceFlush() // adjacent-timestamp window boundary on disk
|
||||
}
|
||||
// A retained, unflushed tail starting 1ms after the flushed content ends.
|
||||
for i := 0; i < 3; i++ {
|
||||
want = append(want, ts(n))
|
||||
h.append(ts(n))
|
||||
n++
|
||||
}
|
||||
|
||||
r := h.subscribe(0, nil)
|
||||
waitForEvents(t, r, want, 5*time.Second)
|
||||
|
||||
// Live tail on top.
|
||||
live := []int64{ts(n), ts(n + 1)}
|
||||
for _, l := range live {
|
||||
h.append(l)
|
||||
}
|
||||
waitForEvents(t, r, append(append([]int64(nil), want...), live...), 5*time.Second)
|
||||
}
|
||||
|
||||
// A bounded subscription delivers through its bound and terminates - it must
|
||||
// not park forever on the gap machinery with its window already served.
|
||||
func TestSubscribeLoop_BoundedSubscriptionTerminates(t *testing.T) {
|
||||
h := newSubscribeHarness(t)
|
||||
|
||||
all := []int64{h.tsAt(0, 0), h.tsAt(0, 1), h.tsAt(1, 0), h.tsAt(1, 1)}
|
||||
for _, ts := range all {
|
||||
h.append(ts)
|
||||
}
|
||||
h.f.LocalMetaLogBuffer.ForceFlush()
|
||||
|
||||
until := all[1]
|
||||
r := h.subscribe(0, func(req *filer_pb.SubscribeMetadataRequest) { req.UntilNs = until })
|
||||
|
||||
select {
|
||||
case err := <-r.done:
|
||||
if err != nil {
|
||||
t.Fatalf("bounded subscription failed: %v", err)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("bounded subscription did not terminate")
|
||||
}
|
||||
for _, ts := range eventTimestamps(r.stream.snapshot()) {
|
||||
if ts > until {
|
||||
t.Fatalf("delivered %d past the bound %d", ts, until)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A bounded subscription against a filer whose meta-log buffer has never taken
|
||||
// a write - fresh restart, zero metadata traffic since - must still terminate.
|
||||
// The empty ring makes every memory read return ResumeFromDiskError, and with
|
||||
// no gap to resolve the loop used to park inside LoopProcessLogData forever,
|
||||
// its idle heartbeats keeping the stream looking healthy to the client.
|
||||
func TestSubscribeLoop_BoundedEmptyFilerTerminates(t *testing.T) {
|
||||
h := newSubscribeHarness(t)
|
||||
|
||||
r := h.subscribe(h.base, func(req *filer_pb.SubscribeMetadataRequest) {
|
||||
req.UntilNs = time.Now().UnixNano()
|
||||
})
|
||||
|
||||
select {
|
||||
case err := <-r.done:
|
||||
if err != nil {
|
||||
t.Fatalf("bounded subscription failed: %v", err)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("bounded subscription on an idle filer did not terminate")
|
||||
}
|
||||
if got := eventTimestamps(r.stream.snapshot()); len(got) > 0 {
|
||||
t.Fatalf("delivered %v from an empty filer", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Vacuumed logs: the flush watermark proves a gap empty even though the files
|
||||
// are gone - the resolver must skip to the retained ring and deliver its
|
||||
// earliest window intact, including a single-entry window whose start and stop
|
||||
// coincide. This is the one path where resolveGapResume itself advances the
|
||||
// stream, and the resume-below-earliest arithmetic is load-bearing.
|
||||
func TestSubscribeLoop_FlushProvenGapSkipsToRetained(t *testing.T) {
|
||||
h := newSubscribeHarness(t)
|
||||
|
||||
windows := log_buffer.PreviousBufferCount + 2
|
||||
for w := 0; w < windows; w++ {
|
||||
h.append(h.tsAt(w, 0))
|
||||
}
|
||||
// The last append seals the window before it, so every window but the
|
||||
// current one is queued for flush. Wait for all of them, not just the one
|
||||
// the eviction watermark names: a flush still in flight writes its file
|
||||
// back after the delete below, and the subscriber then serves that window
|
||||
// from disk instead of taking the gap path this test is about.
|
||||
waitForFlushedFiles(t, h, h.tsAt(windows-2, 0))
|
||||
if h.f.LocalMetaLogBuffer.GetLastEvictedTsNs() == 0 {
|
||||
t.Fatal("precondition: nothing evicted")
|
||||
}
|
||||
deleteAllLogFiles(t, h)
|
||||
|
||||
earliest := h.f.LocalMetaLogBuffer.GetEarliestTime().UnixNano()
|
||||
var retained []int64
|
||||
for w := 0; w < windows; w++ {
|
||||
if ts := h.tsAt(w, 0); ts >= earliest {
|
||||
retained = append(retained, ts)
|
||||
}
|
||||
}
|
||||
|
||||
r := h.subscribe(0, nil)
|
||||
waitForEvents(t, r, retained, 5*time.Second)
|
||||
}
|
||||
|
||||
// waitForFlushedFiles waits until the flush of the window ending at
|
||||
// throughTsNs has landed. Flushes run in queue order on one goroutine, so that
|
||||
// also proves every window sealed before it is on disk and none is in flight.
|
||||
func waitForFlushedFiles(t *testing.T, h *subscribeHarness, throughTsNs int64) {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(3 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
if h.f.LocalMetaLogBuffer.GetLastFlushTsNs() >= throughTsNs {
|
||||
return
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
t.Fatal("flushes did not land")
|
||||
}
|
||||
|
||||
func deleteAllLogFiles(t *testing.T, h *subscribeHarness) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
days, _, err := h.f.ListDirectoryEntries(ctx, filer.SystemLogDir, "", true, 1000, "", "", "")
|
||||
if err != nil {
|
||||
t.Fatalf("list log days: %v", err)
|
||||
}
|
||||
store := h.f.GetStore()
|
||||
for _, day := range days {
|
||||
if err := store.DeleteFolderChildren(ctx, day.FullPath); err != nil {
|
||||
t.Fatalf("delete %s children: %v", day.FullPath, err)
|
||||
}
|
||||
if err := store.DeleteEntry(ctx, day.FullPath); err != nil {
|
||||
t.Fatalf("delete %s: %v", day.FullPath, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A permanently wedged flush ends in the loud give-up skip: the loss is
|
||||
// bounded to the unprovable range, counted, and the stream keeps delivering
|
||||
// what memory still holds - it must not stay silent forever and must not fail.
|
||||
func TestSubscribeLoop_GiveUpSkipsAndKeepsStreaming(t *testing.T) {
|
||||
h := newSubscribeHarness(t)
|
||||
prevStall := maxGapStall
|
||||
maxGapStall = 250 * time.Millisecond
|
||||
t.Cleanup(func() { maxGapStall = prevStall })
|
||||
|
||||
h.blockFlushes()
|
||||
var retained []int64
|
||||
for w := 0; w < log_buffer.PreviousBufferCount+3; w++ {
|
||||
h.append(h.tsAt(w, 0))
|
||||
}
|
||||
// What the ring still holds after eviction is what must arrive post-skip.
|
||||
earliest := h.f.LocalMetaLogBuffer.GetEarliestTime().UnixNano()
|
||||
for w := 0; w < log_buffer.PreviousBufferCount+3; w++ {
|
||||
if ts := h.tsAt(w, 0); ts >= earliest {
|
||||
retained = append(retained, ts)
|
||||
}
|
||||
}
|
||||
|
||||
r := h.subscribe(0, nil)
|
||||
waitForEvents(t, r, retained, 5*time.Second)
|
||||
select {
|
||||
case err := <-r.done:
|
||||
t.Fatalf("stream ended (%v); the give-up must keep it alive", err)
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// Chunk mode, checked against the real client code: everything the marker
|
||||
// claims must be applied by pb.ReadLogFileRefs over the shipped refs, and the
|
||||
// inline stream must start strictly after the marker - the contract whose two
|
||||
// sides drifted in round after round of review.
|
||||
func TestSubscribeLoop_ChunkModeMarkerMatchesClientReplay(t *testing.T) {
|
||||
h := newSubscribeHarness(t)
|
||||
|
||||
var want []int64
|
||||
for w := 0; w < 3; w++ {
|
||||
for i := 0; i < 3; i++ {
|
||||
ts := h.tsAt(w, i)
|
||||
want = append(want, ts)
|
||||
h.append(ts)
|
||||
}
|
||||
}
|
||||
h.f.LocalMetaLogBuffer.ForceFlush()
|
||||
|
||||
r := h.subscribe(0, func(req *filer_pb.SubscribeMetadataRequest) { req.ClientSupportsMetadataChunks = true })
|
||||
|
||||
// Wait for refs plus their transition marker.
|
||||
var refs []*filer_pb.LogFileChunkRef
|
||||
var markerTsNs int64
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
refs = refs[:0]
|
||||
markerTsNs = 0
|
||||
for _, m := range r.stream.snapshot() {
|
||||
if len(m.LogFileRefs) > 0 {
|
||||
refs = append(refs, m.LogFileRefs...)
|
||||
if markerTsNs != 0 {
|
||||
t.Fatal("refs arrived after their batch's marker")
|
||||
}
|
||||
continue
|
||||
}
|
||||
if m.EventNotification != nil && m.EventNotification.NewEntry == nil && m.TsNs > 0 && markerTsNs == 0 {
|
||||
markerTsNs = m.TsNs
|
||||
}
|
||||
}
|
||||
if markerTsNs != 0 {
|
||||
break
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
if markerTsNs == 0 {
|
||||
t.Fatal("no transition marker followed the refs; the client would buffer them forever")
|
||||
}
|
||||
|
||||
// Apply the refs exactly the way the real client does.
|
||||
var applied []int64
|
||||
clientLastTs, err := pb.ReadLogFileRefs(refs,
|
||||
func(chunks []*filer_pb.FileChunk) (io.ReadCloser, error) {
|
||||
return &chunkStreamReader{vol: h.vol, chunks: chunks}, nil
|
||||
},
|
||||
0, 0, pb.PathFilter{},
|
||||
func(resp *filer_pb.SubscribeMetadataResponse) error {
|
||||
applied = append(applied, resp.TsNs)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("client replay: %v", err)
|
||||
}
|
||||
if markerTsNs > clientLastTs {
|
||||
t.Fatalf("marker %d claims more than the client applied through %d; the difference is silently lost", markerTsNs, clientLastTs)
|
||||
}
|
||||
if fmt.Sprint(applied) != fmt.Sprint(want) {
|
||||
t.Fatalf("client applied %v, want %v", applied, want)
|
||||
}
|
||||
|
||||
// The inline stream must not re-deliver ref-covered content.
|
||||
for _, ts := range eventTimestamps(r.stream.snapshot()) {
|
||||
if ts <= markerTsNs {
|
||||
t.Fatalf("inline event %d at or below the marker %d duplicates the client's chunk replay", ts, markerTsNs)
|
||||
}
|
||||
}
|
||||
|
||||
// And a live tail still arrives inline, after the marker.
|
||||
live := h.tsAt(4, 0)
|
||||
h.append(live)
|
||||
waitForEvents(t, r, []int64{live}, 5*time.Second)
|
||||
}
|
||||
|
||||
// Chunk mode with a dead volume mid-file: the marker must stop where the
|
||||
// client's read stops, the stream must keep working, and the events after the
|
||||
// dead chunk's file must still arrive.
|
||||
func TestSubscribeLoop_ChunkModeDeadVolumeAgreesWithClient(t *testing.T) {
|
||||
h := newSubscribeHarness(t)
|
||||
|
||||
// Three flushed windows -> three files; kill the middle file's chunk.
|
||||
var written []int64
|
||||
for w := 0; w < 3; w++ {
|
||||
ts := h.tsAt(w, 0)
|
||||
written = append(written, ts)
|
||||
h.append(ts)
|
||||
}
|
||||
h.f.LocalMetaLogBuffer.ForceFlush()
|
||||
h.vol.kill("t,2")
|
||||
|
||||
r := h.subscribe(0, func(req *filer_pb.SubscribeMetadataRequest) { req.ClientSupportsMetadataChunks = true })
|
||||
|
||||
var refs []*filer_pb.LogFileChunkRef
|
||||
var markerTsNs int64
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
refs = refs[:0]
|
||||
markerTsNs = 0
|
||||
for _, m := range r.stream.snapshot() {
|
||||
if len(m.LogFileRefs) > 0 {
|
||||
refs = append(refs, m.LogFileRefs...)
|
||||
} else if m.EventNotification != nil && m.EventNotification.NewEntry == nil && m.TsNs > markerTsNs {
|
||||
markerTsNs = m.TsNs
|
||||
}
|
||||
}
|
||||
if markerTsNs != 0 {
|
||||
break
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
if markerTsNs == 0 {
|
||||
t.Fatal("no transition marker; a dead volume must not block it")
|
||||
}
|
||||
|
||||
var applied []int64
|
||||
clientLastTs, err := pb.ReadLogFileRefs(refs,
|
||||
func(chunks []*filer_pb.FileChunk) (io.ReadCloser, error) {
|
||||
return &chunkStreamReader{vol: h.vol, chunks: chunks}, nil
|
||||
},
|
||||
0, 0, pb.PathFilter{},
|
||||
func(resp *filer_pb.SubscribeMetadataResponse) error {
|
||||
applied = append(applied, resp.TsNs)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("client replay: %v", err)
|
||||
}
|
||||
if markerTsNs > clientLastTs {
|
||||
t.Fatalf("marker %d ahead of the client's %d with a dead chunk in between; the suffix is silently lost", markerTsNs, clientLastTs)
|
||||
}
|
||||
// The client skips the dead file but applies the later one.
|
||||
sort.Slice(applied, func(i, j int) bool { return applied[i] < applied[j] })
|
||||
appliedStr := fmt.Sprint(applied)
|
||||
if !strings.Contains(appliedStr, fmt.Sprint(written[2])) || strings.Contains(appliedStr, fmt.Sprint(written[1])) {
|
||||
t.Fatalf("client applied %v; want the dead file %d skipped and the later file %d applied", applied, written[1], written[2])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user