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:
792
weed/server/filer_grpc_server_stream_mutate_bench_test.go
Normal file
792
weed/server/filer_grpc_server_stream_mutate_bench_test.go
Normal file
@@ -0,0 +1,792 @@
|
||||
package weed_server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
// fakeFilerServer simulates a filer whose CreateEntry takes a fixed amount of
|
||||
// wall-clock time (meant to model the filer-store commit latency). It implements
|
||||
// only what these benchmarks need: unary CreateEntry and bidi StreamMutateEntry.
|
||||
//
|
||||
// The StreamMutateEntry handler mirrors the production handler's structure:
|
||||
// a single goroutine per stream, processing one request at a time.
|
||||
type fakeFilerServer struct {
|
||||
filer_pb.UnimplementedSeaweedFilerServer
|
||||
serviceDelay time.Duration
|
||||
createCalls atomic.Int64
|
||||
}
|
||||
|
||||
func (s *fakeFilerServer) CreateEntry(ctx context.Context, req *filer_pb.CreateEntryRequest) (*filer_pb.CreateEntryResponse, error) {
|
||||
s.createCalls.Add(1)
|
||||
if s.serviceDelay > 0 {
|
||||
time.Sleep(s.serviceDelay)
|
||||
}
|
||||
return &filer_pb.CreateEntryResponse{}, nil
|
||||
}
|
||||
|
||||
// StreamMutateEntry mirrors the OLD serial handler: one goroutine, strictly
|
||||
// one request at a time.
|
||||
func (s *fakeFilerServer) StreamMutateEntry(stream grpc.BidiStreamingServer[filer_pb.StreamMutateEntryRequest, filer_pb.StreamMutateEntryResponse]) error {
|
||||
for {
|
||||
req, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch r := req.Request.(type) {
|
||||
case *filer_pb.StreamMutateEntryRequest_CreateRequest:
|
||||
resp, createErr := s.CreateEntry(stream.Context(), r.CreateRequest)
|
||||
if createErr != nil {
|
||||
resp = &filer_pb.CreateEntryResponse{Error: createErr.Error()}
|
||||
}
|
||||
out := &filer_pb.StreamMutateEntryResponse{
|
||||
RequestId: req.RequestId,
|
||||
IsLast: true,
|
||||
Response: &filer_pb.StreamMutateEntryResponse_CreateResponse{CreateResponse: resp},
|
||||
}
|
||||
if resp.Error != "" {
|
||||
out.Error = resp.Error
|
||||
out.Errno = int32(syscall.EIO)
|
||||
}
|
||||
if sendErr := stream.Send(out); sendErr != nil {
|
||||
return sendErr
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fakeConcurrentFilerServer mirrors the NEW concurrent handler: per-request
|
||||
// goroutine, bounded by a semaphore, with a Send mutex for stream safety.
|
||||
// Structurally identical to weed/server/filer_grpc_server_stream_mutate.go
|
||||
// after the parallelization patch.
|
||||
type fakeConcurrentFilerServer struct {
|
||||
filer_pb.UnimplementedSeaweedFilerServer
|
||||
serviceDelay time.Duration
|
||||
concurrency int
|
||||
createCalls atomic.Int64
|
||||
}
|
||||
|
||||
func (s *fakeConcurrentFilerServer) CreateEntry(ctx context.Context, req *filer_pb.CreateEntryRequest) (*filer_pb.CreateEntryResponse, error) {
|
||||
s.createCalls.Add(1)
|
||||
if s.serviceDelay > 0 {
|
||||
time.Sleep(s.serviceDelay)
|
||||
}
|
||||
return &filer_pb.CreateEntryResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *fakeConcurrentFilerServer) StreamMutateEntry(stream grpc.BidiStreamingServer[filer_pb.StreamMutateEntryRequest, filer_pb.StreamMutateEntryResponse]) error {
|
||||
var sendMu sync.Mutex
|
||||
send := func(r *filer_pb.StreamMutateEntryResponse) error {
|
||||
sendMu.Lock()
|
||||
defer sendMu.Unlock()
|
||||
return stream.Send(r)
|
||||
}
|
||||
sem := make(chan struct{}, s.concurrency)
|
||||
var wg sync.WaitGroup
|
||||
for {
|
||||
req, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
wg.Wait()
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
wg.Wait()
|
||||
return err
|
||||
}
|
||||
sem <- struct{}{}
|
||||
wg.Add(1)
|
||||
go func(req *filer_pb.StreamMutateEntryRequest) {
|
||||
defer wg.Done()
|
||||
defer func() { <-sem }()
|
||||
switch r := req.Request.(type) {
|
||||
case *filer_pb.StreamMutateEntryRequest_CreateRequest:
|
||||
resp, _ := s.CreateEntry(stream.Context(), r.CreateRequest)
|
||||
_ = send(&filer_pb.StreamMutateEntryResponse{
|
||||
RequestId: req.RequestId,
|
||||
IsLast: true,
|
||||
Response: &filer_pb.StreamMutateEntryResponse_CreateResponse{CreateResponse: resp},
|
||||
})
|
||||
}
|
||||
}(req)
|
||||
}
|
||||
}
|
||||
|
||||
// startFakeConcurrentFilerServer spins up the concurrent-handler variant.
|
||||
func startFakeConcurrentFilerServer(t testing.TB, serviceDelay time.Duration, concurrency int) (string, *fakeConcurrentFilerServer, func()) {
|
||||
t.Helper()
|
||||
lis, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
srv := grpc.NewServer()
|
||||
fake := &fakeConcurrentFilerServer{serviceDelay: serviceDelay, concurrency: concurrency}
|
||||
filer_pb.RegisterSeaweedFilerServer(srv, fake)
|
||||
go srv.Serve(lis)
|
||||
return lis.Addr().String(), fake, func() {
|
||||
srv.GracefulStop()
|
||||
_ = lis.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// fakeSchedulerFilerServer uses the real mutateScheduler from the production
|
||||
// handler to admit requests by (path, kind). Per-path operations serialize;
|
||||
// cross-path operations run in parallel.
|
||||
type fakeSchedulerFilerServer struct {
|
||||
filer_pb.UnimplementedSeaweedFilerServer
|
||||
serviceDelay time.Duration
|
||||
concurrency int
|
||||
createCalls atomic.Int64
|
||||
maxInFlight atomic.Int64 // sampled peak of in-flight create goroutines
|
||||
curInFlight atomic.Int64
|
||||
}
|
||||
|
||||
func (s *fakeSchedulerFilerServer) CreateEntry(ctx context.Context, req *filer_pb.CreateEntryRequest) (*filer_pb.CreateEntryResponse, error) {
|
||||
s.createCalls.Add(1)
|
||||
cur := s.curInFlight.Add(1)
|
||||
for {
|
||||
peak := s.maxInFlight.Load()
|
||||
if cur <= peak || s.maxInFlight.CompareAndSwap(peak, cur) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if s.serviceDelay > 0 {
|
||||
time.Sleep(s.serviceDelay)
|
||||
}
|
||||
s.curInFlight.Add(-1)
|
||||
return &filer_pb.CreateEntryResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *fakeSchedulerFilerServer) StreamMutateEntry(stream grpc.BidiStreamingServer[filer_pb.StreamMutateEntryRequest, filer_pb.StreamMutateEntryResponse]) error {
|
||||
var sendMu sync.Mutex
|
||||
send := func(r *filer_pb.StreamMutateEntryResponse) error {
|
||||
sendMu.Lock()
|
||||
defer sendMu.Unlock()
|
||||
return stream.Send(r)
|
||||
}
|
||||
sched := newMutateScheduler(s.concurrency)
|
||||
var wg sync.WaitGroup
|
||||
for {
|
||||
req, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
wg.Wait()
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
wg.Wait()
|
||||
return err
|
||||
}
|
||||
primary, secondary, kind := classifyMutation(req)
|
||||
sched.Admit(primary, secondary, kind)
|
||||
wg.Add(1)
|
||||
go func(req *filer_pb.StreamMutateEntryRequest) {
|
||||
defer wg.Done()
|
||||
defer sched.Done(primary, secondary, kind)
|
||||
switch r := req.Request.(type) {
|
||||
case *filer_pb.StreamMutateEntryRequest_CreateRequest:
|
||||
resp, _ := s.CreateEntry(stream.Context(), r.CreateRequest)
|
||||
_ = send(&filer_pb.StreamMutateEntryResponse{
|
||||
RequestId: req.RequestId,
|
||||
IsLast: true,
|
||||
Response: &filer_pb.StreamMutateEntryResponse_CreateResponse{CreateResponse: resp},
|
||||
})
|
||||
}
|
||||
}(req)
|
||||
}
|
||||
}
|
||||
|
||||
func startFakeSchedulerFilerServer(t testing.TB, serviceDelay time.Duration, concurrency int) (string, *fakeSchedulerFilerServer, func()) {
|
||||
t.Helper()
|
||||
lis, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
srv := grpc.NewServer()
|
||||
fake := &fakeSchedulerFilerServer{serviceDelay: serviceDelay, concurrency: concurrency}
|
||||
filer_pb.RegisterSeaweedFilerServer(srv, fake)
|
||||
go srv.Serve(lis)
|
||||
return lis.Addr().String(), fake, func() {
|
||||
srv.GracefulStop()
|
||||
_ = lis.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// startFakeFilerServer spins up an in-process gRPC filer with the given per-
|
||||
// request service delay, returns the dial target and a shutdown func.
|
||||
func startFakeFilerServer(t testing.TB, serviceDelay time.Duration) (string, *fakeFilerServer, func()) {
|
||||
t.Helper()
|
||||
lis, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
srv := grpc.NewServer()
|
||||
fake := &fakeFilerServer{serviceDelay: serviceDelay}
|
||||
filer_pb.RegisterSeaweedFilerServer(srv, fake)
|
||||
go srv.Serve(lis)
|
||||
return lis.Addr().String(), fake, func() {
|
||||
srv.GracefulStop()
|
||||
_ = lis.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// dialFakeFiler returns a client connection to the in-process filer.
|
||||
func dialFakeFiler(t testing.TB, addr string) *grpc.ClientConn {
|
||||
t.Helper()
|
||||
conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("dial: %v", err)
|
||||
}
|
||||
return conn
|
||||
}
|
||||
|
||||
// opsForWorker splits `ops` across `concurrency` workers, handing the
|
||||
// remainder to the first `remainder` workers so the total is always exactly
|
||||
// `ops`. Returns the count for worker index g.
|
||||
func opsForWorker(g, concurrency, ops int) int {
|
||||
per := ops / concurrency
|
||||
if g < ops%concurrency {
|
||||
return per + 1
|
||||
}
|
||||
return per
|
||||
}
|
||||
|
||||
// runUnaryCreateWorkload fires `ops` CreateEntry unary RPCs across `concurrency`
|
||||
// goroutines, all sharing a single client connection. Returns total duration.
|
||||
func runUnaryCreateWorkload(t testing.TB, conn *grpc.ClientConn, concurrency, ops int) time.Duration {
|
||||
t.Helper()
|
||||
client := filer_pb.NewSeaweedFilerClient(conn)
|
||||
var wg sync.WaitGroup
|
||||
start := time.Now()
|
||||
for g := 0; g < concurrency; g++ {
|
||||
wg.Add(1)
|
||||
count := opsForWorker(g, concurrency, ops)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < count; i++ {
|
||||
_, err := client.CreateEntry(context.Background(), &filer_pb.CreateEntryRequest{
|
||||
Directory: "/",
|
||||
Entry: &filer_pb.Entry{
|
||||
Name: "f",
|
||||
IsDirectory: false,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("unary CreateEntry: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
return time.Since(start)
|
||||
}
|
||||
|
||||
// pathFn chooses the (directory, name) of the i'th op. The default used by the
|
||||
// existing callers is samePath — all ops hit "/f". distinctPath gives every
|
||||
// op its own name so cross-path parallelism can kick in.
|
||||
type pathFn func(i int) (dir, name string)
|
||||
|
||||
func samePath(_ int) (string, string) { return "/", "f" }
|
||||
func distinctPath(i int) (string, string) { return "/", "f" + itoa(i) }
|
||||
|
||||
// itoa is a tight local decimal formatter — avoids a strconv import inside a
|
||||
// hot benchmark loop.
|
||||
func itoa(n int) string {
|
||||
if n == 0 {
|
||||
return "0"
|
||||
}
|
||||
buf := make([]byte, 0, 12)
|
||||
for n > 0 {
|
||||
buf = append(buf, byte('0'+n%10))
|
||||
n /= 10
|
||||
}
|
||||
for i, j := 0, len(buf)-1; i < j; i, j = i+1, j-1 {
|
||||
buf[i], buf[j] = buf[j], buf[i]
|
||||
}
|
||||
return string(buf)
|
||||
}
|
||||
|
||||
// runStreamCreateWorkload fires `ops` CreateEntry ops through a SINGLE bidi
|
||||
// stream, with `concurrency` goroutines all submitting to the same stream via
|
||||
// a small multiplexer that mirrors weed/mount/weedfs_stream_mutate.go's
|
||||
// sendLoop+recvLoop structure. Returns total duration.
|
||||
func runStreamCreateWorkload(t testing.TB, conn *grpc.ClientConn, concurrency, ops int) time.Duration {
|
||||
return runStreamCreateWorkloadAt(t, conn, concurrency, ops, samePath)
|
||||
}
|
||||
|
||||
func runStreamCreateWorkloadAt(t testing.TB, conn *grpc.ClientConn, concurrency, ops int, path pathFn) time.Duration {
|
||||
t.Helper()
|
||||
client := filer_pb.NewSeaweedFilerClient(conn)
|
||||
stream, err := client.StreamMutateEntry(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("open stream: %v", err)
|
||||
}
|
||||
|
||||
var nextID atomic.Uint64
|
||||
var pending sync.Map // map[uint64]chan struct{}
|
||||
|
||||
// single recv goroutine, dispatching by request id
|
||||
recvErr := make(chan error, 1)
|
||||
go func() {
|
||||
for {
|
||||
resp, err := stream.Recv()
|
||||
if err != nil {
|
||||
recvErr <- err
|
||||
return
|
||||
}
|
||||
if ch, ok := pending.LoadAndDelete(resp.RequestId); ok {
|
||||
close(ch.(chan struct{}))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// dedicated send goroutine (gRPC Send is not concurrent-safe)
|
||||
sendCh := make(chan *filer_pb.StreamMutateEntryRequest, 512)
|
||||
sendErr := make(chan error, 1)
|
||||
go func() {
|
||||
for req := range sendCh {
|
||||
if err := stream.Send(req); err != nil {
|
||||
sendErr <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
sendErr <- nil
|
||||
}()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
start := time.Now()
|
||||
offset := 0
|
||||
for g := 0; g < concurrency; g++ {
|
||||
wg.Add(1)
|
||||
count := opsForWorker(g, concurrency, ops)
|
||||
startOff := offset
|
||||
offset += count
|
||||
go func(startOff, count int) {
|
||||
defer wg.Done()
|
||||
for i := 0; i < count; i++ {
|
||||
id := nextID.Add(1)
|
||||
done := make(chan struct{})
|
||||
pending.Store(id, done)
|
||||
dir, name := path(startOff + i)
|
||||
sendCh <- &filer_pb.StreamMutateEntryRequest{
|
||||
RequestId: id,
|
||||
Request: &filer_pb.StreamMutateEntryRequest_CreateRequest{
|
||||
CreateRequest: &filer_pb.CreateEntryRequest{
|
||||
Directory: dir,
|
||||
Entry: &filer_pb.Entry{Name: name},
|
||||
},
|
||||
},
|
||||
}
|
||||
<-done
|
||||
}
|
||||
}(startOff, count)
|
||||
}
|
||||
wg.Wait()
|
||||
elapsed := time.Since(start)
|
||||
|
||||
close(sendCh)
|
||||
_ = stream.CloseSend()
|
||||
select {
|
||||
case <-sendErr:
|
||||
case <-time.After(time.Second):
|
||||
}
|
||||
select {
|
||||
case <-recvErr:
|
||||
case <-time.After(time.Second):
|
||||
}
|
||||
return elapsed
|
||||
}
|
||||
|
||||
// runStreamAsyncCreateWorkload fires `ops` CreateEntry ops through a single
|
||||
// bidi stream WITHOUT the client waiting for responses between sends. The
|
||||
// timer starts at the first send and stops when the last response is received,
|
||||
// so this measures true end-to-end throughput (not just client-send rate).
|
||||
//
|
||||
// This isolates the question: does relaxing client-side per-request waiting
|
||||
// help throughput when the server is a serial loop?
|
||||
func runStreamAsyncCreateWorkload(t testing.TB, conn *grpc.ClientConn, concurrency, ops int) time.Duration {
|
||||
t.Helper()
|
||||
client := filer_pb.NewSeaweedFilerClient(conn)
|
||||
stream, err := client.StreamMutateEntry(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("open stream: %v", err)
|
||||
}
|
||||
|
||||
var nextID atomic.Uint64
|
||||
var outstanding atomic.Int64
|
||||
allDone := make(chan struct{})
|
||||
|
||||
// Single recv goroutine — signals allDone when the last response arrives.
|
||||
recvErr := make(chan error, 1)
|
||||
go func() {
|
||||
for {
|
||||
_, err := stream.Recv()
|
||||
if err != nil {
|
||||
recvErr <- err
|
||||
return
|
||||
}
|
||||
if outstanding.Add(-1) == 0 {
|
||||
select {
|
||||
case <-allDone:
|
||||
default:
|
||||
close(allDone)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Dedicated send goroutine (gRPC Send is not concurrent-safe).
|
||||
sendCh := make(chan *filer_pb.StreamMutateEntryRequest, 4096)
|
||||
sendErr := make(chan error, 1)
|
||||
go func() {
|
||||
for req := range sendCh {
|
||||
if err := stream.Send(req); err != nil {
|
||||
sendErr <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
sendErr <- nil
|
||||
}()
|
||||
|
||||
outstanding.Store(int64(ops))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
start := time.Now()
|
||||
for g := 0; g < concurrency; g++ {
|
||||
wg.Add(1)
|
||||
count := opsForWorker(g, concurrency, ops)
|
||||
go func(count int) {
|
||||
defer wg.Done()
|
||||
for i := 0; i < count; i++ {
|
||||
id := nextID.Add(1)
|
||||
// Fire-and-forget: push to sendCh, do not wait for response.
|
||||
sendCh <- &filer_pb.StreamMutateEntryRequest{
|
||||
RequestId: id,
|
||||
Request: &filer_pb.StreamMutateEntryRequest_CreateRequest{
|
||||
CreateRequest: &filer_pb.CreateEntryRequest{
|
||||
Directory: "/",
|
||||
Entry: &filer_pb.Entry{Name: "f"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}(count)
|
||||
}
|
||||
wg.Wait()
|
||||
// All requests queued; wait for server to drain all responses.
|
||||
<-allDone
|
||||
elapsed := time.Since(start)
|
||||
|
||||
close(sendCh)
|
||||
_ = stream.CloseSend()
|
||||
select {
|
||||
case <-sendErr:
|
||||
case <-time.After(time.Second):
|
||||
}
|
||||
select {
|
||||
case <-recvErr:
|
||||
case <-time.After(time.Second):
|
||||
}
|
||||
return elapsed
|
||||
}
|
||||
|
||||
// TestReproStreamSerializationCeiling runs a head-to-head: same concurrency,
|
||||
// same op count, same per-request service delay. The only difference is the
|
||||
// transport — unary (one goroutine per RPC at the server) vs single bidi stream
|
||||
// (one goroutine for the whole stream at the server).
|
||||
//
|
||||
// With a 2ms per-request service delay, unary with N=12 should deliver ~6000/s
|
||||
// (12 concurrent handlers, each 2ms), while stream delivers ~500/s (serial).
|
||||
func TestReproStreamSerializationCeiling(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping repro in -short mode")
|
||||
}
|
||||
|
||||
const (
|
||||
concurrency = 12
|
||||
opsPerRun = 1200
|
||||
serviceDelay = 2 * time.Millisecond
|
||||
)
|
||||
|
||||
addr, fake, stop := startFakeFilerServer(t, serviceDelay)
|
||||
defer stop()
|
||||
|
||||
conn := dialFakeFiler(t, addr)
|
||||
defer conn.Close()
|
||||
|
||||
// warm up one call so the gRPC HTTP/2 connection is fully established
|
||||
warmupClient := filer_pb.NewSeaweedFilerClient(conn)
|
||||
if _, err := warmupClient.CreateEntry(context.Background(), &filer_pb.CreateEntryRequest{
|
||||
Directory: "/",
|
||||
Entry: &filer_pb.Entry{Name: "warmup"},
|
||||
}); err != nil {
|
||||
t.Fatalf("warmup: %v", err)
|
||||
}
|
||||
fake.createCalls.Store(0)
|
||||
|
||||
unaryDur := runUnaryCreateWorkload(t, conn, concurrency, opsPerRun)
|
||||
unaryCalls := fake.createCalls.Load()
|
||||
fake.createCalls.Store(0)
|
||||
|
||||
streamDur := runStreamCreateWorkload(t, conn, concurrency, opsPerRun)
|
||||
streamCalls := fake.createCalls.Load()
|
||||
|
||||
unaryQPS := float64(unaryCalls) / unaryDur.Seconds()
|
||||
streamQPS := float64(streamCalls) / streamDur.Seconds()
|
||||
|
||||
t.Logf("service delay: %v", serviceDelay)
|
||||
t.Logf("concurrency: %d", concurrency)
|
||||
t.Logf("ops per run: %d", opsPerRun)
|
||||
t.Logf("unary : %d calls in %v -> %.0f QPS", unaryCalls, unaryDur, unaryQPS)
|
||||
t.Logf("stream : %d calls in %v -> %.0f QPS", streamCalls, streamDur, streamQPS)
|
||||
t.Logf("unary / stream ratio: %.2fx", unaryQPS/streamQPS)
|
||||
|
||||
// The serial stream handler cannot exceed 1 / serviceDelay regardless of
|
||||
// client concurrency. Assert that unary is meaningfully faster — if the
|
||||
// server is ever parallelized, this assertion is safe to revisit.
|
||||
if unaryQPS < streamQPS*2 {
|
||||
t.Fatalf("expected unary to be at least 2x faster than stream, got unary=%.0f stream=%.0f",
|
||||
unaryQPS, streamQPS)
|
||||
}
|
||||
|
||||
// Stream QPS should cluster near the theoretical ceiling of 1/serviceDelay.
|
||||
theoreticalMax := 1.0 / serviceDelay.Seconds()
|
||||
if streamQPS > theoreticalMax*1.25 {
|
||||
t.Fatalf("stream QPS %.0f exceeds theoretical serial ceiling %.0f — handler may have changed",
|
||||
streamQPS, theoreticalMax)
|
||||
}
|
||||
}
|
||||
|
||||
// TestServerSerialVsConcurrentHandler measures the lift from changing the
|
||||
// server-side handler from serial (one goroutine per stream) to concurrent
|
||||
// (per-request goroutine, bounded by a semaphore, Send protected by a mutex).
|
||||
//
|
||||
// Client is identical in both runs (sync stream, N concurrent submitters).
|
||||
// Only the server handler differs.
|
||||
func TestServerSerialVsConcurrentHandler(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping repro in -short mode")
|
||||
}
|
||||
|
||||
const (
|
||||
opsPerRun = 2400
|
||||
serviceDelay = 2 * time.Millisecond
|
||||
serverConcurrency = 64
|
||||
)
|
||||
|
||||
// Serial server (the OLD handler).
|
||||
serialAddr, serialFake, stopSerial := startFakeFilerServer(t, serviceDelay)
|
||||
defer stopSerial()
|
||||
serialConn := dialFakeFiler(t, serialAddr)
|
||||
defer serialConn.Close()
|
||||
|
||||
// Concurrent server (the NEW handler).
|
||||
concAddr, concFake, stopConc := startFakeConcurrentFilerServer(t, serviceDelay, serverConcurrency)
|
||||
defer stopConc()
|
||||
concConn := dialFakeFiler(t, concAddr)
|
||||
defer concConn.Close()
|
||||
|
||||
// Warm up both connections.
|
||||
for _, c := range []*grpc.ClientConn{serialConn, concConn} {
|
||||
client := filer_pb.NewSeaweedFilerClient(c)
|
||||
if _, err := client.CreateEntry(context.Background(), &filer_pb.CreateEntryRequest{
|
||||
Directory: "/",
|
||||
Entry: &filer_pb.Entry{Name: "warmup"},
|
||||
}); err != nil {
|
||||
t.Fatalf("warmup: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
clientConcurrencies := []int{1, 12, 64, 256}
|
||||
theoreticalSerial := 1.0 / serviceDelay.Seconds()
|
||||
theoreticalConcurrent := float64(serverConcurrency) / serviceDelay.Seconds()
|
||||
|
||||
t.Logf("service delay: %v", serviceDelay)
|
||||
t.Logf("server-side concurrency cap: %d", serverConcurrency)
|
||||
t.Logf("theoretical serial ceiling: %.0f QPS", theoreticalSerial)
|
||||
t.Logf("theoretical concurrent cap: %.0f QPS", theoreticalConcurrent)
|
||||
t.Logf("ops per run: %d", opsPerRun)
|
||||
t.Logf("")
|
||||
t.Logf("%-16s %-14s %-14s %-10s",
|
||||
"client workers", "serial QPS", "concurrent QPS", "lift")
|
||||
|
||||
for _, c := range clientConcurrencies {
|
||||
serialFake.createCalls.Store(0)
|
||||
serialDur := runStreamCreateWorkload(t, serialConn, c, opsPerRun)
|
||||
serialCalls := serialFake.createCalls.Load()
|
||||
serialQPS := float64(serialCalls) / serialDur.Seconds()
|
||||
|
||||
concFake.createCalls.Store(0)
|
||||
concDur := runStreamCreateWorkload(t, concConn, c, opsPerRun)
|
||||
concCalls := concFake.createCalls.Load()
|
||||
concQPS := float64(concCalls) / concDur.Seconds()
|
||||
|
||||
t.Logf("%-16d %-14.0f %-14.0f %.2fx",
|
||||
c, serialQPS, concQPS, concQPS/serialQPS)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSchedulerOrderedParallelism compares three server-side handler shapes
|
||||
// under two workloads:
|
||||
//
|
||||
// serial : OLD handler — one goroutine per stream
|
||||
// sem : NEW handler v1 — per-request goroutine + semaphore, no ordering
|
||||
// scheduler : NEW handler v2 — per-path admission + subtree barriers
|
||||
// (mirrors filer.sync's MetadataProcessor)
|
||||
//
|
||||
// Workload "distinct" puts each op on its own path; workload "same" puts every
|
||||
// op on the same path.
|
||||
//
|
||||
// Expected shape:
|
||||
// - serial — both workloads ~1/serviceDelay (no parallelism)
|
||||
// - sem — both workloads lift ~serverConcurrency× (wrong: reorders
|
||||
// same-path ops, acceptable for our filer store but undesired)
|
||||
// - scheduler — distinct workload lifts ~serverConcurrency×
|
||||
// same workload falls back to ~1/serviceDelay (correctness)
|
||||
func TestSchedulerOrderedParallelism(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping repro in -short mode")
|
||||
}
|
||||
|
||||
const (
|
||||
opsPerRun = 2400
|
||||
serviceDelay = 2 * time.Millisecond
|
||||
serverConcurrency = 64
|
||||
clientWorkers = 12
|
||||
)
|
||||
|
||||
// Serial, sem-only concurrent, scheduler-based concurrent.
|
||||
serialAddr, serialFake, stopSerial := startFakeFilerServer(t, serviceDelay)
|
||||
defer stopSerial()
|
||||
serialConn := dialFakeFiler(t, serialAddr)
|
||||
defer serialConn.Close()
|
||||
|
||||
semAddr, semFake, stopSem := startFakeConcurrentFilerServer(t, serviceDelay, serverConcurrency)
|
||||
defer stopSem()
|
||||
semConn := dialFakeFiler(t, semAddr)
|
||||
defer semConn.Close()
|
||||
|
||||
schedAddr, schedFake, stopSched := startFakeSchedulerFilerServer(t, serviceDelay, serverConcurrency)
|
||||
defer stopSched()
|
||||
schedConn := dialFakeFiler(t, schedAddr)
|
||||
defer schedConn.Close()
|
||||
|
||||
// Warm up all three connections.
|
||||
for _, c := range []*grpc.ClientConn{serialConn, semConn, schedConn} {
|
||||
client := filer_pb.NewSeaweedFilerClient(c)
|
||||
if _, err := client.CreateEntry(context.Background(), &filer_pb.CreateEntryRequest{
|
||||
Directory: "/",
|
||||
Entry: &filer_pb.Entry{Name: "warmup"},
|
||||
}); err != nil {
|
||||
t.Fatalf("warmup: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("service delay: %v", serviceDelay)
|
||||
t.Logf("server concurrency: %d", serverConcurrency)
|
||||
t.Logf("client workers: %d", clientWorkers)
|
||||
t.Logf("ops per run: %d", opsPerRun)
|
||||
t.Logf("serial ceiling: %.0f QPS", 1.0/serviceDelay.Seconds())
|
||||
t.Logf("concurrent ceiling: %.0f QPS (%d x %.0f)", float64(serverConcurrency)/serviceDelay.Seconds(), serverConcurrency, 1.0/serviceDelay.Seconds())
|
||||
t.Logf("")
|
||||
|
||||
workloads := []struct {
|
||||
name string
|
||||
fn pathFn
|
||||
}{
|
||||
{"distinct paths", distinctPath},
|
||||
{"same path ", samePath},
|
||||
}
|
||||
|
||||
t.Logf("%-16s %-14s %-14s %-14s %-14s",
|
||||
"workload", "serial QPS", "sem QPS", "sched QPS", "sched peak-conc")
|
||||
|
||||
for _, w := range workloads {
|
||||
serialFake.createCalls.Store(0)
|
||||
serialDur := runStreamCreateWorkloadAt(t, serialConn, clientWorkers, opsPerRun, w.fn)
|
||||
serialQPS := float64(serialFake.createCalls.Load()) / serialDur.Seconds()
|
||||
|
||||
semFake.createCalls.Store(0)
|
||||
semDur := runStreamCreateWorkloadAt(t, semConn, clientWorkers, opsPerRun, w.fn)
|
||||
semQPS := float64(semFake.createCalls.Load()) / semDur.Seconds()
|
||||
|
||||
schedFake.createCalls.Store(0)
|
||||
schedFake.maxInFlight.Store(0)
|
||||
schedDur := runStreamCreateWorkloadAt(t, schedConn, clientWorkers, opsPerRun, w.fn)
|
||||
schedQPS := float64(schedFake.createCalls.Load()) / schedDur.Seconds()
|
||||
schedPeak := schedFake.maxInFlight.Load()
|
||||
|
||||
t.Logf("%-16s %-14.0f %-14.0f %-14.0f %d",
|
||||
w.name, serialQPS, semQPS, schedQPS, schedPeak)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStreamSyncVsAsyncClient measures whether making the CLIENT side
|
||||
// asynchronous (fire-and-forget, no per-request waits) lifts the server-set
|
||||
// ceiling. It runs sync and async workloads against the same serial server
|
||||
// and compares end-to-end throughput.
|
||||
//
|
||||
// Expectation: at steady state, async cannot exceed 1/serviceDelay because the
|
||||
// server still processes one request at a time. Any "improvement" is transient
|
||||
// (in-flight pipelining up to the HTTP/2 window) and disappears as ops >> window.
|
||||
func TestStreamSyncVsAsyncClient(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping repro in -short mode")
|
||||
}
|
||||
|
||||
const (
|
||||
opsPerRun = 2400
|
||||
serviceDelay = 2 * time.Millisecond
|
||||
)
|
||||
|
||||
addr, fake, stop := startFakeFilerServer(t, serviceDelay)
|
||||
defer stop()
|
||||
|
||||
conn := dialFakeFiler(t, addr)
|
||||
defer conn.Close()
|
||||
|
||||
warmupClient := filer_pb.NewSeaweedFilerClient(conn)
|
||||
if _, err := warmupClient.CreateEntry(context.Background(), &filer_pb.CreateEntryRequest{
|
||||
Directory: "/",
|
||||
Entry: &filer_pb.Entry{Name: "warmup"},
|
||||
}); err != nil {
|
||||
t.Fatalf("warmup: %v", err)
|
||||
}
|
||||
|
||||
// Sweep concurrency to show that async does not break the 1/serviceDelay
|
||||
// ceiling no matter how much client-side parallelism we throw at it.
|
||||
concurrencies := []int{1, 12, 64, 256}
|
||||
t.Logf("service delay: %v (theoretical serial ceiling = %.0f QPS)",
|
||||
serviceDelay, 1.0/serviceDelay.Seconds())
|
||||
t.Logf("ops per run: %d", opsPerRun)
|
||||
t.Logf("")
|
||||
t.Logf("%-12s %-12s %-12s %-10s", "concurrency", "sync QPS", "async QPS", "ratio")
|
||||
|
||||
for _, c := range concurrencies {
|
||||
fake.createCalls.Store(0)
|
||||
syncDur := runStreamCreateWorkload(t, conn, c, opsPerRun)
|
||||
syncCalls := fake.createCalls.Load()
|
||||
syncQPS := float64(syncCalls) / syncDur.Seconds()
|
||||
|
||||
fake.createCalls.Store(0)
|
||||
asyncDur := runStreamAsyncCreateWorkload(t, conn, c, opsPerRun)
|
||||
asyncCalls := fake.createCalls.Load()
|
||||
asyncQPS := float64(asyncCalls) / asyncDur.Seconds()
|
||||
|
||||
t.Logf("%-12d %-12.0f %-12.0f %-10.2fx",
|
||||
c, syncQPS, asyncQPS, asyncQPS/syncQPS)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user