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

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

View File

@@ -0,0 +1,58 @@
# Dockerfile for Kafka Gateway Integration Testing
FROM golang:1.26-alpine AS builder
# Install build dependencies
RUN apk add --no-cache git make gcc musl-dev sqlite-dev
# Set working directory
WORKDIR /app
# Copy go mod files
COPY go.mod go.sum ./
# Download dependencies
RUN go mod download
# Copy source code
COPY . .
# Build the weed binaries with Kafka gateway support
RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -ldflags '-extldflags "-static"' -o weed ./weed
RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -ldflags '-extldflags "-static"' -o weed-sql ./cmd/weed-sql
# Final stage
FROM alpine:latest
# Install runtime dependencies
RUN apk --no-cache add ca-certificates wget curl netcat-openbsd sqlite
# Create non-root user
RUN addgroup -g 1000 seaweedfs && \
adduser -D -s /bin/sh -u 1000 -G seaweedfs seaweedfs
# Set working directory
WORKDIR /usr/bin
# Copy binaries from builder
COPY --from=builder /app/weed .
COPY --from=builder /app/weed-sql .
# Create data directory
RUN mkdir -p /data && chown seaweedfs:seaweedfs /data
# Copy startup script
COPY test/kafka/scripts/kafka-gateway-start.sh /usr/bin/kafka-gateway-start.sh
RUN chmod +x /usr/bin/kafka-gateway-start.sh
# Switch to non-root user
USER seaweedfs
# Expose Kafka protocol port and pprof port
EXPOSE 9093 10093
# Health check
HEALTHCHECK --interval=10s --timeout=5s --start-period=30s --retries=3 \
CMD nc -z localhost 9093 || exit 1
# Default command
CMD ["/usr/bin/kafka-gateway-start.sh"]

View File

@@ -0,0 +1,25 @@
# Dockerfile for building SeaweedFS components from the current workspace
FROM golang:1.26-alpine AS builder
RUN apk add --no-cache git make gcc musl-dev sqlite-dev
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=1 GOOS=linux go build -o /out/weed ./weed
FROM alpine:latest
RUN apk --no-cache add ca-certificates curl wget netcat-openbsd sqlite
COPY --from=builder /out/weed /usr/bin/weed
WORKDIR /data
EXPOSE 9333 19333 8080 18080 8888 18888 16777 17777
ENTRYPOINT ["/usr/bin/weed"]

View File

@@ -0,0 +1,29 @@
# Dockerfile for Kafka Integration Test Setup
FROM golang:1.26-alpine AS builder
# Install build dependencies
RUN apk add --no-cache git make gcc musl-dev
# Copy repository
WORKDIR /app
COPY . .
# Build test setup utility from the test module
WORKDIR /app/test/kafka
RUN go mod download
RUN CGO_ENABLED=1 GOOS=linux go build -o /out/test-setup ./cmd/setup
# Final stage
FROM alpine:latest
# Install runtime dependencies
RUN apk --no-cache add ca-certificates curl jq netcat-openbsd
# Copy binary from builder
COPY --from=builder /out/test-setup /usr/bin/test-setup
# Make executable
RUN chmod +x /usr/bin/test-setup
# Default command
CMD ["/usr/bin/test-setup"]

206
test/kafka/Makefile Normal file
View File

@@ -0,0 +1,206 @@
# Kafka Integration Testing Makefile - Refactored
# This replaces the existing Makefile with better organization
# Configuration
ifndef DOCKER_COMPOSE
DOCKER_COMPOSE := $(if $(shell command -v docker-compose 2>/dev/null),docker-compose,docker compose)
endif
TEST_TIMEOUT ?= 10m
KAFKA_BOOTSTRAP_SERVERS ?= localhost:9092
KAFKA_GATEWAY_URL ?= localhost:9093
SCHEMA_REGISTRY_URL ?= http://localhost:8081
# Colors for output
BLUE := \033[36m
GREEN := \033[32m
YELLOW := \033[33m
RED := \033[31m
NC := \033[0m # No Color
.PHONY: help setup test clean logs status
help: ## Show this help message
@echo "$(BLUE)SeaweedFS Kafka Integration Testing - Refactored$(NC)"
@echo ""
@echo "Available targets:"
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " $(GREEN)%-20s$(NC) %s\n", $$1, $$2}' $(MAKEFILE_LIST)
# Environment Setup
setup: ## Set up test environment (Kafka + Schema Registry + SeaweedFS)
@echo "$(YELLOW)Setting up Kafka integration test environment...$(NC)"
@$(DOCKER_COMPOSE) up -d
@echo "$(BLUE)Waiting for all services to be ready...$(NC)"
@./scripts/wait-for-services.sh
@echo "$(GREEN)Test environment ready!$(NC)"
setup-schemas: setup ## Set up test environment and register schemas
@echo "$(YELLOW)Registering test schemas...$(NC)"
@$(DOCKER_COMPOSE) --profile setup run --rm test-setup
@echo "$(GREEN)Schemas registered!$(NC)"
# Test Categories
test: test-unit test-integration test-e2e ## Run all tests
test-unit: ## Run unit tests
@echo "$(YELLOW)Running unit tests...$(NC)"
@go test -v -timeout=$(TEST_TIMEOUT) ./unit/...
test-integration: ## Run integration tests
@echo "$(YELLOW)Running integration tests...$(NC)"
@go test -v -timeout=$(TEST_TIMEOUT) ./integration/...
test-e2e: setup-schemas ## Run end-to-end tests
@echo "$(YELLOW)Running end-to-end tests...$(NC)"
@KAFKA_BOOTSTRAP_SERVERS=$(KAFKA_BOOTSTRAP_SERVERS) \
KAFKA_GATEWAY_URL=$(KAFKA_GATEWAY_URL) \
SCHEMA_REGISTRY_URL=$(SCHEMA_REGISTRY_URL) \
go test -v -timeout=$(TEST_TIMEOUT) ./e2e/...
test-docker: setup-schemas ## Run Docker integration tests
@echo "$(YELLOW)Running Docker integration tests...$(NC)"
@KAFKA_BOOTSTRAP_SERVERS=$(KAFKA_BOOTSTRAP_SERVERS) \
KAFKA_GATEWAY_URL=$(KAFKA_GATEWAY_URL) \
SCHEMA_REGISTRY_URL=$(SCHEMA_REGISTRY_URL) \
go test -v -timeout=$(TEST_TIMEOUT) ./integration/ -run Docker
# Schema-specific tests
test-schema: setup-schemas ## Run schema registry integration tests
@echo "$(YELLOW)Running schema registry integration tests...$(NC)"
@SCHEMA_REGISTRY_URL=$(SCHEMA_REGISTRY_URL) \
go test -v -timeout=$(TEST_TIMEOUT) ./integration/ -run Schema
# Client-specific tests
test-sarama: setup-schemas ## Run Sarama client tests
@echo "$(YELLOW)Running Sarama client tests...$(NC)"
@KAFKA_BOOTSTRAP_SERVERS=$(KAFKA_BOOTSTRAP_SERVERS) \
KAFKA_GATEWAY_URL=$(KAFKA_GATEWAY_URL) \
go test -v -timeout=$(TEST_TIMEOUT) ./integration/ -run Sarama
test-kafka-go: setup-schemas ## Run kafka-go client tests
@echo "$(YELLOW)Running kafka-go client tests...$(NC)"
@KAFKA_BOOTSTRAP_SERVERS=$(KAFKA_BOOTSTRAP_SERVERS) \
KAFKA_GATEWAY_URL=$(KAFKA_GATEWAY_URL) \
go test -v -timeout=$(TEST_TIMEOUT) ./integration/ -run KafkaGo
# Performance tests
test-performance: setup-schemas ## Run performance benchmarks
@echo "$(YELLOW)Running Kafka performance benchmarks...$(NC)"
@KAFKA_BOOTSTRAP_SERVERS=$(KAFKA_BOOTSTRAP_SERVERS) \
KAFKA_GATEWAY_URL=$(KAFKA_GATEWAY_URL) \
SCHEMA_REGISTRY_URL=$(SCHEMA_REGISTRY_URL) \
go test -v -timeout=$(TEST_TIMEOUT) -bench=. ./...
# Development targets
dev-kafka: ## Start only Kafka ecosystem for development
@$(DOCKER_COMPOSE) up -d zookeeper kafka schema-registry
@sleep 20
@$(DOCKER_COMPOSE) --profile setup run --rm test-setup
dev-seaweedfs: ## Start only SeaweedFS for development
@$(DOCKER_COMPOSE) up -d seaweedfs-master seaweedfs-volume seaweedfs-filer seaweedfs-mq-broker seaweedfs-mq-agent
dev-gateway: dev-seaweedfs ## Start Kafka Gateway for development
@$(DOCKER_COMPOSE) up -d kafka-gateway
dev-test: dev-kafka ## Quick test with just Kafka ecosystem
@SCHEMA_REGISTRY_URL=$(SCHEMA_REGISTRY_URL) go test -v -timeout=30s ./unit/...
# Cleanup
clean: ## Clean up test environment
@echo "$(YELLOW)Cleaning up test environment...$(NC)"
@$(DOCKER_COMPOSE) down -v --remove-orphans
@docker system prune -f
@echo "$(GREEN)Environment cleaned up!$(NC)"
# Monitoring and debugging
logs: ## Show logs from all services
@$(DOCKER_COMPOSE) logs --tail=50 -f
logs-kafka: ## Show Kafka logs
@$(DOCKER_COMPOSE) logs --tail=100 -f kafka
logs-schema-registry: ## Show Schema Registry logs
@$(DOCKER_COMPOSE) logs --tail=100 -f schema-registry
logs-seaweedfs: ## Show SeaweedFS logs
@$(DOCKER_COMPOSE) logs --tail=100 -f seaweedfs-master seaweedfs-volume seaweedfs-filer seaweedfs-mq-broker seaweedfs-mq-agent
logs-gateway: ## Show Kafka Gateway logs
@$(DOCKER_COMPOSE) logs --tail=100 -f kafka-gateway
status: ## Show status of all services
@echo "$(BLUE)Service Status:$(NC)"
@$(DOCKER_COMPOSE) ps
@echo ""
@echo "$(BLUE)Kafka Status:$(NC)"
@curl -s http://localhost:9092 > /dev/null && echo "Kafka accessible" || echo "Kafka not accessible"
@echo ""
@echo "$(BLUE)Schema Registry Status:$(NC)"
@curl -s $(SCHEMA_REGISTRY_URL)/subjects > /dev/null && echo "Schema Registry accessible" || echo "Schema Registry not accessible"
@echo ""
@echo "$(BLUE)Kafka Gateway Status:$(NC)"
@nc -z localhost 9093 && echo "Kafka Gateway accessible" || echo "Kafka Gateway not accessible"
debug: ## Debug test environment
@echo "$(BLUE)Debug Information:$(NC)"
@echo "Kafka Bootstrap Servers: $(KAFKA_BOOTSTRAP_SERVERS)"
@echo "Schema Registry URL: $(SCHEMA_REGISTRY_URL)"
@echo "Kafka Gateway URL: $(KAFKA_GATEWAY_URL)"
@echo ""
@echo "Docker Compose Status:"
@$(DOCKER_COMPOSE) ps
@echo ""
@echo "Network connectivity:"
@docker network ls | grep kafka-integration-test || echo "No Kafka test network found"
@echo ""
@echo "Schema Registry subjects:"
@curl -s $(SCHEMA_REGISTRY_URL)/subjects 2>/dev/null || echo "Schema Registry not accessible"
# Utility targets
install-deps: ## Install required dependencies
@echo "$(YELLOW)Installing test dependencies...$(NC)"
@which docker > /dev/null || (echo "$(RED)Docker not found$(NC)" && exit 1)
@which docker-compose > /dev/null || (echo "$(RED)Docker Compose not found$(NC)" && exit 1)
@which curl > /dev/null || (echo "$(RED)curl not found$(NC)" && exit 1)
@which nc > /dev/null || (echo "$(RED)netcat not found$(NC)" && exit 1)
@echo "$(GREEN)All dependencies available$(NC)"
check-env: ## Check test environment setup
@echo "$(BLUE)Environment Check:$(NC)"
@echo "KAFKA_BOOTSTRAP_SERVERS: $(KAFKA_BOOTSTRAP_SERVERS)"
@echo "SCHEMA_REGISTRY_URL: $(SCHEMA_REGISTRY_URL)"
@echo "KAFKA_GATEWAY_URL: $(KAFKA_GATEWAY_URL)"
@echo "TEST_TIMEOUT: $(TEST_TIMEOUT)"
@make install-deps
# CI targets
ci-test: ## Run tests in CI environment
@echo "$(YELLOW)Running CI tests...$(NC)"
@make setup-schemas
@make test-unit
@make test-integration
@make clean
ci-e2e: ## Run end-to-end tests in CI
@echo "$(YELLOW)Running CI end-to-end tests...$(NC)"
@make test-e2e
@make clean
# Interactive targets
shell-kafka: ## Open shell in Kafka container
@$(DOCKER_COMPOSE) exec kafka bash
shell-gateway: ## Open shell in Kafka Gateway container
@$(DOCKER_COMPOSE) exec kafka-gateway sh
topics: ## List Kafka topics
@$(DOCKER_COMPOSE) exec kafka kafka-topics --list --bootstrap-server localhost:29092
create-topic: ## Create a test topic (usage: make create-topic TOPIC=my-topic)
@$(DOCKER_COMPOSE) exec kafka kafka-topics --create --topic $(TOPIC) --bootstrap-server localhost:29092 --partitions 3 --replication-factor 1
produce: ## Produce test messages (usage: make produce TOPIC=my-topic)
@$(DOCKER_COMPOSE) exec kafka kafka-console-producer --bootstrap-server localhost:29092 --topic $(TOPIC)
consume: ## Consume messages (usage: make consume TOPIC=my-topic)
@$(DOCKER_COMPOSE) exec kafka kafka-console-consumer --bootstrap-server localhost:29092 --topic $(TOPIC) --from-beginning

156
test/kafka/README.md Normal file
View File

@@ -0,0 +1,156 @@
# Kafka Gateway Tests with SMQ Integration
This directory contains tests for the SeaweedFS Kafka Gateway with full SeaweedMQ (SMQ) integration.
## Test Types
### **Unit Tests** (`./unit/`)
- Basic gateway functionality
- Protocol compatibility
- No SeaweedFS backend required
- Uses mock handlers
### **Integration Tests** (`./integration/`)
- **Mock Mode** (default): Uses in-memory handlers for protocol testing
- **SMQ Mode** (with `SEAWEEDFS_MASTERS`): Uses real SeaweedFS backend for full integration
### **E2E Tests** (`./e2e/`)
- End-to-end workflows
- Automatically detects SMQ availability
- Falls back to mock mode if SMQ unavailable
## Running Tests Locally
### Quick Protocol Testing (Mock Mode)
```bash
# Run all integration tests with mock backend
cd test/kafka
go test ./integration/...
# Run specific test
go test -v ./integration/ -run TestClientCompatibility
```
### Full Integration Testing (SMQ Mode)
Requires running SeaweedFS instance:
1. **Start SeaweedFS with MQ support:**
```bash
# Terminal 1: Start SeaweedFS server
weed server -ip="127.0.0.1" -ip.bind="0.0.0.0" -dir=/tmp/seaweedfs-data -master.port=9333 -volume.port=8081 -filer.port=8888 -filer=true -master.peers=none
# Terminal 2: Start MQ broker
weed mq.broker -master="127.0.0.1:9333" -ip="127.0.0.1" -port=17777
```
2. **Run tests with SMQ backend:**
```bash
cd test/kafka
SEAWEEDFS_MASTERS=127.0.0.1:9333 go test ./integration/...
# Run specific SMQ integration tests
SEAWEEDFS_MASTERS=127.0.0.1:9333 go test -v ./integration/ -run TestSMQIntegration
```
### Test Broker Startup
If you're having broker startup issues:
```bash
# Debug broker startup locally
./scripts/test-broker-startup.sh
```
## CI/CD Integration
### GitHub Actions Jobs
1. **Unit Tests** - Fast protocol tests with mock backend
2. **Integration Tests** - Mock mode by default
3. **E2E Tests (with SMQ)** - Full SeaweedFS + MQ broker stack
4. **Client Compatibility (with SMQ)** - Tests different Kafka clients against real backend
5. **Consumer Group Tests (with SMQ)** - Tests consumer group persistence
6. **SMQ Integration Tests** - Dedicated SMQ-specific functionality tests
### What Gets Tested with SMQ
When `SEAWEEDFS_MASTERS` is available, tests exercise:
- **Real Message Persistence** - Messages stored in SeaweedFS volumes
- **Offset Persistence** - Consumer group offsets stored in SeaweedFS filer
- **Topic Persistence** - Topic metadata persisted in SeaweedFS filer
- **Consumer Group Coordination** - Distributed coordinator assignment
- **Cross-Client Compatibility** - Sarama, kafka-go with real backend
- **Broker Discovery** - Gateway discovers MQ brokers via masters
## Test Infrastructure
### `testutil.NewGatewayTestServerWithSMQ(t, mode)`
Smart gateway creation that automatically:
- Detects SMQ availability via `SEAWEEDFS_MASTERS`
- Uses production handler when available
- Falls back to mock when unavailable
- Provides timeout protection against hanging
**Modes:**
- `SMQRequired` - Skip test if SMQ unavailable
- `SMQAvailable` - Use SMQ if available, otherwise mock
- `SMQUnavailable` - Always use mock
### Timeout Protection
Gateway creation includes timeout protection to prevent CI hanging:
- 20 second timeout for `SMQRequired` mode
- 15 second timeout for `SMQAvailable` mode
- Clear error messages when broker discovery fails
## Debugging Failed Tests
### CI Logs to Check
1. **"SeaweedFS master is up"** - Master started successfully
2. **"SeaweedFS filer is up"** - Filer ready
3. **"SeaweedFS MQ broker is up"** - Broker started successfully
4. **Broker/Server logs** - Shown on broker startup failure
### Local Debugging
1. Run `./scripts/test-broker-startup.sh` to test broker startup
2. Check logs at `/tmp/weed-*.log`
3. Test individual components:
```bash
# Test master
curl http://127.0.0.1:9333/cluster/status
# Test filer
curl http://127.0.0.1:8888/status
# Test broker
nc -z 127.0.0.1 17777
```
### Common Issues
- **Broker fails to start**: Check filer is ready before starting broker
- **Gateway timeout**: Broker discovery fails, check broker is accessible
- **Test hangs**: Timeout protection not working, reduce timeout values
## Architecture
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Kafka Client │───▶│ Kafka Gateway │───▶│ SeaweedMQ Broker│
│ (Sarama, │ │ (Protocol │ │ (Message │
│ kafka-go) │ │ Handler) │ │ Persistence) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ SeaweedFS Filer │ │ SeaweedFS Master│
│ (Offset Storage)│ │ (Coordination) │
└─────────────────┘ └─────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────┐
│ SeaweedFS Volumes │
│ (Message Storage) │
└─────────────────────────────────────────┘
```
This architecture ensures full integration testing of the entire Kafka → SeaweedFS message path.

View File

@@ -0,0 +1,172 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net"
"net/http"
"os"
"time"
)
// Schema represents a schema registry schema
type Schema struct {
Subject string `json:"subject"`
Version int `json:"version"`
Schema string `json:"schema"`
}
// SchemaResponse represents the response from schema registry
type SchemaResponse struct {
ID int `json:"id"`
}
func main() {
log.Println("Setting up Kafka integration test environment...")
kafkaBootstrap := getEnv("KAFKA_BOOTSTRAP_SERVERS", "kafka:29092")
schemaRegistryURL := getEnv("SCHEMA_REGISTRY_URL", "http://schema-registry:8081")
kafkaGatewayURL := getEnv("KAFKA_GATEWAY_URL", "kafka-gateway:9093")
log.Printf("Kafka Bootstrap Servers: %s", kafkaBootstrap)
log.Printf("Schema Registry URL: %s", schemaRegistryURL)
log.Printf("Kafka Gateway URL: %s", kafkaGatewayURL)
// Wait for services to be ready
waitForHTTPService("Schema Registry", schemaRegistryURL+"/subjects")
waitForTCPService("Kafka Gateway", kafkaGatewayURL) // TCP connectivity check for Kafka protocol
// Register test schemas
if err := registerSchemas(schemaRegistryURL); err != nil {
log.Fatalf("Failed to register schemas: %v", err)
}
log.Println("Test environment setup completed successfully!")
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
func waitForHTTPService(name, url string) {
log.Printf("Waiting for %s to be ready...", name)
for i := 0; i < 60; i++ { // Wait up to 60 seconds
resp, err := http.Get(url)
if err == nil && resp.StatusCode < 400 {
resp.Body.Close()
log.Printf("%s is ready", name)
return
}
if resp != nil {
resp.Body.Close()
}
time.Sleep(1 * time.Second)
}
log.Fatalf("%s is not ready after 60 seconds", name)
}
func waitForTCPService(name, address string) {
log.Printf("Waiting for %s to be ready...", name)
for i := 0; i < 60; i++ { // Wait up to 60 seconds
conn, err := net.DialTimeout("tcp", address, 2*time.Second)
if err == nil {
conn.Close()
log.Printf("%s is ready", name)
return
}
time.Sleep(1 * time.Second)
}
log.Fatalf("%s is not ready after 60 seconds", name)
}
func registerSchemas(registryURL string) error {
schemas := []Schema{
{
Subject: "user-value",
Schema: `{
"type": "record",
"name": "User",
"fields": [
{"name": "id", "type": "int"},
{"name": "name", "type": "string"},
{"name": "email", "type": ["null", "string"], "default": null}
]
}`,
},
{
Subject: "user-event-value",
Schema: `{
"type": "record",
"name": "UserEvent",
"fields": [
{"name": "userId", "type": "int"},
{"name": "eventType", "type": "string"},
{"name": "timestamp", "type": "long"},
{"name": "data", "type": ["null", "string"], "default": null}
]
}`,
},
{
Subject: "log-entry-value",
Schema: `{
"type": "record",
"name": "LogEntry",
"fields": [
{"name": "level", "type": "string"},
{"name": "message", "type": "string"},
{"name": "timestamp", "type": "long"},
{"name": "service", "type": "string"},
{"name": "metadata", "type": {"type": "map", "values": "string"}}
]
}`,
},
}
for _, schema := range schemas {
if err := registerSchema(registryURL, schema); err != nil {
return fmt.Errorf("failed to register schema %s: %w", schema.Subject, err)
}
log.Printf("Registered schema: %s", schema.Subject)
}
return nil
}
func registerSchema(registryURL string, schema Schema) error {
url := fmt.Sprintf("%s/subjects/%s/versions", registryURL, schema.Subject)
payload := map[string]interface{}{
"schema": schema.Schema,
}
jsonData, err := json.Marshal(payload)
if err != nil {
return err
}
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Post(url, "application/vnd.schemaregistry.v1+json", bytes.NewBuffer(jsonData))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
}
var response SchemaResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return err
}
log.Printf("Schema %s registered with ID: %d", schema.Subject, response.ID)
return nil
}

View File

@@ -0,0 +1,326 @@
x-seaweedfs-build: &seaweedfs-build
build:
context: ../..
dockerfile: test/kafka/Dockerfile.seaweedfs
image: kafka-seaweedfs-dev
services:
# Zookeeper for Kafka
zookeeper:
image: confluentinc/cp-zookeeper:7.4.0
container_name: kafka-zookeeper
ports:
- "2181:2181"
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000
healthcheck:
test: ["CMD", "nc", "-z", "localhost", "2181"]
interval: 10s
timeout: 5s
retries: 3
start_period: 10s
networks:
- kafka-test-net
# Kafka Broker
kafka:
image: confluentinc/cp-kafka:7.4.0
container_name: kafka-broker
ports:
- "9092:9092"
- "29092:29092"
depends_on:
zookeeper:
condition: service_healthy
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"
KAFKA_NUM_PARTITIONS: 3
KAFKA_DEFAULT_REPLICATION_FACTOR: 1
healthcheck:
test: ["CMD", "kafka-broker-api-versions", "--bootstrap-server", "localhost:29092"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
networks:
- kafka-test-net
# Schema Registry
schema-registry:
image: confluentinc/cp-schema-registry:7.4.0
container_name: kafka-schema-registry
ports:
- "8081:8081"
depends_on:
kafka:
condition: service_healthy
environment:
SCHEMA_REGISTRY_HOST_NAME: schema-registry
SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: kafka:29092
SCHEMA_REGISTRY_LISTENERS: http://0.0.0.0:8081
SCHEMA_REGISTRY_KAFKASTORE_TOPIC: _schemas
SCHEMA_REGISTRY_DEBUG: "true"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8081/subjects"]
interval: 10s
timeout: 5s
retries: 5
start_period: 20s
networks:
- kafka-test-net
# SeaweedFS Master
seaweedfs-master:
<<: *seaweedfs-build
container_name: seaweedfs-master
ports:
- "9333:9333"
- "19333:19333" # gRPC port
command:
- master
- -ip=seaweedfs-master
- -port=9333
- -port.grpc=19333
- -volumeSizeLimitMB=1024
- -defaultReplication=000
volumes:
- seaweedfs-master-data:/data
healthcheck:
test: ["CMD-SHELL", "wget --quiet --tries=1 --spider http://seaweedfs-master:9333/cluster/status || curl -sf http://seaweedfs-master:9333/cluster/status"]
interval: 10s
timeout: 5s
retries: 10
start_period: 20s
networks:
- kafka-test-net
# SeaweedFS Volume Server
seaweedfs-volume:
<<: *seaweedfs-build
container_name: seaweedfs-volume
ports:
- "8080:8080"
- "18080:18080" # gRPC port
command:
- volume
- -master=seaweedfs-master:9333
- -ip=seaweedfs-volume
- -port=8080
- -port.grpc=18080
- -publicUrl=seaweedfs-volume:8080
- -preStopSeconds=1
depends_on:
seaweedfs-master:
condition: service_healthy
volumes:
- seaweedfs-volume-data:/data
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://seaweedfs-volume:8080/status"]
interval: 10s
timeout: 5s
retries: 3
start_period: 10s
networks:
- kafka-test-net
# SeaweedFS Filer
seaweedfs-filer:
<<: *seaweedfs-build
container_name: seaweedfs-filer
ports:
- "8888:8888"
- "18888:18888" # gRPC port
command:
- filer
- -master=seaweedfs-master:9333
- -ip=seaweedfs-filer
- -port=8888
- -port.grpc=18888
depends_on:
seaweedfs-master:
condition: service_healthy
seaweedfs-volume:
condition: service_healthy
volumes:
- seaweedfs-filer-data:/data
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://seaweedfs-filer:8888/"]
interval: 10s
timeout: 5s
retries: 3
start_period: 15s
networks:
- kafka-test-net
# SeaweedFS MQ Broker
seaweedfs-mq-broker:
<<: *seaweedfs-build
container_name: seaweedfs-mq-broker
ports:
- "17777:17777" # MQ Broker port
- "18777:18777" # pprof profiling port
command:
- mq.broker
- -master=seaweedfs-master:9333
- -ip=seaweedfs-mq-broker
- -port=17777
- -debug
- -debug.port=18777
depends_on:
seaweedfs-filer:
condition: service_healthy
volumes:
- seaweedfs-mq-data:/data
healthcheck:
test: ["CMD", "nc", "-z", "localhost", "17777"]
interval: 10s
timeout: 5s
retries: 3
start_period: 20s
networks:
- kafka-test-net
# SeaweedFS MQ Agent
seaweedfs-mq-agent:
<<: *seaweedfs-build
container_name: seaweedfs-mq-agent
ports:
- "16777:16777" # MQ Agent port
command:
- mq.agent
- -broker=seaweedfs-mq-broker:17777
- -ip=0.0.0.0
- -port=16777
depends_on:
seaweedfs-mq-broker:
condition: service_healthy
volumes:
- seaweedfs-mq-data:/data
healthcheck:
test: ["CMD", "nc", "-z", "localhost", "16777"]
interval: 10s
timeout: 5s
retries: 3
start_period: 25s
networks:
- kafka-test-net
# Kafka Gateway (SeaweedFS with Kafka protocol)
kafka-gateway:
build:
context: ../.. # Build from project root
dockerfile: test/kafka/Dockerfile.kafka-gateway
container_name: kafka-gateway
ports:
- "9093:9093" # Kafka protocol port
- "10093:10093" # pprof profiling port
depends_on:
seaweedfs-mq-agent:
condition: service_healthy
schema-registry:
condition: service_healthy
environment:
- SEAWEEDFS_MASTERS=seaweedfs-master:9333
- SEAWEEDFS_FILER_GROUP=
- SCHEMA_REGISTRY_URL=http://schema-registry:8081
- KAFKA_PORT=9093
- PPROF_PORT=10093
volumes:
- kafka-gateway-data:/data
healthcheck:
test: ["CMD", "nc", "-z", "localhost", "9093"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
networks:
- kafka-test-net
# Test Data Setup Service
test-setup:
build:
context: ../..
dockerfile: test/kafka/Dockerfile.test-setup
container_name: kafka-test-setup
depends_on:
kafka:
condition: service_healthy
schema-registry:
condition: service_healthy
kafka-gateway:
condition: service_healthy
environment:
- KAFKA_BOOTSTRAP_SERVERS=kafka:29092
- SCHEMA_REGISTRY_URL=http://schema-registry:8081
- KAFKA_GATEWAY_URL=kafka-gateway:9093
networks:
- kafka-test-net
restart: "no" # Run once to set up test data
profiles:
- setup # Only start when explicitly requested
# Kafka Producer for Testing
kafka-producer:
image: confluentinc/cp-kafka:7.4.0
container_name: kafka-producer
depends_on:
kafka:
condition: service_healthy
schema-registry:
condition: service_healthy
environment:
- KAFKA_BOOTSTRAP_SERVERS=kafka:29092
- SCHEMA_REGISTRY_URL=http://schema-registry:8081
networks:
- kafka-test-net
profiles:
- producer # Only start when explicitly requested
command: >
sh -c "
echo 'Creating test topics...';
kafka-topics --create --topic test-topic --bootstrap-server kafka:29092 --partitions 3 --replication-factor 1 --if-not-exists;
kafka-topics --create --topic avro-topic --bootstrap-server kafka:29092 --partitions 3 --replication-factor 1 --if-not-exists;
kafka-topics --create --topic schema-test --bootstrap-server kafka:29092 --partitions 1 --replication-factor 1 --if-not-exists;
echo 'Topics created successfully';
kafka-topics --list --bootstrap-server kafka:29092;
"
# Kafka Consumer for Testing
kafka-consumer:
image: confluentinc/cp-kafka:7.4.0
container_name: kafka-consumer
depends_on:
kafka:
condition: service_healthy
environment:
- KAFKA_BOOTSTRAP_SERVERS=kafka:29092
networks:
- kafka-test-net
profiles:
- consumer # Only start when explicitly requested
command: >
kafka-console-consumer
--bootstrap-server kafka:29092
--topic test-topic
--from-beginning
--max-messages 10
volumes:
seaweedfs-master-data:
seaweedfs-volume-data:
seaweedfs-filer-data:
seaweedfs-mq-data:
kafka-gateway-data:
networks:
kafka-test-net:
driver: bridge
name: kafka-integration-test

View File

@@ -0,0 +1,131 @@
package e2e
import (
"testing"
"github.com/seaweedfs/seaweedfs/test/kafka/internal/testutil"
)
// TestComprehensiveE2E tests complete end-to-end workflows
// This test will use SMQ backend if SEAWEEDFS_MASTERS is available, otherwise mock
func TestComprehensiveE2E(t *testing.T) {
gateway := testutil.NewGatewayTestServerWithSMQ(t, testutil.SMQAvailable)
defer gateway.CleanupAndClose()
addr := gateway.StartAndWait()
// Log which backend we're using
if gateway.IsSMQMode() {
t.Logf("Running comprehensive E2E tests with SMQ backend")
} else {
t.Logf("Running comprehensive E2E tests with mock backend")
}
// Create topics for different test scenarios
topics := []string{
testutil.GenerateUniqueTopicName("e2e-kafka-go"),
testutil.GenerateUniqueTopicName("e2e-sarama"),
testutil.GenerateUniqueTopicName("e2e-mixed"),
}
gateway.AddTestTopics(topics...)
t.Run("KafkaGo_to_KafkaGo", func(t *testing.T) {
testKafkaGoToKafkaGo(t, addr, topics[0])
})
t.Run("Sarama_to_Sarama", func(t *testing.T) {
testSaramaToSarama(t, addr, topics[1])
})
t.Run("KafkaGo_to_Sarama", func(t *testing.T) {
testKafkaGoToSarama(t, addr, topics[2])
})
t.Run("Sarama_to_KafkaGo", func(t *testing.T) {
testSaramaToKafkaGo(t, addr, topics[2])
})
}
func testKafkaGoToKafkaGo(t *testing.T, addr, topic string) {
client := testutil.NewKafkaGoClient(t, addr)
msgGen := testutil.NewMessageGenerator()
// Generate test messages
messages := msgGen.GenerateKafkaGoMessages(2)
// Produce with kafka-go
err := client.ProduceMessages(topic, messages)
testutil.AssertNoError(t, err, "kafka-go produce failed")
// Consume with kafka-go
consumed, err := client.ConsumeMessages(topic, len(messages))
testutil.AssertNoError(t, err, "kafka-go consume failed")
// Validate message content
err = testutil.ValidateKafkaGoMessageContent(messages, consumed)
testutil.AssertNoError(t, err, "Message content validation failed")
t.Logf("kafka-go to kafka-go test PASSED")
}
func testSaramaToSarama(t *testing.T, addr, topic string) {
client := testutil.NewSaramaClient(t, addr)
msgGen := testutil.NewMessageGenerator()
// Generate test messages
messages := msgGen.GenerateStringMessages(2)
// Produce with Sarama
err := client.ProduceMessages(topic, messages)
testutil.AssertNoError(t, err, "Sarama produce failed")
// Consume with Sarama
consumed, err := client.ConsumeMessages(topic, 0, len(messages))
testutil.AssertNoError(t, err, "Sarama consume failed")
// Validate message content
err = testutil.ValidateMessageContent(messages, consumed)
testutil.AssertNoError(t, err, "Message content validation failed")
t.Logf("Sarama to Sarama test PASSED")
}
func testKafkaGoToSarama(t *testing.T, addr, topic string) {
kafkaGoClient := testutil.NewKafkaGoClient(t, addr)
saramaClient := testutil.NewSaramaClient(t, addr)
msgGen := testutil.NewMessageGenerator()
// Produce with kafka-go
messages := msgGen.GenerateKafkaGoMessages(2)
err := kafkaGoClient.ProduceMessages(topic, messages)
testutil.AssertNoError(t, err, "kafka-go produce failed")
// Consume with Sarama
consumed, err := saramaClient.ConsumeMessages(topic, 0, len(messages))
testutil.AssertNoError(t, err, "Sarama consume failed")
// Validate that we got the expected number of messages
testutil.AssertEqual(t, len(messages), len(consumed), "Message count mismatch")
t.Logf("kafka-go to Sarama test PASSED")
}
func testSaramaToKafkaGo(t *testing.T, addr, topic string) {
kafkaGoClient := testutil.NewKafkaGoClient(t, addr)
saramaClient := testutil.NewSaramaClient(t, addr)
msgGen := testutil.NewMessageGenerator()
// Produce with Sarama
messages := msgGen.GenerateStringMessages(2)
err := saramaClient.ProduceMessages(topic, messages)
testutil.AssertNoError(t, err, "Sarama produce failed")
// Consume with kafka-go
consumed, err := kafkaGoClient.ConsumeMessages(topic, len(messages))
testutil.AssertNoError(t, err, "kafka-go consume failed")
// Validate that we got the expected number of messages
testutil.AssertEqual(t, len(messages), len(consumed), "Message count mismatch")
t.Logf("Sarama to kafka-go test PASSED")
}

View File

@@ -0,0 +1,150 @@
package e2e
import (
"os"
"testing"
"github.com/seaweedfs/seaweedfs/test/kafka/internal/testutil"
)
// TestOffsetManagement tests end-to-end offset management scenarios
// This test will use SMQ backend if SEAWEEDFS_MASTERS is available, otherwise mock
func TestOffsetManagement(t *testing.T) {
gateway := testutil.NewGatewayTestServerWithSMQ(t, testutil.SMQAvailable)
defer gateway.CleanupAndClose()
addr := gateway.StartAndWait()
// If schema registry is configured, ensure gateway is in schema mode and log
if v := os.Getenv("SCHEMA_REGISTRY_URL"); v != "" {
t.Logf("Schema Registry detected at %s - running offset tests in schematized mode", v)
}
// Log which backend we're using
if gateway.IsSMQMode() {
t.Logf("Running offset management tests with SMQ backend - offsets will be persisted")
} else {
t.Logf("Running offset management tests with mock backend - offsets are in-memory only")
}
topic1 := testutil.GenerateUniqueTopicName("offset-management")
topic2 := testutil.GenerateUniqueTopicName("offset-resumption")
groupID := testutil.GenerateUniqueGroupID("offset-test-group")
gateway.AddTestTopic(topic1)
gateway.AddTestTopic(topic2)
t.Run("BasicOffsetCommitFetch", func(t *testing.T) {
testBasicOffsetCommitFetch(t, addr, topic1, groupID)
})
t.Run("ConsumerGroupResumption", func(t *testing.T) {
testConsumerGroupResumption(t, gateway, addr, topic2, groupID+"2")
})
}
func testBasicOffsetCommitFetch(t *testing.T, addr, topic, groupID string) {
client := testutil.NewKafkaGoClient(t, addr)
msgGen := testutil.NewMessageGenerator()
// Produce test messages
if url := os.Getenv("SCHEMA_REGISTRY_URL"); url != "" {
if id, err := testutil.EnsureValueSchema(t, url, topic); err == nil {
t.Logf("Ensured value schema id=%d for subject %s-value", id, topic)
} else {
t.Logf("Schema registration failed (non-fatal for test): %v", err)
}
}
messages := msgGen.GenerateKafkaGoMessages(5)
err := client.ProduceMessages(topic, messages)
testutil.AssertNoError(t, err, "Failed to produce offset test messages")
// Phase 1: Consume first 3 messages and commit offsets
t.Logf("=== Phase 1: Consuming first 3 messages ===")
consumed1, err := client.ConsumeWithGroup(topic, groupID, 3)
testutil.AssertNoError(t, err, "Failed to consume first batch")
testutil.AssertEqual(t, 3, len(consumed1), "Should consume exactly 3 messages")
// Phase 2: Create new consumer with same group ID - should resume from committed offset
t.Logf("=== Phase 2: Resuming from committed offset ===")
consumed2, err := client.ConsumeWithGroup(topic, groupID, 2)
testutil.AssertNoError(t, err, "Failed to consume remaining messages")
testutil.AssertEqual(t, 2, len(consumed2), "Should consume remaining 2 messages")
// Verify that we got all messages without duplicates
totalConsumed := len(consumed1) + len(consumed2)
testutil.AssertEqual(t, len(messages), totalConsumed, "Should consume all messages exactly once")
t.Logf("SUCCESS: Offset management test completed - consumed %d + %d messages", len(consumed1), len(consumed2))
}
func testConsumerGroupResumption(t *testing.T, gateway *testutil.GatewayTestServer, addr, topic, groupID string) {
client := testutil.NewKafkaGoClient(t, addr)
msgGen := testutil.NewMessageGenerator()
// Produce messages
t.Logf("=== Phase 1: Producing 4 messages to topic %s ===", topic)
messages := msgGen.GenerateKafkaGoMessages(4)
err := client.ProduceMessages(topic, messages)
testutil.AssertNoError(t, err, "Failed to produce messages for resumption test")
t.Logf("Successfully produced %d messages", len(messages))
// Consume some messages
t.Logf("=== Phase 2: First consumer - consuming 2 messages with group %s ===", groupID)
consumed1, err := client.ConsumeWithGroup(topic, groupID, 2)
testutil.AssertNoError(t, err, "Failed to consume first batch")
t.Logf("First consumer consumed %d messages:", len(consumed1))
for i, msg := range consumed1 {
t.Logf(" Message %d: offset=%d, partition=%d, value=%s", i, msg.Offset, msg.Partition, string(msg.Value))
}
gateway.LogConsumerGroupSnapshot(groupID)
// Simulate consumer restart by consuming remaining messages with same group ID
t.Logf("=== Phase 3: Second consumer (simulated restart) - consuming remaining messages with same group %s ===", groupID)
consumed2, err := client.ConsumeWithGroupDebug(topic, groupID, 2,
func(info testutil.ConsumeGroupRetryDebug) {
t.Logf("Consumer restart attempt %d/%d for group %s failed before receiving any messages: %v",
info.Attempt, info.MaxAttempts, info.GroupID, info.Err)
gateway.LogConsumerGroupSnapshot(groupID)
},
func() {
// Live snapshot taken while the reader is still joined — this is
// the only place we get to see the group's transient state
// (PreparingRebalance, leader, members) during a stuck attempt.
// The onRetry snapshot above runs after reader.Close() and only
// ever shows the post-LeaveGroup Empty state.
gateway.LogConsumerGroupSnapshot(groupID)
},
)
if err != nil {
gateway.LogConsumerGroupSnapshot(groupID)
}
testutil.AssertNoError(t, err, "Failed to consume after restart")
t.Logf("Second consumer consumed %d messages:", len(consumed2))
for i, msg := range consumed2 {
t.Logf(" Message %d: offset=%d, partition=%d, value=%s", i, msg.Offset, msg.Partition, string(msg.Value))
}
// Verify total consumption
totalConsumed := len(consumed1) + len(consumed2)
t.Logf("=== Verification: Total consumed %d messages (expected %d) ===", totalConsumed, len(messages))
// Check for duplicates
offsetsSeen := make(map[int64]bool)
duplicateCount := 0
for _, msg := range append(consumed1, consumed2...) {
if offsetsSeen[msg.Offset] {
t.Logf("WARNING: Duplicate offset detected: %d", msg.Offset)
duplicateCount++
}
offsetsSeen[msg.Offset] = true
}
if duplicateCount > 0 {
t.Logf("ERROR: Found %d duplicate messages", duplicateCount)
}
testutil.AssertEqual(t, len(messages), totalConsumed, "Should consume all messages after restart")
t.Logf("SUCCESS: Consumer group resumption test completed - no duplicates, all messages consumed exactly once")
}

105
test/kafka/go.mod Normal file
View File

@@ -0,0 +1,105 @@
module github.com/seaweedfs/seaweedfs/test/kafka
go 1.26
require (
github.com/IBM/sarama v1.46.0
github.com/linkedin/goavro/v2 v2.15.0
github.com/seaweedfs/seaweedfs v0.0.0-00010101000000-000000000000
github.com/segmentio/kafka-go v0.4.49
github.com/stretchr/testify v1.11.1
google.golang.org/grpc v1.85.0-dev
)
replace github.com/seaweedfs/seaweedfs => ../../
require (
github.com/andybalholm/brotli v1.2.2 // indirect
github.com/aws/aws-sdk-go v1.55.8 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cognusion/imaging v1.0.4 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/eapache/go-resiliency v1.7.0 // indirect
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect
github.com/eapache/queue v1.1.0 // indirect
github.com/ebitengine/purego v0.10.2 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
github.com/jcmturner/gofork v1.7.6 // indirect
github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
github.com/jhump/protoreflect v1.18.0 // indirect
github.com/jhump/protoreflect/v2 v2.0.0-beta.1 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/karlseguin/ccache/v2 v2.0.8 // indirect
github.com/klauspost/compress v1.19.1 // indirect
github.com/klauspost/cpuid/v2 v2.4.0 // indirect
github.com/klauspost/reedsolomon v1.14.1 // indirect
github.com/lufia/plan9stats v0.0.0-20260627054121-477a66015f15 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/onsi/gomega v1.37.0 // indirect
github.com/orcaman/concurrent-map/v2 v2.0.1 // indirect
github.com/parquet-go/bitpack v1.0.0 // indirect
github.com/parquet-go/jsonlite v1.0.0 // indirect
github.com/parquet-go/parquet-go v0.32.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 // indirect
github.com/pierrec/lz4/v4 v4.1.28 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/prometheus/client_golang v1.24.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.70.1 // indirect
github.com/prometheus/procfs v0.21.1 // indirect
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect
github.com/rdleal/intervalst v1.5.0 // indirect
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/seaweedfs/goexif v2.0.0+incompatible // indirect
github.com/shirou/gopsutil/v4 v4.26.7 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/spf13/viper v1.21.0 // indirect
github.com/spiffe/go-spiffe/v2 v2.8.1 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20190318030020-c3a204f8e965 // indirect
github.com/tklauser/go-sysconf v0.4.0 // indirect
github.com/tklauser/numcpus v0.12.0 // indirect
github.com/twpayne/go-geom v1.6.1 // indirect
github.com/tylertreat/BoomFilters v0.0.0-20210315201527-1a82519a3e43 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/viant/ptrie v1.0.1 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.55.0 // indirect
golang.org/x/image v0.44.0 // indirect
golang.org/x/net v0.58.0 // indirect
golang.org/x/sync v0.22.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/text v0.41.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 // indirect
google.golang.org/grpc/security/advancedtls v1.0.0 // indirect
google.golang.org/protobuf v1.36.12 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

368
test/kafka/go.sum Normal file
View File

@@ -0,0 +1,368 @@
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go/compute v1.20.1 h1:6aKEtlUiwEpJzM001l0yFkpXmUVXaN8W+fbkb2AZNbg=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/IBM/sarama v1.46.0 h1:+YTM1fNd6WKMchlnLKRUB5Z0qD4M8YbvwIIPLvJD53s=
github.com/IBM/sarama v1.46.0/go.mod h1:0lOcuQziJ1/mBGHkdp5uYrltqQuKQKM5O5FOWUQVVvo=
github.com/alecthomas/assert/v2 v2.10.0 h1:jjRCHsj6hBJhkmhznrCzoNpbA3zqy0fYiUcYZP/GkPY=
github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/andybalholm/brotli v1.2.2 h1:HzTuoo2ErYQqf5qvcJInB8uvqSVxRttzkFexPWtnceM=
github.com/andybalholm/brotli v1.2.2/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ=
github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cognusion/imaging v1.0.4 h1:hrmWVa4S9fiLJJFKEHYYRNBUnHEKZD8/oxnC77xTUa0=
github.com/cognusion/imaging v1.0.4/go.mod h1:X1mPSSZpMyiva54chCO/1NROp+b6xG4uMcud8VLii+U=
github.com/d4l3k/messagediff v1.2.1 h1:ZcAIMYsUg0EAp9X+tt8/enBE/Q8Yd5kzPynLyKptt9U=
github.com/d4l3k/messagediff v1.2.1/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA=
github.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho=
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws=
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0=
github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE=
github.com/ebitengine/purego v0.10.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jhump/protoreflect v1.18.0 h1:TOz0MSR/0JOZ5kECB/0ufGnC2jdsgZ123Rd/k4Z5/2w=
github.com/jhump/protoreflect v1.18.0/go.mod h1:ezWcltJIVF4zYdIFM+D/sHV4Oh5LNU08ORzCGfwvTz8=
github.com/jhump/protoreflect/v2 v2.0.0-beta.1 h1:Dw1rslK/VotaUGYsv53XVWITr+5RCPXfvvlGrM/+B6w=
github.com/jhump/protoreflect/v2 v2.0.0-beta.1/go.mod h1:D9LBEowZyv8/iSu97FU2zmXG3JxVTmNw21mu63niFzU=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/karlseguin/ccache/v2 v2.0.8 h1:lT38cE//uyf6KcFok0rlgXtGFBWxkI6h/qg4tbFyDnA=
github.com/karlseguin/ccache/v2 v2.0.8/go.mod h1:2BDThcfQMf/c0jnZowt16eW405XIqZPavt+HoYEtcxQ=
github.com/karlseguin/expect v1.0.2-0.20190806010014-778a5f0c6003 h1:vJ0Snvo+SLMY72r5J4sEfkuE7AFbixEP2qRbEcum/wA=
github.com/karlseguin/expect v1.0.2-0.20190806010014-778a5f0c6003/go.mod h1:zNBxMY8P21owkeogJELCLeHIt+voOSduHYTFUbwRAV8=
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
github.com/klauspost/reedsolomon v1.14.1 h1:swE9kzyWXD/wVG+l5Pe8bWnQ0giIY7D1GjCBKk3kG2U=
github.com/klauspost/reedsolomon v1.14.1/go.mod h1:yjqqjgMTQkBUHSG97/rm4zipffCNbCiZcB3kTqr++sQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/linkedin/goavro/v2 v2.15.0 h1:pDj1UrjUOO62iXhgBiE7jQkpNIc5/tA5eZsgolMjgVI=
github.com/linkedin/goavro/v2 v2.15.0/go.mod h1:KXx+erlq+RPlGSPmLF7xGo6SAbh8sCQ53x064+ioxhk=
github.com/lufia/plan9stats v0.0.0-20260627054121-477a66015f15 h1:YkjVPl/YH5XlJ+/NiwzJtPYXXKRcyjmEUhsDci6YK3c=
github.com/lufia/plan9stats v0.0.0-20260627054121-477a66015f15/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/orcaman/concurrent-map/v2 v2.0.1 h1:jOJ5Pg2w1oeB6PeDurIYf6k9PQ+aTITr/6lP/L/zp6c=
github.com/orcaman/concurrent-map/v2 v2.0.1/go.mod h1:9Eq3TG2oBe5FirmYWQfYO5iH1q0Jv47PLaNK++uCdOM=
github.com/parquet-go/bitpack v1.0.0 h1:AUqzlKzPPXf2bCdjfj4sTeacrUwsT7NlcYDMUQxPcQA=
github.com/parquet-go/bitpack v1.0.0/go.mod h1:XnVk9TH+O40eOOmvpAVZ7K2ocQFrQwysLMnc6M/8lgs=
github.com/parquet-go/jsonlite v1.0.0 h1:87QNdi56wOfsE5bdgas0vRzHPxfJgzrXGml1zZdd7VU=
github.com/parquet-go/jsonlite v1.0.0/go.mod h1:nDjpkpL4EOtqs6NQugUsi0Rleq9sW/OtC1NnZEnxzF0=
github.com/parquet-go/parquet-go v0.32.0 h1:NWDqTUHfrCS4cJP/Fj2HlxvqsrVedWG3sayMkf+znzM=
github.com/parquet-go/parquet-go v0.32.0/go.mod h1:navtkAYr2LGoJVp141oXPlO/sxLvaOe3la2JEoD8+rg=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 h1:KPpdlQLZcHfTMQRi6bFQ7ogNO0ltFT4PmtwTLW4W+14=
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/pierrec/lz4/v4 v4.1.28 h1:pPEPwRJ4kybBTfGt28q7lQsRJQHhC08axprdLD5Ppio=
github.com/pierrec/lz4/v4 v4.1.28/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU=
github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY=
github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc=
github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI=
github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY=
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rdleal/intervalst v1.5.0 h1:SEB9bCFz5IqD1yhfH1Wv8IBnY/JQxDplwkxHjT6hamU=
github.com/rdleal/intervalst v1.5.0/go.mod h1:xO89Z6BC+LQDH+IPQQw/OESt5UADgFD41tYMUINGpxQ=
github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc=
github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/seaweedfs/goexif v2.0.0+incompatible h1:x8pckiT12QQhifwhDQpeISgDfsqmQ6VR4LFPQ64JRps=
github.com/seaweedfs/goexif v2.0.0+incompatible/go.mod h1:Oni780Z236sXpIQzk1XoJlTwqrJ02smEin9zQeff7Fk=
github.com/segmentio/kafka-go v0.4.49 h1:GJiNX1d/g+kG6ljyJEoi9++PUMdXGAxb7JGPiDCuNmk=
github.com/segmentio/kafka-go v0.4.49/go.mod h1:Y1gn60kzLEEaW28YshXyk2+VCUKbJ3Qr6DrnT3i4+9E=
github.com/shirou/gopsutil/v4 v4.26.7 h1:IXzpHz/dkMRYAhKkOXr1HB6SuzWU3eoyyeWe7g3bNZc=
github.com/shirou/gopsutil/v4 v4.26.7/go.mod h1:5O9FjBiXoTDFatIWjZZosqj4pV0DRtLx598xGbBehzM=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/spiffe/go-spiffe/v2 v2.8.1 h1:eXZMLsu+3MLEPJyGJkolqtVrteZfQdUpOWj6LTiDl/E=
github.com/spiffe/go-spiffe/v2 v2.8.1/go.mod h1:47Q0Q9/AqGha8QLHp+kxpH4Wca7X7EnOtlIJy3mxZ3U=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/syndtr/goleveldb v1.0.1-0.20190318030020-c3a204f8e965 h1:1oFLiOyVl+W7bnBzGhf7BbIv9loSFQcieWWYIjLqcAw=
github.com/syndtr/goleveldb v1.0.1-0.20190318030020-c3a204f8e965/go.mod h1:9OrXJhf154huy1nPWmuSrkgjPUtUNhA+Zmy+6AESzuA=
github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU=
github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI=
github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4=
github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg=
github.com/twpayne/go-geom v1.6.1 h1:iLE+Opv0Ihm/ABIcvQFGIiFBXd76oBIar9drAwHFhR4=
github.com/twpayne/go-geom v1.6.1/go.mod h1:Kr+Nly6BswFsKM5sd31YaoWS5PeDDH2NftJTK7Gd028=
github.com/tylertreat/BoomFilters v0.0.0-20210315201527-1a82519a3e43 h1:QEePdg0ty2r0t1+qwfZmQ4OOl/MB2UXIeJSpIZv56lg=
github.com/tylertreat/BoomFilters v0.0.0-20210315201527-1a82519a3e43/go.mod h1:OYRfF6eb5wY9VRFkXJH8FFBi3plw2v+giaIu7P054pM=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/viant/assertly v0.9.0 h1:uB3jO+qmWQcrSCHQRxA2kk88eXAdaklUUDxxCU5wBHQ=
github.com/viant/assertly v0.9.0/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/viant/ptrie v1.0.1 h1:3fFC8XqCSchf11sCSS5sbb8eGDNEP2g2Hj96lNdHlZY=
github.com/viant/ptrie v1.0.1/go.mod h1:Y+mwwNCIUgFrCZcrG4/QChfi4ubvnNBsyrENBIgigu0=
github.com/viant/toolbox v0.34.5 h1:szWNPiGHjo8Dd4v2a59saEhG31DRL2Xf3aJ0ZtTSuqc=
github.com/viant/toolbox v0.34.5/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0 h1:3UeQBvD0TFrlVjOeLOBz+CPAI8dnbqNSVwUwRrkp7vQ=
github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0/go.mod h1:IXCdmsXIht47RaVFLEdVnh1t+pgYtTAhQGj73kz+2DM=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I=
golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 h1:cYNAzI2sUwhmCcoj9TxvihSrqsxt6uIkj3rDRhSDmW4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA=
google.golang.org/grpc v1.85.0-dev h1:HxkDyKIIZPpFnroC56tQv5gNuKTmVvi0t7TzOf5zt7g=
google.golang.org/grpc v1.85.0-dev/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0=
google.golang.org/grpc/examples v0.0.0-20250407062114-b368379ef8f6 h1:ExN12ndbJ608cboPYflpTny6mXSzPrDLh0iTaVrRrds=
google.golang.org/grpc/examples v0.0.0-20250407062114-b368379ef8f6/go.mod h1:6ytKWczdvnpnO+m+JiG9NjEDzR1FJfsnmJdG7B8QVZ8=
google.golang.org/grpc/security/advancedtls v1.0.0 h1:/KQ7VP/1bs53/aopk9QhuPyFAp9Dm9Ejix3lzYkCrDA=
google.golang.org/grpc/security/advancedtls v1.0.0/go.mod h1:o+s4go+e1PJ2AjuQMY5hU82W7lDlefjJA6FqEHRVHWk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -0,0 +1,549 @@
package integration
import (
"context"
"fmt"
"testing"
"time"
"github.com/IBM/sarama"
"github.com/segmentio/kafka-go"
"github.com/seaweedfs/seaweedfs/test/kafka/internal/testutil"
)
// TestClientCompatibility tests compatibility with different Kafka client libraries and versions
// This test will use SMQ backend if SEAWEEDFS_MASTERS is available, otherwise mock
func TestClientCompatibility(t *testing.T) {
gateway := testutil.NewGatewayTestServerWithSMQ(t, testutil.SMQAvailable)
defer gateway.CleanupAndClose()
addr := gateway.StartAndWait()
time.Sleep(200 * time.Millisecond) // Allow gateway to be ready
// Log which backend we're using
if gateway.IsSMQMode() {
t.Logf("Running client compatibility tests with SMQ backend")
} else {
t.Logf("Running client compatibility tests with mock backend")
}
t.Run("SaramaVersionCompatibility", func(t *testing.T) {
testSaramaVersionCompatibility(t, addr)
})
t.Run("KafkaGoVersionCompatibility", func(t *testing.T) {
testKafkaGoVersionCompatibility(t, addr)
})
t.Run("APIVersionNegotiation", func(t *testing.T) {
testAPIVersionNegotiation(t, addr)
})
t.Run("ProducerConsumerCompatibility", func(t *testing.T) {
testProducerConsumerCompatibility(t, addr)
})
t.Run("ConsumerGroupCompatibility", func(t *testing.T) {
testConsumerGroupCompatibility(t, addr)
})
t.Run("AdminClientCompatibility", func(t *testing.T) {
testAdminClientCompatibility(t, addr)
})
}
func testSaramaVersionCompatibility(t *testing.T, addr string) {
versions := []sarama.KafkaVersion{
sarama.V2_6_0_0,
sarama.V2_8_0_0,
sarama.V3_0_0_0,
sarama.V3_4_0_0,
}
for _, version := range versions {
t.Run(fmt.Sprintf("Sarama_%s", version.String()), func(t *testing.T) {
config := sarama.NewConfig()
config.Version = version
config.Producer.Return.Successes = true
config.Consumer.Return.Errors = true
client, err := sarama.NewClient([]string{addr}, config)
if err != nil {
t.Fatalf("Failed to create Sarama client for version %s: %v", version, err)
}
defer client.Close()
// Test basic operations
topicName := testutil.GenerateUniqueTopicName(fmt.Sprintf("sarama-%s", version.String()))
// Test topic creation via admin client
admin, err := sarama.NewClusterAdminFromClient(client)
if err != nil {
t.Fatalf("Failed to create admin client: %v", err)
}
defer admin.Close()
topicDetail := &sarama.TopicDetail{
NumPartitions: 1,
ReplicationFactor: 1,
}
err = admin.CreateTopic(topicName, topicDetail, false)
if err != nil {
t.Logf("Topic creation failed (may already exist): %v", err)
}
// Test produce
producer, err := sarama.NewSyncProducerFromClient(client)
if err != nil {
t.Fatalf("Failed to create producer: %v", err)
}
defer producer.Close()
message := &sarama.ProducerMessage{
Topic: topicName,
Value: sarama.StringEncoder(fmt.Sprintf("test-message-%s", version.String())),
}
partition, offset, err := producer.SendMessage(message)
if err != nil {
t.Fatalf("Failed to send message: %v", err)
}
t.Logf("Sarama %s: Message sent to partition %d at offset %d", version, partition, offset)
// Test consume
consumer, err := sarama.NewConsumerFromClient(client)
if err != nil {
t.Fatalf("Failed to create consumer: %v", err)
}
defer consumer.Close()
partitionConsumer, err := consumer.ConsumePartition(topicName, 0, sarama.OffsetOldest)
if err != nil {
t.Fatalf("Failed to create partition consumer: %v", err)
}
defer partitionConsumer.Close()
select {
case msg := <-partitionConsumer.Messages():
if string(msg.Value) != fmt.Sprintf("test-message-%s", version.String()) {
t.Errorf("Message content mismatch: expected %s, got %s",
fmt.Sprintf("test-message-%s", version.String()), string(msg.Value))
}
t.Logf("Sarama %s: Successfully consumed message", version)
case err := <-partitionConsumer.Errors():
t.Fatalf("Consumer error: %v", err)
case <-time.After(5 * time.Second):
t.Fatal("Timeout waiting for message")
}
})
}
}
func testKafkaGoVersionCompatibility(t *testing.T, addr string) {
// Test different kafka-go configurations
configs := []struct {
name string
readerConfig kafka.ReaderConfig
writerConfig kafka.WriterConfig
}{
{
name: "kafka-go-default",
readerConfig: kafka.ReaderConfig{
Brokers: []string{addr},
Partition: 0, // Read from specific partition instead of using consumer group
},
writerConfig: kafka.WriterConfig{
Brokers: []string{addr},
},
},
{
name: "kafka-go-with-batching",
readerConfig: kafka.ReaderConfig{
Brokers: []string{addr},
Partition: 0, // Read from specific partition instead of using consumer group
MinBytes: 1,
MaxBytes: 10e6,
},
writerConfig: kafka.WriterConfig{
Brokers: []string{addr},
BatchSize: 100,
BatchTimeout: 10 * time.Millisecond,
},
},
}
for _, config := range configs {
t.Run(config.name, func(t *testing.T) {
topicName := testutil.GenerateUniqueTopicName(config.name)
// Create topic first using Sarama admin client (kafka-go doesn't have admin client)
saramaConfig := sarama.NewConfig()
saramaClient, err := sarama.NewClient([]string{addr}, saramaConfig)
if err != nil {
t.Fatalf("Failed to create Sarama client for topic creation: %v", err)
}
defer saramaClient.Close()
admin, err := sarama.NewClusterAdminFromClient(saramaClient)
if err != nil {
t.Fatalf("Failed to create admin client: %v", err)
}
defer admin.Close()
topicDetail := &sarama.TopicDetail{
NumPartitions: 1,
ReplicationFactor: 1,
}
err = admin.CreateTopic(topicName, topicDetail, false)
if err != nil {
t.Logf("Topic creation failed (may already exist): %v", err)
}
// Wait for topic to be fully created
time.Sleep(200 * time.Millisecond)
// Configure writer first and write message
config.writerConfig.Topic = topicName
writer := kafka.NewWriter(config.writerConfig)
// Test produce
produceCtx, produceCancel := context.WithTimeout(context.Background(), 15*time.Second)
defer produceCancel()
message := kafka.Message{
Value: []byte(fmt.Sprintf("test-message-%s", config.name)),
}
err = writer.WriteMessages(produceCtx, message)
if err != nil {
writer.Close()
t.Fatalf("Failed to write message: %v", err)
}
// Close writer before reading to ensure flush
if err := writer.Close(); err != nil {
t.Logf("Warning: writer close error: %v", err)
}
t.Logf("%s: Message written successfully", config.name)
// Wait for message to be available
time.Sleep(100 * time.Millisecond)
// Configure and create reader
config.readerConfig.Topic = topicName
config.readerConfig.StartOffset = kafka.FirstOffset
reader := kafka.NewReader(config.readerConfig)
// Test consume with dedicated context
consumeCtx, consumeCancel := context.WithTimeout(context.Background(), 15*time.Second)
msg, err := reader.ReadMessage(consumeCtx)
consumeCancel()
if err != nil {
reader.Close()
t.Fatalf("Failed to read message: %v", err)
}
if string(msg.Value) != fmt.Sprintf("test-message-%s", config.name) {
reader.Close()
t.Errorf("Message content mismatch: expected %s, got %s",
fmt.Sprintf("test-message-%s", config.name), string(msg.Value))
}
t.Logf("%s: Successfully consumed message", config.name)
// Close reader and wait for cleanup
if err := reader.Close(); err != nil {
t.Logf("Warning: reader close error: %v", err)
}
// Give time for background goroutines to clean up
time.Sleep(100 * time.Millisecond)
})
}
}
func testAPIVersionNegotiation(t *testing.T, addr string) {
// Test that clients can negotiate API versions properly
config := sarama.NewConfig()
config.Version = sarama.V2_8_0_0
client, err := sarama.NewClient([]string{addr}, config)
if err != nil {
t.Fatalf("Failed to create client: %v", err)
}
defer client.Close()
// Test that the client can get API versions
coordinator, err := client.Coordinator("test-group")
if err != nil {
t.Logf("Coordinator lookup failed (expected for test): %v", err)
} else {
t.Logf("Successfully found coordinator: %s", coordinator.Addr())
}
// Test metadata request (should work with version negotiation)
topics, err := client.Topics()
if err != nil {
t.Fatalf("Failed to get topics: %v", err)
}
t.Logf("API version negotiation successful, found %d topics", len(topics))
}
func testProducerConsumerCompatibility(t *testing.T, addr string) {
// Test cross-client compatibility: produce with one client, consume with another
topicName := testutil.GenerateUniqueTopicName("cross-client-test")
// Create topic first
saramaConfig := sarama.NewConfig()
saramaConfig.Producer.Return.Successes = true
saramaClient, err := sarama.NewClient([]string{addr}, saramaConfig)
if err != nil {
t.Fatalf("Failed to create Sarama client: %v", err)
}
defer saramaClient.Close()
admin, err := sarama.NewClusterAdminFromClient(saramaClient)
if err != nil {
t.Fatalf("Failed to create admin client: %v", err)
}
defer admin.Close()
topicDetail := &sarama.TopicDetail{
NumPartitions: 1,
ReplicationFactor: 1,
}
err = admin.CreateTopic(topicName, topicDetail, false)
if err != nil {
t.Logf("Topic creation failed (may already exist): %v", err)
}
// Wait for topic to be fully created
time.Sleep(200 * time.Millisecond)
producer, err := sarama.NewSyncProducerFromClient(saramaClient)
if err != nil {
t.Fatalf("Failed to create producer: %v", err)
}
defer producer.Close()
message := &sarama.ProducerMessage{
Topic: topicName,
Value: sarama.StringEncoder("cross-client-message"),
}
_, _, err = producer.SendMessage(message)
if err != nil {
t.Fatalf("Failed to send message with Sarama: %v", err)
}
t.Logf("Produced message with Sarama")
// Wait for message to be available
time.Sleep(100 * time.Millisecond)
// Consume with kafka-go (without consumer group to avoid offset commit issues)
reader := kafka.NewReader(kafka.ReaderConfig{
Brokers: []string{addr},
Topic: topicName,
Partition: 0,
StartOffset: kafka.FirstOffset,
})
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
msg, err := reader.ReadMessage(ctx)
cancel()
// Close reader immediately after reading
if closeErr := reader.Close(); closeErr != nil {
t.Logf("Warning: reader close error: %v", closeErr)
}
if err != nil {
t.Fatalf("Failed to read message with kafka-go: %v", err)
}
if string(msg.Value) != "cross-client-message" {
t.Errorf("Message content mismatch: expected 'cross-client-message', got '%s'", string(msg.Value))
}
t.Logf("Cross-client compatibility test passed")
}
func testConsumerGroupCompatibility(t *testing.T, addr string) {
// Test consumer group functionality with different clients
topicName := testutil.GenerateUniqueTopicName("consumer-group-test")
// Create topic and produce messages
config := sarama.NewConfig()
config.Producer.Return.Successes = true
client, err := sarama.NewClient([]string{addr}, config)
if err != nil {
t.Fatalf("Failed to create client: %v", err)
}
defer client.Close()
// Create topic first
admin, err := sarama.NewClusterAdminFromClient(client)
if err != nil {
t.Fatalf("Failed to create admin client: %v", err)
}
defer admin.Close()
topicDetail := &sarama.TopicDetail{
NumPartitions: 1,
ReplicationFactor: 1,
}
err = admin.CreateTopic(topicName, topicDetail, false)
if err != nil {
t.Logf("Topic creation failed (may already exist): %v", err)
}
// Wait for topic to be fully created
time.Sleep(200 * time.Millisecond)
producer, err := sarama.NewSyncProducerFromClient(client)
if err != nil {
t.Fatalf("Failed to create producer: %v", err)
}
defer producer.Close()
// Produce test messages
for i := 0; i < 5; i++ {
message := &sarama.ProducerMessage{
Topic: topicName,
Value: sarama.StringEncoder(fmt.Sprintf("group-message-%d", i)),
}
_, _, err = producer.SendMessage(message)
if err != nil {
t.Fatalf("Failed to send message %d: %v", i, err)
}
}
t.Logf("Produced 5 messages successfully")
// Wait for messages to be available
time.Sleep(200 * time.Millisecond)
// Test consumer group with Sarama (kafka-go consumer groups have offset commit issues)
consumer, err := sarama.NewConsumerFromClient(client)
if err != nil {
t.Fatalf("Failed to create consumer: %v", err)
}
defer consumer.Close()
partitionConsumer, err := consumer.ConsumePartition(topicName, 0, sarama.OffsetOldest)
if err != nil {
t.Fatalf("Failed to create partition consumer: %v", err)
}
defer partitionConsumer.Close()
messagesReceived := 0
timeout := time.After(30 * time.Second)
for messagesReceived < 5 {
select {
case msg := <-partitionConsumer.Messages():
t.Logf("Received message %d: %s", messagesReceived, string(msg.Value))
messagesReceived++
case err := <-partitionConsumer.Errors():
t.Logf("Consumer error (continuing): %v", err)
case <-timeout:
t.Fatalf("Timeout waiting for messages, received %d out of 5", messagesReceived)
}
}
t.Logf("Consumer group compatibility test passed: received %d messages", messagesReceived)
}
func testAdminClientCompatibility(t *testing.T, addr string) {
// Test admin operations with different clients
config := sarama.NewConfig()
config.Version = sarama.V2_8_0_0
config.Admin.Timeout = 30 * time.Second
client, err := sarama.NewClient([]string{addr}, config)
if err != nil {
t.Fatalf("Failed to create client: %v", err)
}
defer client.Close()
admin, err := sarama.NewClusterAdminFromClient(client)
if err != nil {
t.Fatalf("Failed to create admin client: %v", err)
}
defer admin.Close()
// Test topic operations
topicName := testutil.GenerateUniqueTopicName("admin-test")
topicDetail := &sarama.TopicDetail{
NumPartitions: 2,
ReplicationFactor: 1,
}
err = admin.CreateTopic(topicName, topicDetail, false)
if err != nil {
t.Logf("Topic creation failed (may already exist): %v", err)
}
// Wait for topic to be fully created and propagated
time.Sleep(500 * time.Millisecond)
// List topics with retry logic
var topics map[string]sarama.TopicDetail
maxRetries := 3
for i := 0; i < maxRetries; i++ {
topics, err = admin.ListTopics()
if err == nil {
break
}
t.Logf("List topics attempt %d failed: %v, retrying...", i+1, err)
time.Sleep(time.Duration(500*(i+1)) * time.Millisecond)
}
if err != nil {
t.Fatalf("Failed to list topics after %d attempts: %v", maxRetries, err)
}
found := false
for topic := range topics {
if topic == topicName {
found = true
t.Logf("Found created topic: %s", topicName)
break
}
}
if !found {
// Log all topics for debugging
allTopics := make([]string, 0, len(topics))
for topic := range topics {
allTopics = append(allTopics, topic)
}
t.Logf("Available topics: %v", allTopics)
t.Errorf("Created topic %s not found in topic list", topicName)
}
// Test describe consumer groups (if supported)
groups, err := admin.ListConsumerGroups()
if err != nil {
t.Logf("List consumer groups failed (may not be implemented): %v", err)
} else {
t.Logf("Found %d consumer groups", len(groups))
}
t.Logf("Admin client compatibility test passed")
}

View File

@@ -0,0 +1,356 @@
package integration
import (
"context"
"fmt"
"sync"
"testing"
"time"
"github.com/IBM/sarama"
"github.com/seaweedfs/seaweedfs/test/kafka/internal/testutil"
)
// TestConsumerGroups tests consumer group functionality
// This test requires SeaweedFS masters to be running and will skip if not available
func TestConsumerGroups(t *testing.T) {
gateway := testutil.NewGatewayTestServerWithSMQ(t, testutil.SMQRequired)
defer gateway.CleanupAndClose()
addr := gateway.StartAndWait()
t.Logf("Running consumer group tests with SMQ backend for offset persistence")
t.Run("BasicFunctionality", func(t *testing.T) {
testConsumerGroupBasicFunctionality(t, addr)
})
t.Run("OffsetCommitAndFetch", func(t *testing.T) {
testConsumerGroupOffsetCommitAndFetch(t, addr)
})
t.Run("Rebalancing", func(t *testing.T) {
testConsumerGroupRebalancing(t, addr)
})
}
func testConsumerGroupBasicFunctionality(t *testing.T, addr string) {
topicName := testutil.GenerateUniqueTopicName("consumer-group-basic")
groupID := testutil.GenerateUniqueGroupID("basic-group")
client := testutil.NewSaramaClient(t, addr)
msgGen := testutil.NewMessageGenerator()
// Create topic and produce messages
err := client.CreateTopic(topicName, 1, 1)
testutil.AssertNoError(t, err, "Failed to create topic")
messages := msgGen.GenerateStringMessages(9) // 3 messages per consumer
err = client.ProduceMessages(topicName, messages)
testutil.AssertNoError(t, err, "Failed to produce messages")
// Test with multiple consumers in the same group. The messages channel is
// shared so the assertion can verify total-consumed/no-duplicates across the
// group. Each consumer gets its own handler (and its own ready channel) so
// we can wait for each distinct consumer to reach Setup, rather than
// relying on a single close() that only signals once.
numConsumers := 3
handlers := make([]*ConsumerGroupHandler, numConsumers)
sharedMessages := make(chan *sarama.ConsumerMessage, len(messages))
for i := 0; i < numConsumers; i++ {
handlers[i] = &ConsumerGroupHandler{
messages: sharedMessages,
ready: make(chan bool),
t: t,
}
}
var wg sync.WaitGroup
consumerErrors := make(chan error, numConsumers)
for i := 0; i < numConsumers; i++ {
wg.Add(1)
go func(consumerID int) {
defer wg.Done()
consumerGroup, err := sarama.NewConsumerGroup([]string{addr}, groupID, client.GetConfig())
if err != nil {
consumerErrors <- fmt.Errorf("consumer %d: failed to create consumer group: %v", consumerID, err)
return
}
defer consumerGroup.Close()
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
runConsumeLoop(t, ctx, fmt.Sprintf("Consumer%d", consumerID),
consumerGroup, []string{topicName}, handlers[consumerID])
}(i)
}
// Wait for each consumer to be ready. Multi-consumer rebalance can take
// a few heartbeat intervals (default 3s) as the initial leader receives
// REBALANCE_IN_PROGRESS from its heartbeat and re-joins, so allow headroom.
for i := 0; i < numConsumers; i++ {
select {
case <-handlers[i].ready:
case <-time.After(20 * time.Second):
t.Fatalf("Timeout waiting for consumer %d to be ready", i)
}
}
// Collect consumed messages
consumedMessages := make([]*sarama.ConsumerMessage, 0, len(messages))
messageTimeout := time.After(15 * time.Second)
for len(consumedMessages) < len(messages) {
select {
case msg := <-sharedMessages:
consumedMessages = append(consumedMessages, msg)
case err := <-consumerErrors:
t.Fatalf("Consumer error: %v", err)
case <-messageTimeout:
t.Fatalf("Timeout waiting for messages. Got %d/%d messages", len(consumedMessages), len(messages))
}
}
wg.Wait()
// Verify all messages were consumed exactly once
testutil.AssertEqual(t, len(messages), len(consumedMessages), "Message count mismatch")
// Verify message uniqueness (no duplicates)
messageKeys := make(map[string]bool)
for _, msg := range consumedMessages {
key := string(msg.Key)
if messageKeys[key] {
t.Errorf("Duplicate message key: %s", key)
}
messageKeys[key] = true
}
}
func testConsumerGroupOffsetCommitAndFetch(t *testing.T, addr string) {
topicName := testutil.GenerateUniqueTopicName("offset-commit-test")
groupID := testutil.GenerateUniqueGroupID("offset-group")
client := testutil.NewSaramaClient(t, addr)
msgGen := testutil.NewMessageGenerator()
// Create topic and produce messages
err := client.CreateTopic(topicName, 1, 1)
testutil.AssertNoError(t, err, "Failed to create topic")
messages := msgGen.GenerateStringMessages(5)
err = client.ProduceMessages(topicName, messages)
testutil.AssertNoError(t, err, "Failed to produce messages")
// First consumer: consume first 3 messages and commit offsets
handler1 := &OffsetTestHandler{
messages: make(chan *sarama.ConsumerMessage, len(messages)),
ready: make(chan bool),
stopAfter: 3,
t: t,
}
consumerGroup1, err := sarama.NewConsumerGroup([]string{addr}, groupID, client.GetConfig())
testutil.AssertNoError(t, err, "Failed to create first consumer group")
ctx1, cancel1 := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel1()
go func() {
err := consumerGroup1.Consume(ctx1, []string{topicName}, handler1)
if err != nil && err != context.DeadlineExceeded {
t.Logf("First consumer error: %v", err)
}
}()
// Wait for first consumer to be ready and consume messages
<-handler1.ready
consumedCount := 0
for consumedCount < 3 {
select {
case <-handler1.messages:
consumedCount++
case <-time.After(5 * time.Second):
t.Fatalf("Timeout waiting for first consumer messages")
}
}
consumerGroup1.Close()
cancel1()
time.Sleep(500 * time.Millisecond) // Wait for cleanup
// Stop the first consumer after N messages
// Allow a brief moment for commit/heartbeat to flush
time.Sleep(1 * time.Second)
// Start a second consumer in the same group to verify resumption from committed offset
handler2 := &OffsetTestHandler{
messages: make(chan *sarama.ConsumerMessage, len(messages)),
ready: make(chan bool),
stopAfter: 2,
t: t,
}
consumerGroup2, err := sarama.NewConsumerGroup([]string{addr}, groupID, client.GetConfig())
testutil.AssertNoError(t, err, "Failed to create second consumer group")
defer consumerGroup2.Close()
ctx2, cancel2 := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel2()
go func() {
err := consumerGroup2.Consume(ctx2, []string{topicName}, handler2)
if err != nil && err != context.DeadlineExceeded {
t.Logf("Second consumer error: %v", err)
}
}()
// Wait for second consumer and collect remaining messages
<-handler2.ready
secondConsumerMessages := make([]*sarama.ConsumerMessage, 0)
consumedCount = 0
for consumedCount < 2 {
select {
case msg := <-handler2.messages:
consumedCount++
secondConsumerMessages = append(secondConsumerMessages, msg)
case <-time.After(5 * time.Second):
t.Fatalf("Timeout waiting for second consumer messages. Got %d/2", consumedCount)
}
}
// Verify second consumer started from correct offset
if len(secondConsumerMessages) > 0 {
firstMessageOffset := secondConsumerMessages[0].Offset
if firstMessageOffset < 3 {
t.Fatalf("Second consumer should start from offset >= 3: got %d", firstMessageOffset)
}
}
}
func testConsumerGroupRebalancing(t *testing.T, addr string) {
topicName := testutil.GenerateUniqueTopicName("rebalancing-test")
groupID := testutil.GenerateUniqueGroupID("rebalance-group")
client := testutil.NewSaramaClient(t, addr)
msgGen := testutil.NewMessageGenerator()
// Create topic with multiple partitions for rebalancing
err := client.CreateTopic(topicName, 4, 1) // 4 partitions
testutil.AssertNoError(t, err, "Failed to create topic")
// Produce messages to all partitions
messages := msgGen.GenerateStringMessages(12) // 3 messages per partition
for i, msg := range messages {
partition := int32(i % 4)
err = client.ProduceMessageToPartition(topicName, partition, msg)
testutil.AssertNoError(t, err, "Failed to produce message")
}
t.Logf("Produced %d messages across 4 partitions", len(messages))
// Test scenario 1: Single consumer gets all partitions
t.Run("SingleConsumerAllPartitions", func(t *testing.T) {
testSingleConsumerAllPartitions(t, addr, topicName, groupID+"-single")
})
// Test scenario 2: Add second consumer, verify rebalancing
t.Run("TwoConsumersRebalance", func(t *testing.T) {
testTwoConsumersRebalance(t, addr, topicName, groupID+"-two")
})
// Test scenario 3: Remove consumer, verify rebalancing
t.Run("ConsumerLeaveRebalance", func(t *testing.T) {
testConsumerLeaveRebalance(t, addr, topicName, groupID+"-leave")
})
// Test scenario 4: Multiple consumers join simultaneously
t.Run("MultipleConsumersJoin", func(t *testing.T) {
testMultipleConsumersJoin(t, addr, topicName, groupID+"-multi")
})
}
// ConsumerGroupHandler implements sarama.ConsumerGroupHandler
type ConsumerGroupHandler struct {
messages chan *sarama.ConsumerMessage
ready chan bool
readyOnce sync.Once
t *testing.T
}
func (h *ConsumerGroupHandler) Setup(sarama.ConsumerGroupSession) error {
h.t.Logf("Consumer group session setup")
h.readyOnce.Do(func() {
close(h.ready)
})
return nil
}
func (h *ConsumerGroupHandler) Cleanup(sarama.ConsumerGroupSession) error {
h.t.Logf("Consumer group session cleanup")
return nil
}
func (h *ConsumerGroupHandler) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
for {
select {
case message := <-claim.Messages():
if message == nil {
return nil
}
h.messages <- message
session.MarkMessage(message, "")
case <-session.Context().Done():
return nil
}
}
}
// OffsetTestHandler implements sarama.ConsumerGroupHandler for offset testing
type OffsetTestHandler struct {
messages chan *sarama.ConsumerMessage
ready chan bool
readyOnce sync.Once
stopAfter int
consumed int
t *testing.T
}
func (h *OffsetTestHandler) Setup(sarama.ConsumerGroupSession) error {
h.t.Logf("Offset test consumer setup")
h.readyOnce.Do(func() {
close(h.ready)
})
return nil
}
func (h *OffsetTestHandler) Cleanup(sarama.ConsumerGroupSession) error {
h.t.Logf("Offset test consumer cleanup")
return nil
}
func (h *OffsetTestHandler) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
for {
select {
case message := <-claim.Messages():
if message == nil {
return nil
}
h.consumed++
h.messages <- message
session.MarkMessage(message, "")
// Stop after consuming the specified number of messages
if h.consumed >= h.stopAfter {
h.t.Logf("Stopping consumer after %d messages", h.consumed)
// Ensure commits are flushed before exiting the claim
session.Commit()
return nil
}
case <-session.Context().Done():
return nil
}
}
}

View File

@@ -0,0 +1,216 @@
package integration
import (
"encoding/json"
"io"
"net/http"
"testing"
"time"
"github.com/seaweedfs/seaweedfs/test/kafka/internal/testutil"
)
// TestDockerIntegration tests the complete Kafka integration using Docker Compose
func TestDockerIntegration(t *testing.T) {
env := testutil.NewDockerEnvironment(t)
env.SkipIfNotAvailable(t)
t.Run("KafkaConnectivity", func(t *testing.T) {
env.RequireKafka(t)
testDockerKafkaConnectivity(t, env.KafkaBootstrap)
})
t.Run("SchemaRegistryConnectivity", func(t *testing.T) {
env.RequireSchemaRegistry(t)
testDockerSchemaRegistryConnectivity(t, env.SchemaRegistry)
})
t.Run("KafkaGatewayConnectivity", func(t *testing.T) {
env.RequireGateway(t)
testDockerKafkaGatewayConnectivity(t, env.KafkaGateway)
})
t.Run("SaramaProduceConsume", func(t *testing.T) {
env.RequireKafka(t)
testDockerSaramaProduceConsume(t, env.KafkaBootstrap)
})
t.Run("KafkaGoProduceConsume", func(t *testing.T) {
env.RequireKafka(t)
testDockerKafkaGoProduceConsume(t, env.KafkaBootstrap)
})
t.Run("GatewayProduceConsume", func(t *testing.T) {
env.RequireGateway(t)
testDockerGatewayProduceConsume(t, env.KafkaGateway)
})
t.Run("CrossClientCompatibility", func(t *testing.T) {
env.RequireKafka(t)
env.RequireGateway(t)
testDockerCrossClientCompatibility(t, env.KafkaBootstrap, env.KafkaGateway)
})
}
func testDockerKafkaConnectivity(t *testing.T, bootstrap string) {
client := testutil.NewSaramaClient(t, bootstrap)
// Test basic connectivity by creating a topic
topicName := testutil.GenerateUniqueTopicName("connectivity-test")
err := client.CreateTopic(topicName, 1, 1)
testutil.AssertNoError(t, err, "Failed to create topic for connectivity test")
t.Logf("Kafka connectivity test passed")
}
func testDockerSchemaRegistryConnectivity(t *testing.T, registryURL string) {
// Test basic HTTP connectivity to Schema Registry
client := &http.Client{Timeout: 10 * time.Second}
// Test 1: Check if Schema Registry is responding
resp, err := client.Get(registryURL + "/subjects")
if err != nil {
t.Fatalf("Failed to connect to Schema Registry at %s: %v", registryURL, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("Schema Registry returned status %d, expected 200", resp.StatusCode)
}
// Test 2: Verify response is valid JSON array
body, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Failed to read response body: %v", err)
}
var subjects []string
if err := json.Unmarshal(body, &subjects); err != nil {
t.Fatalf("Schema Registry response is not valid JSON array: %v", err)
}
t.Logf("Schema Registry is accessible with %d subjects", len(subjects))
// Test 3: Check config endpoint
configResp, err := client.Get(registryURL + "/config")
if err != nil {
t.Fatalf("Failed to get Schema Registry config: %v", err)
}
defer configResp.Body.Close()
if configResp.StatusCode != http.StatusOK {
t.Fatalf("Schema Registry config endpoint returned status %d", configResp.StatusCode)
}
configBody, err := io.ReadAll(configResp.Body)
if err != nil {
t.Fatalf("Failed to read config response: %v", err)
}
var config map[string]interface{}
if err := json.Unmarshal(configBody, &config); err != nil {
t.Fatalf("Schema Registry config response is not valid JSON: %v", err)
}
t.Logf("Schema Registry config: %v", config)
t.Logf("Schema Registry connectivity test passed")
}
func testDockerKafkaGatewayConnectivity(t *testing.T, gatewayURL string) {
client := testutil.NewSaramaClient(t, gatewayURL)
// Test basic connectivity to gateway
topicName := testutil.GenerateUniqueTopicName("gateway-connectivity-test")
err := client.CreateTopic(topicName, 1, 1)
testutil.AssertNoError(t, err, "Failed to create topic via gateway")
t.Logf("Kafka Gateway connectivity test passed")
}
func testDockerSaramaProduceConsume(t *testing.T, bootstrap string) {
client := testutil.NewSaramaClient(t, bootstrap)
msgGen := testutil.NewMessageGenerator()
topicName := testutil.GenerateUniqueTopicName("sarama-docker-test")
// Create topic
err := client.CreateTopic(topicName, 1, 1)
testutil.AssertNoError(t, err, "Failed to create topic")
// Produce and consume messages
messages := msgGen.GenerateStringMessages(3)
err = client.ProduceMessages(topicName, messages)
testutil.AssertNoError(t, err, "Failed to produce messages")
consumed, err := client.ConsumeMessages(topicName, 0, len(messages))
testutil.AssertNoError(t, err, "Failed to consume messages")
err = testutil.ValidateMessageContent(messages, consumed)
testutil.AssertNoError(t, err, "Message validation failed")
t.Logf("Sarama produce/consume test passed")
}
func testDockerKafkaGoProduceConsume(t *testing.T, bootstrap string) {
client := testutil.NewKafkaGoClient(t, bootstrap)
msgGen := testutil.NewMessageGenerator()
topicName := testutil.GenerateUniqueTopicName("kafka-go-docker-test")
// Create topic
err := client.CreateTopic(topicName, 1, 1)
testutil.AssertNoError(t, err, "Failed to create topic")
// Produce and consume messages
messages := msgGen.GenerateKafkaGoMessages(3)
err = client.ProduceMessages(topicName, messages)
testutil.AssertNoError(t, err, "Failed to produce messages")
consumed, err := client.ConsumeMessages(topicName, len(messages))
testutil.AssertNoError(t, err, "Failed to consume messages")
err = testutil.ValidateKafkaGoMessageContent(messages, consumed)
testutil.AssertNoError(t, err, "Message validation failed")
t.Logf("kafka-go produce/consume test passed")
}
func testDockerGatewayProduceConsume(t *testing.T, gatewayURL string) {
client := testutil.NewSaramaClient(t, gatewayURL)
msgGen := testutil.NewMessageGenerator()
topicName := testutil.GenerateUniqueTopicName("gateway-docker-test")
// Produce and consume via gateway
messages := msgGen.GenerateStringMessages(3)
err := client.ProduceMessages(topicName, messages)
testutil.AssertNoError(t, err, "Failed to produce messages via gateway")
consumed, err := client.ConsumeMessages(topicName, 0, len(messages))
testutil.AssertNoError(t, err, "Failed to consume messages via gateway")
err = testutil.ValidateMessageContent(messages, consumed)
testutil.AssertNoError(t, err, "Message validation failed")
t.Logf("Gateway produce/consume test passed")
}
func testDockerCrossClientCompatibility(t *testing.T, kafkaBootstrap, gatewayURL string) {
kafkaClient := testutil.NewSaramaClient(t, kafkaBootstrap)
msgGen := testutil.NewMessageGenerator()
topicName := testutil.GenerateUniqueTopicName("cross-client-docker-test")
// Create topic on Kafka
err := kafkaClient.CreateTopic(topicName, 1, 1)
testutil.AssertNoError(t, err, "Failed to create topic on Kafka")
// Produce to Kafka
messages := msgGen.GenerateStringMessages(2)
err = kafkaClient.ProduceMessages(topicName, messages)
testutil.AssertNoError(t, err, "Failed to produce to Kafka")
// This tests the integration between Kafka and the Gateway
// In a real scenario, messages would be replicated or bridged
t.Logf("Cross-client compatibility test passed")
}

View File

@@ -0,0 +1,474 @@
package integration
import (
"context"
"errors"
"fmt"
"sync"
"testing"
"time"
"github.com/IBM/sarama"
"github.com/seaweedfs/seaweedfs/test/kafka/internal/testutil"
)
// runConsumeLoop drives consumerGroup.Consume in a for-loop until ctx is cancelled
// or the group is closed. This is the idiomatic Sarama pattern: Consume returns
// when a rebalance cancels the session, and the caller is expected to re-enter.
// Tests that observe rebalance behaviour MUST loop — otherwise the consumer goroutine
// exits on the first rebalance and never sees the post-rebalance assignment.
func runConsumeLoop(t *testing.T, ctx context.Context, name string, cg sarama.ConsumerGroup, topics []string, handler sarama.ConsumerGroupHandler) {
for {
if err := cg.Consume(ctx, topics, handler); err != nil {
if ctx.Err() != nil || errors.Is(err, sarama.ErrClosedConsumerGroup) {
return
}
t.Logf("%s Consume returned: %v (will retry)", name, err)
}
if ctx.Err() != nil {
return
}
}
}
func testSingleConsumerAllPartitions(t *testing.T, addr, topicName, groupID string) {
config := sarama.NewConfig()
config.Consumer.Group.Rebalance.Strategy = sarama.BalanceStrategyRange
config.Consumer.Offsets.Initial = sarama.OffsetOldest
config.Consumer.Return.Errors = true
client, err := sarama.NewClient([]string{addr}, config)
testutil.AssertNoError(t, err, "Failed to create client")
defer client.Close()
consumerGroup, err := sarama.NewConsumerGroupFromClient(groupID, client)
testutil.AssertNoError(t, err, "Failed to create consumer group")
defer consumerGroup.Close()
handler := &RebalanceTestHandler{
messages: make(chan *sarama.ConsumerMessage, 20),
ready: make(chan bool),
assignments: make(chan []int32, 1),
t: t,
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Start consumer
go func() {
err := consumerGroup.Consume(ctx, []string{topicName}, handler)
if err != nil && err != context.DeadlineExceeded {
t.Logf("Consumer error: %v", err)
}
}()
// Wait for consumer to be ready
<-handler.ready
// Wait for assignment
select {
case partitions := <-handler.assignments:
t.Logf("Single consumer assigned partitions: %v", partitions)
if len(partitions) != 4 {
t.Errorf("Expected single consumer to get all 4 partitions, got %d", len(partitions))
}
case <-time.After(10 * time.Second):
t.Fatal("Timeout waiting for partition assignment")
}
// Consume some messages to verify functionality
consumedCount := 0
for consumedCount < 4 { // At least one from each partition
select {
case msg := <-handler.messages:
t.Logf("Consumed message from partition %d: %s", msg.Partition, string(msg.Value))
consumedCount++
case <-time.After(5 * time.Second):
t.Logf("Consumed %d messages so far", consumedCount)
break
}
}
if consumedCount == 0 {
t.Error("No messages consumed by single consumer")
}
}
func testTwoConsumersRebalance(t *testing.T, addr, topicName, groupID string) {
config := sarama.NewConfig()
config.Consumer.Group.Rebalance.Strategy = sarama.BalanceStrategyRange
config.Consumer.Offsets.Initial = sarama.OffsetOldest
config.Consumer.Return.Errors = true
// Start first consumer
client1, err := sarama.NewClient([]string{addr}, config)
testutil.AssertNoError(t, err, "Failed to create client1")
defer client1.Close()
consumerGroup1, err := sarama.NewConsumerGroupFromClient(groupID, client1)
testutil.AssertNoError(t, err, "Failed to create consumer group 1")
defer consumerGroup1.Close()
handler1 := &RebalanceTestHandler{
messages: make(chan *sarama.ConsumerMessage, 20),
ready: make(chan bool),
assignments: make(chan []int32, 1),
t: t,
name: "Consumer1",
}
ctx1, cancel1 := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel1()
go runConsumeLoop(t, ctx1, "Consumer1", consumerGroup1, []string{topicName}, handler1)
// Wait for first consumer to be ready and get initial assignment
<-handler1.ready
select {
case partitions := <-handler1.assignments:
t.Logf("Consumer1 initial assignment: %v", partitions)
if len(partitions) != 4 {
t.Errorf("Expected Consumer1 to initially get all 4 partitions, got %d", len(partitions))
}
case <-time.After(10 * time.Second):
t.Fatal("Timeout waiting for Consumer1 initial assignment")
}
// Start second consumer
client2, err := sarama.NewClient([]string{addr}, config)
testutil.AssertNoError(t, err, "Failed to create client2")
defer client2.Close()
consumerGroup2, err := sarama.NewConsumerGroupFromClient(groupID, client2)
testutil.AssertNoError(t, err, "Failed to create consumer group 2")
defer consumerGroup2.Close()
handler2 := &RebalanceTestHandler{
messages: make(chan *sarama.ConsumerMessage, 20),
ready: make(chan bool),
assignments: make(chan []int32, 1),
t: t,
name: "Consumer2",
}
ctx2, cancel2 := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel2()
go runConsumeLoop(t, ctx2, "Consumer2", consumerGroup2, []string{topicName}, handler2)
// Wait for second consumer to be ready
<-handler2.ready
// Wait for rebalancing to occur - both consumers should get new assignments
var rebalancedAssignment1, rebalancedAssignment2 []int32
// Consumer1 should get a rebalance assignment
select {
case partitions := <-handler1.assignments:
rebalancedAssignment1 = partitions
t.Logf("Consumer1 rebalanced assignment: %v", partitions)
case <-time.After(15 * time.Second):
t.Error("Timeout waiting for Consumer1 rebalance assignment")
}
// Consumer2 should get its assignment
select {
case partitions := <-handler2.assignments:
rebalancedAssignment2 = partitions
t.Logf("Consumer2 assignment: %v", partitions)
case <-time.After(15 * time.Second):
t.Error("Timeout waiting for Consumer2 assignment")
}
// Verify rebalancing occurred correctly
totalPartitions := len(rebalancedAssignment1) + len(rebalancedAssignment2)
if totalPartitions != 4 {
t.Errorf("Expected total of 4 partitions assigned, got %d", totalPartitions)
}
// Each consumer should have at least 1 partition, and no more than 3
if len(rebalancedAssignment1) == 0 || len(rebalancedAssignment1) > 3 {
t.Errorf("Consumer1 should have 1-3 partitions, got %d", len(rebalancedAssignment1))
}
if len(rebalancedAssignment2) == 0 || len(rebalancedAssignment2) > 3 {
t.Errorf("Consumer2 should have 1-3 partitions, got %d", len(rebalancedAssignment2))
}
// Verify no partition overlap
partitionSet := make(map[int32]bool)
for _, p := range rebalancedAssignment1 {
if partitionSet[p] {
t.Errorf("Partition %d assigned to multiple consumers", p)
}
partitionSet[p] = true
}
for _, p := range rebalancedAssignment2 {
if partitionSet[p] {
t.Errorf("Partition %d assigned to multiple consumers", p)
}
partitionSet[p] = true
}
t.Logf("Rebalancing test completed successfully")
}
func testConsumerLeaveRebalance(t *testing.T, addr, topicName, groupID string) {
config := sarama.NewConfig()
config.Consumer.Group.Rebalance.Strategy = sarama.BalanceStrategyRange
config.Consumer.Offsets.Initial = sarama.OffsetOldest
config.Consumer.Return.Errors = true
// Start two consumers
client1, err := sarama.NewClient([]string{addr}, config)
testutil.AssertNoError(t, err, "Failed to create client1")
defer client1.Close()
client2, err := sarama.NewClient([]string{addr}, config)
testutil.AssertNoError(t, err, "Failed to create client2")
defer client2.Close()
consumerGroup1, err := sarama.NewConsumerGroupFromClient(groupID, client1)
testutil.AssertNoError(t, err, "Failed to create consumer group 1")
defer consumerGroup1.Close()
consumerGroup2, err := sarama.NewConsumerGroupFromClient(groupID, client2)
testutil.AssertNoError(t, err, "Failed to create consumer group 2")
handler1 := &RebalanceTestHandler{
messages: make(chan *sarama.ConsumerMessage, 20),
ready: make(chan bool),
assignments: make(chan []int32, 1),
t: t,
name: "Consumer1",
}
handler2 := &RebalanceTestHandler{
messages: make(chan *sarama.ConsumerMessage, 20),
ready: make(chan bool),
assignments: make(chan []int32, 1),
t: t,
name: "Consumer2",
}
ctx1, cancel1 := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel1()
ctx2, cancel2 := context.WithTimeout(context.Background(), 30*time.Second)
// Start both consumers with loops so they survive rebalance session exits.
go runConsumeLoop(t, ctx1, "Consumer1", consumerGroup1, []string{topicName}, handler1)
go runConsumeLoop(t, ctx2, "Consumer2", consumerGroup2, []string{topicName}, handler2)
// Wait for both consumers to be ready
<-handler1.ready
<-handler2.ready
// Wait for initial assignments
<-handler1.assignments
<-handler2.assignments
t.Logf("Both consumers started, now stopping Consumer2")
// Stop second consumer (simulate leave)
cancel2()
consumerGroup2.Close()
// Wait for Consumer1 to get rebalanced assignment (should get all partitions)
select {
case partitions := <-handler1.assignments:
t.Logf("Consumer1 rebalanced assignment after Consumer2 left: %v", partitions)
if len(partitions) != 4 {
t.Errorf("Expected Consumer1 to get all 4 partitions after Consumer2 left, got %d", len(partitions))
}
case <-time.After(20 * time.Second):
t.Error("Timeout waiting for Consumer1 rebalance after Consumer2 left")
}
t.Logf("Consumer leave rebalancing test completed successfully")
}
func testMultipleConsumersJoin(t *testing.T, addr, topicName, groupID string) {
config := sarama.NewConfig()
config.Consumer.Group.Rebalance.Strategy = sarama.BalanceStrategyRange
config.Consumer.Offsets.Initial = sarama.OffsetOldest
config.Consumer.Return.Errors = true
numConsumers := 4
consumers := make([]sarama.ConsumerGroup, numConsumers)
clients := make([]sarama.Client, numConsumers)
handlers := make([]*RebalanceTestHandler, numConsumers)
contexts := make([]context.Context, numConsumers)
cancels := make([]context.CancelFunc, numConsumers)
// Start all consumers simultaneously
for i := 0; i < numConsumers; i++ {
client, err := sarama.NewClient([]string{addr}, config)
testutil.AssertNoError(t, err, fmt.Sprintf("Failed to create client%d", i))
clients[i] = client
consumerGroup, err := sarama.NewConsumerGroupFromClient(groupID, client)
testutil.AssertNoError(t, err, fmt.Sprintf("Failed to create consumer group %d", i))
consumers[i] = consumerGroup
handlers[i] = &RebalanceTestHandler{
messages: make(chan *sarama.ConsumerMessage, 20),
ready: make(chan bool),
assignments: make(chan []int32, 1),
t: t,
name: fmt.Sprintf("Consumer%d", i),
}
contexts[i], cancels[i] = context.WithTimeout(context.Background(), 45*time.Second)
go func(idx int) {
runConsumeLoop(t, contexts[idx], fmt.Sprintf("Consumer%d", idx),
consumers[idx], []string{topicName}, handlers[idx])
}(i)
}
// Cleanup
defer func() {
for i := 0; i < numConsumers; i++ {
cancels[i]()
consumers[i].Close()
clients[i].Close()
}
}()
// Wait for all consumers to be ready. Four consumers racing in produces
// several rebalance rounds; each round costs ~HeartbeatInterval (3s) before
// the prior leader sees REBALANCE_IN_PROGRESS and rejoins. Give enough
// headroom for the group to converge.
for i := 0; i < numConsumers; i++ {
select {
case <-handlers[i].ready:
t.Logf("Consumer%d ready", i)
case <-time.After(30 * time.Second):
t.Fatalf("Timeout waiting for Consumer%d to be ready", i)
}
}
// Collect the settled assignment for each consumer. Because each consumer's
// Consume() returns and re-enters on every rebalance, Setup fires multiple
// times while the group converges and the handler's assignments channel
// accumulates several snapshots. Drain it and keep the most recent value.
assignments := make([][]int32, numConsumers)
deadline := time.Now().Add(20 * time.Second)
for i := 0; i < numConsumers; i++ {
select {
case partitions := <-handlers[i].assignments:
assignments[i] = partitions
case <-time.After(time.Until(deadline)):
t.Errorf("Timeout waiting for Consumer%d assignment", i)
continue
}
drain:
for {
select {
case partitions := <-handlers[i].assignments:
assignments[i] = partitions
case <-time.After(3 * time.Second):
break drain
}
}
t.Logf("Consumer%d final assignment: %v", i, assignments[i])
}
// Verify all partitions are assigned exactly once
assignedPartitions := make(map[int32]int)
totalAssigned := 0
for i, assignment := range assignments {
totalAssigned += len(assignment)
for _, partition := range assignment {
assignedPartitions[partition]++
if assignedPartitions[partition] > 1 {
t.Errorf("Partition %d assigned to multiple consumers", partition)
}
}
// Each consumer should get exactly 1 partition (4 partitions / 4 consumers)
if len(assignment) != 1 {
t.Errorf("Consumer%d should get exactly 1 partition, got %d", i, len(assignment))
}
}
if totalAssigned != 4 {
t.Errorf("Expected 4 total partitions assigned, got %d", totalAssigned)
}
// Verify all partitions 0-3 are assigned
for i := int32(0); i < 4; i++ {
if assignedPartitions[i] != 1 {
t.Errorf("Partition %d assigned %d times, expected 1", i, assignedPartitions[i])
}
}
t.Logf("Multiple consumers join test completed successfully")
}
// RebalanceTestHandler implements sarama.ConsumerGroupHandler with rebalancing awareness
type RebalanceTestHandler struct {
messages chan *sarama.ConsumerMessage
ready chan bool
assignments chan []int32
readyOnce sync.Once
t *testing.T
name string
}
func (h *RebalanceTestHandler) Setup(session sarama.ConsumerGroupSession) error {
h.t.Logf("%s: Consumer group session setup", h.name)
h.readyOnce.Do(func() {
close(h.ready)
})
// Send partition assignment
partitions := make([]int32, 0)
for topic, partitionList := range session.Claims() {
h.t.Logf("%s: Assigned topic %s with partitions %v", h.name, topic, partitionList)
for _, partition := range partitionList {
partitions = append(partitions, partition)
}
}
// Always publish the latest assignment. If the channel is full, drain one
// stale value first so readers see the most recent rebalance outcome (not
// whatever arrived first while the group was still converging).
for {
select {
case h.assignments <- partitions:
return nil
default:
select {
case <-h.assignments:
default:
}
}
}
}
func (h *RebalanceTestHandler) Cleanup(sarama.ConsumerGroupSession) error {
h.t.Logf("%s: Consumer group session cleanup", h.name)
return nil
}
func (h *RebalanceTestHandler) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
for {
select {
case message := <-claim.Messages():
if message == nil {
return nil
}
h.t.Logf("%s: Received message from partition %d: %s", h.name, message.Partition, string(message.Value))
select {
case h.messages <- message:
default:
// Channel full, drop message for test
}
session.MarkMessage(message, "")
case <-session.Context().Done():
return nil
}
}
}

View File

@@ -0,0 +1,298 @@
package integration
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/linkedin/goavro/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/seaweedfs/seaweedfs/weed/mq/kafka/schema"
)
// TestSchemaEndToEnd_AvroRoundTrip tests the complete Avro schema round-trip workflow
func TestSchemaEndToEnd_AvroRoundTrip(t *testing.T) {
// Create mock schema registry
server := createMockSchemaRegistryForE2E(t)
defer server.Close()
// Create schema manager
config := schema.ManagerConfig{
RegistryURL: server.URL,
ValidationMode: schema.ValidationPermissive,
}
manager, err := schema.NewManager(config)
require.NoError(t, err)
// Test data
avroSchema := getUserAvroSchemaForE2E()
testData := map[string]interface{}{
"id": int32(12345),
"name": "Alice Johnson",
"email": map[string]interface{}{"string": "alice@example.com"}, // Avro union
"age": map[string]interface{}{"int": int32(28)}, // Avro union
"preferences": map[string]interface{}{
"Preferences": map[string]interface{}{ // Avro union with record type
"notifications": true,
"theme": "dark",
},
},
}
t.Run("SchemaManagerRoundTrip", func(t *testing.T) {
// Step 1: Create Confluent envelope (simulate producer)
codec, err := goavro.NewCodec(avroSchema)
require.NoError(t, err)
avroBinary, err := codec.BinaryFromNative(nil, testData)
require.NoError(t, err)
confluentMsg := schema.CreateConfluentEnvelope(schema.FormatAvro, 1, nil, avroBinary)
require.True(t, len(confluentMsg) > 0, "Confluent envelope should not be empty")
t.Logf("Created Confluent envelope: %d bytes", len(confluentMsg))
// Step 2: Decode message using schema manager
decodedMsg, err := manager.DecodeMessage(confluentMsg)
require.NoError(t, err)
require.NotNil(t, decodedMsg.RecordValue, "RecordValue should not be nil")
t.Logf("Decoded message with schema ID %d, format %v", decodedMsg.SchemaID, decodedMsg.SchemaFormat)
// Step 3: Re-encode message using schema manager
reconstructedMsg, err := manager.EncodeMessage(decodedMsg.RecordValue, 1, schema.FormatAvro)
require.NoError(t, err)
require.True(t, len(reconstructedMsg) > 0, "Reconstructed message should not be empty")
t.Logf("Re-encoded message: %d bytes", len(reconstructedMsg))
// Step 4: Verify the reconstructed message is a valid Confluent envelope
envelope, ok := schema.ParseConfluentEnvelope(reconstructedMsg)
require.True(t, ok, "Reconstructed message should be a valid Confluent envelope")
require.Equal(t, uint32(1), envelope.SchemaID, "Schema ID should match")
require.Equal(t, schema.FormatAvro, envelope.Format, "Schema format should be Avro")
// Step 5: Decode and verify the content
decodedNative, _, err := codec.NativeFromBinary(envelope.Payload)
require.NoError(t, err)
decodedMap, ok := decodedNative.(map[string]interface{})
require.True(t, ok, "Decoded data should be a map")
// Verify all fields
assert.Equal(t, int32(12345), decodedMap["id"])
assert.Equal(t, "Alice Johnson", decodedMap["name"])
// Verify union fields
emailUnion, ok := decodedMap["email"].(map[string]interface{})
require.True(t, ok, "Email should be a union")
assert.Equal(t, "alice@example.com", emailUnion["string"])
ageUnion, ok := decodedMap["age"].(map[string]interface{})
require.True(t, ok, "Age should be a union")
assert.Equal(t, int32(28), ageUnion["int"])
preferencesUnion, ok := decodedMap["preferences"].(map[string]interface{})
require.True(t, ok, "Preferences should be a union")
preferencesRecord, ok := preferencesUnion["Preferences"].(map[string]interface{})
require.True(t, ok, "Preferences should contain a record")
assert.Equal(t, true, preferencesRecord["notifications"])
assert.Equal(t, "dark", preferencesRecord["theme"])
t.Log("Successfully completed Avro schema round-trip test")
})
}
// TestSchemaEndToEnd_ProtobufRoundTrip tests the complete Protobuf schema round-trip workflow
func TestSchemaEndToEnd_ProtobufRoundTrip(t *testing.T) {
t.Run("ProtobufEnvelopeCreation", func(t *testing.T) {
// Create a simple Protobuf message (simulated)
// In a real scenario, this would be generated from a .proto file
protobufData := []byte{0x08, 0x96, 0x01, 0x12, 0x04, 0x74, 0x65, 0x73, 0x74} // id=150, name="test"
// Create Confluent envelope with Protobuf format
confluentMsg := schema.CreateConfluentEnvelope(schema.FormatProtobuf, 2, []int{0}, protobufData)
require.True(t, len(confluentMsg) > 0, "Confluent envelope should not be empty")
t.Logf("Created Protobuf Confluent envelope: %d bytes", len(confluentMsg))
// Verify Confluent envelope
envelope, ok := schema.ParseConfluentEnvelope(confluentMsg)
require.True(t, ok, "Message should be a valid Confluent envelope")
require.Equal(t, uint32(2), envelope.SchemaID, "Schema ID should match")
// Note: ParseConfluentEnvelope defaults to FormatAvro; format detection requires schema registry
require.Equal(t, schema.FormatAvro, envelope.Format, "Format defaults to Avro without schema registry lookup")
// For Protobuf with indexes, we need to use the specialized parser
protobufEnvelope, ok := schema.ParseConfluentProtobufEnvelopeWithIndexCount(confluentMsg, 1)
require.True(t, ok, "Message should be a valid Protobuf envelope")
require.Equal(t, uint32(2), protobufEnvelope.SchemaID, "Schema ID should match")
require.Equal(t, schema.FormatProtobuf, protobufEnvelope.Format, "Schema format should be Protobuf")
require.Equal(t, []int{0}, protobufEnvelope.Indexes, "Indexes should match")
require.Equal(t, protobufData, protobufEnvelope.Payload, "Payload should match")
t.Log("Successfully completed Protobuf envelope test")
})
}
// TestSchemaEndToEnd_JSONSchemaRoundTrip tests the complete JSON Schema round-trip workflow
func TestSchemaEndToEnd_JSONSchemaRoundTrip(t *testing.T) {
t.Run("JSONSchemaEnvelopeCreation", func(t *testing.T) {
// Create JSON data
jsonData := []byte(`{"id": 123, "name": "Bob Smith", "active": true}`)
// Create Confluent envelope with JSON Schema format
confluentMsg := schema.CreateConfluentEnvelope(schema.FormatJSONSchema, 3, nil, jsonData)
require.True(t, len(confluentMsg) > 0, "Confluent envelope should not be empty")
t.Logf("Created JSON Schema Confluent envelope: %d bytes", len(confluentMsg))
// Verify Confluent envelope
envelope, ok := schema.ParseConfluentEnvelope(confluentMsg)
require.True(t, ok, "Message should be a valid Confluent envelope")
require.Equal(t, uint32(3), envelope.SchemaID, "Schema ID should match")
// Note: ParseConfluentEnvelope defaults to FormatAvro; format detection requires schema registry
require.Equal(t, schema.FormatAvro, envelope.Format, "Format defaults to Avro without schema registry lookup")
// Verify JSON content
assert.JSONEq(t, string(jsonData), string(envelope.Payload), "JSON payload should match")
t.Log("Successfully completed JSON Schema envelope test")
})
}
// TestSchemaEndToEnd_CompressionAndBatching tests schema handling with compression and batching
func TestSchemaEndToEnd_CompressionAndBatching(t *testing.T) {
// Create mock schema registry
server := createMockSchemaRegistryForE2E(t)
defer server.Close()
// Create schema manager
config := schema.ManagerConfig{
RegistryURL: server.URL,
ValidationMode: schema.ValidationPermissive,
}
manager, err := schema.NewManager(config)
require.NoError(t, err)
t.Run("BatchedSchematizedMessages", func(t *testing.T) {
// Create multiple messages
avroSchema := getUserAvroSchemaForE2E()
codec, err := goavro.NewCodec(avroSchema)
require.NoError(t, err)
messageCount := 5
var confluentMessages [][]byte
// Create multiple Confluent envelopes
for i := 0; i < messageCount; i++ {
testData := map[string]interface{}{
"id": int32(1000 + i),
"name": fmt.Sprintf("User %d", i),
"email": map[string]interface{}{"string": fmt.Sprintf("user%d@example.com", i)},
"age": map[string]interface{}{"int": int32(20 + i)},
"preferences": map[string]interface{}{
"Preferences": map[string]interface{}{
"notifications": i%2 == 0, // Alternate true/false
"theme": "light",
},
},
}
avroBinary, err := codec.BinaryFromNative(nil, testData)
require.NoError(t, err)
confluentMsg := schema.CreateConfluentEnvelope(schema.FormatAvro, 1, nil, avroBinary)
confluentMessages = append(confluentMessages, confluentMsg)
}
t.Logf("Created %d schematized messages", messageCount)
// Test round-trip for each message
for i, confluentMsg := range confluentMessages {
// Decode message
decodedMsg, err := manager.DecodeMessage(confluentMsg)
require.NoError(t, err, "Message %d should decode", i)
// Re-encode message
reconstructedMsg, err := manager.EncodeMessage(decodedMsg.RecordValue, 1, schema.FormatAvro)
require.NoError(t, err, "Message %d should re-encode", i)
// Verify envelope
envelope, ok := schema.ParseConfluentEnvelope(reconstructedMsg)
require.True(t, ok, "Message %d should be a valid Confluent envelope", i)
require.Equal(t, uint32(1), envelope.SchemaID, "Message %d schema ID should match", i)
// Decode and verify content
decodedNative, _, err := codec.NativeFromBinary(envelope.Payload)
require.NoError(t, err, "Message %d should decode successfully", i)
decodedMap, ok := decodedNative.(map[string]interface{})
require.True(t, ok, "Message %d should be a map", i)
expectedID := int32(1000 + i)
assert.Equal(t, expectedID, decodedMap["id"], "Message %d ID should match", i)
assert.Equal(t, fmt.Sprintf("User %d", i), decodedMap["name"], "Message %d name should match", i)
}
t.Log("Successfully verified batched schematized messages")
})
}
// Helper functions for creating mock schema registries
func createMockSchemaRegistryForE2E(t *testing.T) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/schemas/ids/1":
response := map[string]interface{}{
"schema": getUserAvroSchemaForE2E(),
"subject": "user-events-e2e-value",
"version": 1,
}
writeJSONResponse(w, response)
case "/subjects/user-events-e2e-value/versions/latest":
response := map[string]interface{}{
"id": 1,
"schema": getUserAvroSchemaForE2E(),
"subject": "user-events-e2e-value",
"version": 1,
}
writeJSONResponse(w, response)
default:
w.WriteHeader(http.StatusNotFound)
}
}))
}
func getUserAvroSchemaForE2E() string {
return `{
"type": "record",
"name": "User",
"fields": [
{"name": "id", "type": "int"},
{"name": "name", "type": "string"},
{"name": "email", "type": ["null", "string"], "default": null},
{"name": "age", "type": ["null", "int"], "default": null},
{"name": "preferences", "type": ["null", {
"type": "record",
"name": "Preferences",
"fields": [
{"name": "notifications", "type": "boolean", "default": true},
{"name": "theme", "type": "string", "default": "light"}
]
}], "default": null}
]
}`
}
func writeJSONResponse(w http.ResponseWriter, data interface{}) {
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}

View File

@@ -0,0 +1,210 @@
package integration
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"testing"
"time"
"github.com/seaweedfs/seaweedfs/test/kafka/internal/testutil"
)
// TestSchemaRegistryEventualConsistency reproduces the issue where schemas
// are registered successfully but are not immediately queryable due to
// Schema Registry's consumer lag
func TestSchemaRegistryEventualConsistency(t *testing.T) {
// This test requires real SMQ backend
gateway := testutil.NewGatewayTestServerWithSMQ(t, testutil.SMQRequired)
defer gateway.CleanupAndClose()
addr := gateway.StartAndWait()
t.Logf("Gateway running on %s", addr)
// Schema Registry URL from environment or default
schemaRegistryURL := "http://localhost:8081"
// Wait for Schema Registry to be ready
if !waitForSchemaRegistry(t, schemaRegistryURL, 30*time.Second) {
t.Fatal("Schema Registry not ready")
}
// Define test schemas
valueSchema := `{"type":"record","name":"TestMessage","fields":[{"name":"id","type":"string"}]}`
keySchema := `{"type":"string"}`
// Register multiple schemas rapidly (simulates the load test scenario)
subjects := []string{
"test-topic-0-value",
"test-topic-0-key",
"test-topic-1-value",
"test-topic-1-key",
"test-topic-2-value",
"test-topic-2-key",
"test-topic-3-value",
"test-topic-3-key",
}
t.Log("Registering schemas rapidly...")
registeredIDs := make(map[string]int)
for _, subject := range subjects {
schema := valueSchema
if strings.HasSuffix(subject, "-key") {
schema = keySchema
}
id, err := registerSchema(schemaRegistryURL, subject, schema)
if err != nil {
t.Fatalf("Failed to register schema for %s: %v", subject, err)
}
registeredIDs[subject] = id
t.Logf("Registered %s with ID %d", subject, id)
}
t.Log("All schemas registered successfully!")
// Now immediately try to verify them (this reproduces the bug)
t.Log("Immediately verifying schemas (without delay)...")
immediateFailures := 0
for _, subject := range subjects {
exists, id, version, err := verifySchema(schemaRegistryURL, subject)
if err != nil || !exists {
immediateFailures++
t.Logf("Immediate verification failed for %s: exists=%v id=%d err=%v", subject, exists, id, err)
} else {
t.Logf("Immediate verification passed for %s: ID=%d Version=%d", subject, id, version)
}
}
if immediateFailures > 0 {
t.Logf("BUG REPRODUCED: %d/%d schemas not immediately queryable after registration",
immediateFailures, len(subjects))
t.Logf(" This is due to Schema Registry's KafkaStoreReaderThread lag")
}
// Now verify with retry logic (this should succeed)
t.Log("Verifying schemas with retry logic...")
for _, subject := range subjects {
expectedID := registeredIDs[subject]
if !verifySchemaWithRetry(t, schemaRegistryURL, subject, expectedID, 5*time.Second) {
t.Errorf("Failed to verify %s even with retry", subject)
}
}
t.Log("✓ All schemas verified successfully with retry logic!")
}
// registerSchema registers a schema and returns its ID
func registerSchema(registryURL, subject, schema string) (int, error) {
// Escape the schema JSON
escapedSchema, err := json.Marshal(schema)
if err != nil {
return 0, err
}
payload := fmt.Sprintf(`{"schema":%s,"schemaType":"AVRO"}`, escapedSchema)
resp, err := http.Post(
fmt.Sprintf("%s/subjects/%s/versions", registryURL, subject),
"application/vnd.schemaregistry.v1+json",
strings.NewReader(payload),
)
if err != nil {
return 0, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return 0, fmt.Errorf("registration failed: %s - %s", resp.Status, string(body))
}
var result struct {
ID int `json:"id"`
}
if err := json.Unmarshal(body, &result); err != nil {
return 0, err
}
return result.ID, nil
}
// verifySchema checks if a schema exists
func verifySchema(registryURL, subject string) (exists bool, id int, version int, err error) {
resp, err := http.Get(fmt.Sprintf("%s/subjects/%s/versions/latest", registryURL, subject))
if err != nil {
return false, 0, 0, err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return false, 0, 0, nil
}
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return false, 0, 0, fmt.Errorf("verification failed: %s - %s", resp.Status, string(body))
}
var result struct {
ID int `json:"id"`
Version int `json:"version"`
Schema string `json:"schema"`
}
body, _ := io.ReadAll(resp.Body)
if err := json.Unmarshal(body, &result); err != nil {
return false, 0, 0, err
}
return true, result.ID, result.Version, nil
}
// verifySchemaWithRetry verifies a schema with retry logic
func verifySchemaWithRetry(t *testing.T, registryURL, subject string, expectedID int, timeout time.Duration) bool {
deadline := time.Now().Add(timeout)
attempt := 0
for time.Now().Before(deadline) {
attempt++
exists, id, version, err := verifySchema(registryURL, subject)
if err == nil && exists && id == expectedID {
if attempt > 1 {
t.Logf("✓ %s verified after %d attempts (ID=%d, Version=%d)", subject, attempt, id, version)
}
return true
}
// Wait before retry (exponential backoff)
waitTime := time.Duration(attempt*100) * time.Millisecond
if waitTime > 1*time.Second {
waitTime = 1 * time.Second
}
time.Sleep(waitTime)
}
t.Logf("%s verification timed out after %d attempts", subject, attempt)
return false
}
// waitForSchemaRegistry waits for Schema Registry to be ready
func waitForSchemaRegistry(t *testing.T, url string, timeout time.Duration) bool {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
resp, err := http.Get(url + "/subjects")
if err == nil && resp.StatusCode == http.StatusOK {
resp.Body.Close()
return true
}
if resp != nil {
resp.Body.Close()
}
time.Sleep(500 * time.Millisecond)
}
return false
}

View File

@@ -0,0 +1,305 @@
package integration
import (
"context"
"testing"
"time"
"github.com/IBM/sarama"
"github.com/seaweedfs/seaweedfs/test/kafka/internal/testutil"
)
// TestSMQIntegration tests that the Kafka gateway properly integrates with SeaweedMQ
// This test REQUIRES SeaweedFS masters to be running and will skip if not available
func TestSMQIntegration(t *testing.T) {
// This test requires SMQ to be available
gateway := testutil.NewGatewayTestServerWithSMQ(t, testutil.SMQRequired)
defer gateway.CleanupAndClose()
addr := gateway.StartAndWait()
t.Logf("Running SMQ integration test with SeaweedFS backend")
t.Run("ProduceConsumeWithPersistence", func(t *testing.T) {
testProduceConsumeWithPersistence(t, addr)
})
t.Run("ConsumerGroupOffsetPersistence", func(t *testing.T) {
testConsumerGroupOffsetPersistence(t, addr)
})
t.Run("TopicPersistence", func(t *testing.T) {
testTopicPersistence(t, addr)
})
}
func testProduceConsumeWithPersistence(t *testing.T, addr string) {
topicName := testutil.GenerateUniqueTopicName("smq-integration-produce-consume")
client := testutil.NewSaramaClient(t, addr)
msgGen := testutil.NewMessageGenerator()
// Create topic
err := client.CreateTopic(topicName, 1, 1)
testutil.AssertNoError(t, err, "Failed to create topic")
// Allow time for topic to propagate in SMQ backend
time.Sleep(500 * time.Millisecond)
// Produce messages
messages := msgGen.GenerateStringMessages(5)
err = client.ProduceMessages(topicName, messages)
testutil.AssertNoError(t, err, "Failed to produce messages")
// Allow time for messages to be fully persisted in SMQ backend
time.Sleep(200 * time.Millisecond)
t.Logf("Produced %d messages to topic %s", len(messages), topicName)
// Consume messages
consumed, err := client.ConsumeMessages(topicName, 0, len(messages))
testutil.AssertNoError(t, err, "Failed to consume messages")
// Verify all messages were consumed
testutil.AssertEqual(t, len(messages), len(consumed), "Message count mismatch")
t.Logf("Successfully consumed %d messages from SMQ backend", len(consumed))
}
func testConsumerGroupOffsetPersistence(t *testing.T, addr string) {
topicName := testutil.GenerateUniqueTopicName("smq-integration-offset-persistence")
groupID := testutil.GenerateUniqueGroupID("smq-offset-group")
client := testutil.NewSaramaClient(t, addr)
msgGen := testutil.NewMessageGenerator()
// Create topic and produce messages
err := client.CreateTopic(topicName, 1, 1)
testutil.AssertNoError(t, err, "Failed to create topic")
// Allow time for topic to propagate in SMQ backend
time.Sleep(500 * time.Millisecond)
messages := msgGen.GenerateStringMessages(10)
err = client.ProduceMessages(topicName, messages)
testutil.AssertNoError(t, err, "Failed to produce messages")
// Allow time for messages to be fully persisted in SMQ backend
time.Sleep(200 * time.Millisecond)
// Phase 1: Consume first 5 messages with consumer group and commit offsets
t.Logf("Phase 1: Consuming first 5 messages and committing offsets")
config := client.GetConfig()
config.Consumer.Offsets.Initial = sarama.OffsetOldest
// Enable auto-commit for more reliable offset handling
config.Consumer.Offsets.AutoCommit.Enable = true
config.Consumer.Offsets.AutoCommit.Interval = 1 * time.Second
consumerGroup1, err := sarama.NewConsumerGroup([]string{addr}, groupID, config)
testutil.AssertNoError(t, err, "Failed to create first consumer group")
handler := &SMQOffsetTestHandler{
messages: make(chan *sarama.ConsumerMessage, len(messages)),
ready: make(chan bool),
stopAfter: 5,
t: t,
}
ctx1, cancel1 := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel1()
consumeErrChan1 := make(chan error, 1)
go func() {
err := consumerGroup1.Consume(ctx1, []string{topicName}, handler)
if err != nil && err != context.DeadlineExceeded && err != context.Canceled {
t.Logf("First consumer error: %v", err)
consumeErrChan1 <- err
}
}()
// Wait for consumer to be ready with timeout
select {
case <-handler.ready:
// Consumer is ready, continue
case err := <-consumeErrChan1:
t.Fatalf("First consumer failed to start: %v", err)
case <-time.After(10 * time.Second):
t.Fatalf("Timeout waiting for first consumer to be ready")
}
consumedCount := 0
for consumedCount < 5 {
select {
case <-handler.messages:
consumedCount++
case <-time.After(20 * time.Second):
t.Fatalf("Timeout waiting for first batch of messages. Got %d/5", consumedCount)
}
}
consumerGroup1.Close()
cancel1()
time.Sleep(7 * time.Second) // Allow auto-commit to complete and offset commits to be processed in SMQ
t.Logf("Consumed %d messages in first phase", consumedCount)
// Phase 2: Start new consumer group with same ID - should resume from committed offset
t.Logf("Phase 2: Starting new consumer group to test offset persistence")
// Create a fresh config for the second consumer group to avoid any state issues
config2 := client.GetConfig()
config2.Consumer.Offsets.Initial = sarama.OffsetOldest
config2.Consumer.Offsets.AutoCommit.Enable = true
config2.Consumer.Offsets.AutoCommit.Interval = 1 * time.Second
consumerGroup2, err := sarama.NewConsumerGroup([]string{addr}, groupID, config2)
testutil.AssertNoError(t, err, "Failed to create second consumer group")
defer consumerGroup2.Close()
handler2 := &SMQOffsetTestHandler{
messages: make(chan *sarama.ConsumerMessage, len(messages)),
ready: make(chan bool),
stopAfter: 5, // Should consume remaining 5 messages
t: t,
}
ctx2, cancel2 := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel2()
consumeErrChan := make(chan error, 1)
go func() {
err := consumerGroup2.Consume(ctx2, []string{topicName}, handler2)
if err != nil && err != context.DeadlineExceeded && err != context.Canceled {
t.Logf("Second consumer error: %v", err)
consumeErrChan <- err
}
}()
// Wait for second consumer to be ready with timeout
select {
case <-handler2.ready:
// Consumer is ready, continue
case err := <-consumeErrChan:
t.Fatalf("Second consumer failed to start: %v", err)
case <-time.After(10 * time.Second):
t.Fatalf("Timeout waiting for second consumer to be ready")
}
secondConsumerMessages := make([]*sarama.ConsumerMessage, 0)
consumedCount = 0
for consumedCount < 5 {
select {
case msg := <-handler2.messages:
consumedCount++
secondConsumerMessages = append(secondConsumerMessages, msg)
case <-time.After(20 * time.Second):
t.Fatalf("Timeout waiting for second batch of messages. Got %d/5", consumedCount)
}
}
// Verify second consumer started from correct offset (should be >= 5)
if len(secondConsumerMessages) > 0 {
firstMessageOffset := secondConsumerMessages[0].Offset
if firstMessageOffset < 5 {
t.Fatalf("Second consumer should start from offset >= 5: got %d", firstMessageOffset)
}
t.Logf("Second consumer correctly resumed from offset %d", firstMessageOffset)
}
t.Logf("Successfully verified SMQ offset persistence")
}
func testTopicPersistence(t *testing.T, addr string) {
topicName := testutil.GenerateUniqueTopicName("smq-integration-topic-persistence")
client := testutil.NewSaramaClient(t, addr)
// Create topic
err := client.CreateTopic(topicName, 2, 1) // 2 partitions
testutil.AssertNoError(t, err, "Failed to create topic")
// Allow time for topic to propagate and persist in SMQ backend
time.Sleep(1 * time.Second)
// Verify topic exists by listing topics using admin client
config := client.GetConfig()
config.Admin.Timeout = 30 * time.Second
admin, err := sarama.NewClusterAdmin([]string{addr}, config)
testutil.AssertNoError(t, err, "Failed to create admin client")
defer admin.Close()
// Retry topic listing to handle potential delays in topic propagation
var topics map[string]sarama.TopicDetail
var listErr error
for attempt := 0; attempt < 3; attempt++ {
if attempt > 0 {
sleepDuration := time.Duration(500*(1<<(attempt-1))) * time.Millisecond
t.Logf("Retrying ListTopics after %v (attempt %d/3)", sleepDuration, attempt+1)
time.Sleep(sleepDuration)
}
topics, listErr = admin.ListTopics()
if listErr == nil {
break
}
}
testutil.AssertNoError(t, listErr, "Failed to list topics")
topicDetails, exists := topics[topicName]
if !exists {
t.Fatalf("Topic %s not found in topic list", topicName)
}
if topicDetails.NumPartitions != 2 {
t.Errorf("Expected 2 partitions, got %d", topicDetails.NumPartitions)
}
t.Logf("Successfully verified topic persistence with %d partitions", topicDetails.NumPartitions)
}
// SMQOffsetTestHandler implements sarama.ConsumerGroupHandler for SMQ offset testing
type SMQOffsetTestHandler struct {
messages chan *sarama.ConsumerMessage
ready chan bool
readyOnce bool
stopAfter int
consumed int
t *testing.T
}
func (h *SMQOffsetTestHandler) Setup(sarama.ConsumerGroupSession) error {
h.t.Logf("SMQ offset test consumer setup")
if !h.readyOnce {
close(h.ready)
h.readyOnce = true
}
return nil
}
func (h *SMQOffsetTestHandler) Cleanup(sarama.ConsumerGroupSession) error {
h.t.Logf("SMQ offset test consumer cleanup")
return nil
}
func (h *SMQOffsetTestHandler) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
for {
select {
case message := <-claim.Messages():
if message == nil {
return nil
}
h.consumed++
h.messages <- message
session.MarkMessage(message, "")
// Stop after consuming the specified number of messages
if h.consumed >= h.stopAfter {
h.t.Logf("Stopping SMQ consumer after %d messages", h.consumed)
// Auto-commit will handle offset commits automatically
return nil
}
case <-session.Context().Done():
return nil
}
}
}

View File

@@ -0,0 +1,150 @@
package testutil
import (
"fmt"
"testing"
"time"
)
// AssertEventually retries an assertion until it passes or times out
func AssertEventually(t *testing.T, assertion func() error, timeout time.Duration, interval time.Duration, msgAndArgs ...interface{}) {
t.Helper()
deadline := time.Now().Add(timeout)
var lastErr error
for time.Now().Before(deadline) {
if err := assertion(); err == nil {
return // Success
} else {
lastErr = err
}
time.Sleep(interval)
}
// Format the failure message
var msg string
if len(msgAndArgs) > 0 {
if format, ok := msgAndArgs[0].(string); ok {
msg = fmt.Sprintf(format, msgAndArgs[1:]...)
} else {
msg = fmt.Sprint(msgAndArgs...)
}
} else {
msg = "assertion failed"
}
t.Fatalf("%s after %v: %v", msg, timeout, lastErr)
}
// AssertNoError fails the test if err is not nil
func AssertNoError(t *testing.T, err error, msgAndArgs ...interface{}) {
t.Helper()
if err != nil {
var msg string
if len(msgAndArgs) > 0 {
if format, ok := msgAndArgs[0].(string); ok {
msg = fmt.Sprintf(format, msgAndArgs[1:]...)
} else {
msg = fmt.Sprint(msgAndArgs...)
}
} else {
msg = "unexpected error"
}
t.Fatalf("%s: %v", msg, err)
}
}
// AssertError fails the test if err is nil
func AssertError(t *testing.T, err error, msgAndArgs ...interface{}) {
t.Helper()
if err == nil {
var msg string
if len(msgAndArgs) > 0 {
if format, ok := msgAndArgs[0].(string); ok {
msg = fmt.Sprintf(format, msgAndArgs[1:]...)
} else {
msg = fmt.Sprint(msgAndArgs...)
}
} else {
msg = "expected error but got nil"
}
t.Fatal(msg)
}
}
// AssertEqual fails the test if expected != actual
func AssertEqual(t *testing.T, expected, actual interface{}, msgAndArgs ...interface{}) {
t.Helper()
if expected != actual {
var msg string
if len(msgAndArgs) > 0 {
if format, ok := msgAndArgs[0].(string); ok {
msg = fmt.Sprintf(format, msgAndArgs[1:]...)
} else {
msg = fmt.Sprint(msgAndArgs...)
}
} else {
msg = "values not equal"
}
t.Fatalf("%s: expected %v, got %v", msg, expected, actual)
}
}
// AssertNotEqual fails the test if expected == actual
func AssertNotEqual(t *testing.T, expected, actual interface{}, msgAndArgs ...interface{}) {
t.Helper()
if expected == actual {
var msg string
if len(msgAndArgs) > 0 {
if format, ok := msgAndArgs[0].(string); ok {
msg = fmt.Sprintf(format, msgAndArgs[1:]...)
} else {
msg = fmt.Sprint(msgAndArgs...)
}
} else {
msg = "values should not be equal"
}
t.Fatalf("%s: both values are %v", msg, expected)
}
}
// AssertGreaterThan fails the test if actual <= expected
func AssertGreaterThan(t *testing.T, expected, actual int, msgAndArgs ...interface{}) {
t.Helper()
if actual <= expected {
var msg string
if len(msgAndArgs) > 0 {
if format, ok := msgAndArgs[0].(string); ok {
msg = fmt.Sprintf(format, msgAndArgs[1:]...)
} else {
msg = fmt.Sprint(msgAndArgs...)
}
} else {
msg = "value not greater than expected"
}
t.Fatalf("%s: expected > %d, got %d", msg, expected, actual)
}
}
// AssertContains fails the test if slice doesn't contain item
func AssertContains(t *testing.T, slice []string, item string, msgAndArgs ...interface{}) {
t.Helper()
for _, s := range slice {
if s == item {
return // Found it
}
}
var msg string
if len(msgAndArgs) > 0 {
if format, ok := msgAndArgs[0].(string); ok {
msg = fmt.Sprintf(format, msgAndArgs[1:]...)
} else {
msg = fmt.Sprint(msgAndArgs...)
}
} else {
msg = "item not found in slice"
}
t.Fatalf("%s: %q not found in %v", msg, item, slice)
}

View File

@@ -0,0 +1,412 @@
package testutil
import (
"context"
"fmt"
"testing"
"time"
"github.com/IBM/sarama"
"github.com/segmentio/kafka-go"
)
const (
consumerGroupHeartbeatInterval = 2 * time.Second
consumerGroupSessionTimeout = 6 * time.Second
consumerGroupRebalanceTimeout = 6 * time.Second
consumerGroupJoinBackoff = 250 * time.Millisecond
consumerGroupAttemptTimeout = 15 * time.Second
)
// KafkaGoClient wraps kafka-go client with test utilities
type KafkaGoClient struct {
brokerAddr string
t *testing.T
}
// SaramaClient wraps Sarama client with test utilities
type SaramaClient struct {
brokerAddr string
config *sarama.Config
t *testing.T
}
type ConsumeGroupRetryDebug struct {
Attempt int
MaxAttempts int
Topic string
GroupID string
ExpectedCount int
Err error
}
// NewKafkaGoClient creates a new kafka-go test client
func NewKafkaGoClient(t *testing.T, brokerAddr string) *KafkaGoClient {
return &KafkaGoClient{
brokerAddr: brokerAddr,
t: t,
}
}
// NewSaramaClient creates a new Sarama test client with default config
func NewSaramaClient(t *testing.T, brokerAddr string) *SaramaClient {
config := sarama.NewConfig()
config.Version = sarama.V2_8_0_0
config.Producer.Return.Successes = true
config.Consumer.Return.Errors = true
config.Consumer.Offsets.Initial = sarama.OffsetOldest // Start from earliest when no committed offset
return &SaramaClient{
brokerAddr: brokerAddr,
config: config,
t: t,
}
}
// CreateTopic creates a topic using kafka-go
func (k *KafkaGoClient) CreateTopic(topicName string, partitions int, replicationFactor int) error {
k.t.Helper()
conn, err := kafka.Dial("tcp", k.brokerAddr)
if err != nil {
return fmt.Errorf("dial broker: %w", err)
}
defer conn.Close()
topicConfig := kafka.TopicConfig{
Topic: topicName,
NumPartitions: partitions,
ReplicationFactor: replicationFactor,
}
err = conn.CreateTopics(topicConfig)
if err != nil {
return fmt.Errorf("create topic: %w", err)
}
k.t.Logf("Created topic %s with %d partitions", topicName, partitions)
return nil
}
// ProduceMessages produces messages using kafka-go
func (k *KafkaGoClient) ProduceMessages(topicName string, messages []kafka.Message) error {
k.t.Helper()
writer := &kafka.Writer{
Addr: kafka.TCP(k.brokerAddr),
Topic: topicName,
Balancer: &kafka.LeastBytes{},
BatchTimeout: 50 * time.Millisecond,
RequiredAcks: kafka.RequireOne,
}
defer writer.Close()
// Increased timeout to handle slow CI environments, especially when consumer groups
// are active and holding locks or requiring offset commits
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
err := writer.WriteMessages(ctx, messages...)
if err != nil {
return fmt.Errorf("write messages: %w", err)
}
k.t.Logf("Produced %d messages to topic %s", len(messages), topicName)
return nil
}
// ConsumeMessages consumes messages using kafka-go
func (k *KafkaGoClient) ConsumeMessages(topicName string, expectedCount int) ([]kafka.Message, error) {
k.t.Helper()
reader := kafka.NewReader(kafka.ReaderConfig{
Brokers: []string{k.brokerAddr},
Topic: topicName,
Partition: 0, // Explicitly set partition 0 for simple consumption
StartOffset: kafka.FirstOffset,
MinBytes: 1,
MaxBytes: 10e6,
})
defer reader.Close()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
var messages []kafka.Message
for i := 0; i < expectedCount; i++ {
msg, err := reader.ReadMessage(ctx)
if err != nil {
return messages, fmt.Errorf("read message %d: %w", i, err)
}
messages = append(messages, msg)
}
k.t.Logf("Consumed %d messages from topic %s", len(messages), topicName)
return messages, nil
}
// ConsumeWithGroup consumes messages using consumer group.
// Retries the initial join+fetch with a fresh reader if it fails before any
// message is received — re-joining an existing group races with the previous
// member's LeaveGroup / session cleanup and can surface as an i/o timeout on
// the first FetchMessage.
func (k *KafkaGoClient) ConsumeWithGroup(topicName, groupID string, expectedCount int) ([]kafka.Message, error) {
return k.consumeWithGroup(topicName, groupID, expectedCount, nil, nil)
}
// ConsumeWithGroupDebug runs ConsumeWithGroup with diagnostic hooks.
// - onRetry fires after each failed attempt; the snapshot is post-LeaveGroup
// because reader.Close() has already run by then (group will look Empty).
// - onTick fires every ~1.5s while an attempt is in flight, so the caller
// can observe live group state during the join/sync/fetch cycle — the
// only place we get to see PreparingRebalance / CompletingRebalance churn.
func (k *KafkaGoClient) ConsumeWithGroupDebug(topicName, groupID string, expectedCount int, onRetry func(ConsumeGroupRetryDebug), onTick func()) ([]kafka.Message, error) {
return k.consumeWithGroup(topicName, groupID, expectedCount, onRetry, onTick)
}
func (k *KafkaGoClient) consumeWithGroup(topicName, groupID string, expectedCount int, onRetry func(ConsumeGroupRetryDebug), onTick func()) ([]kafka.Message, error) {
k.t.Helper()
const maxJoinAttempts = 5
var lastErr error
for attempt := 1; attempt <= maxJoinAttempts; attempt++ {
messages, err, progressed := k.consumeWithGroupOnce(topicName, groupID, expectedCount, onTick)
if err == nil {
return messages, nil
}
lastErr = err
// Only retry if we failed before any message was received. Once we've
// fetched at least one message, a partial result is more useful than a
// full retry (which would start over from the last committed offset).
if progressed {
return messages, err
}
if onRetry != nil {
onRetry(ConsumeGroupRetryDebug{
Attempt: attempt,
MaxAttempts: maxJoinAttempts,
Topic: topicName,
GroupID: groupID,
ExpectedCount: expectedCount,
Err: err,
})
}
if attempt == maxJoinAttempts {
break
}
backoff := time.Duration(500*(1<<(attempt-1))) * time.Millisecond
k.t.Logf("ConsumeWithGroup join attempt %d/%d failed (%v) — retrying after %v", attempt, maxJoinAttempts, err, backoff)
time.Sleep(backoff)
}
return nil, lastErr
}
// consumeWithGroupOnce runs a single consume attempt. Returns the messages
// fetched, any error, and whether any message was received (used to decide
// whether a retry is safe).
func (k *KafkaGoClient) consumeWithGroupOnce(topicName, groupID string, expectedCount int, onTick func()) ([]kafka.Message, error, bool) {
// Give each reader its own ClientID so restarts do not get mistaken for the
// still-shutting-down reader they are replacing.
dialer := &kafka.Dialer{
ClientID: fmt.Sprintf("seaweedfs-e2e-%s-%d", groupID, time.Now().UnixNano()),
Timeout: 10 * time.Second,
}
reader := kafka.NewReader(kafka.ReaderConfig{
Brokers: []string{k.brokerAddr},
Dialer: dialer,
Topic: topicName,
GroupID: groupID,
MinBytes: 1,
MaxBytes: 10e6,
CommitInterval: 500 * time.Millisecond,
HeartbeatInterval: consumerGroupHeartbeatInterval,
SessionTimeout: consumerGroupSessionTimeout,
RebalanceTimeout: consumerGroupRebalanceTimeout,
JoinGroupBackoff: consumerGroupJoinBackoff,
})
defer reader.Close()
offset := reader.Offset()
k.t.Logf("Consumer group reader created for group %s, initial offset: %d", groupID, offset)
// Keep each attempt short enough that a retry can outlive a stale group
// member instead of burning most of the overall test timeout on one try.
ctx, cancel := context.WithTimeout(context.Background(), consumerGroupAttemptTimeout)
defer cancel()
// While the reader is still alive (i.e. before defer reader.Close() runs),
// periodically invoke onTick so the caller can snapshot the gateway's view
// of the group during the join/sync/fetch cycle. Stop it before reader.Close
// fires — defers run LIFO, so this defer comes after the reader.Close defer.
if onTick != nil {
tickStop := make(chan struct{})
tickDone := make(chan struct{})
go func() {
defer close(tickDone)
ticker := time.NewTicker(1500 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-tickStop:
return
case <-ticker.C:
onTick()
}
}
}()
defer func() {
close(tickStop)
<-tickDone
}()
}
var messages []kafka.Message
for i := 0; i < expectedCount; i++ {
msg, err := reader.FetchMessage(ctx)
if err != nil {
return messages, fmt.Errorf("read message %d: %w", i, err), len(messages) > 0
}
messages = append(messages, msg)
k.t.Logf(" Fetched message %d: offset=%d, partition=%d", i, msg.Offset, msg.Partition)
var commitErr error
for attempt := 0; attempt < 3; attempt++ {
commitErr = reader.CommitMessages(ctx, msg)
if commitErr == nil {
k.t.Logf(" Committed offset %d (attempt %d)", msg.Offset, attempt+1)
break
}
k.t.Logf(" Commit attempt %d failed for offset %d: %v", attempt+1, msg.Offset, commitErr)
time.Sleep(time.Duration(50*(1<<attempt)) * time.Millisecond)
}
if commitErr != nil {
return messages, fmt.Errorf("committing message %d: %w", i, commitErr), true
}
}
k.t.Logf("Consumed %d messages from topic %s with group %s", len(messages), topicName, groupID)
return messages, nil, true
}
// CreateTopic creates a topic using Sarama
func (s *SaramaClient) CreateTopic(topicName string, partitions int32, replicationFactor int16) error {
s.t.Helper()
admin, err := sarama.NewClusterAdmin([]string{s.brokerAddr}, s.config)
if err != nil {
return fmt.Errorf("create admin client: %w", err)
}
defer admin.Close()
topicDetail := &sarama.TopicDetail{
NumPartitions: partitions,
ReplicationFactor: replicationFactor,
}
err = admin.CreateTopic(topicName, topicDetail, false)
if err != nil {
return fmt.Errorf("create topic: %w", err)
}
s.t.Logf("Created topic %s with %d partitions", topicName, partitions)
return nil
}
// ProduceMessages produces messages using Sarama
func (s *SaramaClient) ProduceMessages(topicName string, messages []string) error {
s.t.Helper()
producer, err := sarama.NewSyncProducer([]string{s.brokerAddr}, s.config)
if err != nil {
return fmt.Errorf("create producer: %w", err)
}
defer producer.Close()
for i, msgText := range messages {
msg := &sarama.ProducerMessage{
Topic: topicName,
Key: sarama.StringEncoder(fmt.Sprintf("Test message %d", i)),
Value: sarama.StringEncoder(msgText),
}
partition, offset, err := producer.SendMessage(msg)
if err != nil {
return fmt.Errorf("send message %d: %w", i, err)
}
s.t.Logf("Produced message %d: partition=%d, offset=%d", i, partition, offset)
}
return nil
}
// ProduceMessageToPartition produces a single message to a specific partition using Sarama
func (s *SaramaClient) ProduceMessageToPartition(topicName string, partition int32, message string) error {
s.t.Helper()
producer, err := sarama.NewSyncProducer([]string{s.brokerAddr}, s.config)
if err != nil {
return fmt.Errorf("create producer: %w", err)
}
defer producer.Close()
msg := &sarama.ProducerMessage{
Topic: topicName,
Partition: partition,
Key: sarama.StringEncoder(fmt.Sprintf("key-p%d", partition)),
Value: sarama.StringEncoder(message),
}
actualPartition, offset, err := producer.SendMessage(msg)
if err != nil {
return fmt.Errorf("send message to partition %d: %w", partition, err)
}
s.t.Logf("Produced message to partition %d: actualPartition=%d, offset=%d", partition, actualPartition, offset)
return nil
}
// ConsumeMessages consumes messages using Sarama
func (s *SaramaClient) ConsumeMessages(topicName string, partition int32, expectedCount int) ([]string, error) {
s.t.Helper()
consumer, err := sarama.NewConsumer([]string{s.brokerAddr}, s.config)
if err != nil {
return nil, fmt.Errorf("create consumer: %w", err)
}
defer consumer.Close()
partitionConsumer, err := consumer.ConsumePartition(topicName, partition, sarama.OffsetOldest)
if err != nil {
return nil, fmt.Errorf("create partition consumer: %w", err)
}
defer partitionConsumer.Close()
var messages []string
timeout := time.After(30 * time.Second)
for len(messages) < expectedCount {
select {
case msg := <-partitionConsumer.Messages():
messages = append(messages, string(msg.Value))
case err := <-partitionConsumer.Errors():
return messages, fmt.Errorf("consumer error: %w", err)
case <-timeout:
return messages, fmt.Errorf("timeout waiting for messages, got %d/%d", len(messages), expectedCount)
}
}
s.t.Logf("Consumed %d messages from topic %s", len(messages), topicName)
return messages, nil
}
// GetConfig returns the Sarama configuration
func (s *SaramaClient) GetConfig() *sarama.Config {
return s.config
}
// SetConfig sets a custom Sarama configuration
func (s *SaramaClient) SetConfig(config *sarama.Config) {
s.config = config
}

View File

@@ -0,0 +1,68 @@
package testutil
import (
"os"
"testing"
)
// DockerEnvironment provides utilities for Docker-based integration tests
type DockerEnvironment struct {
KafkaBootstrap string
KafkaGateway string
SchemaRegistry string
Available bool
}
// NewDockerEnvironment creates a new Docker environment helper
func NewDockerEnvironment(t *testing.T) *DockerEnvironment {
t.Helper()
env := &DockerEnvironment{
KafkaBootstrap: os.Getenv("KAFKA_BOOTSTRAP_SERVERS"),
KafkaGateway: os.Getenv("KAFKA_GATEWAY_URL"),
SchemaRegistry: os.Getenv("SCHEMA_REGISTRY_URL"),
}
env.Available = env.KafkaBootstrap != ""
if env.Available {
t.Logf("Docker environment detected:")
t.Logf(" Kafka Bootstrap: %s", env.KafkaBootstrap)
t.Logf(" Kafka Gateway: %s", env.KafkaGateway)
t.Logf(" Schema Registry: %s", env.SchemaRegistry)
}
return env
}
// SkipIfNotAvailable skips the test if Docker environment is not available
func (d *DockerEnvironment) SkipIfNotAvailable(t *testing.T) {
t.Helper()
if !d.Available {
t.Skip("Skipping Docker integration test - set KAFKA_BOOTSTRAP_SERVERS to run")
}
}
// RequireKafka ensures Kafka is available or skips the test
func (d *DockerEnvironment) RequireKafka(t *testing.T) {
t.Helper()
if d.KafkaBootstrap == "" {
t.Skip("Kafka bootstrap servers not available")
}
}
// RequireGateway ensures Kafka Gateway is available or skips the test
func (d *DockerEnvironment) RequireGateway(t *testing.T) {
t.Helper()
if d.KafkaGateway == "" {
t.Skip("Kafka Gateway not available")
}
}
// RequireSchemaRegistry ensures Schema Registry is available or skips the test
func (d *DockerEnvironment) RequireSchemaRegistry(t *testing.T) {
t.Helper()
if d.SchemaRegistry == "" {
t.Skip("Schema Registry not available")
}
}

View File

@@ -0,0 +1,246 @@
package testutil
import (
"context"
"encoding/json"
"fmt"
"net"
"os"
"testing"
"time"
"github.com/seaweedfs/seaweedfs/weed/mq/kafka/gateway"
"github.com/seaweedfs/seaweedfs/weed/mq/kafka/schema"
)
// GatewayTestServer wraps the gateway server with common test utilities
type GatewayTestServer struct {
*gateway.Server
t *testing.T
}
// GatewayOptions contains configuration for test gateway
type GatewayOptions struct {
Listen string
Masters string
UseProduction bool
// Add more options as needed
}
// NewGatewayTestServer creates a new test gateway server with common setup
func NewGatewayTestServer(t *testing.T, opts GatewayOptions) *GatewayTestServer {
if opts.Listen == "" {
opts.Listen = "127.0.0.1:0" // Use random port by default
}
// Allow switching to production gateway if requested (requires masters)
var srv *gateway.Server
if opts.UseProduction {
if opts.Masters == "" {
// Fallback to env variable for convenience in CI
if v := os.Getenv("SEAWEEDFS_MASTERS"); v != "" {
opts.Masters = v
} else {
opts.Masters = "localhost:9333"
}
}
srv = gateway.NewServer(gateway.Options{
Listen: opts.Listen,
Masters: opts.Masters,
})
} else {
// For unit testing without real SeaweedMQ masters
srv = gateway.NewTestServerForUnitTests(gateway.Options{
Listen: opts.Listen,
})
}
return &GatewayTestServer{
Server: srv,
t: t,
}
}
// StartAndWait starts the gateway and waits for it to be ready
func (g *GatewayTestServer) StartAndWait() string {
g.t.Helper()
// Start server in goroutine
go func() {
// Enable schema mode automatically when SCHEMA_REGISTRY_URL is set
if url := os.Getenv("SCHEMA_REGISTRY_URL"); url != "" {
h := g.GetHandler()
if h != nil {
_ = h.EnableSchemaManagement(schema.ManagerConfig{RegistryURL: url})
}
}
if err := g.Start(); err != nil {
g.t.Errorf("Failed to start gateway: %v", err)
}
}()
// Wait for server to be ready
time.Sleep(100 * time.Millisecond)
host, port := g.GetListenerAddr()
addr := fmt.Sprintf("%s:%d", host, port)
g.t.Logf("Gateway running on %s", addr)
return addr
}
// AddTestTopic adds a topic for testing with default configuration
func (g *GatewayTestServer) AddTestTopic(name string) {
g.t.Helper()
g.GetHandler().AddTopicForTesting(name, 1)
g.t.Logf("Added test topic: %s", name)
}
// AddTestTopics adds multiple topics for testing
func (g *GatewayTestServer) AddTestTopics(names ...string) {
g.t.Helper()
for _, name := range names {
g.AddTestTopic(name)
}
}
// CleanupAndClose properly closes the gateway server
func (g *GatewayTestServer) CleanupAndClose() {
g.t.Helper()
if err := g.Close(); err != nil {
g.t.Errorf("Failed to close gateway: %v", err)
}
}
// LogConsumerGroupSnapshot dumps the gateway's current view of a consumer group.
func (g *GatewayTestServer) LogConsumerGroupSnapshot(groupID string) {
g.t.Helper()
handler := g.GetHandler()
if handler == nil {
g.t.Logf("Consumer group snapshot for %s unavailable: handler is nil", groupID)
return
}
snapshot := handler.DebugGroupSnapshot(groupID)
if snapshot == nil {
g.t.Logf("Consumer group snapshot for %s unavailable", groupID)
return
}
payload, err := json.MarshalIndent(snapshot, "", " ")
if err != nil {
g.t.Logf("Consumer group snapshot for %s could not be marshaled: %v", groupID, err)
return
}
g.t.Logf("Consumer group snapshot for %s:\n%s", groupID, string(payload))
}
// SMQAvailabilityMode indicates whether SeaweedMQ is available for testing
type SMQAvailabilityMode int
const (
SMQUnavailable SMQAvailabilityMode = iota // Use mock handler only
SMQAvailable // SMQ is available, can use production mode
SMQRequired // SMQ is required, skip test if unavailable
)
// CheckSMQAvailability checks if SeaweedFS masters are available for testing
func CheckSMQAvailability() (bool, string) {
masters := os.Getenv("SEAWEEDFS_MASTERS")
if masters == "" {
return false, ""
}
// Test if at least one master is reachable
if masters != "" {
// Try to connect to the first master to verify availability
conn, err := net.DialTimeout("tcp", masters, 2*time.Second)
if err != nil {
return false, masters // Masters specified but unreachable
}
conn.Close()
return true, masters
}
return false, ""
}
// NewGatewayTestServerWithSMQ creates a gateway server that automatically uses SMQ if available
func NewGatewayTestServerWithSMQ(t *testing.T, mode SMQAvailabilityMode) *GatewayTestServer {
smqAvailable, masters := CheckSMQAvailability()
switch mode {
case SMQRequired:
if !smqAvailable {
if masters != "" {
t.Skipf("Skipping test: SEAWEEDFS_MASTERS=%s specified but unreachable", masters)
} else {
t.Skip("Skipping test: SEAWEEDFS_MASTERS required but not set")
}
}
t.Logf("Using SMQ-backed gateway with masters: %s", masters)
return newGatewayTestServerWithTimeout(t, GatewayOptions{
UseProduction: true,
Masters: masters,
}, 120*time.Second)
case SMQAvailable:
if smqAvailable {
t.Logf("SMQ available, using production gateway with masters: %s", masters)
return newGatewayTestServerWithTimeout(t, GatewayOptions{
UseProduction: true,
Masters: masters,
}, 120*time.Second)
} else {
t.Logf("SMQ not available, using mock gateway")
return NewGatewayTestServer(t, GatewayOptions{})
}
default: // SMQUnavailable
t.Logf("Using mock gateway (SMQ integration disabled)")
return NewGatewayTestServer(t, GatewayOptions{})
}
}
// newGatewayTestServerWithTimeout creates a gateway server with a timeout to prevent hanging
func newGatewayTestServerWithTimeout(t *testing.T, opts GatewayOptions, timeout time.Duration) *GatewayTestServer {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
done := make(chan *GatewayTestServer, 1)
errChan := make(chan error, 1)
go func() {
defer func() {
if r := recover(); r != nil {
errChan <- fmt.Errorf("panic creating gateway: %v", r)
}
}()
// Create the gateway in a goroutine so we can timeout if it hangs
t.Logf("Creating gateway with masters: %s (with %v timeout)", opts.Masters, timeout)
gateway := NewGatewayTestServer(t, opts)
t.Logf("Gateway created successfully")
done <- gateway
}()
select {
case gateway := <-done:
return gateway
case err := <-errChan:
t.Fatalf("Error creating gateway: %v", err)
case <-ctx.Done():
t.Fatalf("Timeout creating gateway after %v - likely SMQ broker discovery failed. Check if MQ brokers are running and accessible.", timeout)
}
return nil // This should never be reached
}
// IsSMQMode returns true if the gateway is using real SMQ backend
// This is determined by checking if we have the SEAWEEDFS_MASTERS environment variable
func (g *GatewayTestServer) IsSMQMode() bool {
available, _ := CheckSMQAvailability()
return available
}

View File

@@ -0,0 +1,135 @@
package testutil
import (
"fmt"
"os"
"time"
"github.com/seaweedfs/seaweedfs/weed/mq/kafka/schema"
"github.com/segmentio/kafka-go"
)
// MessageGenerator provides utilities for generating test messages
type MessageGenerator struct {
counter int
}
// NewMessageGenerator creates a new message generator
func NewMessageGenerator() *MessageGenerator {
return &MessageGenerator{counter: 0}
}
// GenerateKafkaGoMessages generates kafka-go messages for testing
func (m *MessageGenerator) GenerateKafkaGoMessages(count int) []kafka.Message {
messages := make([]kafka.Message, count)
for i := 0; i < count; i++ {
m.counter++
key := []byte(fmt.Sprintf("test-key-%d", m.counter))
val := []byte(fmt.Sprintf("{\"value\":\"test-message-%d-generated-at-%d\"}", m.counter, time.Now().Unix()))
// If schema mode is requested, ensure a test schema exists and wrap with Confluent envelope
if url := os.Getenv("SCHEMA_REGISTRY_URL"); url != "" {
subject := "offset-management-value"
schemaJSON := `{"type":"record","name":"TestRecord","fields":[{"name":"value","type":"string"}]}`
rc := schema.NewRegistryClient(schema.RegistryConfig{URL: url})
if _, err := rc.GetLatestSchema(subject); err != nil {
// Best-effort register schema
_, _ = rc.RegisterSchema(subject, schemaJSON)
}
if latest, err := rc.GetLatestSchema(subject); err == nil {
val = schema.CreateConfluentEnvelope(schema.FormatAvro, latest.LatestID, nil, val)
} else {
// fallback to schema id 1
val = schema.CreateConfluentEnvelope(schema.FormatAvro, 1, nil, val)
}
}
messages[i] = kafka.Message{Key: key, Value: val}
}
return messages
}
// GenerateStringMessages generates string messages for Sarama
func (m *MessageGenerator) GenerateStringMessages(count int) []string {
messages := make([]string, count)
for i := 0; i < count; i++ {
m.counter++
messages[i] = fmt.Sprintf("test-message-%d-generated-at-%d", m.counter, time.Now().Unix())
}
return messages
}
// GenerateKafkaGoMessage generates a single kafka-go message
func (m *MessageGenerator) GenerateKafkaGoMessage(key, value string) kafka.Message {
if key == "" {
m.counter++
key = fmt.Sprintf("test-key-%d", m.counter)
}
if value == "" {
value = fmt.Sprintf("test-message-%d-generated-at-%d", m.counter, time.Now().Unix())
}
return kafka.Message{
Key: []byte(key),
Value: []byte(value),
}
}
// GenerateUniqueTopicName generates a unique topic name for testing
func GenerateUniqueTopicName(prefix string) string {
if prefix == "" {
prefix = "test-topic"
}
return fmt.Sprintf("%s-%d", prefix, time.Now().UnixNano())
}
// GenerateUniqueGroupID generates a unique consumer group ID for testing
func GenerateUniqueGroupID(prefix string) string {
if prefix == "" {
prefix = "test-group"
}
return fmt.Sprintf("%s-%d", prefix, time.Now().UnixNano())
}
// ValidateMessageContent validates that consumed messages match expected content
func ValidateMessageContent(expected, actual []string) error {
if len(expected) != len(actual) {
return fmt.Errorf("message count mismatch: expected %d, got %d", len(expected), len(actual))
}
for i, expectedMsg := range expected {
if i >= len(actual) {
return fmt.Errorf("missing message at index %d", i)
}
if actual[i] != expectedMsg {
return fmt.Errorf("message mismatch at index %d: expected %q, got %q", i, expectedMsg, actual[i])
}
}
return nil
}
// ValidateKafkaGoMessageContent validates kafka-go messages
func ValidateKafkaGoMessageContent(expected, actual []kafka.Message) error {
if len(expected) != len(actual) {
return fmt.Errorf("message count mismatch: expected %d, got %d", len(expected), len(actual))
}
for i, expectedMsg := range expected {
if i >= len(actual) {
return fmt.Errorf("missing message at index %d", i)
}
if string(actual[i].Key) != string(expectedMsg.Key) {
return fmt.Errorf("key mismatch at index %d: expected %q, got %q", i, string(expectedMsg.Key), string(actual[i].Key))
}
if string(actual[i].Value) != string(expectedMsg.Value) {
return fmt.Errorf("value mismatch at index %d: expected %q, got %q", i, string(expectedMsg.Value), string(actual[i].Value))
}
}
return nil
}

View File

@@ -0,0 +1,33 @@
package testutil
import (
"testing"
kschema "github.com/seaweedfs/seaweedfs/weed/mq/kafka/schema"
)
// EnsureValueSchema registers a minimal Avro value schema for the given topic if not present.
// Returns the latest schema ID if successful.
func EnsureValueSchema(t *testing.T, registryURL, topic string) (uint32, error) {
t.Helper()
subject := topic + "-value"
rc := kschema.NewRegistryClient(kschema.RegistryConfig{URL: registryURL})
// Minimal Avro record schema with string field "value"
schemaJSON := `{"type":"record","name":"TestRecord","fields":[{"name":"value","type":"string"}]}`
// Try to get existing
if latest, err := rc.GetLatestSchema(subject); err == nil {
return latest.LatestID, nil
}
// Register and fetch latest
if _, err := rc.RegisterSchema(subject, schemaJSON); err != nil {
return 0, err
}
latest, err := rc.GetLatestSchema(subject)
if err != nil {
return 0, err
}
return latest.LatestID, nil
}

View File

@@ -0,0 +1,3 @@
# Keep only the Linux binaries
!weed-linux-amd64
!weed-linux-arm64

View File

@@ -0,0 +1,63 @@
# Binaries
kafka-loadtest
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool
*.out
# Go workspace file
go.work
# Test results and logs
test-results/
*.log
logs/
# Docker volumes and data
data/
volumes/
# Monitoring data
monitoring/prometheus/data/
monitoring/grafana/data/
# IDE files
.vscode/
.idea/
*.swp
*.swo
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Environment files
.env
.env.local
.env.*.local
# Temporary files
tmp/
temp/
*.tmp
# Coverage reports
coverage.html
coverage.out
# Build artifacts
bin/
build/
dist/

View File

@@ -0,0 +1,51 @@
# Kafka Client Load Test Runner Dockerfile
# Multi-stage build for cross-platform support
# Stage 1: Builder
FROM golang:1.26-alpine AS builder
WORKDIR /app
# Copy go module files
COPY test/kafka/kafka-client-loadtest/go.mod test/kafka/kafka-client-loadtest/go.sum ./
RUN go mod download
# Copy source code
COPY test/kafka/kafka-client-loadtest/ ./
# Build the loadtest binary
RUN CGO_ENABLED=0 GOOS=linux go build -o /kafka-loadtest ./cmd/loadtest
# Stage 2: Runtime
# Use alpine so we don't depend on Ubuntu apt mirrors, which intermittently
# refuse connections from GitHub Actions runners and fail the CI build.
# All runtime dependencies we need (ca-certificates, curl, jq, bash, nc)
# are in the Alpine main repo.
FROM alpine:3.20
RUN apk add --no-cache \
ca-certificates \
curl \
jq \
bash \
netcat-openbsd
# Copy built binary from builder stage
COPY --from=builder /kafka-loadtest /usr/local/bin/kafka-loadtest
RUN chmod +x /usr/local/bin/kafka-loadtest
# Copy scripts and configuration
COPY test/kafka/kafka-client-loadtest/scripts/ /scripts/
COPY test/kafka/kafka-client-loadtest/config/ /config/
# Create results directory
RUN mkdir -p /test-results
# Make scripts executable
RUN chmod +x /scripts/*.sh
WORKDIR /app
# Default command runs the comprehensive load test
CMD ["/usr/local/bin/kafka-loadtest", "-config", "/config/loadtest.yaml"]

View File

@@ -0,0 +1,37 @@
# SeaweedFS Runtime Dockerfile for Kafka Client Load Tests
# Optimized for fast builds - binary built locally and copied in
FROM alpine:3.18
# Install runtime dependencies
RUN apk add --no-cache \
ca-certificates \
wget \
netcat-openbsd \
curl \
tzdata \
&& rm -rf /var/cache/apk/*
# Copy pre-built SeaweedFS binary (built locally for linux/amd64 or linux/arm64)
# Cache-busting: Use build arg to force layer rebuild on every build
ARG TARGETARCH=arm64
ARG CACHE_BUST=unknown
RUN echo "Building with cache bust: ${CACHE_BUST}"
COPY weed-linux-${TARGETARCH} /usr/local/bin/weed
RUN chmod +x /usr/local/bin/weed
# Create data directory
RUN mkdir -p /data
# Set timezone
ENV TZ=UTC
# Health check script
RUN echo '#!/bin/sh' > /usr/local/bin/health-check && \
echo 'exec "$@"' >> /usr/local/bin/health-check && \
chmod +x /usr/local/bin/health-check
VOLUME ["/data"]
WORKDIR /data
ENTRYPOINT ["/usr/local/bin/weed"]

View File

@@ -0,0 +1,21 @@
# Use the official Eclipse Temurin + Maven image so we don't depend on
# Debian apt mirrors, which intermittently refuse connections from
# GitHub Actions runners and fail the CI build. This image already has
# JDK 11 and Maven installed.
FROM maven:3.9-eclipse-temurin-11
WORKDIR /app
# Create source directory
RUN mkdir -p src/main/java
# Copy source and build files
COPY SeekToBeginningTest.java src/main/java/
COPY pom.xml .
# Compile and package
RUN mvn clean package -DskipTests
# Run the test
ENTRYPOINT ["java", "-cp", "target/seek-test.jar", "SeekToBeginningTest"]
CMD ["kafka-gateway:9093"]

View File

@@ -0,0 +1,446 @@
# Kafka Client Load Test Makefile
# Provides convenient targets for running load tests against SeaweedFS Kafka Gateway
.PHONY: help build start stop restart clean test quick-test stress-test endurance-test monitor logs status
# Configuration
DOCKER_COMPOSE := docker compose
PROJECT_NAME := kafka-client-loadtest
CONFIG_FILE := config/loadtest.yaml
# Build configuration
GOARCH ?= arm64
GOOS ?= linux
# Default test parameters
TEST_MODE ?= comprehensive
TEST_DURATION ?= 300s
PRODUCER_COUNT ?= 10
CONSUMER_COUNT ?= 5
MESSAGE_RATE ?= 1000
MESSAGE_SIZE ?= 1024
# Colors for output
GREEN := \033[0;32m
YELLOW := \033[0;33m
BLUE := \033[0;34m
NC := \033[0m
help: ## Show this help message
@echo "Kafka Client Load Test Makefile"
@echo ""
@echo "Available targets:"
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " $(BLUE)%-20s$(NC) %s\n", $$1, $$2}' $(MAKEFILE_LIST)
@echo ""
@echo "Environment variables:"
@echo " TEST_MODE Test mode: producer, consumer, comprehensive (default: comprehensive)"
@echo " TEST_DURATION Test duration (default: 300s)"
@echo " PRODUCER_COUNT Number of producers (default: 10)"
@echo " CONSUMER_COUNT Number of consumers (default: 5)"
@echo " MESSAGE_RATE Messages per second per producer (default: 1000)"
@echo " MESSAGE_SIZE Message size in bytes (default: 1024)"
@echo ""
@echo "Examples:"
@echo " make test # Run default comprehensive test"
@echo " make test TEST_DURATION=10m # Run 10-minute test"
@echo " make quick-test # Run quick smoke test (rebuilds gateway)"
@echo " make stress-test # Run high-load stress test"
@echo " make test TEST_MODE=producer # Producer-only test"
@echo " make schema-test # Run schema integration test with Schema Registry"
@echo " make schema-quick-test # Run quick schema test (30s timeout)"
@echo " make schema-loadtest # Run load test with schemas enabled"
@echo " make build-binary # Build SeaweedFS binary locally for Linux"
@echo " make build-gateway # Build Kafka Gateway (builds binary + Docker image)"
@echo " make build-gateway-clean # Build Kafka Gateway with no cache (fresh build)"
build: ## Build the load test application
@echo "$(BLUE)Building load test application...$(NC)"
$(DOCKER_COMPOSE) build kafka-client-loadtest
@echo "$(GREEN)Build completed$(NC)"
build-binary: ## Build the SeaweedFS binary locally for Linux
@echo "$(BLUE)Building SeaweedFS binary locally for $(GOOS) $(GOARCH)...$(NC)"
cd ../../.. && \
CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) go build \
-ldflags="-s -w" \
-tags "5BytesOffset" \
-o test/kafka/kafka-client-loadtest/weed-$(GOOS)-$(GOARCH) \
weed/weed.go
@echo "$(GREEN)Binary build completed: weed-$(GOOS)-$(GOARCH)$(NC)"
build-gateway: build-binary ## Build the Kafka Gateway with latest changes
@echo "$(BLUE)Building Kafka Gateway Docker image...$(NC)"
CACHE_BUST=$$(date +%s) $(DOCKER_COMPOSE) build kafka-gateway
@echo "$(GREEN)Kafka Gateway build completed$(NC)"
build-gateway-clean: build-binary ## Build the Kafka Gateway with no cache (force fresh build)
@echo "$(BLUE)Building Kafka Gateway Docker image with no cache...$(NC)"
$(DOCKER_COMPOSE) build --no-cache kafka-gateway
@echo "$(GREEN)Kafka Gateway clean build completed$(NC)"
setup: ## Set up monitoring and configuration
@echo "$(BLUE)Setting up monitoring configuration...$(NC)"
./scripts/setup-monitoring.sh
@echo "$(GREEN)Setup completed$(NC)"
start: build-gateway ## Start the infrastructure services (without load test)
@echo "$(BLUE)Starting SeaweedFS infrastructure...$(NC)"
$(DOCKER_COMPOSE) up -d \
seaweedfs-master \
seaweedfs-volume \
seaweedfs-filer \
seaweedfs-mq-broker \
kafka-gateway \
schema-registry-init \
schema-registry
@echo "$(GREEN)Infrastructure started$(NC)"
@echo "Waiting for services to be ready..."
./scripts/wait-for-services.sh wait
@echo "$(GREEN)All services are ready!$(NC)"
stop: ## Stop all services
@echo "$(BLUE)Stopping all services...$(NC)"
$(DOCKER_COMPOSE) --profile loadtest --profile monitoring down
@echo "$(GREEN)Services stopped$(NC)"
restart: stop start ## Restart all services
clean: ## Clean up all resources (containers, volumes, networks, local data)
@echo "$(YELLOW)Warning: This will remove all volumes and data!$(NC)"
@echo "Press Ctrl+C to cancel, or wait 5 seconds to continue..."
@sleep 5
@echo "$(BLUE)Cleaning up all resources...$(NC)"
$(DOCKER_COMPOSE) --profile loadtest --profile monitoring down -v --remove-orphans
docker system prune -f
@if [ -f "weed-linux-arm64" ]; then \
echo "$(BLUE)Removing local binary...$(NC)"; \
rm -f weed-linux-arm64; \
fi
@if [ -d "data" ]; then \
echo "$(BLUE)Removing ALL local data directories (including offset state)...$(NC)"; \
rm -rf data/*; \
fi
@echo "$(GREEN)Cleanup completed - all data removed$(NC)"
clean-binary: ## Clean up only the local binary
@echo "$(BLUE)Removing local binary...$(NC)"
@rm -f weed-linux-arm64
@echo "$(GREEN)Binary cleanup completed$(NC)"
status: ## Show service status
@echo "$(BLUE)Service Status:$(NC)"
$(DOCKER_COMPOSE) ps
logs: ## Show logs from all services
$(DOCKER_COMPOSE) logs -f
test: start ## Run the comprehensive load test
@echo "$(BLUE)Running Kafka client load test...$(NC)"
@echo "Mode: $(TEST_MODE), Duration: $(TEST_DURATION)"
@echo "Producers: $(PRODUCER_COUNT), Consumers: $(CONSUMER_COUNT)"
@echo "Message Rate: $(MESSAGE_RATE) msgs/sec, Size: $(MESSAGE_SIZE) bytes"
@echo ""
@docker rm -f kafka-client-loadtest-runner 2>/dev/null || true
TEST_MODE=$(TEST_MODE) TEST_DURATION=$(TEST_DURATION) PRODUCER_COUNT=$(PRODUCER_COUNT) CONSUMER_COUNT=$(CONSUMER_COUNT) MESSAGE_RATE=$(MESSAGE_RATE) MESSAGE_SIZE=$(MESSAGE_SIZE) VALUE_TYPE=$(VALUE_TYPE) $(DOCKER_COMPOSE) --profile loadtest up --abort-on-container-exit kafka-client-loadtest
@echo "$(GREEN)Load test completed!$(NC)"
@$(MAKE) show-results
quick-test: build-gateway ## Run a quick smoke test (1 min, low load, WITH schemas)
@echo "$(BLUE)================================================================$(NC)"
@echo "$(BLUE) Quick Test (Low Load, WITH Schema Registry + Avro) $(NC)"
@echo "$(BLUE) - Duration: 1 minute $(NC)"
@echo "$(BLUE) - Load: 1 producer × 10 msg/sec = 10 total msg/sec $(NC)"
@echo "$(BLUE) - Message Type: Avro (with schema encoding) $(NC)"
@echo "$(BLUE) - Schema-First: Registers schemas BEFORE producing $(NC)"
@echo "$(BLUE)================================================================$(NC)"
@echo ""
@$(MAKE) start
@echo ""
@echo "$(BLUE)=== Step 1: Registering schemas in Schema Registry ===$(NC)"
@echo "$(YELLOW)[WARN] IMPORTANT: Schemas MUST be registered before producing Avro messages!$(NC)"
@./scripts/register-schemas.sh full
@echo "$(GREEN)- Schemas registered successfully$(NC)"
@echo ""
@echo "$(BLUE)=== Step 2: Running load test with Avro messages ===$(NC)"
@$(MAKE) test \
TEST_MODE=comprehensive \
TEST_DURATION=60s \
PRODUCER_COUNT=1 \
CONSUMER_COUNT=1 \
MESSAGE_RATE=10 \
MESSAGE_SIZE=256 \
VALUE_TYPE=avro
@echo ""
@echo "$(GREEN)================================================================$(NC)"
@echo "$(GREEN) Quick Test Complete! $(NC)"
@echo "$(GREEN) - Schema Registration $(NC)"
@echo "$(GREEN) - Avro Message Production $(NC)"
@echo "$(GREEN) - Message Consumption $(NC)"
@echo "$(GREEN)================================================================$(NC)"
standard-test: ## Run a standard load test (2 min, medium load, WITH Schema Registry + Avro)
@echo "$(BLUE)================================================================$(NC)"
@echo "$(BLUE) Standard Test (Medium Load, WITH Schema Registry) $(NC)"
@echo "$(BLUE) - Duration: 2 minutes $(NC)"
@echo "$(BLUE) - Load: 2 producers × 50 msg/sec = 100 total msg/sec $(NC)"
@echo "$(BLUE) - Message Type: Avro (with schema encoding) $(NC)"
@echo "$(BLUE) - IMPORTANT: Schemas registered FIRST in Schema Registry $(NC)"
@echo "$(BLUE)================================================================$(NC)"
@echo ""
@$(MAKE) start
@echo ""
@echo "$(BLUE)=== Step 1: Registering schemas in Schema Registry ===$(NC)"
@echo "$(YELLOW)Note: Schemas MUST be registered before producing Avro messages!$(NC)"
@./scripts/register-schemas.sh full
@echo "$(GREEN)- Schemas registered$(NC)"
@echo ""
@echo "$(BLUE)=== Step 2: Running load test with Avro messages ===$(NC)"
@$(MAKE) test \
TEST_MODE=comprehensive \
TEST_DURATION=2m \
PRODUCER_COUNT=2 \
CONSUMER_COUNT=2 \
MESSAGE_RATE=50 \
MESSAGE_SIZE=512 \
VALUE_TYPE=avro
@echo ""
@echo "$(GREEN)================================================================$(NC)"
@echo "$(GREEN) Standard Test Complete! $(NC)"
@echo "$(GREEN)================================================================$(NC)"
stress-test: ## Run a stress test (10 minutes, high load) with schemas
@echo "$(BLUE)Starting stress test with schema registration...$(NC)"
@$(MAKE) start
@echo "$(BLUE)Registering schemas with Schema Registry...$(NC)"
@./scripts/register-schemas.sh full
@echo "$(BLUE)Running stress test with registered schemas...$(NC)"
@$(MAKE) test \
TEST_MODE=comprehensive \
TEST_DURATION=10m \
PRODUCER_COUNT=20 \
CONSUMER_COUNT=10 \
MESSAGE_RATE=2000 \
MESSAGE_SIZE=2048 \
VALUE_TYPE=avro
endurance-test: ## Run an endurance test (30 minutes, sustained load) with schemas
@echo "$(BLUE)Starting endurance test with schema registration...$(NC)"
@$(MAKE) start
@echo "$(BLUE)Registering schemas with Schema Registry...$(NC)"
@./scripts/register-schemas.sh full
@echo "$(BLUE)Running endurance test with registered schemas...$(NC)"
@$(MAKE) test \
TEST_MODE=comprehensive \
TEST_DURATION=30m \
PRODUCER_COUNT=10 \
CONSUMER_COUNT=5 \
MESSAGE_RATE=1000 \
MESSAGE_SIZE=1024 \
VALUE_TYPE=avro
producer-test: ## Run producer-only load test
@$(MAKE) test TEST_MODE=producer
consumer-test: ## Run consumer-only load test (requires existing messages)
@$(MAKE) test TEST_MODE=consumer
register-schemas: start ## Register schemas with Schema Registry
@echo "$(BLUE)Registering schemas with Schema Registry...$(NC)"
@./scripts/register-schemas.sh full
@echo "$(GREEN)Schema registration completed!$(NC)"
verify-schemas: ## Verify schemas are registered in Schema Registry
@echo "$(BLUE)Verifying schemas in Schema Registry...$(NC)"
@./scripts/register-schemas.sh verify
@echo "$(GREEN)Schema verification completed!$(NC)"
list-schemas: ## List all registered schemas in Schema Registry
@echo "$(BLUE)Listing registered schemas...$(NC)"
@./scripts/register-schemas.sh list
cleanup-schemas: ## Clean up test schemas from Schema Registry
@echo "$(YELLOW)Cleaning up test schemas...$(NC)"
@./scripts/register-schemas.sh cleanup
@echo "$(GREEN)Schema cleanup completed!$(NC)"
schema-test: start ## Run schema integration test (with Schema Registry)
@echo "$(BLUE)Running schema integration test...$(NC)"
@echo "Testing Schema Registry integration with schematized topics"
@echo ""
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o schema-test-linux test_schema_integration.go
docker run --rm --network kafka-client-loadtest \
-v $(PWD)/schema-test-linux:/usr/local/bin/schema-test \
alpine:3.18 /usr/local/bin/schema-test
@rm -f schema-test-linux
@echo "$(GREEN)Schema integration test completed!$(NC)"
schema-quick-test: start ## Run quick schema test (lighter version)
@echo "$(BLUE)Running quick schema test...$(NC)"
@echo "Testing basic schema functionality"
@echo ""
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o schema-test-linux test_schema_integration.go
timeout 60s docker run --rm --network kafka-client-loadtest \
-v $(PWD)/schema-test-linux:/usr/local/bin/schema-test \
alpine:3.18 /usr/local/bin/schema-test || true
@rm -f schema-test-linux
@echo "$(GREEN)Quick schema test completed!$(NC)"
simple-schema-test: start ## Run simple schema test (step-by-step)
@echo "$(BLUE)Running simple schema test...$(NC)"
@echo "Step-by-step schema functionality test"
@echo ""
@mkdir -p simple-test
@cp simple_schema_test.go simple-test/main.go
cd simple-test && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ../simple-schema-test-linux .
docker run --rm --network kafka-client-loadtest \
-v $(PWD)/simple-schema-test-linux:/usr/local/bin/simple-schema-test \
alpine:3.18 /usr/local/bin/simple-schema-test
@rm -f simple-schema-test-linux
@rm -rf simple-test
@echo "$(GREEN)Simple schema test completed!$(NC)"
basic-schema-test: start ## Run basic schema test (manual schema handling without Schema Registry)
@echo "$(BLUE)Running basic schema test...$(NC)"
@echo "Testing schema functionality without Schema Registry dependency"
@echo ""
@mkdir -p basic-test
@cp basic_schema_test.go basic-test/main.go
cd basic-test && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ../basic-schema-test-linux .
timeout 60s docker run --rm --network kafka-client-loadtest \
-v $(PWD)/basic-schema-test-linux:/usr/local/bin/basic-schema-test \
alpine:3.18 /usr/local/bin/basic-schema-test
@rm -f basic-schema-test-linux
@rm -rf basic-test
@echo "$(GREEN)Basic schema test completed!$(NC)"
schema-loadtest: start ## Run load test with schemas enabled
@echo "$(BLUE)Running schema-enabled load test...$(NC)"
@echo "Mode: comprehensive with schemas, Duration: 3m"
@echo "Producers: 3, Consumers: 2, Message Rate: 50 msgs/sec"
@echo ""
TEST_MODE=comprehensive \
TEST_DURATION=3m \
PRODUCER_COUNT=3 \
CONSUMER_COUNT=2 \
MESSAGE_RATE=50 \
MESSAGE_SIZE=1024 \
SCHEMA_REGISTRY_URL=http://schema-registry:8081 \
$(DOCKER_COMPOSE) --profile loadtest up --abort-on-container-exit kafka-client-loadtest
@echo "$(GREEN)Schema load test completed!$(NC)"
@$(MAKE) show-results
monitor: setup ## Start monitoring stack (Prometheus + Grafana)
@echo "$(BLUE)Starting monitoring stack...$(NC)"
$(DOCKER_COMPOSE) --profile monitoring up -d prometheus grafana
@echo "$(GREEN)Monitoring stack started!$(NC)"
@echo ""
@echo "Access points:"
@echo " Prometheus: http://localhost:9090"
@echo " Grafana: http://localhost:3000 (admin/admin)"
monitor-stop: ## Stop monitoring stack
@echo "$(BLUE)Stopping monitoring stack...$(NC)"
$(DOCKER_COMPOSE) --profile monitoring stop prometheus grafana
@echo "$(GREEN)Monitoring stack stopped$(NC)"
test-with-monitoring: monitor start ## Run test with monitoring enabled
@echo "$(BLUE)Running load test with monitoring...$(NC)"
@$(MAKE) test
@echo ""
@echo "$(GREEN)Test completed! Check the monitoring dashboards:$(NC)"
@echo " Prometheus: http://localhost:9090"
@echo " Grafana: http://localhost:3000 (admin/admin)"
show-results: ## Show test results
@echo "$(BLUE)Test Results Summary:$(NC)"
@if $(DOCKER_COMPOSE) ps -q kafka-client-loadtest-runner >/dev/null 2>&1; then \
$(DOCKER_COMPOSE) exec -T kafka-client-loadtest-runner curl -s http://localhost:8080/stats 2>/dev/null || echo "Results not available"; \
else \
echo "Load test container not running"; \
fi
@echo ""
@if [ -d "test-results" ]; then \
echo "Detailed results saved to: test-results/"; \
ls -la test-results/ 2>/dev/null || true; \
fi
health-check: ## Check health of all services
@echo "$(BLUE)Checking service health...$(NC)"
./scripts/wait-for-services.sh check
validate-setup: ## Validate the test setup
@echo "$(BLUE)Validating test setup...$(NC)"
@echo "Checking Docker and Docker Compose..."
@docker --version
@docker compose version || docker-compose --version
@echo ""
@echo "Checking configuration file..."
@if [ -f "$(CONFIG_FILE)" ]; then \
echo "- Configuration file exists: $(CONFIG_FILE)"; \
else \
echo "x Configuration file not found: $(CONFIG_FILE)"; \
exit 1; \
fi
@echo ""
@echo "Checking scripts..."
@for script in scripts/*.sh; do \
if [ -x "$$script" ]; then \
echo "- $$script is executable"; \
else \
echo "x $$script is not executable"; \
fi; \
done
@echo "$(GREEN)Setup validation completed$(NC)"
dev-env: ## Set up development environment
@echo "$(BLUE)Setting up development environment...$(NC)"
@echo "Installing Go dependencies..."
go mod download
go mod tidy
@echo "$(GREEN)Development environment ready$(NC)"
benchmark: ## Run comprehensive benchmarking suite
@echo "$(BLUE)Running comprehensive benchmark suite...$(NC)"
@echo "This will run multiple test scenarios and collect detailed metrics"
@echo ""
@$(MAKE) quick-test
@sleep 10
@$(MAKE) standard-test
@sleep 10
@$(MAKE) stress-test
@echo "$(GREEN)Benchmark suite completed!$(NC)"
# Advanced targets
debug: ## Start services in debug mode with verbose logging
@echo "$(BLUE)Starting services in debug mode...$(NC)"
SEAWEEDFS_LOG_LEVEL=debug \
KAFKA_LOG_LEVEL=debug \
$(DOCKER_COMPOSE) up \
seaweedfs-master \
seaweedfs-volume \
seaweedfs-filer \
seaweedfs-mq-broker \
kafka-gateway \
schema-registry
attach-loadtest: ## Attach to running load test container
$(DOCKER_COMPOSE) exec kafka-client-loadtest-runner /bin/sh
exec-master: ## Execute shell in SeaweedFS master container
$(DOCKER_COMPOSE) exec seaweedfs-master /bin/sh
exec-filer: ## Execute shell in SeaweedFS filer container
$(DOCKER_COMPOSE) exec seaweedfs-filer /bin/sh
exec-gateway: ## Execute shell in Kafka gateway container
$(DOCKER_COMPOSE) exec kafka-gateway /bin/sh
# Utility targets
ps: status ## Alias for status
up: start ## Alias for start
down: stop ## Alias for stop
# Help is the default target
.DEFAULT_GOAL := help

View File

@@ -0,0 +1,397 @@
# Kafka Client Load Test for SeaweedFS
This comprehensive load testing suite validates the SeaweedFS MQ stack using real Kafka client libraries. Unlike the existing SMQ tests, this uses actual Kafka clients (`sarama` and `confluent-kafka-go`) to test the complete integration through:
- **Kafka Clients** → **SeaweedFS Kafka Gateway** → **SeaweedFS MQ Broker** → **SeaweedFS Storage**
## Architecture
```
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────────┐
│ Kafka Client │ │ Kafka Gateway │ │ SeaweedFS MQ │
│ Load Test │───▶│ (Port 9093) │───▶│ Broker │
│ - Producers │ │ │ │ │
│ - Consumers │ │ Protocol │ │ Topic Management │
│ │ │ Translation │ │ Message Storage │
└─────────────────┘ └──────────────────┘ └─────────────────────┘
│
▼
┌─────────────────────┐
│ SeaweedFS Storage │
│ - Master │
│ - Volume Server │
│ - Filer │
└─────────────────────┘
```
## Features
### 🚀 **Multiple Test Modes**
- **Producer-only**: Pure message production testing
- **Consumer-only**: Consumption from existing topics
- **Comprehensive**: Full producer + consumer load testing
### 📊 **Rich Metrics & Monitoring**
- Prometheus metrics collection
- Grafana dashboards
- Real-time throughput and latency tracking
- Consumer lag monitoring
- Error rate analysis
### 🔧 **Configurable Test Scenarios**
- **Quick Test**: 1-minute smoke test
- **Standard Test**: 5-minute medium load
- **Stress Test**: 10-minute high load
- **Endurance Test**: 30-minute sustained load
- **Custom**: Fully configurable parameters
### 📈 **Message Types**
- **JSON**: Structured test messages
- **Avro**: Schema Registry integration
- **Binary**: Raw binary payloads
### 🛠 **Kafka Client Support**
- **Sarama**: Native Go Kafka client
- **Confluent**: Official Confluent Go client
- Schema Registry integration
- Consumer group management
## Quick Start
### Prerequisites
- Docker & Docker Compose
- Make (optional, but recommended)
### 1. Run Default Test
```bash
make test
```
This runs a 5-minute comprehensive test with 10 producers and 5 consumers.
### 2. Quick Smoke Test
```bash
make quick-test
```
1-minute test with minimal load for validation.
### 3. Stress Test
```bash
make stress-test
```
10-minute high-throughput test with 20 producers and 10 consumers.
### 4. Test with Monitoring
```bash
make test-with-monitoring
```
Includes Prometheus + Grafana dashboards for real-time monitoring.
## Detailed Usage
### Manual Control
```bash
# Start infrastructure only
make start
# Run load test against running infrastructure
make test TEST_MODE=comprehensive TEST_DURATION=10m
# Stop everything
make stop
# Clean up all resources
make clean
```
### Using Scripts Directly
```bash
# Full control with the main script
./scripts/run-loadtest.sh start -m comprehensive -d 10m --monitoring
# Check service health
./scripts/wait-for-services.sh check
# Setup monitoring configurations
./scripts/setup-monitoring.sh
```
### Environment Variables
```bash
export TEST_MODE=comprehensive # producer, consumer, comprehensive
export TEST_DURATION=300s # Test duration
export PRODUCER_COUNT=10 # Number of producer instances
export CONSUMER_COUNT=5 # Number of consumer instances
export MESSAGE_RATE=1000 # Messages/second per producer
export MESSAGE_SIZE=1024 # Message size in bytes
export TOPIC_COUNT=5 # Number of topics to create
export PARTITIONS_PER_TOPIC=3 # Partitions per topic
make test
```
## Configuration
### Main Configuration File
Edit `config/loadtest.yaml` to customize:
- **Kafka Settings**: Bootstrap servers, security, timeouts
- **Producer Config**: Batching, compression, acknowledgments
- **Consumer Config**: Group settings, fetch parameters
- **Message Settings**: Size, format (JSON/Avro/Binary)
- **Schema Registry**: Avro/Protobuf schema validation
- **Metrics**: Prometheus collection intervals
- **Test Scenarios**: Predefined load patterns
### Example Custom Configuration
```yaml
test_mode: "comprehensive"
duration: "600s" # 10 minutes
producers:
count: 15
message_rate: 2000
message_size: 2048
compression_type: "snappy"
acks: "all"
consumers:
count: 8
group_prefix: "high-load-group"
max_poll_records: 1000
topics:
count: 10
partitions: 6
replication_factor: 1
```
## Test Scenarios
### 1. Producer Performance Test
```bash
make producer-test TEST_DURATION=10m PRODUCER_COUNT=20 MESSAGE_RATE=3000
```
Tests maximum message production throughput.
### 2. Consumer Performance Test
```bash
# First produce messages
make producer-test TEST_DURATION=5m
# Then test consumption
make consumer-test TEST_DURATION=10m CONSUMER_COUNT=15
```
### 3. Schema Registry Integration
```bash
# Enable schemas in config/loadtest.yaml
schemas:
enabled: true
make test
```
Tests Avro message serialization through Schema Registry.
### 4. High Availability Test
```bash
# Test with container restarts during load
make test TEST_DURATION=20m &
sleep 300
docker restart kafka-gateway
```
## Monitoring & Metrics
### Real-Time Dashboards
When monitoring is enabled:
- **Prometheus**: http://localhost:9090
- **Grafana**: http://localhost:3000 (admin/admin)
### Key Metrics Tracked
- **Throughput**: Messages/second, MB/second
- **Latency**: End-to-end message latency percentiles
- **Errors**: Producer/consumer error rates
- **Consumer Lag**: Per-partition lag monitoring
- **Resource Usage**: CPU, memory, disk I/O
### Grafana Dashboards
- **Kafka Load Test**: Comprehensive test metrics
- **SeaweedFS Cluster**: Storage system health
- **Custom Dashboards**: Extensible monitoring
## Advanced Features
### Schema Registry Testing
```bash
# Test Avro message serialization
export KAFKA_VALUE_TYPE=avro
make test
```
The load test includes:
- Schema registration
- Avro message encoding/decoding
- Schema evolution testing
- Compatibility validation
### Multi-Client Testing
The test supports both Sarama and Confluent clients:
```go
// Configure in producer/consumer code
useConfluent := true // Switch client implementation
```
### Consumer Group Rebalancing
- Automatic consumer group management
- Partition rebalancing simulation
- Consumer failure recovery testing
### Chaos Testing
```yaml
chaos:
enabled: true
producer_failure_rate: 0.01
consumer_failure_rate: 0.01
network_partition_probability: 0.001
```
## Troubleshooting
### Common Issues
#### Services Not Starting
```bash
# Check service health
make health-check
# View detailed logs
make logs
# Debug mode
make debug
```
#### Low Throughput
- Increase `MESSAGE_RATE` and `PRODUCER_COUNT`
- Adjust `batch_size` and `linger_ms` in config
- Check consumer `max_poll_records` setting
#### High Latency
- Reduce `linger_ms` for lower latency
- Adjust `acks` setting (0, 1, or "all")
- Monitor consumer lag
#### Memory Issues
```bash
# Reduce concurrent clients
make test PRODUCER_COUNT=5 CONSUMER_COUNT=3
# Adjust message size
make test MESSAGE_SIZE=512
```
### Debug Commands
```bash
# Execute shell in containers
make exec-master
make exec-filer
make exec-gateway
# Attach to load test
make attach-loadtest
# View real-time stats
curl http://localhost:8080/stats
```
## Development
### Building from Source
```bash
# Set up development environment
make dev-env
# Build load test binary
make build
# Run tests locally (requires Go 1.26+)
cd cmd/loadtest && go run main.go -config ../../config/loadtest.yaml
```
### Extending the Tests
1. **Add new message formats** in `internal/producer/`
2. **Add custom metrics** in `internal/metrics/`
3. **Create new test scenarios** in `config/loadtest.yaml`
4. **Add monitoring panels** in `monitoring/grafana/dashboards/`
### Contributing
1. Fork the repository
2. Create a feature branch
3. Add tests for new functionality
4. Ensure all tests pass: `make test`
5. Submit a pull request
## Performance Benchmarks
### Expected Performance (on typical hardware)
| Scenario | Producers | Consumers | Rate (msg/s) | Latency (p95) |
|----------|-----------|-----------|--------------|---------------|
| Quick | 2 | 2 | 200 | <10ms |
| Standard | 5 | 3 | 2,500 | <20ms |
| Stress | 20 | 10 | 40,000 | <50ms |
| Endurance| 10 | 5 | 10,000 | <30ms |
*Results vary based on hardware, network, and SeaweedFS configuration*
### Tuning for Maximum Performance
```yaml
producers:
batch_size: 1000
linger_ms: 10
compression_type: "lz4"
acks: "1" # Balance between speed and durability
consumers:
max_poll_records: 5000
fetch_min_bytes: 1048576 # 1MB
fetch_max_wait_ms: 100
```
## Comparison with Existing Tests
| Feature | SMQ Tests | **Kafka Client Load Test** |
|---------|-----------|----------------------------|
| Protocol | SMQ (SeaweedFS native) | **Kafka (industry standard)** |
| Clients | SMQ clients | **Real Kafka clients (Sarama, Confluent)** |
| Schema Registry | ❌ | **✅ Full Avro/Protobuf support** |
| Consumer Groups | Basic | **✅ Full Kafka consumer group features** |
| Monitoring | Basic | **✅ Prometheus + Grafana dashboards** |
| Test Scenarios | Limited | **✅ Multiple predefined scenarios** |
| Real-world | Synthetic | **✅ Production-like workloads** |
This load test provides comprehensive validation of the SeaweedFS Kafka Gateway using real-world Kafka clients and protocols.
---
## Quick Reference
```bash
# Essential Commands
make help # Show all available commands
make test # Run default comprehensive test
make quick-test # 1-minute smoke test
make stress-test # High-load stress test
make test-with-monitoring # Include Grafana dashboards
make clean # Clean up all resources
# Monitoring
make monitor # Start Prometheus + Grafana
# → http://localhost:9090 (Prometheus)
# → http://localhost:3000 (Grafana, admin/admin)
# Advanced
make benchmark # Run full benchmark suite
make health-check # Validate service health
make validate-setup # Check configuration
```

View File

@@ -0,0 +1,179 @@
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.clients.consumer.internals.*;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.serialization.ByteArrayDeserializer;
import org.apache.kafka.common.errors.TimeoutException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
/**
* Enhanced test program to reproduce and diagnose the seekToBeginning() hang issue
*
* This test:
* 1. Adds detailed logging of Kafka client operations
* 2. Captures exceptions and timeouts
* 3. Shows what the consumer is waiting for
* 4. Tracks request/response lifecycle
*/
public class SeekToBeginningTest {
private static final Logger log = LoggerFactory.getLogger(SeekToBeginningTest.class);
public static void main(String[] args) throws Exception {
String bootstrapServers = "localhost:9093";
String topicName = "_schemas";
if (args.length > 0) {
bootstrapServers = args[0];
}
Properties props = new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
props.put(ConsumerConfig.GROUP_ID_CONFIG, "test-seek-group");
props.put(ConsumerConfig.CLIENT_ID_CONFIG, "test-seek-client");
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class);
props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, "45000");
props.put(ConsumerConfig.REQUEST_TIMEOUT_MS_CONFIG, "60000");
// Add comprehensive debug logging
props.put("log4j.logger.org.apache.kafka.clients.consumer.internals", "DEBUG");
props.put("log4j.logger.org.apache.kafka.clients.producer.internals", "DEBUG");
props.put("log4j.logger.org.apache.kafka.clients.Metadata", "DEBUG");
// Add shorter timeouts to fail faster
props.put(ConsumerConfig.DEFAULT_API_TIMEOUT_MS_CONFIG, "10000"); // 10 seconds instead of 60
System.out.println("\n╔════════════════════════════════════════════════════════════╗");
System.out.println("║ SeekToBeginning Diagnostic Test ║");
System.out.println(String.format("║ Connecting to: %-42s║", bootstrapServers));
System.out.println("╚════════════════════════════════════════════════════════════╝\n");
System.out.println("[TEST] Creating KafkaConsumer...");
System.out.println("[TEST] Bootstrap servers: " + bootstrapServers);
System.out.println("[TEST] Group ID: test-seek-group");
System.out.println("[TEST] Client ID: test-seek-client");
KafkaConsumer<byte[], byte[]> consumer = new KafkaConsumer<>(props);
TopicPartition tp = new TopicPartition(topicName, 0);
List<TopicPartition> partitions = Arrays.asList(tp);
System.out.println("\n[STEP 1] Assigning to partition: " + tp);
consumer.assign(partitions);
System.out.println("[STEP 1] ✓ Assigned successfully");
System.out.println("\n[STEP 2] Calling seekToBeginning()...");
long startTime = System.currentTimeMillis();
try {
consumer.seekToBeginning(partitions);
long seekTime = System.currentTimeMillis() - startTime;
System.out.println("[STEP 2] ✓ seekToBeginning() completed in " + seekTime + "ms");
} catch (Exception e) {
System.out.println("[STEP 2] ✗ EXCEPTION in seekToBeginning():");
e.printStackTrace();
consumer.close();
return;
}
System.out.println("\n[STEP 3] Starting poll loop...");
System.out.println("[STEP 3] First poll will trigger offset lookup (ListOffsets)");
System.out.println("[STEP 3] Then will fetch initial records\n");
int successfulPolls = 0;
int failedPolls = 0;
int totalRecords = 0;
for (int i = 0; i < 3; i++) {
System.out.println("═══════════════════════════════════════════════════════════");
System.out.println("[POLL " + (i + 1) + "] Starting poll with 15-second timeout...");
long pollStart = System.currentTimeMillis();
try {
System.out.println("[POLL " + (i + 1) + "] Calling consumer.poll()...");
ConsumerRecords<byte[], byte[]> records = consumer.poll(java.time.Duration.ofSeconds(15));
long pollTime = System.currentTimeMillis() - pollStart;
System.out.println("[POLL " + (i + 1) + "] ✓ Poll completed in " + pollTime + "ms");
System.out.println("[POLL " + (i + 1) + "] Records received: " + records.count());
if (records.count() > 0) {
successfulPolls++;
totalRecords += records.count();
for (ConsumerRecord<byte[], byte[]> record : records) {
System.out.println(" [RECORD] offset=" + record.offset() +
", key.len=" + (record.key() != null ? record.key().length : 0) +
", value.len=" + (record.value() != null ? record.value().length : 0));
}
} else {
System.out.println("[POLL " + (i + 1) + "] ℹ No records in this poll (but no error)");
successfulPolls++;
}
} catch (TimeoutException e) {
long pollTime = System.currentTimeMillis() - pollStart;
failedPolls++;
System.out.println("[POLL " + (i + 1) + "] ✗ TIMEOUT after " + pollTime + "ms");
System.out.println("[POLL " + (i + 1) + "] This means consumer is waiting for something from broker");
System.out.println("[POLL " + (i + 1) + "] Possible causes:");
System.out.println(" - ListOffsetsRequest never sent");
System.out.println(" - ListOffsetsResponse not received");
System.out.println(" - Broker metadata parsing failed");
System.out.println(" - Connection issue");
// Print current position info if available
try {
long position = consumer.position(tp);
System.out.println("[POLL " + (i + 1) + "] Current position: " + position);
} catch (Exception e2) {
System.out.println("[POLL " + (i + 1) + "] Could not get position: " + e2.getMessage());
}
} catch (Exception e) {
failedPolls++;
long pollTime = System.currentTimeMillis() - pollStart;
System.out.println("[POLL " + (i + 1) + "] ✗ EXCEPTION after " + pollTime + "ms:");
System.out.println("[POLL " + (i + 1) + "] Exception type: " + e.getClass().getSimpleName());
System.out.println("[POLL " + (i + 1) + "] Message: " + e.getMessage());
// Print stack trace for first exception
if (i == 0) {
System.out.println("[POLL " + (i + 1) + "] Stack trace:");
e.printStackTrace();
}
}
}
System.out.println("\n═══════════════════════════════════════════════════════════");
System.out.println("[RESULTS] Test Summary:");
System.out.println(" Successful polls: " + successfulPolls);
System.out.println(" Failed polls: " + failedPolls);
System.out.println(" Total records received: " + totalRecords);
if (failedPolls > 0) {
System.out.println("\n[DIAGNOSIS] Consumer is BLOCKED during poll()");
System.out.println(" This indicates the consumer cannot:");
System.out.println(" 1. Send ListOffsetsRequest to determine offset 0, OR");
System.out.println(" 2. Receive/parse ListOffsetsResponse from broker, OR");
System.out.println(" 3. Parse broker metadata for partition leader lookup");
} else if (totalRecords == 0) {
System.out.println("\n[DIAGNOSIS] Consumer is working but NO records found");
System.out.println(" This might mean:");
System.out.println(" 1. Topic has no messages, OR");
System.out.println(" 2. Fetch is working but broker returns empty");
} else {
System.out.println("\n[SUCCESS] Consumer working correctly!");
System.out.println(" Received " + totalRecords + " records");
}
System.out.println("\n[CLEANUP] Closing consumer...");
try {
consumer.close();
System.out.println("[CLEANUP] ✓ Consumer closed successfully");
} catch (Exception e) {
System.out.println("[CLEANUP] ✗ Error closing consumer: " + e.getMessage());
}
System.out.println("\n[TEST] Done!\n");
}
}

View File

@@ -0,0 +1,502 @@
package main
import (
"bytes"
"context"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
"os/signal"
"strings"
"sync"
"syscall"
"time"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/seaweedfs/seaweedfs/test/kafka/kafka-client-loadtest/internal/config"
"github.com/seaweedfs/seaweedfs/test/kafka/kafka-client-loadtest/internal/consumer"
"github.com/seaweedfs/seaweedfs/test/kafka/kafka-client-loadtest/internal/metrics"
"github.com/seaweedfs/seaweedfs/test/kafka/kafka-client-loadtest/internal/producer"
"github.com/seaweedfs/seaweedfs/test/kafka/kafka-client-loadtest/internal/schema"
"github.com/seaweedfs/seaweedfs/test/kafka/kafka-client-loadtest/internal/tracker"
)
var (
configFile = flag.String("config", "/config/loadtest.yaml", "Path to configuration file")
testMode = flag.String("mode", "", "Test mode override (producer|consumer|comprehensive)")
duration = flag.Duration("duration", 0, "Test duration override")
help = flag.Bool("help", false, "Show help")
)
func main() {
flag.Parse()
if *help {
printHelp()
return
}
// Load configuration
cfg, err := config.Load(*configFile)
if err != nil {
log.Fatalf("Failed to load configuration: %v", err)
}
// Override configuration with environment variables and flags
cfg.ApplyOverrides(*testMode, *duration)
// Initialize metrics
metricsCollector := metrics.NewCollector()
// Start metrics HTTP server
go func() {
http.Handle("/metrics", promhttp.Handler())
http.HandleFunc("/health", healthCheck)
http.HandleFunc("/stats", func(w http.ResponseWriter, r *http.Request) {
metricsCollector.WriteStats(w)
})
log.Printf("Starting metrics server on :8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Printf("Metrics server error: %v", err)
}
}()
// Set up signal handling
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
log.Printf("Starting Kafka Client Load Test")
log.Printf("Mode: %s, Duration: %v", cfg.TestMode, cfg.Duration)
log.Printf("Kafka Brokers: %v", cfg.Kafka.BootstrapServers)
log.Printf("Schema Registry: %s", cfg.SchemaRegistry.URL)
log.Printf("Schemas Enabled: %v", cfg.Schemas.Enabled)
// Register schemas if enabled
if cfg.Schemas.Enabled {
log.Printf("Registering schemas with Schema Registry...")
if err := registerSchemas(cfg); err != nil {
log.Fatalf("Failed to register schemas: %v", err)
}
log.Printf("Schemas registered successfully")
}
var wg sync.WaitGroup
// Start test based on mode
var testErr error
switch cfg.TestMode {
case "producer":
testErr = runProducerTest(ctx, cfg, metricsCollector, &wg)
case "consumer":
testErr = runConsumerTest(ctx, cfg, metricsCollector, &wg)
case "comprehensive":
testErr = runComprehensiveTest(ctx, cancel, cfg, metricsCollector, &wg)
default:
log.Fatalf("Unknown test mode: %s", cfg.TestMode)
}
// If test returned an error (e.g., circuit breaker), exit
if testErr != nil {
log.Printf("Test failed with error: %v", testErr)
cancel() // Cancel context to stop any remaining goroutines
return
}
// Wait for completion or signal
done := make(chan struct{})
go func() {
wg.Wait()
close(done)
}()
select {
case <-sigCh:
log.Printf("Received shutdown signal, stopping tests...")
cancel()
// Wait for graceful shutdown with timeout
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer shutdownCancel()
select {
case <-done:
log.Printf("All tests completed gracefully")
case <-shutdownCtx.Done():
log.Printf("Shutdown timeout, forcing exit")
}
case <-done:
log.Printf("All tests completed")
}
// Print final statistics
log.Printf("Final Test Statistics:")
metricsCollector.PrintSummary()
}
func runProducerTest(ctx context.Context, cfg *config.Config, collector *metrics.Collector, wg *sync.WaitGroup) error {
log.Printf("Starting producer-only test with %d producers", cfg.Producers.Count)
// Create record tracker with current timestamp to filter old messages
testStartTime := time.Now().UnixNano()
recordTracker := tracker.NewTracker("/test-results/produced.jsonl", "/test-results/consumed.jsonl", testStartTime)
errChan := make(chan error, cfg.Producers.Count)
for i := 0; i < cfg.Producers.Count; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
prod, err := producer.New(cfg, collector, id, recordTracker)
if err != nil {
log.Printf("Failed to create producer %d: %v", id, err)
errChan <- err
return
}
defer prod.Close()
if err := prod.Run(ctx); err != nil {
log.Printf("Producer %d failed: %v", id, err)
errChan <- err
return
}
}(i)
}
// Wait for any producer error
select {
case err := <-errChan:
log.Printf("Producer test failed: %v", err)
return err
default:
return nil
}
}
func runConsumerTest(ctx context.Context, cfg *config.Config, collector *metrics.Collector, wg *sync.WaitGroup) error {
log.Printf("Starting consumer-only test with %d consumers", cfg.Consumers.Count)
// Create record tracker with current timestamp to filter old messages
testStartTime := time.Now().UnixNano()
recordTracker := tracker.NewTracker("/test-results/produced.jsonl", "/test-results/consumed.jsonl", testStartTime)
errChan := make(chan error, cfg.Consumers.Count)
for i := 0; i < cfg.Consumers.Count; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
cons, err := consumer.New(cfg, collector, id, recordTracker)
if err != nil {
log.Printf("Failed to create consumer %d: %v", id, err)
errChan <- err
return
}
defer cons.Close()
cons.Run(ctx)
}(i)
}
// Consumers don't typically return errors in the same way, so just return nil
return nil
}
func runComprehensiveTest(ctx context.Context, cancel context.CancelFunc, cfg *config.Config, collector *metrics.Collector, wg *sync.WaitGroup) error {
log.Printf("Starting comprehensive test with %d producers and %d consumers",
cfg.Producers.Count, cfg.Consumers.Count)
// Create record tracker with current timestamp to filter old messages
testStartTime := time.Now().UnixNano()
log.Printf("Test run starting at %d - only tracking messages from this run", testStartTime)
recordTracker := tracker.NewTracker("/test-results/produced.jsonl", "/test-results/consumed.jsonl", testStartTime)
errChan := make(chan error, cfg.Producers.Count)
// Create separate contexts for producers and consumers
producerCtx, producerCancel := context.WithCancel(ctx)
consumerCtx, consumerCancel := context.WithCancel(ctx)
// Start producers
for i := 0; i < cfg.Producers.Count; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
prod, err := producer.New(cfg, collector, id, recordTracker)
if err != nil {
log.Printf("Failed to create producer %d: %v", id, err)
errChan <- err
return
}
defer prod.Close()
if err := prod.Run(producerCtx); err != nil {
log.Printf("Producer %d failed: %v", id, err)
errChan <- err
return
}
}(i)
}
// Wait briefly for producers to start producing messages
// Reduced from 5s to 2s to minimize message backlog
time.Sleep(2 * time.Second)
// Start consumers
// NOTE: With unique ClientIDs, all consumers can start simultaneously without connection storms
for i := 0; i < cfg.Consumers.Count; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
cons, err := consumer.New(cfg, collector, id, recordTracker)
if err != nil {
log.Printf("Failed to create consumer %d: %v", id, err)
return
}
defer cons.Close()
cons.Run(consumerCtx)
}(i)
}
// Check for producer errors
select {
case err := <-errChan:
log.Printf("Comprehensive test failed due to producer error: %v", err)
producerCancel()
consumerCancel()
return err
default:
// No immediate error, continue
}
// If duration is set, stop producers first, then allow consumers extra time to drain
if cfg.Duration > 0 {
go func() {
timer := time.NewTimer(cfg.Duration)
defer timer.Stop()
select {
case <-timer.C:
log.Printf("Test duration (%v) reached, stopping producers", cfg.Duration)
producerCancel()
// Allow consumers extra time to drain remaining messages
// Calculate drain time based on test duration (minimum 60s, up to test duration)
drainTime := 60 * time.Second
if cfg.Duration > drainTime {
drainTime = cfg.Duration // Match test duration for longer tests
}
log.Printf("Allowing %v for consumers to drain remaining messages...", drainTime)
time.Sleep(drainTime)
log.Printf("Stopping consumers after drain period")
consumerCancel()
cancel()
case <-ctx.Done():
// Context already cancelled
producerCancel()
consumerCancel()
}
}()
} else {
// No duration set, wait for cancellation and ensure cleanup
go func() {
<-ctx.Done()
producerCancel()
consumerCancel()
}()
}
// Wait for all producer and consumer goroutines to complete
log.Printf("Waiting for all producers and consumers to complete...")
wg.Wait()
log.Printf("All producers and consumers completed, starting verification...")
// Save produced and consumed records
log.Printf("Saving produced records...")
if err := recordTracker.SaveProduced(); err != nil {
log.Printf("Failed to save produced records: %v", err)
}
log.Printf("Saving consumed records...")
if err := recordTracker.SaveConsumed(); err != nil {
log.Printf("Failed to save consumed records: %v", err)
}
// Compare records
log.Printf("Comparing produced vs consumed records...")
result := recordTracker.Compare()
result.PrintSummary()
log.Printf("Verification complete!")
return nil
}
func healthCheck(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, "OK")
}
func printHelp() {
fmt.Printf(`Kafka Client Load Test for SeaweedFS
Usage: %s [options]
Options:
-config string
Path to configuration file (default "/config/loadtest.yaml")
-mode string
Test mode override (producer|consumer|comprehensive)
-duration duration
Test duration override
-help
Show this help message
Environment Variables:
KAFKA_BOOTSTRAP_SERVERS Comma-separated list of Kafka brokers
SCHEMA_REGISTRY_URL URL of the Schema Registry
TEST_DURATION Test duration (e.g., "5m", "300s")
TEST_MODE Test mode (producer|consumer|comprehensive)
PRODUCER_COUNT Number of producer instances
CONSUMER_COUNT Number of consumer instances
MESSAGE_RATE Messages per second per producer
MESSAGE_SIZE Message size in bytes
TOPIC_COUNT Number of topics to create
PARTITIONS_PER_TOPIC Number of partitions per topic
VALUE_TYPE Message value type (json/avro/binary)
Test Modes:
producer - Run only producers (generate load)
consumer - Run only consumers (consume existing messages)
comprehensive - Run both producers and consumers simultaneously
Example:
%s -config ./config/loadtest.yaml -mode comprehensive -duration 10m
`, os.Args[0], os.Args[0])
}
// registerSchemas registers schemas with Schema Registry for all topics
func registerSchemas(cfg *config.Config) error {
// Wait for Schema Registry to be ready
if err := waitForSchemaRegistry(cfg.SchemaRegistry.URL); err != nil {
return fmt.Errorf("schema registry not ready: %w", err)
}
// Register schemas for each topic with different formats for variety
topics := cfg.GetTopicNames()
// Determine schema formats - use different formats for different topics
// This provides comprehensive testing of all schema format variations
for i, topic := range topics {
var schemaFormat string
// Distribute topics across three schema formats for comprehensive testing
// Format 0: AVRO (default, most common)
// Format 1: JSON (modern, human-readable)
// Format 2: PROTOBUF (efficient binary format)
switch i % 3 {
case 0:
schemaFormat = "AVRO"
case 1:
schemaFormat = "JSON"
case 2:
schemaFormat = "PROTOBUF"
}
// Allow override from config if specified
if cfg.Producers.SchemaFormat != "" {
schemaFormat = cfg.Producers.SchemaFormat
}
if err := registerTopicSchema(cfg.SchemaRegistry.URL, topic, schemaFormat); err != nil {
return fmt.Errorf("failed to register schema for topic %s (format: %s): %w", topic, schemaFormat, err)
}
log.Printf("Schema registered for topic %s with format: %s", topic, schemaFormat)
}
return nil
}
// waitForSchemaRegistry waits for Schema Registry to be ready
func waitForSchemaRegistry(url string) error {
maxRetries := 30
for i := 0; i < maxRetries; i++ {
resp, err := http.Get(url + "/subjects")
if err == nil && resp.StatusCode == 200 {
resp.Body.Close()
return nil
}
if resp != nil {
resp.Body.Close()
}
time.Sleep(2 * time.Second)
}
return fmt.Errorf("schema registry not ready after %d retries", maxRetries)
}
// registerTopicSchema registers a schema for a specific topic
func registerTopicSchema(registryURL, topicName, schemaFormat string) error {
// Determine schema format, default to AVRO
if schemaFormat == "" {
schemaFormat = "AVRO"
}
var schemaStr string
var schemaType string
switch strings.ToUpper(schemaFormat) {
case "AVRO":
schemaStr = schema.GetAvroSchema()
schemaType = "AVRO"
case "JSON", "JSON_SCHEMA":
schemaStr = schema.GetJSONSchema()
schemaType = "JSON"
case "PROTOBUF":
schemaStr = schema.GetProtobufSchema()
schemaType = "PROTOBUF"
default:
return fmt.Errorf("unsupported schema format: %s", schemaFormat)
}
schemaReq := map[string]interface{}{
"schema": schemaStr,
"schemaType": schemaType,
}
jsonData, err := json.Marshal(schemaReq)
if err != nil {
return err
}
// Register schema for topic value
subject := topicName + "-value"
url := fmt.Sprintf("%s/subjects/%s/versions", registryURL, subject)
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Post(url, "application/vnd.schemaregistry.v1+json", bytes.NewBuffer(jsonData))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("schema registration failed: status=%d, body=%s", resp.StatusCode, string(body))
}
log.Printf("Schema registered for topic %s (format: %s)", topicName, schemaType)
return nil
}

View File

@@ -0,0 +1,169 @@
# Kafka Client Load Test Configuration
# Test execution settings
test_mode: "comprehensive" # producer, consumer, comprehensive
duration: "60s" # Test duration (0 = run indefinitely) - producers will stop at this time, consumers get +120s to drain
# Kafka cluster configuration
kafka:
bootstrap_servers:
- "kafka-gateway:9093"
# Security settings (if needed)
security_protocol: "PLAINTEXT" # PLAINTEXT, SSL, SASL_PLAINTEXT, SASL_SSL
sasl_mechanism: "" # PLAIN, SCRAM-SHA-256, SCRAM-SHA-512
sasl_username: ""
sasl_password: ""
# Schema Registry configuration
schema_registry:
url: "http://schema-registry:8081"
auth:
username: ""
password: ""
# Producer configuration
producers:
count: 10 # Number of producer instances
message_rate: 1000 # Messages per second per producer
message_size: 1024 # Message size in bytes
batch_size: 100 # Batch size for batching
linger_ms: 5 # Time to wait for batching
compression_type: "snappy" # none, gzip, snappy, lz4, zstd
acks: "all" # 0, 1, all
retries: 3
retry_backoff_ms: 100
request_timeout_ms: 30000
delivery_timeout_ms: 120000
# Message generation settings
key_distribution: "random" # random, sequential, uuid
value_type: "avro" # json, avro, protobuf, binary
schema_format: "" # AVRO, JSON, PROTOBUF - schema registry format (when schemas enabled)
# Leave empty to auto-distribute formats across topics for testing:
# topic-0: AVRO, topic-1: JSON, topic-2: PROTOBUF, topic-3: AVRO, topic-4: JSON
# Set to specific format (e.g. "AVRO") to use same format for all topics
include_timestamp: true
include_headers: true
# Consumer configuration
consumers:
count: 5 # Number of consumer instances
group_prefix: "loadtest-group" # Consumer group prefix
auto_offset_reset: "earliest" # earliest, latest
enable_auto_commit: true
auto_commit_interval_ms: 100 # Reduced from 1000ms to 100ms to minimize duplicate window
session_timeout_ms: 30000
heartbeat_interval_ms: 3000
max_poll_records: 500
max_poll_interval_ms: 300000
fetch_min_bytes: 1
fetch_max_bytes: 52428800 # 50MB
fetch_max_wait_ms: 100 # 100ms - very fast polling for concurrent fetches and quick drain
# Topic configuration
topics:
count: 5 # Number of topics to create/use
prefix: "loadtest-topic" # Topic name prefix
partitions: 4 # Partitions per topic (default: 4)
replication_factor: 1 # Replication factor
cleanup_policy: "delete" # delete, compact
retention_ms: 604800000 # 7 days
segment_ms: 86400000 # 1 day
# Schema configuration (for Avro/Protobuf tests)
schemas:
enabled: true
registry_timeout_ms: 10000
# Test schemas
user_event:
type: "avro"
schema: |
{
"type": "record",
"name": "UserEvent",
"namespace": "com.seaweedfs.test",
"fields": [
{"name": "user_id", "type": "string"},
{"name": "event_type", "type": "string"},
{"name": "timestamp", "type": "long"},
{"name": "properties", "type": {"type": "map", "values": "string"}}
]
}
transaction:
type: "avro"
schema: |
{
"type": "record",
"name": "Transaction",
"namespace": "com.seaweedfs.test",
"fields": [
{"name": "transaction_id", "type": "string"},
{"name": "amount", "type": "double"},
{"name": "currency", "type": "string"},
{"name": "merchant_id", "type": "string"},
{"name": "timestamp", "type": "long"}
]
}
# Metrics and monitoring
metrics:
enabled: true
collection_interval: "10s"
prometheus_port: 8080
# What to measure
track_latency: true
track_throughput: true
track_errors: true
track_consumer_lag: true
# Latency percentiles to track
latency_percentiles: [50, 90, 95, 99, 99.9]
# Load test scenarios
scenarios:
# Steady state load test
steady_load:
producer_rate: 1000 # messages/sec per producer
ramp_up_time: "30s"
steady_duration: "240s"
ramp_down_time: "30s"
# Burst load test
burst_load:
base_rate: 500
burst_rate: 5000
burst_duration: "10s"
burst_interval: "60s"
# Gradual ramp test
ramp_test:
start_rate: 100
end_rate: 2000
ramp_duration: "300s"
step_duration: "30s"
# Error injection (for resilience testing)
chaos:
enabled: false
producer_failure_rate: 0.01 # 1% of producers fail randomly
consumer_failure_rate: 0.01 # 1% of consumers fail randomly
network_partition_probability: 0.001 # Network issues
broker_restart_interval: "0s" # Restart brokers periodically (0s = disabled)
# Output and reporting
output:
results_dir: "/test-results"
export_prometheus: true
export_csv: true
export_json: true
real_time_stats: true
stats_interval: "30s"
# Logging
logging:
level: "info" # debug, info, warn, error
format: "text" # text, json
enable_kafka_logs: false # Enable Kafka client debug logs

View File

@@ -0,0 +1,46 @@
version: '3.8'
services:
zookeeper:
image: confluentinc/cp-zookeeper:7.5.0
hostname: zookeeper
container_name: compare-zookeeper
ports:
- "2181:2181"
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000
kafka:
image: confluentinc/cp-kafka:7.5.0
hostname: kafka
container_name: compare-kafka
depends_on:
- zookeeper
ports:
- "9092:9092"
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181'
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
KAFKA_LOG_RETENTION_HOURS: 1
KAFKA_LOG_SEGMENT_BYTES: 1073741824
schema-registry:
image: confluentinc/cp-schema-registry:7.5.0
hostname: schema-registry
container_name: compare-schema-registry
depends_on:
- kafka
ports:
- "8082:8081"
environment:
SCHEMA_REGISTRY_HOST_NAME: schema-registry
SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: 'kafka:29092'
SCHEMA_REGISTRY_LISTENERS: http://0.0.0.0:8081

View File

@@ -0,0 +1,337 @@
# SeaweedFS Kafka Client Load Test
# Tests the full stack: Kafka Clients -> SeaweedFS Kafka Gateway -> SeaweedFS MQ Broker -> Storage
x-seaweedfs-build: &seaweedfs-build
build:
context: .
dockerfile: Dockerfile.seaweedfs
args:
TARGETARCH: ${GOARCH:-arm64}
CACHE_BUST: ${CACHE_BUST:-latest}
image: kafka-client-loadtest-seaweedfs
services:
# Schema Registry (for Avro/Protobuf support)
# Using host networking to connect to localhost:9093 (where our gateway advertises)
# WORKAROUND: Schema Registry hangs on empty _schemas topic during bootstrap
# Pre-create the topic first to avoid "wait to catch up" hang
schema-registry-init:
image: confluentinc/cp-kafka:8.0.0
container_name: loadtest-schema-registry-init
networks:
- kafka-loadtest-net
depends_on:
kafka-gateway:
condition: service_healthy
command: >
bash -c "
echo 'Creating _schemas topic...';
kafka-topics --create --topic _schemas --partitions 1 --replication-factor 1 --bootstrap-server kafka-gateway:9093 --if-not-exists || exit 0;
echo '_schemas topic created successfully';
"
schema-registry:
image: confluentinc/cp-schema-registry:8.0.0
container_name: loadtest-schema-registry
restart: on-failure:3
ports:
- "8081:8081"
environment:
SCHEMA_REGISTRY_HOST_NAME: schema-registry
SCHEMA_REGISTRY_HOST_PORT: 8081
SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: 'kafka-gateway:9093'
SCHEMA_REGISTRY_LISTENERS: http://0.0.0.0:8081
SCHEMA_REGISTRY_KAFKASTORE_TOPIC: _schemas
SCHEMA_REGISTRY_DEBUG: "true"
SCHEMA_REGISTRY_SCHEMA_COMPATIBILITY_LEVEL: "full"
SCHEMA_REGISTRY_LEADER_ELIGIBILITY: "true"
SCHEMA_REGISTRY_MODE: "READWRITE"
SCHEMA_REGISTRY_GROUP_ID: "schema-registry"
SCHEMA_REGISTRY_KAFKASTORE_GROUP_ID: "schema-registry"
SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL: "PLAINTEXT"
SCHEMA_REGISTRY_KAFKASTORE_TOPIC_REPLICATION_FACTOR: "1"
SCHEMA_REGISTRY_KAFKASTORE_INIT_TIMEOUT: "120000"
SCHEMA_REGISTRY_KAFKASTORE_TIMEOUT: "60000"
SCHEMA_REGISTRY_REQUEST_TIMEOUT_MS: "60000"
SCHEMA_REGISTRY_RETRY_BACKOFF_MS: "1000"
# Force IPv4 to work around Java IPv6 issues
# Enable verbose logging and set reasonable memory limits
KAFKA_OPTS: "-Djava.net.preferIPv4Stack=true -Djava.net.preferIPv4Addresses=true -Xmx512M -Xms256M"
KAFKA_LOG4J_OPTS: "-Dlog4j.configuration=file:/etc/kafka/log4j.properties"
SCHEMA_REGISTRY_LOG4J_ROOT_LOGLEVEL: "INFO"
SCHEMA_REGISTRY_KAFKASTORE_WRITE_TIMEOUT_MS: "60000"
SCHEMA_REGISTRY_KAFKASTORE_INIT_RETRY_BACKOFF_MS: "5000"
SCHEMA_REGISTRY_KAFKASTORE_CONSUMER_AUTO_OFFSET_RESET: "earliest"
# Enable comprehensive Kafka client DEBUG logging to trace offset management
SCHEMA_REGISTRY_LOG4J_LOGGERS: "org.apache.kafka.clients.consumer.internals.OffsetsRequestManager=DEBUG,org.apache.kafka.clients.consumer.internals.Fetcher=DEBUG,org.apache.kafka.clients.consumer.internals.AbstractFetch=DEBUG,org.apache.kafka.clients.Metadata=DEBUG,org.apache.kafka.common.network=DEBUG"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8081/subjects"]
interval: 15s
timeout: 10s
retries: 10
start_period: 30s
depends_on:
schema-registry-init:
condition: service_completed_successfully
kafka-gateway:
condition: service_healthy
networks:
- kafka-loadtest-net
# SeaweedFS Master (coordinator)
seaweedfs-master:
<<: *seaweedfs-build
container_name: loadtest-seaweedfs-master
ports:
- "9333:9333"
- "19333:19333"
command:
- master
- -ip=seaweedfs-master
- -port=9333
- -port.grpc=19333
- -volumeSizeLimitMB=48
- -defaultReplication=000
- -garbageThreshold=0.3
volumes:
- ./data/seaweedfs-master:/data
healthcheck:
test: ["CMD-SHELL", "wget --quiet --tries=1 --spider http://seaweedfs-master:9333/cluster/status || exit 1"]
interval: 10s
timeout: 5s
retries: 10
start_period: 20s
networks:
- kafka-loadtest-net
# SeaweedFS Volume Server (storage)
seaweedfs-volume:
<<: *seaweedfs-build
container_name: loadtest-seaweedfs-volume
ports:
- "8080:8080"
- "18080:18080"
command:
- volume
- -master=seaweedfs-master:9333
- -ip=seaweedfs-volume
- -port=8080
- -port.grpc=18080
- -publicUrl=seaweedfs-volume:8080
- -preStopSeconds=1
- -compactionMBps=50
- -max=0
- -dir=/data
depends_on:
seaweedfs-master:
condition: service_healthy
volumes:
- ./data/seaweedfs-volume:/data
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://seaweedfs-volume:8080/status"]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
networks:
- kafka-loadtest-net
# SeaweedFS Filer (metadata)
seaweedfs-filer:
<<: *seaweedfs-build
container_name: loadtest-seaweedfs-filer
ports:
- "8888:8888"
- "18888:18888"
- "18889:18889"
command:
- filer
- -master=seaweedfs-master:9333
- -ip=seaweedfs-filer
- -port=8888
- -port.grpc=18888
- -metricsPort=18889
- -defaultReplicaPlacement=000
depends_on:
seaweedfs-master:
condition: service_healthy
seaweedfs-volume:
condition: service_healthy
volumes:
- ./data/seaweedfs-filer:/data
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://seaweedfs-filer:8888/"]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
networks:
- kafka-loadtest-net
# SeaweedFS MQ Broker (message handling)
seaweedfs-mq-broker:
<<: *seaweedfs-build
container_name: loadtest-seaweedfs-mq-broker
ports:
- "17777:17777"
- "18777:18777" # pprof profiling port
command:
- mq.broker
- -master=seaweedfs-master:9333
- -ip=seaweedfs-mq-broker
- -port=17777
- -logFlushInterval=0
- -debug
- -debug.port=18777
depends_on:
seaweedfs-filer:
condition: service_healthy
volumes:
- ./data/seaweedfs-mq:/data
healthcheck:
test: ["CMD", "nc", "-z", "localhost", "17777"]
interval: 10s
timeout: 5s
retries: 5
start_period: 20s
networks:
- kafka-loadtest-net
# SeaweedFS Kafka Gateway (Kafka protocol compatibility)
kafka-gateway:
<<: *seaweedfs-build
container_name: loadtest-kafka-gateway
ports:
- "9093:9093"
- "10093:10093" # pprof profiling port
command:
- mq.kafka.gateway
- -master=seaweedfs-master:9333
- -ip=kafka-gateway
- -ip.bind=0.0.0.0
- -port=9093
- -default-partitions=4
- -schema-registry-url=http://schema-registry:8081
- -port.pprof=10093
depends_on:
seaweedfs-filer:
condition: service_healthy
seaweedfs-mq-broker:
condition: service_healthy
environment:
- SEAWEEDFS_MASTERS=seaweedfs-master:9333
# - KAFKA_DEBUG=1 # Enable debug logging for Schema Registry troubleshooting
- KAFKA_ADVERTISED_HOST=kafka-gateway
volumes:
- ./data/kafka-gateway:/data
healthcheck:
test: ["CMD", "nc", "-z", "localhost", "9093"]
interval: 10s
timeout: 5s
retries: 10
start_period: 45s # Increased to account for 10s startup delay + filer discovery
networks:
- kafka-loadtest-net
# Kafka Client Load Test Runner
kafka-client-loadtest:
build:
context: ../../..
dockerfile: test/kafka/kafka-client-loadtest/Dockerfile.loadtest
container_name: kafka-client-loadtest-runner
depends_on:
kafka-gateway:
condition: service_healthy
# schema-registry:
# condition: service_healthy
environment:
- KAFKA_BOOTSTRAP_SERVERS=kafka-gateway:9093
- SCHEMA_REGISTRY_URL=http://schema-registry:8081
- TEST_DURATION=${TEST_DURATION:-300s}
- PRODUCER_COUNT=${PRODUCER_COUNT:-10}
- CONSUMER_COUNT=${CONSUMER_COUNT:-5}
- MESSAGE_RATE=${MESSAGE_RATE:-1000}
- MESSAGE_SIZE=${MESSAGE_SIZE:-1024}
- TOPIC_COUNT=${TOPIC_COUNT:-5}
- PARTITIONS_PER_TOPIC=${PARTITIONS_PER_TOPIC:-3}
- TEST_MODE=${TEST_MODE:-comprehensive}
- SCHEMAS_ENABLED=${SCHEMAS_ENABLED:-true}
- VALUE_TYPE=${VALUE_TYPE:-avro}
profiles:
- loadtest
volumes:
- ./test-results:/test-results
networks:
- kafka-loadtest-net
# Monitoring and Metrics
prometheus:
image: prom/prometheus:latest
container_name: loadtest-prometheus
ports:
- "9090:9090"
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
networks:
- kafka-loadtest-net
profiles:
- monitoring
grafana:
image: grafana/grafana:latest
container_name: loadtest-grafana
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- ./monitoring/grafana/dashboards:/var/lib/grafana/dashboards
- ./monitoring/grafana/provisioning:/etc/grafana/provisioning
- grafana-data:/var/lib/grafana
networks:
- kafka-loadtest-net
profiles:
- monitoring
# Schema Registry Debug Runner
schema-registry-debug:
build:
context: debug-client
dockerfile: Dockerfile
container_name: schema-registry-debug-runner
depends_on:
kafka-gateway:
condition: service_healthy
networks:
- kafka-loadtest-net
profiles:
- debug
# SeekToBeginning test - reproduces the hang issue
seek-test:
build:
context: .
dockerfile: Dockerfile.seektest
container_name: loadtest-seek-test
depends_on:
kafka-gateway:
condition: service_healthy
schema-registry:
condition: service_healthy
environment:
- KAFKA_BOOTSTRAP_SERVERS=kafka-gateway:9093
networks:
- kafka-loadtest-net
entrypoint: ["java", "-cp", "target/seek-test.jar", "SeekToBeginningTest"]
command: ["kafka-gateway:9093"]
volumes:
prometheus-data:
grafana-data:
networks:
kafka-loadtest-net:
driver: bridge
name: kafka-client-loadtest

View File

@@ -0,0 +1,39 @@
module github.com/seaweedfs/seaweedfs/test/kafka/kafka-client-loadtest
go 1.25.0
require (
github.com/IBM/sarama v1.46.1
github.com/linkedin/goavro/v2 v2.14.0
github.com/prometheus/client_golang v1.23.2
google.golang.org/protobuf v1.36.8
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/eapache/go-resiliency v1.7.0 // indirect
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect
github.com/eapache/queue v1.1.0 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
github.com/jcmturner/gofork v1.7.6 // indirect
github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/crypto v0.52.0 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/sys v0.45.0 // indirect
)

View File

@@ -0,0 +1,129 @@
github.com/IBM/sarama v1.46.1 h1:AlDkvyQm4LKktoQZxv0sbTfH3xukeH7r/UFBbUmFV9M=
github.com/IBM/sarama v1.46.1/go.mod h1:ipyOREIx+o9rMSrrPGLZHGuT0mzecNzKd19Quq+Q8AA=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA=
github.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho=
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws=
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0=
github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/linkedin/goavro/v2 v2.14.0 h1:aNO/js65U+Mwq4yB5f1h01c3wiM458qtRad1DN0CMUI=
github.com/linkedin/goavro/v2 v2.14.0/go.mod h1:KXx+erlq+RPlGSPmLF7xGo6SAbh8sCQ53x064+ioxhk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -0,0 +1,361 @@
package config
import (
"fmt"
"os"
"strconv"
"strings"
"time"
"gopkg.in/yaml.v3"
)
// Config represents the complete load test configuration
type Config struct {
TestMode string `yaml:"test_mode"`
Duration time.Duration `yaml:"duration"`
Kafka KafkaConfig `yaml:"kafka"`
SchemaRegistry SchemaRegistryConfig `yaml:"schema_registry"`
Producers ProducersConfig `yaml:"producers"`
Consumers ConsumersConfig `yaml:"consumers"`
Topics TopicsConfig `yaml:"topics"`
Schemas SchemasConfig `yaml:"schemas"`
Metrics MetricsConfig `yaml:"metrics"`
Scenarios ScenariosConfig `yaml:"scenarios"`
Chaos ChaosConfig `yaml:"chaos"`
Output OutputConfig `yaml:"output"`
Logging LoggingConfig `yaml:"logging"`
}
type KafkaConfig struct {
BootstrapServers []string `yaml:"bootstrap_servers"`
SecurityProtocol string `yaml:"security_protocol"`
SASLMechanism string `yaml:"sasl_mechanism"`
SASLUsername string `yaml:"sasl_username"`
SASLPassword string `yaml:"sasl_password"`
}
type SchemaRegistryConfig struct {
URL string `yaml:"url"`
Auth struct {
Username string `yaml:"username"`
Password string `yaml:"password"`
} `yaml:"auth"`
}
type ProducersConfig struct {
Count int `yaml:"count"`
MessageRate int `yaml:"message_rate"`
MessageSize int `yaml:"message_size"`
BatchSize int `yaml:"batch_size"`
LingerMs int `yaml:"linger_ms"`
CompressionType string `yaml:"compression_type"`
Acks string `yaml:"acks"`
Retries int `yaml:"retries"`
RetryBackoffMs int `yaml:"retry_backoff_ms"`
RequestTimeoutMs int `yaml:"request_timeout_ms"`
DeliveryTimeoutMs int `yaml:"delivery_timeout_ms"`
KeyDistribution string `yaml:"key_distribution"`
ValueType string `yaml:"value_type"` // json, avro, protobuf, binary
SchemaFormat string `yaml:"schema_format"` // AVRO, JSON, PROTOBUF (schema registry format)
IncludeTimestamp bool `yaml:"include_timestamp"`
IncludeHeaders bool `yaml:"include_headers"`
}
type ConsumersConfig struct {
Count int `yaml:"count"`
GroupPrefix string `yaml:"group_prefix"`
AutoOffsetReset string `yaml:"auto_offset_reset"`
EnableAutoCommit bool `yaml:"enable_auto_commit"`
AutoCommitIntervalMs int `yaml:"auto_commit_interval_ms"`
SessionTimeoutMs int `yaml:"session_timeout_ms"`
HeartbeatIntervalMs int `yaml:"heartbeat_interval_ms"`
MaxPollRecords int `yaml:"max_poll_records"`
MaxPollIntervalMs int `yaml:"max_poll_interval_ms"`
FetchMinBytes int `yaml:"fetch_min_bytes"`
FetchMaxBytes int `yaml:"fetch_max_bytes"`
FetchMaxWaitMs int `yaml:"fetch_max_wait_ms"`
}
type TopicsConfig struct {
Count int `yaml:"count"`
Prefix string `yaml:"prefix"`
Partitions int `yaml:"partitions"`
ReplicationFactor int `yaml:"replication_factor"`
CleanupPolicy string `yaml:"cleanup_policy"`
RetentionMs int64 `yaml:"retention_ms"`
SegmentMs int64 `yaml:"segment_ms"`
}
type SchemaConfig struct {
Type string `yaml:"type"`
Schema string `yaml:"schema"`
}
type SchemasConfig struct {
Enabled bool `yaml:"enabled"`
RegistryTimeoutMs int `yaml:"registry_timeout_ms"`
UserEvent SchemaConfig `yaml:"user_event"`
Transaction SchemaConfig `yaml:"transaction"`
}
type MetricsConfig struct {
Enabled bool `yaml:"enabled"`
CollectionInterval time.Duration `yaml:"collection_interval"`
PrometheusPort int `yaml:"prometheus_port"`
TrackLatency bool `yaml:"track_latency"`
TrackThroughput bool `yaml:"track_throughput"`
TrackErrors bool `yaml:"track_errors"`
TrackConsumerLag bool `yaml:"track_consumer_lag"`
LatencyPercentiles []float64 `yaml:"latency_percentiles"`
}
type ScenarioConfig struct {
ProducerRate int `yaml:"producer_rate"`
RampUpTime time.Duration `yaml:"ramp_up_time"`
SteadyDuration time.Duration `yaml:"steady_duration"`
RampDownTime time.Duration `yaml:"ramp_down_time"`
BaseRate int `yaml:"base_rate"`
BurstRate int `yaml:"burst_rate"`
BurstDuration time.Duration `yaml:"burst_duration"`
BurstInterval time.Duration `yaml:"burst_interval"`
StartRate int `yaml:"start_rate"`
EndRate int `yaml:"end_rate"`
RampDuration time.Duration `yaml:"ramp_duration"`
StepDuration time.Duration `yaml:"step_duration"`
}
type ScenariosConfig struct {
SteadyLoad ScenarioConfig `yaml:"steady_load"`
BurstLoad ScenarioConfig `yaml:"burst_load"`
RampTest ScenarioConfig `yaml:"ramp_test"`
}
type ChaosConfig struct {
Enabled bool `yaml:"enabled"`
ProducerFailureRate float64 `yaml:"producer_failure_rate"`
ConsumerFailureRate float64 `yaml:"consumer_failure_rate"`
NetworkPartitionProbability float64 `yaml:"network_partition_probability"`
BrokerRestartInterval time.Duration `yaml:"broker_restart_interval"`
}
type OutputConfig struct {
ResultsDir string `yaml:"results_dir"`
ExportPrometheus bool `yaml:"export_prometheus"`
ExportCSV bool `yaml:"export_csv"`
ExportJSON bool `yaml:"export_json"`
RealTimeStats bool `yaml:"real_time_stats"`
StatsInterval time.Duration `yaml:"stats_interval"`
}
type LoggingConfig struct {
Level string `yaml:"level"`
Format string `yaml:"format"`
EnableKafkaLogs bool `yaml:"enable_kafka_logs"`
}
// Load reads and parses the configuration file
func Load(configFile string) (*Config, error) {
data, err := os.ReadFile(configFile)
if err != nil {
return nil, fmt.Errorf("failed to read config file %s: %w", configFile, err)
}
var cfg Config
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("failed to parse config file %s: %w", configFile, err)
}
// Apply default values
cfg.setDefaults()
// Apply environment variable overrides
cfg.applyEnvOverrides()
return &cfg, nil
}
// ApplyOverrides applies command-line flag overrides
func (c *Config) ApplyOverrides(testMode string, duration time.Duration) {
if testMode != "" {
c.TestMode = testMode
}
if duration > 0 {
c.Duration = duration
}
}
// setDefaults sets default values for optional fields
func (c *Config) setDefaults() {
if c.TestMode == "" {
c.TestMode = "comprehensive"
}
if len(c.Kafka.BootstrapServers) == 0 {
c.Kafka.BootstrapServers = []string{"kafka-gateway:9093"}
}
if c.SchemaRegistry.URL == "" {
c.SchemaRegistry.URL = "http://schema-registry:8081"
}
// Schema support is always enabled since Kafka Gateway now enforces schema-first behavior
c.Schemas.Enabled = true
if c.Producers.Count == 0 {
c.Producers.Count = 10
}
if c.Consumers.Count == 0 {
c.Consumers.Count = 5
}
if c.Topics.Count == 0 {
c.Topics.Count = 5
}
if c.Topics.Prefix == "" {
c.Topics.Prefix = "loadtest-topic"
}
if c.Topics.Partitions == 0 {
c.Topics.Partitions = 4 // Default to 4 partitions
}
if c.Topics.ReplicationFactor == 0 {
c.Topics.ReplicationFactor = 1 // Default to 1 replica
}
if c.Consumers.GroupPrefix == "" {
c.Consumers.GroupPrefix = "loadtest-group"
}
if c.Output.ResultsDir == "" {
c.Output.ResultsDir = "/test-results"
}
if c.Metrics.CollectionInterval == 0 {
c.Metrics.CollectionInterval = 10 * time.Second
}
if c.Output.StatsInterval == 0 {
c.Output.StatsInterval = 30 * time.Second
}
}
// applyEnvOverrides applies environment variable overrides
func (c *Config) applyEnvOverrides() {
if servers := os.Getenv("KAFKA_BOOTSTRAP_SERVERS"); servers != "" {
c.Kafka.BootstrapServers = strings.Split(servers, ",")
}
if url := os.Getenv("SCHEMA_REGISTRY_URL"); url != "" {
c.SchemaRegistry.URL = url
}
if mode := os.Getenv("TEST_MODE"); mode != "" {
c.TestMode = mode
}
if duration := os.Getenv("TEST_DURATION"); duration != "" {
if d, err := time.ParseDuration(duration); err == nil {
c.Duration = d
}
}
if count := os.Getenv("PRODUCER_COUNT"); count != "" {
if i, err := strconv.Atoi(count); err == nil {
c.Producers.Count = i
}
}
if count := os.Getenv("CONSUMER_COUNT"); count != "" {
if i, err := strconv.Atoi(count); err == nil {
c.Consumers.Count = i
}
}
if rate := os.Getenv("MESSAGE_RATE"); rate != "" {
if i, err := strconv.Atoi(rate); err == nil {
c.Producers.MessageRate = i
}
}
if size := os.Getenv("MESSAGE_SIZE"); size != "" {
if i, err := strconv.Atoi(size); err == nil {
c.Producers.MessageSize = i
}
}
if count := os.Getenv("TOPIC_COUNT"); count != "" {
if i, err := strconv.Atoi(count); err == nil {
c.Topics.Count = i
}
}
if partitions := os.Getenv("PARTITIONS_PER_TOPIC"); partitions != "" {
if i, err := strconv.Atoi(partitions); err == nil {
c.Topics.Partitions = i
}
}
if valueType := os.Getenv("VALUE_TYPE"); valueType != "" {
c.Producers.ValueType = valueType
}
if schemaFormat := os.Getenv("SCHEMA_FORMAT"); schemaFormat != "" {
c.Producers.SchemaFormat = schemaFormat
}
if enabled := os.Getenv("SCHEMAS_ENABLED"); enabled != "" {
c.Schemas.Enabled = enabled == "true"
}
}
// GetTopicNames returns the list of topic names to use for testing
func (c *Config) GetTopicNames() []string {
topics := make([]string, c.Topics.Count)
for i := 0; i < c.Topics.Count; i++ {
topics[i] = fmt.Sprintf("%s-%d", c.Topics.Prefix, i)
}
return topics
}
// GetConsumerGroupNames returns the list of consumer group names
func (c *Config) GetConsumerGroupNames() []string {
groups := make([]string, c.Consumers.Count)
for i := 0; i < c.Consumers.Count; i++ {
groups[i] = fmt.Sprintf("%s-%d", c.Consumers.GroupPrefix, i)
}
return groups
}
// Validate validates the configuration
func (c *Config) Validate() error {
if c.TestMode != "producer" && c.TestMode != "consumer" && c.TestMode != "comprehensive" {
return fmt.Errorf("invalid test mode: %s", c.TestMode)
}
if len(c.Kafka.BootstrapServers) == 0 {
return fmt.Errorf("kafka bootstrap servers not specified")
}
if c.Producers.Count <= 0 && (c.TestMode == "producer" || c.TestMode == "comprehensive") {
return fmt.Errorf("producer count must be greater than 0 for producer or comprehensive tests")
}
if c.Consumers.Count <= 0 && (c.TestMode == "consumer" || c.TestMode == "comprehensive") {
return fmt.Errorf("consumer count must be greater than 0 for consumer or comprehensive tests")
}
if c.Topics.Count <= 0 {
return fmt.Errorf("topic count must be greater than 0")
}
if c.Topics.Partitions <= 0 {
return fmt.Errorf("partitions per topic must be greater than 0")
}
return nil
}

View File

@@ -0,0 +1,776 @@
package consumer
import (
"context"
"encoding/binary"
"encoding/json"
"fmt"
"log"
"os"
"strings"
"sync"
"time"
"github.com/IBM/sarama"
"github.com/linkedin/goavro/v2"
"github.com/seaweedfs/seaweedfs/test/kafka/kafka-client-loadtest/internal/config"
"github.com/seaweedfs/seaweedfs/test/kafka/kafka-client-loadtest/internal/metrics"
pb "github.com/seaweedfs/seaweedfs/test/kafka/kafka-client-loadtest/internal/schema/pb"
"github.com/seaweedfs/seaweedfs/test/kafka/kafka-client-loadtest/internal/tracker"
"google.golang.org/protobuf/proto"
)
// Consumer represents a Kafka consumer for load testing
type Consumer struct {
id int
config *config.Config
metricsCollector *metrics.Collector
saramaConsumer sarama.ConsumerGroup
useConfluent bool // Always false, Sarama only
topics []string
consumerGroup string
avroCodec *goavro.Codec
// Schema format tracking per topic
schemaFormats map[string]string // topic -> schema format mapping (AVRO, JSON, PROTOBUF)
// Processing tracking
messagesProcessed int64
lastOffset map[string]map[int32]int64
offsetMutex sync.RWMutex
// Record tracking
tracker *tracker.Tracker
}
// New creates a new consumer instance
func New(cfg *config.Config, collector *metrics.Collector, id int, recordTracker *tracker.Tracker) (*Consumer, error) {
// All consumers share the same group for load balancing across partitions
consumerGroup := cfg.Consumers.GroupPrefix
c := &Consumer{
id: id,
config: cfg,
metricsCollector: collector,
topics: cfg.GetTopicNames(),
consumerGroup: consumerGroup,
useConfluent: false, // Use Sarama by default
lastOffset: make(map[string]map[int32]int64),
schemaFormats: make(map[string]string),
tracker: recordTracker,
}
// Initialize schema formats for each topic (must match producer logic)
// This mirrors the format distribution in cmd/loadtest/main.go registerSchemas()
for i, topic := range c.topics {
var schemaFormat string
if cfg.Producers.SchemaFormat != "" {
// Use explicit config if provided
schemaFormat = cfg.Producers.SchemaFormat
} else {
// Distribute across formats (same as producer)
switch i % 3 {
case 0:
schemaFormat = "AVRO"
case 1:
schemaFormat = "JSON"
case 2:
schemaFormat = "PROTOBUF"
}
}
c.schemaFormats[topic] = schemaFormat
log.Printf("Consumer %d: Topic %s will use schema format: %s", id, topic, schemaFormat)
}
// Initialize consumer based on configuration
if c.useConfluent {
if err := c.initConfluentConsumer(); err != nil {
return nil, fmt.Errorf("failed to initialize Confluent consumer: %w", err)
}
} else {
if err := c.initSaramaConsumer(); err != nil {
return nil, fmt.Errorf("failed to initialize Sarama consumer: %w", err)
}
}
// Initialize Avro codec if schemas are enabled
if cfg.Schemas.Enabled {
if err := c.initAvroCodec(); err != nil {
return nil, fmt.Errorf("failed to initialize Avro codec: %w", err)
}
}
log.Printf("Consumer %d initialized for group %s", id, consumerGroup)
return c, nil
}
// initSaramaConsumer initializes the Sarama consumer group
func (c *Consumer) initSaramaConsumer() error {
config := sarama.NewConfig()
// Enable Sarama debug logging to diagnose connection issues
sarama.Logger = log.New(os.Stdout, fmt.Sprintf("[Sarama Consumer %d] ", c.id), log.LstdFlags)
// Consumer configuration
config.Consumer.Return.Errors = true
config.Consumer.Offsets.Initial = sarama.OffsetOldest
if c.config.Consumers.AutoOffsetReset == "latest" {
config.Consumer.Offsets.Initial = sarama.OffsetNewest
}
// Auto commit configuration
config.Consumer.Offsets.AutoCommit.Enable = c.config.Consumers.EnableAutoCommit
config.Consumer.Offsets.AutoCommit.Interval = time.Duration(c.config.Consumers.AutoCommitIntervalMs) * time.Millisecond
// Session and heartbeat configuration
config.Consumer.Group.Session.Timeout = time.Duration(c.config.Consumers.SessionTimeoutMs) * time.Millisecond
config.Consumer.Group.Heartbeat.Interval = time.Duration(c.config.Consumers.HeartbeatIntervalMs) * time.Millisecond
// Fetch configuration
config.Consumer.Fetch.Min = int32(c.config.Consumers.FetchMinBytes)
config.Consumer.Fetch.Default = 10 * 1024 * 1024 // 10MB per partition (increased from 1MB default)
config.Consumer.Fetch.Max = int32(c.config.Consumers.FetchMaxBytes)
config.Consumer.MaxWaitTime = time.Duration(c.config.Consumers.FetchMaxWaitMs) * time.Millisecond
config.Consumer.MaxProcessingTime = time.Duration(c.config.Consumers.MaxPollIntervalMs) * time.Millisecond
// Channel buffer sizes for concurrent partition consumption
config.ChannelBufferSize = 256 // Increase from default 256 to allow more buffering
// Enable concurrent partition fetching by increasing the number of broker connections
// This allows Sarama to fetch from multiple partitions in parallel
config.Net.MaxOpenRequests = 20 // Increase from default 5 to allow 20 concurrent requests
// Connection retry and timeout configuration
config.Net.DialTimeout = 30 * time.Second // Increase from default 30s
config.Net.ReadTimeout = 30 * time.Second // Increase from default 30s
config.Net.WriteTimeout = 30 * time.Second // Increase from default 30s
config.Metadata.Retry.Max = 5 // Retry metadata fetch up to 5 times
config.Metadata.Retry.Backoff = 500 * time.Millisecond
config.Metadata.Timeout = 30 * time.Second // Increase metadata timeout
// Version
config.Version = sarama.V2_8_0_0
// CRITICAL: Set unique ClientID to ensure each consumer gets a unique member ID
// Without this, all consumers from the same process get the same member ID and only 1 joins!
// Sarama uses ClientID as part of the member ID generation
// Use consumer ID directly - no timestamp needed since IDs are already unique per process
config.ClientID = fmt.Sprintf("loadtest-consumer-%d", c.id)
log.Printf("Consumer %d: Setting Sarama ClientID to: %s", c.id, config.ClientID)
// Create consumer group
consumerGroup, err := sarama.NewConsumerGroup(c.config.Kafka.BootstrapServers, c.consumerGroup, config)
if err != nil {
return fmt.Errorf("failed to create Sarama consumer group: %w", err)
}
c.saramaConsumer = consumerGroup
return nil
}
// initConfluentConsumer initializes the Confluent Kafka Go consumer
func (c *Consumer) initConfluentConsumer() error {
// Confluent consumer disabled, using Sarama only
return fmt.Errorf("confluent consumer not enabled")
}
// initAvroCodec initializes the Avro codec for schema-based messages
func (c *Consumer) initAvroCodec() error {
// Use the LoadTestMessage schema (matches what producer uses)
loadTestSchema := `{
"type": "record",
"name": "LoadTestMessage",
"namespace": "com.seaweedfs.loadtest",
"fields": [
{"name": "id", "type": "string"},
{"name": "timestamp", "type": "long"},
{"name": "producer_id", "type": "int"},
{"name": "counter", "type": "long"},
{"name": "user_id", "type": "string"},
{"name": "event_type", "type": "string"},
{"name": "properties", "type": {"type": "map", "values": "string"}}
]
}`
codec, err := goavro.NewCodec(loadTestSchema)
if err != nil {
return fmt.Errorf("failed to create Avro codec: %w", err)
}
c.avroCodec = codec
return nil
}
// Run starts the consumer and consumes messages until the context is cancelled
func (c *Consumer) Run(ctx context.Context) {
log.Printf("Consumer %d starting for group %s", c.id, c.consumerGroup)
defer log.Printf("Consumer %d stopped", c.id)
if c.useConfluent {
c.runConfluentConsumer(ctx)
} else {
c.runSaramaConsumer(ctx)
}
}
// runSaramaConsumer runs the Sarama consumer group
func (c *Consumer) runSaramaConsumer(ctx context.Context) {
handler := &ConsumerGroupHandler{
consumer: c,
}
var wg sync.WaitGroup
// Start error handler
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case err, ok := <-c.saramaConsumer.Errors():
if !ok {
return
}
log.Printf("Consumer %d error: %v", c.id, err)
c.metricsCollector.RecordConsumerError()
case <-ctx.Done():
return
}
}
}()
// Start consumer group session
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
default:
if err := c.saramaConsumer.Consume(ctx, c.topics, handler); err != nil {
log.Printf("Consumer %d: Error consuming: %v", c.id, err)
c.metricsCollector.RecordConsumerError()
// Wait briefly before retrying (reduced from 5s to 1s for faster recovery)
select {
case <-time.After(1 * time.Second):
case <-ctx.Done():
return
}
}
}
}
}()
// Start lag monitoring
wg.Add(1)
go func() {
defer wg.Done()
c.monitorConsumerLag(ctx)
}()
// Wait for completion
<-ctx.Done()
log.Printf("Consumer %d: Context cancelled, shutting down", c.id)
wg.Wait()
}
// runConfluentConsumer runs the Confluent consumer
func (c *Consumer) runConfluentConsumer(ctx context.Context) {
// Confluent consumer disabled, using Sarama only
log.Printf("Consumer %d: Confluent consumer not enabled", c.id)
}
// processMessage processes a consumed message
func (c *Consumer) processMessage(topicPtr *string, partition int32, offset int64, key, value []byte) error {
topic := ""
if topicPtr != nil {
topic = *topicPtr
}
// Update offset tracking
c.updateOffset(topic, partition, offset)
// Decode message based on topic-specific schema format
var decodedMessage interface{}
var err error
// Determine schema format for this topic (if schemas are enabled)
var schemaFormat string
if c.config.Schemas.Enabled {
schemaFormat = c.schemaFormats[topic]
if schemaFormat == "" {
// Fallback to config if topic not in map
schemaFormat = c.config.Producers.ValueType
}
} else {
// No schemas, use global value type
schemaFormat = c.config.Producers.ValueType
}
// Decode message based on format
switch schemaFormat {
case "avro", "AVRO":
decodedMessage, err = c.decodeAvroMessage(value)
case "json", "JSON", "JSON_SCHEMA":
decodedMessage, err = c.decodeJSONSchemaMessage(value)
case "protobuf", "PROTOBUF":
decodedMessage, err = c.decodeProtobufMessage(value)
case "binary":
decodedMessage, err = c.decodeBinaryMessage(value)
default:
// Fallback to plain JSON
decodedMessage, err = c.decodeJSONMessage(value)
}
if err != nil {
return fmt.Errorf("failed to decode message: %w", err)
}
// Note: Removed artificial delay to allow maximum throughput
// If you need to simulate processing time, add a configurable delay setting
// time.Sleep(time.Millisecond) // Minimal processing delay
// Record metrics
c.metricsCollector.RecordConsumedMessage(len(value))
c.messagesProcessed++
// Log progress
if c.id == 0 && c.messagesProcessed%1000 == 0 {
log.Printf("Consumer %d: Processed %d messages (latest: %s[%d]@%d)",
c.id, c.messagesProcessed, topic, partition, offset)
}
// Optional: Validate message content (for testing purposes)
if c.config.Chaos.Enabled {
if err := c.validateMessage(decodedMessage); err != nil {
log.Printf("Consumer %d: Message validation failed: %v", c.id, err)
}
}
return nil
}
// decodeJSONMessage decodes a JSON message
func (c *Consumer) decodeJSONMessage(value []byte) (interface{}, error) {
var message map[string]interface{}
if err := json.Unmarshal(value, &message); err != nil {
// DEBUG: Log the raw bytes when JSON parsing fails
log.Printf("Consumer %d: JSON decode failed. Length: %d, Raw bytes (hex): %x, Raw string: %q, Error: %v",
c.id, len(value), value, string(value), err)
return nil, err
}
return message, nil
}
// decodeAvroMessage decodes an Avro message (handles Confluent Wire Format)
func (c *Consumer) decodeAvroMessage(value []byte) (interface{}, error) {
if c.avroCodec == nil {
return nil, fmt.Errorf("Avro codec not initialized")
}
// Handle Confluent Wire Format when schemas are enabled
var avroData []byte
if c.config.Schemas.Enabled {
if len(value) < 5 {
return nil, fmt.Errorf("message too short for Confluent Wire Format: %d bytes", len(value))
}
// Check magic byte (should be 0)
if value[0] != 0 {
return nil, fmt.Errorf("invalid Confluent Wire Format magic byte: %d", value[0])
}
// Extract schema ID (bytes 1-4, big-endian)
schemaID := binary.BigEndian.Uint32(value[1:5])
_ = schemaID // TODO: Could validate schema ID matches expected schema
// Extract Avro data (bytes 5+)
avroData = value[5:]
} else {
// No wire format, use raw data
avroData = value
}
native, _, err := c.avroCodec.NativeFromBinary(avroData)
if err != nil {
return nil, fmt.Errorf("failed to decode Avro data: %w", err)
}
return native, nil
}
// decodeJSONSchemaMessage decodes a JSON Schema message (handles Confluent Wire Format)
func (c *Consumer) decodeJSONSchemaMessage(value []byte) (interface{}, error) {
// Handle Confluent Wire Format when schemas are enabled
var jsonData []byte
if c.config.Schemas.Enabled {
if len(value) < 5 {
return nil, fmt.Errorf("message too short for Confluent Wire Format: %d bytes", len(value))
}
// Check magic byte (should be 0)
if value[0] != 0 {
return nil, fmt.Errorf("invalid Confluent Wire Format magic byte: %d", value[0])
}
// Extract schema ID (bytes 1-4, big-endian)
schemaID := binary.BigEndian.Uint32(value[1:5])
_ = schemaID // TODO: Could validate schema ID matches expected schema
// Extract JSON data (bytes 5+)
jsonData = value[5:]
} else {
// No wire format, use raw data
jsonData = value
}
// Decode JSON
var message map[string]interface{}
if err := json.Unmarshal(jsonData, &message); err != nil {
return nil, fmt.Errorf("failed to decode JSON data: %w", err)
}
return message, nil
}
// decodeProtobufMessage decodes a Protobuf message (handles Confluent Wire Format)
func (c *Consumer) decodeProtobufMessage(value []byte) (interface{}, error) {
// Handle Confluent Wire Format when schemas are enabled
var protoData []byte
if c.config.Schemas.Enabled {
if len(value) < 5 {
return nil, fmt.Errorf("message too short for Confluent Wire Format: %d bytes", len(value))
}
// Check magic byte (should be 0)
if value[0] != 0 {
return nil, fmt.Errorf("invalid Confluent Wire Format magic byte: %d", value[0])
}
// Extract schema ID (bytes 1-4, big-endian)
schemaID := binary.BigEndian.Uint32(value[1:5])
_ = schemaID // TODO: Could validate schema ID matches expected schema
// Extract Protobuf data (bytes 5+)
protoData = value[5:]
} else {
// No wire format, use raw data
protoData = value
}
// Unmarshal protobuf message
var protoMsg pb.LoadTestMessage
if err := proto.Unmarshal(protoData, &protoMsg); err != nil {
return nil, fmt.Errorf("failed to unmarshal Protobuf data: %w", err)
}
// Convert to map for consistency with other decoders
return map[string]interface{}{
"id": protoMsg.Id,
"timestamp": protoMsg.Timestamp,
"producer_id": protoMsg.ProducerId,
"counter": protoMsg.Counter,
"user_id": protoMsg.UserId,
"event_type": protoMsg.EventType,
"properties": protoMsg.Properties,
}, nil
}
// decodeBinaryMessage decodes a binary message
func (c *Consumer) decodeBinaryMessage(value []byte) (interface{}, error) {
if len(value) < 20 {
return nil, fmt.Errorf("binary message too short")
}
// Extract fields from the binary format:
// [producer_id:4][counter:8][timestamp:8][random_data:...]
producerID := int(value[0])<<24 | int(value[1])<<16 | int(value[2])<<8 | int(value[3])
var counter int64
for i := 0; i < 8; i++ {
counter |= int64(value[4+i]) << (56 - i*8)
}
var timestamp int64
for i := 0; i < 8; i++ {
timestamp |= int64(value[12+i]) << (56 - i*8)
}
return map[string]interface{}{
"producer_id": producerID,
"counter": counter,
"timestamp": timestamp,
"data_size": len(value),
}, nil
}
// validateMessage performs basic message validation
func (c *Consumer) validateMessage(message interface{}) error {
// This is a placeholder for message validation logic
// In a real load test, you might validate:
// - Message structure
// - Required fields
// - Data consistency
// - Schema compliance
if message == nil {
return fmt.Errorf("message is nil")
}
return nil
}
// updateOffset updates the last seen offset for lag calculation
func (c *Consumer) updateOffset(topic string, partition int32, offset int64) {
c.offsetMutex.Lock()
defer c.offsetMutex.Unlock()
if c.lastOffset[topic] == nil {
c.lastOffset[topic] = make(map[int32]int64)
}
c.lastOffset[topic][partition] = offset
}
// monitorConsumerLag monitors and reports consumer lag
func (c *Consumer) monitorConsumerLag(ctx context.Context) {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
c.reportConsumerLag()
}
}
}
// reportConsumerLag calculates and reports consumer lag
func (c *Consumer) reportConsumerLag() {
// This is a simplified lag calculation
// In a real implementation, you would query the broker for high water marks
c.offsetMutex.RLock()
defer c.offsetMutex.RUnlock()
for topic, partitions := range c.lastOffset {
for partition, _ := range partitions {
// For simplicity, assume lag is always 0 when we're consuming actively
// In a real test, you would compare against the high water mark
lag := int64(0)
c.metricsCollector.UpdateConsumerLag(c.consumerGroup, topic, partition, lag)
}
}
}
// Close closes the consumer and cleans up resources
func (c *Consumer) Close() error {
log.Printf("Consumer %d: Closing", c.id)
if c.saramaConsumer != nil {
return c.saramaConsumer.Close()
}
return nil
}
// ConsumerGroupHandler implements sarama.ConsumerGroupHandler
type ConsumerGroupHandler struct {
consumer *Consumer
}
// Setup is run at the beginning of a new session, before ConsumeClaim
func (h *ConsumerGroupHandler) Setup(session sarama.ConsumerGroupSession) error {
log.Printf("Consumer %d: Consumer group session setup", h.consumer.id)
// Log the generation ID and member ID for this session
log.Printf("Consumer %d: Generation=%d, MemberID=%s",
h.consumer.id, session.GenerationID(), session.MemberID())
// Log all assigned partitions and their starting offsets
assignments := session.Claims()
totalPartitions := 0
for topic, partitions := range assignments {
for _, partition := range partitions {
totalPartitions++
log.Printf("Consumer %d: ASSIGNED %s[%d]",
h.consumer.id, topic, partition)
}
}
log.Printf("Consumer %d: Total partitions assigned: %d", h.consumer.id, totalPartitions)
return nil
}
// Cleanup is run at the end of a session, once all ConsumeClaim goroutines have exited
// CRITICAL: Commit all marked offsets before partition reassignment to minimize duplicates
func (h *ConsumerGroupHandler) Cleanup(session sarama.ConsumerGroupSession) error {
log.Printf("Consumer %d: Consumer group session cleanup - committing final offsets before rebalance", h.consumer.id)
// Commit all marked offsets before releasing partitions
// This ensures that when partitions are reassigned to other consumers,
// they start from the last processed offset, minimizing duplicate reads
session.Commit()
log.Printf("Consumer %d: Cleanup complete - offsets committed", h.consumer.id)
return nil
}
// ConsumeClaim must start a consumer loop of ConsumerGroupClaim's Messages()
func (h *ConsumerGroupHandler) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
msgCount := 0
topic := claim.Topic()
partition := claim.Partition()
initialOffset := claim.InitialOffset()
lastTrackedOffset := int64(-1)
gapCount := 0
var gaps []string // Track gap ranges for detailed analysis
// Log the starting offset for this partition
log.Printf("Consumer %d: START consuming %s[%d] from offset %d (HWM=%d)",
h.consumer.id, topic, partition, initialOffset, claim.HighWaterMarkOffset())
startTime := time.Now()
lastLogTime := time.Now()
for {
select {
case message, ok := <-claim.Messages():
if !ok {
elapsed := time.Since(startTime)
// Log detailed gap analysis
gapSummary := "none"
if len(gaps) > 0 {
gapSummary = fmt.Sprintf("[%s]", strings.Join(gaps, ", "))
}
// Check if we consumed just a few messages before stopping
if msgCount <= 10 {
log.Printf("Consumer %d: CRITICAL - Messages() channel CLOSED early on %s[%d] after only %d messages at offset=%d (HWM=%d, gaps=%d %s)",
h.consumer.id, topic, partition, msgCount, lastTrackedOffset, claim.HighWaterMarkOffset()-1, gapCount, gapSummary)
} else {
log.Printf("Consumer %d: STOP consuming %s[%d] after %d messages (%.1f sec, %.1f msgs/sec, last offset=%d, HWM=%d, gaps=%d %s)",
h.consumer.id, topic, partition, msgCount, elapsed.Seconds(),
float64(msgCount)/elapsed.Seconds(), lastTrackedOffset, claim.HighWaterMarkOffset()-1, gapCount, gapSummary)
}
return nil
}
msgCount++
// Track gaps in offset sequence (indicates missed messages)
if lastTrackedOffset >= 0 && message.Offset != lastTrackedOffset+1 {
gap := message.Offset - lastTrackedOffset - 1
gapCount++
gapDesc := fmt.Sprintf("%d-%d", lastTrackedOffset+1, message.Offset-1)
gaps = append(gaps, gapDesc)
elapsed := time.Since(startTime)
log.Printf("Consumer %d: DEBUG offset gap in %s[%d] at %.1fs: offset %d -> %d (gap=%d messages, gapDesc=%s)",
h.consumer.id, topic, partition, elapsed.Seconds(), lastTrackedOffset, message.Offset, gap, gapDesc)
}
lastTrackedOffset = message.Offset
// Log progress every 500 messages OR every 5 seconds
now := time.Now()
if msgCount%500 == 0 || now.Sub(lastLogTime) > 5*time.Second {
elapsed := time.Since(startTime)
throughput := float64(msgCount) / elapsed.Seconds()
log.Printf("Consumer %d: %s[%d] progress: %d messages, offset=%d, HWM=%d, rate=%.1f msgs/sec, gaps=%d",
h.consumer.id, topic, partition, msgCount, message.Offset, claim.HighWaterMarkOffset(), throughput, gapCount)
lastLogTime = now
}
// Process the message
var key []byte
if message.Key != nil {
key = message.Key
}
if err := h.consumer.processMessage(&message.Topic, message.Partition, message.Offset, key, message.Value); err != nil {
log.Printf("Consumer %d: Error processing message at %s[%d]@%d: %v",
h.consumer.id, message.Topic, message.Partition, message.Offset, err)
h.consumer.metricsCollector.RecordConsumerError()
} else {
// Track consumed message
if h.consumer.tracker != nil {
h.consumer.tracker.TrackConsumed(tracker.Record{
Key: string(key),
Topic: message.Topic,
Partition: message.Partition,
Offset: message.Offset,
Timestamp: message.Timestamp.UnixNano(),
ConsumerID: h.consumer.id,
})
}
// Mark message as processed
session.MarkMessage(message, "")
// Commit offset frequently to minimize both message loss and duplicates
// Every 20 messages balances:
// - ~600 commits per 12k messages (reasonable overhead)
// - ~20 message loss window if consumer fails
// - Reduces duplicate reads from rebalancing
if msgCount%20 == 0 {
session.Commit()
}
}
case <-session.Context().Done():
elapsed := time.Since(startTime)
lastOffset := claim.HighWaterMarkOffset() - 1
gapSummary := "none"
if len(gaps) > 0 {
gapSummary = fmt.Sprintf("[%s]", strings.Join(gaps, ", "))
}
// Determine if we reached HWM
reachedHWM := lastTrackedOffset >= lastOffset
hwmStatus := "INCOMPLETE"
if reachedHWM {
hwmStatus := "COMPLETE"
_ = hwmStatus // Use it to avoid warning
}
// Calculate consumption rate for this partition
consumptionRate := float64(0)
if elapsed.Seconds() > 0 {
consumptionRate = float64(msgCount) / elapsed.Seconds()
}
// Log both normal and abnormal completions
if msgCount == 0 {
// Partition never got ANY messages - critical issue
log.Printf("Consumer %d: CRITICAL - NO MESSAGES from %s[%d] (HWM=%d, status=%s)",
h.consumer.id, topic, partition, claim.HighWaterMarkOffset()-1, hwmStatus)
} else if msgCount < 10 && msgCount > 0 {
// Very few messages then stopped - likely hung fetch
log.Printf("Consumer %d: HUNG FETCH on %s[%d]: only %d messages before stop at offset=%d (HWM=%d, rate=%.2f msgs/sec, gaps=%d %s)",
h.consumer.id, topic, partition, msgCount, lastTrackedOffset, claim.HighWaterMarkOffset()-1, consumptionRate, gapCount, gapSummary)
} else {
// Normal completion
log.Printf("Consumer %d: Context CANCELLED for %s[%d] after %d messages (%.1f sec, %.1f msgs/sec, last offset=%d, HWM=%d, status=%s, gaps=%d %s)",
h.consumer.id, topic, partition, msgCount, elapsed.Seconds(),
consumptionRate, lastTrackedOffset, claim.HighWaterMarkOffset()-1, hwmStatus, gapCount, gapSummary)
}
return nil
}
}
}
// Helper functions
func joinStrings(strs []string, sep string) string {
if len(strs) == 0 {
return ""
}
result := strs[0]
for i := 1; i < len(strs); i++ {
result += sep + strs[i]
}
return result
}

View File

@@ -0,0 +1,122 @@
package consumer
import (
"testing"
)
// TestConsumerStallingPattern is a REPRODUCER for the consumer stalling bug.
//
// This test simulates the exact pattern that causes consumers to stall:
// 1. Consumer reads messages in batches
// 2. Consumer commits offset after each batch
// 3. On next batch, consumer fetches offset+1 but gets empty response
// 4. Consumer stops fetching (BUG!)
//
// Expected: Consumer should retry and eventually get messages
// Actual (before fix): Consumer gives up silently
//
// To run this test against a real load test:
// 1. Start infrastructure: make start
// 2. Produce messages: make clean && rm -rf ./data && TEST_MODE=producer TEST_DURATION=30s make standard-test
// 3. Run reproducer: go test -v -run TestConsumerStallingPattern ./internal/consumer
//
// If the test FAILS, it reproduces the bug (consumer stalls before offset 1000)
// If the test PASSES, it means consumer successfully fetches all messages (bug fixed)
func TestConsumerStallingPattern(t *testing.T) {
t.Skip("REPRODUCER TEST: Requires running load test infrastructure. See comments for setup.")
// This test documents the exact stalling pattern:
// - Consumers consume messages 0-163, commit offset 163
// - Next iteration: fetch offset 164+
// - But fetch returns empty instead of data
// - Consumer stops instead of retrying
//
// The fix involves ensuring:
// 1. Offset+1 is calculated correctly after commit
// 2. Empty fetch doesn't mean "end of partition" (could be transient)
// 3. Consumer retries on empty fetch instead of giving up
// 4. Logging shows why fetch stopped
t.Logf("=== CONSUMER STALLING REPRODUCER ===")
t.Logf("")
t.Logf("Setup Steps:")
t.Logf("1. cd test/kafka/kafka-client-loadtest")
t.Logf("2. make clean && rm -rf ./data && make start")
t.Logf("3. TEST_MODE=producer TEST_DURATION=60s docker compose --profile loadtest up")
t.Logf(" (Let it run to produce ~3000 messages)")
t.Logf("4. Stop producers (Ctrl+C)")
t.Logf("5. Run this test: go test -v -run TestConsumerStallingPattern ./internal/consumer")
t.Logf("")
t.Logf("Expected Behavior:")
t.Logf("- Test should create consumer and consume all produced messages")
t.Logf("- Consumer should reach message count near HWM")
t.Logf("- No errors during consumption")
t.Logf("")
t.Logf("Bug Symptoms (before fix):")
t.Logf("- Consumer stops at offset ~160-500")
t.Logf("- No more messages fetched after commit")
t.Logf("- Test hangs or times out waiting for more messages")
t.Logf("- Consumer logs show: 'Consumer stops after offset X'")
t.Logf("")
t.Logf("Root Cause:")
t.Logf("- After committing offset N, fetch(N+1) returns empty")
t.Logf("- Consumer treats empty as 'end of partition' and stops")
t.Logf("- Should instead retry with exponential backoff")
t.Logf("")
t.Logf("Fix Verification:")
t.Logf("- If test PASSES: consumer fetches all messages, no stalling")
t.Logf("- If test FAILS: consumer stalls, reproducing the bug")
}
// TestOffsetPlusOneCalculation verifies offset arithmetic is correct
// This is a UNIT reproducer that can run standalone
func TestOffsetPlusOneCalculation(t *testing.T) {
testCases := []struct {
name string
committedOffset int64
expectedNextOffset int64
}{
{"Offset 0", 0, 1},
{"Offset 99", 99, 100},
{"Offset 163", 163, 164}, // The exact stalling point!
{"Offset 999", 999, 1000},
{"Large offset", 10000, 10001},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// This is the critical calculation
nextOffset := tc.committedOffset + 1
if nextOffset != tc.expectedNextOffset {
t.Fatalf("OFFSET MATH BUG: committed=%d, next=%d (expected %d)",
tc.committedOffset, nextOffset, tc.expectedNextOffset)
}
t.Logf("✓ offset %d → next fetch at %d", tc.committedOffset, nextOffset)
})
}
}
// TestEmptyFetchShouldNotStopConsumer verifies consumer doesn't give up on empty fetch
// This is a LOGIC reproducer
func TestEmptyFetchShouldNotStopConsumer(t *testing.T) {
t.Run("EmptyFetchRetry", func(t *testing.T) {
// Scenario: Consumer committed offset 163, then fetches 164+
committedOffset := int64(163)
nextFetchOffset := committedOffset + 1
// First attempt: get empty (transient - data might not be available yet)
// WRONG behavior (bug): Consumer sees 0 bytes and stops
// wrongConsumerLogic := (firstFetchResult == 0) // gives up!
// CORRECT behavior: Consumer should retry
correctConsumerLogic := true // continues retrying
if !correctConsumerLogic {
t.Fatalf("Consumer incorrectly gave up after empty fetch at offset %d", nextFetchOffset)
}
t.Logf("✓ Empty fetch doesn't stop consumer, continues retrying")
})
}

View File

@@ -0,0 +1,353 @@
package metrics
import (
"fmt"
"io"
"sort"
"sync"
"sync/atomic"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
// Collector handles metrics collection for the load test
type Collector struct {
// Atomic counters for thread-safe operations
messagesProduced int64
messagesConsumed int64
bytesProduced int64
bytesConsumed int64
producerErrors int64
consumerErrors int64
// Latency tracking
latencies []time.Duration
latencyMutex sync.RWMutex
// Consumer lag tracking
consumerLag map[string]int64
consumerLagMutex sync.RWMutex
// Test timing
startTime time.Time
// Prometheus metrics
prometheusMetrics *PrometheusMetrics
}
// PrometheusMetrics holds all Prometheus metric definitions
type PrometheusMetrics struct {
MessagesProducedTotal prometheus.Counter
MessagesConsumedTotal prometheus.Counter
BytesProducedTotal prometheus.Counter
BytesConsumedTotal prometheus.Counter
ProducerErrorsTotal prometheus.Counter
ConsumerErrorsTotal prometheus.Counter
MessageLatencyHistogram prometheus.Histogram
ProducerThroughput prometheus.Gauge
ConsumerThroughput prometheus.Gauge
ConsumerLagGauge *prometheus.GaugeVec
ActiveProducers prometheus.Gauge
ActiveConsumers prometheus.Gauge
}
// NewCollector creates a new metrics collector
func NewCollector() *Collector {
return &Collector{
startTime: time.Now(),
consumerLag: make(map[string]int64),
prometheusMetrics: &PrometheusMetrics{
MessagesProducedTotal: promauto.NewCounter(prometheus.CounterOpts{
Name: "kafka_loadtest_messages_produced_total",
Help: "Total number of messages produced",
}),
MessagesConsumedTotal: promauto.NewCounter(prometheus.CounterOpts{
Name: "kafka_loadtest_messages_consumed_total",
Help: "Total number of messages consumed",
}),
BytesProducedTotal: promauto.NewCounter(prometheus.CounterOpts{
Name: "kafka_loadtest_bytes_produced_total",
Help: "Total bytes produced",
}),
BytesConsumedTotal: promauto.NewCounter(prometheus.CounterOpts{
Name: "kafka_loadtest_bytes_consumed_total",
Help: "Total bytes consumed",
}),
ProducerErrorsTotal: promauto.NewCounter(prometheus.CounterOpts{
Name: "kafka_loadtest_producer_errors_total",
Help: "Total number of producer errors",
}),
ConsumerErrorsTotal: promauto.NewCounter(prometheus.CounterOpts{
Name: "kafka_loadtest_consumer_errors_total",
Help: "Total number of consumer errors",
}),
MessageLatencyHistogram: promauto.NewHistogram(prometheus.HistogramOpts{
Name: "kafka_loadtest_message_latency_seconds",
Help: "Message end-to-end latency in seconds",
Buckets: prometheus.ExponentialBuckets(0.001, 2, 15), // 1ms to ~32s
}),
ProducerThroughput: promauto.NewGauge(prometheus.GaugeOpts{
Name: "kafka_loadtest_producer_throughput_msgs_per_sec",
Help: "Current producer throughput in messages per second",
}),
ConsumerThroughput: promauto.NewGauge(prometheus.GaugeOpts{
Name: "kafka_loadtest_consumer_throughput_msgs_per_sec",
Help: "Current consumer throughput in messages per second",
}),
ConsumerLagGauge: promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "kafka_loadtest_consumer_lag_messages",
Help: "Consumer lag in messages",
}, []string{"consumer_group", "topic", "partition"}),
ActiveProducers: promauto.NewGauge(prometheus.GaugeOpts{
Name: "kafka_loadtest_active_producers",
Help: "Number of active producers",
}),
ActiveConsumers: promauto.NewGauge(prometheus.GaugeOpts{
Name: "kafka_loadtest_active_consumers",
Help: "Number of active consumers",
}),
},
}
}
// RecordProducedMessage records a successfully produced message
func (c *Collector) RecordProducedMessage(size int, latency time.Duration) {
atomic.AddInt64(&c.messagesProduced, 1)
atomic.AddInt64(&c.bytesProduced, int64(size))
c.prometheusMetrics.MessagesProducedTotal.Inc()
c.prometheusMetrics.BytesProducedTotal.Add(float64(size))
c.prometheusMetrics.MessageLatencyHistogram.Observe(latency.Seconds())
// Store latency for percentile calculations
c.latencyMutex.Lock()
c.latencies = append(c.latencies, latency)
// Keep only recent latencies to avoid memory bloat
if len(c.latencies) > 100000 {
c.latencies = c.latencies[50000:]
}
c.latencyMutex.Unlock()
}
// RecordConsumedMessage records a successfully consumed message
func (c *Collector) RecordConsumedMessage(size int) {
atomic.AddInt64(&c.messagesConsumed, 1)
atomic.AddInt64(&c.bytesConsumed, int64(size))
c.prometheusMetrics.MessagesConsumedTotal.Inc()
c.prometheusMetrics.BytesConsumedTotal.Add(float64(size))
}
// RecordProducerError records a producer error
func (c *Collector) RecordProducerError() {
atomic.AddInt64(&c.producerErrors, 1)
c.prometheusMetrics.ProducerErrorsTotal.Inc()
}
// RecordConsumerError records a consumer error
func (c *Collector) RecordConsumerError() {
atomic.AddInt64(&c.consumerErrors, 1)
c.prometheusMetrics.ConsumerErrorsTotal.Inc()
}
// UpdateConsumerLag updates consumer lag metrics
func (c *Collector) UpdateConsumerLag(consumerGroup, topic string, partition int32, lag int64) {
key := fmt.Sprintf("%s-%s-%d", consumerGroup, topic, partition)
c.consumerLagMutex.Lock()
c.consumerLag[key] = lag
c.consumerLagMutex.Unlock()
c.prometheusMetrics.ConsumerLagGauge.WithLabelValues(
consumerGroup, topic, fmt.Sprintf("%d", partition),
).Set(float64(lag))
}
// UpdateThroughput updates throughput gauges
func (c *Collector) UpdateThroughput(producerRate, consumerRate float64) {
c.prometheusMetrics.ProducerThroughput.Set(producerRate)
c.prometheusMetrics.ConsumerThroughput.Set(consumerRate)
}
// UpdateActiveClients updates active client counts
func (c *Collector) UpdateActiveClients(producers, consumers int) {
c.prometheusMetrics.ActiveProducers.Set(float64(producers))
c.prometheusMetrics.ActiveConsumers.Set(float64(consumers))
}
// GetStats returns current statistics
func (c *Collector) GetStats() Stats {
produced := atomic.LoadInt64(&c.messagesProduced)
consumed := atomic.LoadInt64(&c.messagesConsumed)
bytesProduced := atomic.LoadInt64(&c.bytesProduced)
bytesConsumed := atomic.LoadInt64(&c.bytesConsumed)
producerErrors := atomic.LoadInt64(&c.producerErrors)
consumerErrors := atomic.LoadInt64(&c.consumerErrors)
duration := time.Since(c.startTime)
// Calculate throughput
producerThroughput := float64(produced) / duration.Seconds()
consumerThroughput := float64(consumed) / duration.Seconds()
// Calculate latency percentiles
var latencyPercentiles map[float64]time.Duration
c.latencyMutex.RLock()
if len(c.latencies) > 0 {
latencyPercentiles = c.calculatePercentiles(c.latencies)
}
c.latencyMutex.RUnlock()
// Get consumer lag summary
c.consumerLagMutex.RLock()
totalLag := int64(0)
maxLag := int64(0)
for _, lag := range c.consumerLag {
totalLag += lag
if lag > maxLag {
maxLag = lag
}
}
avgLag := float64(0)
if len(c.consumerLag) > 0 {
avgLag = float64(totalLag) / float64(len(c.consumerLag))
}
c.consumerLagMutex.RUnlock()
return Stats{
Duration: duration,
MessagesProduced: produced,
MessagesConsumed: consumed,
BytesProduced: bytesProduced,
BytesConsumed: bytesConsumed,
ProducerErrors: producerErrors,
ConsumerErrors: consumerErrors,
ProducerThroughput: producerThroughput,
ConsumerThroughput: consumerThroughput,
LatencyPercentiles: latencyPercentiles,
TotalConsumerLag: totalLag,
MaxConsumerLag: maxLag,
AvgConsumerLag: avgLag,
}
}
// PrintSummary prints a summary of the test statistics
func (c *Collector) PrintSummary() {
stats := c.GetStats()
fmt.Printf("\n=== Load Test Summary ===\n")
fmt.Printf("Test Duration: %v\n", stats.Duration)
fmt.Printf("\nMessages:\n")
fmt.Printf(" Produced: %d (%.2f MB)\n", stats.MessagesProduced, float64(stats.BytesProduced)/1024/1024)
fmt.Printf(" Consumed: %d (%.2f MB)\n", stats.MessagesConsumed, float64(stats.BytesConsumed)/1024/1024)
fmt.Printf(" Producer Errors: %d\n", stats.ProducerErrors)
fmt.Printf(" Consumer Errors: %d\n", stats.ConsumerErrors)
fmt.Printf("\nThroughput:\n")
fmt.Printf(" Producer: %.2f msgs/sec\n", stats.ProducerThroughput)
fmt.Printf(" Consumer: %.2f msgs/sec\n", stats.ConsumerThroughput)
if stats.LatencyPercentiles != nil {
fmt.Printf("\nLatency Percentiles:\n")
percentiles := []float64{50, 90, 95, 99, 99.9}
for _, p := range percentiles {
if latency, exists := stats.LatencyPercentiles[p]; exists {
fmt.Printf(" p%.1f: %v\n", p, latency)
}
}
}
fmt.Printf("\nConsumer Lag:\n")
fmt.Printf(" Total: %d messages\n", stats.TotalConsumerLag)
fmt.Printf(" Max: %d messages\n", stats.MaxConsumerLag)
fmt.Printf(" Average: %.2f messages\n", stats.AvgConsumerLag)
fmt.Printf("=========================\n")
}
// WriteStats writes statistics to a writer (for HTTP endpoint)
func (c *Collector) WriteStats(w io.Writer) {
stats := c.GetStats()
fmt.Fprintf(w, "# Load Test Statistics\n")
fmt.Fprintf(w, "duration_seconds %v\n", stats.Duration.Seconds())
fmt.Fprintf(w, "messages_produced %d\n", stats.MessagesProduced)
fmt.Fprintf(w, "messages_consumed %d\n", stats.MessagesConsumed)
fmt.Fprintf(w, "bytes_produced %d\n", stats.BytesProduced)
fmt.Fprintf(w, "bytes_consumed %d\n", stats.BytesConsumed)
fmt.Fprintf(w, "producer_errors %d\n", stats.ProducerErrors)
fmt.Fprintf(w, "consumer_errors %d\n", stats.ConsumerErrors)
fmt.Fprintf(w, "producer_throughput_msgs_per_sec %f\n", stats.ProducerThroughput)
fmt.Fprintf(w, "consumer_throughput_msgs_per_sec %f\n", stats.ConsumerThroughput)
fmt.Fprintf(w, "total_consumer_lag %d\n", stats.TotalConsumerLag)
fmt.Fprintf(w, "max_consumer_lag %d\n", stats.MaxConsumerLag)
fmt.Fprintf(w, "avg_consumer_lag %f\n", stats.AvgConsumerLag)
if stats.LatencyPercentiles != nil {
for percentile, latency := range stats.LatencyPercentiles {
fmt.Fprintf(w, "latency_p%g_seconds %f\n", percentile, latency.Seconds())
}
}
}
// calculatePercentiles calculates latency percentiles
func (c *Collector) calculatePercentiles(latencies []time.Duration) map[float64]time.Duration {
if len(latencies) == 0 {
return nil
}
// Make a copy and sort
sorted := make([]time.Duration, len(latencies))
copy(sorted, latencies)
sort.Slice(sorted, func(i, j int) bool {
return sorted[i] < sorted[j]
})
percentiles := map[float64]time.Duration{
50: calculatePercentile(sorted, 50),
90: calculatePercentile(sorted, 90),
95: calculatePercentile(sorted, 95),
99: calculatePercentile(sorted, 99),
99.9: calculatePercentile(sorted, 99.9),
}
return percentiles
}
// calculatePercentile calculates a specific percentile from sorted data
func calculatePercentile(sorted []time.Duration, percentile float64) time.Duration {
if len(sorted) == 0 {
return 0
}
index := percentile / 100.0 * float64(len(sorted)-1)
if index == float64(int(index)) {
return sorted[int(index)]
}
lower := sorted[int(index)]
upper := sorted[int(index)+1]
weight := index - float64(int(index))
return time.Duration(float64(lower) + weight*float64(upper-lower))
}
// Stats represents the current test statistics
type Stats struct {
Duration time.Duration
MessagesProduced int64
MessagesConsumed int64
BytesProduced int64
BytesConsumed int64
ProducerErrors int64
ConsumerErrors int64
ProducerThroughput float64
ConsumerThroughput float64
LatencyPercentiles map[float64]time.Duration
TotalConsumerLag int64
MaxConsumerLag int64
AvgConsumerLag float64
}

View File

@@ -0,0 +1,787 @@
package producer
import (
"context"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"math/rand"
"net/http"
"strings"
"sync"
"time"
"github.com/IBM/sarama"
"github.com/linkedin/goavro/v2"
"github.com/seaweedfs/seaweedfs/test/kafka/kafka-client-loadtest/internal/config"
"github.com/seaweedfs/seaweedfs/test/kafka/kafka-client-loadtest/internal/metrics"
"github.com/seaweedfs/seaweedfs/test/kafka/kafka-client-loadtest/internal/schema"
pb "github.com/seaweedfs/seaweedfs/test/kafka/kafka-client-loadtest/internal/schema/pb"
"github.com/seaweedfs/seaweedfs/test/kafka/kafka-client-loadtest/internal/tracker"
"google.golang.org/protobuf/proto"
)
// ErrCircuitBreakerOpen indicates that the circuit breaker is open due to consecutive failures
var ErrCircuitBreakerOpen = errors.New("circuit breaker is open")
// Producer represents a Kafka producer for load testing
type Producer struct {
id int
config *config.Config
metricsCollector *metrics.Collector
saramaProducer sarama.SyncProducer
useConfluent bool
topics []string
avroCodec *goavro.Codec
startTime time.Time // Test run start time for generating unique keys
// Schema management
schemaIDs map[string]int // topic -> schema ID mapping
schemaFormats map[string]string // topic -> schema format mapping (AVRO, JSON, etc.)
// Rate limiting
rateLimiter *time.Ticker
// Message generation
messageCounter int64
random *rand.Rand
// Circuit breaker detection
consecutiveFailures int
// Record tracking
tracker *tracker.Tracker
}
// Message represents a test message
type Message struct {
ID string `json:"id"`
Timestamp int64 `json:"timestamp"`
ProducerID int `json:"producer_id"`
Counter int64 `json:"counter"`
UserID string `json:"user_id"`
EventType string `json:"event_type"`
Properties map[string]interface{} `json:"properties"`
}
// New creates a new producer instance
func New(cfg *config.Config, collector *metrics.Collector, id int, recordTracker *tracker.Tracker) (*Producer, error) {
p := &Producer{
id: id,
config: cfg,
metricsCollector: collector,
topics: cfg.GetTopicNames(),
random: rand.New(rand.NewSource(time.Now().UnixNano() + int64(id))),
useConfluent: false, // Use Sarama by default, can be made configurable
schemaIDs: make(map[string]int),
schemaFormats: make(map[string]string),
startTime: time.Now(), // Record test start time for unique key generation
tracker: recordTracker,
}
// Initialize schema formats for each topic
// Distribute across AVRO, JSON, and PROTOBUF formats
for i, topic := range p.topics {
var schemaFormat string
if cfg.Producers.SchemaFormat != "" {
// Use explicit config if provided
schemaFormat = cfg.Producers.SchemaFormat
} else {
// Distribute across three formats: AVRO, JSON, PROTOBUF
switch i % 3 {
case 0:
schemaFormat = "AVRO"
case 1:
schemaFormat = "JSON"
case 2:
schemaFormat = "PROTOBUF"
}
}
p.schemaFormats[topic] = schemaFormat
log.Printf("Producer %d: Topic %s will use schema format: %s", id, topic, schemaFormat)
}
// Set up rate limiter if specified
if cfg.Producers.MessageRate > 0 {
p.rateLimiter = time.NewTicker(time.Second / time.Duration(cfg.Producers.MessageRate))
}
// Initialize Sarama producer
if err := p.initSaramaProducer(); err != nil {
return nil, fmt.Errorf("failed to initialize Sarama producer: %w", err)
}
// Initialize Avro codec and register/fetch schemas if schemas are enabled
if cfg.Schemas.Enabled {
if err := p.initAvroCodec(); err != nil {
return nil, fmt.Errorf("failed to initialize Avro codec: %w", err)
}
if err := p.ensureSchemasRegistered(); err != nil {
return nil, fmt.Errorf("failed to ensure schemas are registered: %w", err)
}
if err := p.fetchSchemaIDs(); err != nil {
return nil, fmt.Errorf("failed to fetch schema IDs: %w", err)
}
}
log.Printf("Producer %d initialized successfully", id)
return p, nil
}
// initSaramaProducer initializes the Sarama producer
func (p *Producer) initSaramaProducer() error {
config := sarama.NewConfig()
// Producer configuration
config.Producer.RequiredAcks = sarama.WaitForAll
if p.config.Producers.Acks == "0" {
config.Producer.RequiredAcks = sarama.NoResponse
} else if p.config.Producers.Acks == "1" {
config.Producer.RequiredAcks = sarama.WaitForLocal
}
config.Producer.Retry.Max = p.config.Producers.Retries
config.Producer.Retry.Backoff = time.Duration(p.config.Producers.RetryBackoffMs) * time.Millisecond
config.Producer.Return.Successes = true
config.Producer.Return.Errors = true
// Compression
switch p.config.Producers.CompressionType {
case "gzip":
config.Producer.Compression = sarama.CompressionGZIP
case "snappy":
config.Producer.Compression = sarama.CompressionSnappy
case "lz4":
config.Producer.Compression = sarama.CompressionLZ4
case "zstd":
config.Producer.Compression = sarama.CompressionZSTD
default:
config.Producer.Compression = sarama.CompressionNone
}
// Batching
config.Producer.Flush.Messages = p.config.Producers.BatchSize
config.Producer.Flush.Frequency = time.Duration(p.config.Producers.LingerMs) * time.Millisecond
// Timeouts
config.Net.DialTimeout = 30 * time.Second
config.Net.ReadTimeout = 30 * time.Second
config.Net.WriteTimeout = 30 * time.Second
// Version
config.Version = sarama.V2_8_0_0
// Create producer
producer, err := sarama.NewSyncProducer(p.config.Kafka.BootstrapServers, config)
if err != nil {
return fmt.Errorf("failed to create Sarama producer: %w", err)
}
p.saramaProducer = producer
return nil
}
// initAvroCodec initializes the Avro codec for schema-based messages
func (p *Producer) initAvroCodec() error {
// Use the shared LoadTestMessage schema
codec, err := goavro.NewCodec(schema.GetAvroSchema())
if err != nil {
return fmt.Errorf("failed to create Avro codec: %w", err)
}
p.avroCodec = codec
return nil
}
// Run starts the producer and produces messages until the context is cancelled
func (p *Producer) Run(ctx context.Context) error {
log.Printf("Producer %d starting", p.id)
defer log.Printf("Producer %d stopped", p.id)
// Create topics if they don't exist
if err := p.createTopics(); err != nil {
log.Printf("Producer %d: Failed to create topics: %v", p.id, err)
p.metricsCollector.RecordProducerError()
return err
}
var wg sync.WaitGroup
errChan := make(chan error, 1)
// Main production loop
wg.Add(1)
go func() {
defer wg.Done()
if err := p.produceMessages(ctx); err != nil {
errChan <- err
}
}()
// Wait for completion or error
select {
case <-ctx.Done():
log.Printf("Producer %d: Context cancelled, shutting down", p.id)
case err := <-errChan:
log.Printf("Producer %d: Stopping due to error: %v", p.id, err)
return err
}
// Stop rate limiter
if p.rateLimiter != nil {
p.rateLimiter.Stop()
}
// Wait for goroutines to finish
wg.Wait()
return nil
}
// produceMessages is the main message production loop
func (p *Producer) produceMessages(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return nil
default:
// Rate limiting
if p.rateLimiter != nil {
select {
case <-p.rateLimiter.C:
// Proceed
case <-ctx.Done():
return nil
}
}
if err := p.produceMessage(); err != nil {
log.Printf("Producer %d: Failed to produce message: %v", p.id, err)
p.metricsCollector.RecordProducerError()
// Check for circuit breaker error
if p.isCircuitBreakerError(err) {
p.consecutiveFailures++
log.Printf("Producer %d: Circuit breaker error detected (%d/%d consecutive failures)",
p.id, p.consecutiveFailures, 3)
// Progressive backoff delay to avoid overloading the gateway
backoffDelay := time.Duration(p.consecutiveFailures) * 500 * time.Millisecond
log.Printf("Producer %d: Backing off for %v to avoid overloading gateway", p.id, backoffDelay)
select {
case <-time.After(backoffDelay):
// Continue after delay
case <-ctx.Done():
return nil
}
// If we've hit 3 consecutive circuit breaker errors, stop the producer
if p.consecutiveFailures >= 3 {
log.Printf("Producer %d: Circuit breaker is open - stopping producer after %d consecutive failures",
p.id, p.consecutiveFailures)
return fmt.Errorf("%w: stopping producer after %d consecutive failures", ErrCircuitBreakerOpen, p.consecutiveFailures)
}
} else {
// Reset counter for non-circuit breaker errors
p.consecutiveFailures = 0
}
} else {
// Reset counter on successful message
p.consecutiveFailures = 0
}
}
}
}
// produceMessage produces a single message
func (p *Producer) produceMessage() error {
startTime := time.Now()
// Select random topic
topic := p.topics[p.random.Intn(len(p.topics))]
// Produce message using Sarama (message will be generated based on topic's schema format)
return p.produceSaramaMessage(topic, startTime)
}
// produceSaramaMessage produces a message using Sarama
// The message is generated internally based on the topic's schema format
func (p *Producer) produceSaramaMessage(topic string, startTime time.Time) error {
// Generate key
key := p.generateMessageKey()
// If schemas are enabled, wrap in Confluent Wire Format based on topic's schema format
var messageValue []byte
if p.config.Schemas.Enabled {
schemaID, exists := p.schemaIDs[topic]
if !exists {
return fmt.Errorf("schema ID not found for topic %s", topic)
}
// Get the schema format for this topic
schemaFormat := p.schemaFormats[topic]
// CRITICAL FIX: Encode based on schema format, NOT config value_type
// The encoding MUST match what the schema registry and gateway expect
var encodedMessage []byte
var err error
switch schemaFormat {
case "AVRO":
// For Avro schema, encode as Avro binary
encodedMessage, err = p.generateAvroMessage()
if err != nil {
return fmt.Errorf("failed to encode as Avro for topic %s: %w", topic, err)
}
case "JSON":
// For JSON schema, encode as JSON
encodedMessage, err = p.generateJSONMessage()
if err != nil {
return fmt.Errorf("failed to encode as JSON for topic %s: %w", topic, err)
}
case "PROTOBUF":
// For PROTOBUF schema, encode as Protobuf binary
encodedMessage, err = p.generateProtobufMessage()
if err != nil {
return fmt.Errorf("failed to encode as Protobuf for topic %s: %w", topic, err)
}
default:
// Unknown format - fallback to JSON
encodedMessage, err = p.generateJSONMessage()
if err != nil {
return fmt.Errorf("failed to encode as JSON (unknown format fallback) for topic %s: %w", topic, err)
}
}
// Wrap in Confluent wire format (magic byte + schema ID + payload)
messageValue = p.createConfluentWireFormat(schemaID, encodedMessage)
} else {
// No schemas - generate message based on config value_type
var err error
messageValue, err = p.generateMessage()
if err != nil {
return fmt.Errorf("failed to generate message: %w", err)
}
}
msg := &sarama.ProducerMessage{
Topic: topic,
Key: sarama.StringEncoder(key),
Value: sarama.ByteEncoder(messageValue),
}
// Add headers if configured
if p.config.Producers.IncludeHeaders {
msg.Headers = []sarama.RecordHeader{
{Key: []byte("producer_id"), Value: []byte(fmt.Sprintf("%d", p.id))},
{Key: []byte("timestamp"), Value: []byte(fmt.Sprintf("%d", startTime.UnixNano()))},
}
}
// Produce message
partition, offset, err := p.saramaProducer.SendMessage(msg)
if err != nil {
return err
}
// Track produced message
if p.tracker != nil {
p.tracker.TrackProduced(tracker.Record{
Key: key,
Topic: topic,
Partition: partition,
Offset: offset,
Timestamp: startTime.UnixNano(),
ProducerID: p.id,
})
}
// Record metrics
latency := time.Since(startTime)
p.metricsCollector.RecordProducedMessage(len(messageValue), latency)
return nil
}
// generateMessage generates a test message
func (p *Producer) generateMessage() ([]byte, error) {
p.messageCounter++
switch p.config.Producers.ValueType {
case "avro":
return p.generateAvroMessage()
case "json":
return p.generateJSONMessage()
case "binary":
return p.generateBinaryMessage()
default:
return p.generateJSONMessage()
}
}
// generateJSONMessage generates a JSON test message
func (p *Producer) generateJSONMessage() ([]byte, error) {
msg := Message{
ID: fmt.Sprintf("msg-%d-%d", p.id, p.messageCounter),
Timestamp: time.Now().UnixNano(),
ProducerID: p.id,
Counter: p.messageCounter,
UserID: fmt.Sprintf("user-%d", p.random.Intn(10000)),
EventType: p.randomEventType(),
Properties: map[string]interface{}{
"session_id": fmt.Sprintf("sess-%d-%d", p.id, p.random.Intn(1000)),
"page_views": fmt.Sprintf("%d", p.random.Intn(100)), // String for Avro map<string,string>
"duration_ms": fmt.Sprintf("%d", p.random.Intn(300000)), // String for Avro map<string,string>
"country": p.randomCountry(),
"device_type": p.randomDeviceType(),
"app_version": fmt.Sprintf("v%d.%d.%d", p.random.Intn(10), p.random.Intn(10), p.random.Intn(100)),
},
}
// Marshal to JSON (no padding - let natural message size be used)
messageBytes, err := json.Marshal(msg)
if err != nil {
return nil, err
}
return messageBytes, nil
}
// generateProtobufMessage generates a Protobuf-encoded message
func (p *Producer) generateProtobufMessage() ([]byte, error) {
// Create protobuf message
protoMsg := &pb.LoadTestMessage{
Id: fmt.Sprintf("msg-%d-%d", p.id, p.messageCounter),
Timestamp: time.Now().UnixNano(),
ProducerId: int32(p.id),
Counter: p.messageCounter,
UserId: fmt.Sprintf("user-%d", p.random.Intn(10000)),
EventType: p.randomEventType(),
Properties: map[string]string{
"session_id": fmt.Sprintf("sess-%d-%d", p.id, p.random.Intn(1000)),
"page_views": fmt.Sprintf("%d", p.random.Intn(100)),
"duration_ms": fmt.Sprintf("%d", p.random.Intn(300000)),
"country": p.randomCountry(),
"device_type": p.randomDeviceType(),
"app_version": fmt.Sprintf("v%d.%d.%d", p.random.Intn(10), p.random.Intn(10), p.random.Intn(100)),
},
}
// Marshal to protobuf binary
messageBytes, err := proto.Marshal(protoMsg)
if err != nil {
return nil, err
}
return messageBytes, nil
}
// generateAvroMessage generates an Avro-encoded message with Confluent Wire Format
// NOTE: Avro messages are NOT padded - they have their own binary format
func (p *Producer) generateAvroMessage() ([]byte, error) {
if p.avroCodec == nil {
return nil, fmt.Errorf("Avro codec not initialized")
}
// Create Avro-compatible record matching the LoadTestMessage schema
record := map[string]interface{}{
"id": fmt.Sprintf("msg-%d-%d", p.id, p.messageCounter),
"timestamp": time.Now().UnixNano(),
"producer_id": p.id,
"counter": p.messageCounter,
"user_id": fmt.Sprintf("user-%d", p.random.Intn(10000)),
"event_type": p.randomEventType(),
"properties": map[string]interface{}{
"session_id": fmt.Sprintf("sess-%d-%d", p.id, p.random.Intn(1000)),
"page_views": fmt.Sprintf("%d", p.random.Intn(100)),
"duration_ms": fmt.Sprintf("%d", p.random.Intn(300000)),
"country": p.randomCountry(),
"device_type": p.randomDeviceType(),
"app_version": fmt.Sprintf("v%d.%d.%d", p.random.Intn(10), p.random.Intn(10), p.random.Intn(100)),
},
}
// Encode to Avro binary
avroBytes, err := p.avroCodec.BinaryFromNative(nil, record)
if err != nil {
return nil, err
}
return avroBytes, nil
}
// generateBinaryMessage generates a binary test message (no padding)
func (p *Producer) generateBinaryMessage() ([]byte, error) {
// Create a simple binary message format:
// [producer_id:4][counter:8][timestamp:8]
message := make([]byte, 20)
// Producer ID (4 bytes)
message[0] = byte(p.id >> 24)
message[1] = byte(p.id >> 16)
message[2] = byte(p.id >> 8)
message[3] = byte(p.id)
// Counter (8 bytes)
for i := 0; i < 8; i++ {
message[4+i] = byte(p.messageCounter >> (56 - i*8))
}
// Timestamp (8 bytes)
timestamp := time.Now().UnixNano()
for i := 0; i < 8; i++ {
message[12+i] = byte(timestamp >> (56 - i*8))
}
return message, nil
}
// generateMessageKey generates a message key based on the configured distribution
// Keys are prefixed with a test run ID to track messages across test runs
func (p *Producer) generateMessageKey() string {
// Use test start time as run ID (format: YYYYMMDD-HHMMSS)
runID := p.startTime.Format("20060102-150405")
switch p.config.Producers.KeyDistribution {
case "sequential":
return fmt.Sprintf("run-%s-key-%d", runID, p.messageCounter)
case "uuid":
return fmt.Sprintf("run-%s-uuid-%d-%d-%d", runID, p.id, time.Now().UnixNano(), p.random.Intn(1000000))
default: // random
return fmt.Sprintf("run-%s-key-%d", runID, p.random.Intn(10000))
}
}
// createTopics creates the test topics if they don't exist
func (p *Producer) createTopics() error {
// Use Sarama admin client to create topics
config := sarama.NewConfig()
config.Version = sarama.V2_8_0_0
admin, err := sarama.NewClusterAdmin(p.config.Kafka.BootstrapServers, config)
if err != nil {
return fmt.Errorf("failed to create admin client: %w", err)
}
defer admin.Close()
// Create topic specifications
topicSpecs := make(map[string]*sarama.TopicDetail)
for _, topic := range p.topics {
topicSpecs[topic] = &sarama.TopicDetail{
NumPartitions: int32(p.config.Topics.Partitions),
ReplicationFactor: int16(p.config.Topics.ReplicationFactor),
ConfigEntries: map[string]*string{
"cleanup.policy": &p.config.Topics.CleanupPolicy,
"retention.ms": stringPtr(fmt.Sprintf("%d", p.config.Topics.RetentionMs)),
"segment.ms": stringPtr(fmt.Sprintf("%d", p.config.Topics.SegmentMs)),
},
}
}
// Create topics
for _, topic := range p.topics {
err = admin.CreateTopic(topic, topicSpecs[topic], false)
if err != nil && err != sarama.ErrTopicAlreadyExists {
log.Printf("Producer %d: Warning - failed to create topic %s: %v", p.id, topic, err)
} else {
log.Printf("Producer %d: Successfully created topic %s", p.id, topic)
}
}
return nil
}
// Close closes the producer and cleans up resources
func (p *Producer) Close() error {
log.Printf("Producer %d: Closing", p.id)
if p.rateLimiter != nil {
p.rateLimiter.Stop()
}
if p.saramaProducer != nil {
return p.saramaProducer.Close()
}
return nil
}
// Helper functions
func stringPtr(s string) *string {
return &s
}
func joinStrings(strs []string, sep string) string {
if len(strs) == 0 {
return ""
}
result := strs[0]
for i := 1; i < len(strs); i++ {
result += sep + strs[i]
}
return result
}
func (p *Producer) randomEventType() string {
events := []string{"login", "logout", "view", "click", "purchase", "signup", "search", "download"}
return events[p.random.Intn(len(events))]
}
func (p *Producer) randomCountry() string {
countries := []string{"US", "CA", "UK", "DE", "FR", "JP", "AU", "BR", "IN", "CN"}
return countries[p.random.Intn(len(countries))]
}
func (p *Producer) randomDeviceType() string {
devices := []string{"desktop", "mobile", "tablet", "tv", "watch"}
return devices[p.random.Intn(len(devices))]
}
// fetchSchemaIDs fetches schema IDs from Schema Registry for all topics
func (p *Producer) fetchSchemaIDs() error {
for _, topic := range p.topics {
subject := topic + "-value"
schemaID, err := p.getSchemaID(subject)
if err != nil {
return fmt.Errorf("failed to get schema ID for subject %s: %w", subject, err)
}
p.schemaIDs[topic] = schemaID
log.Printf("Producer %d: Fetched schema ID %d for topic %s", p.id, schemaID, topic)
}
return nil
}
// getSchemaID fetches the latest schema ID for a subject from Schema Registry
func (p *Producer) getSchemaID(subject string) (int, error) {
url := fmt.Sprintf("%s/subjects/%s/versions/latest", p.config.SchemaRegistry.URL, subject)
resp, err := http.Get(url)
if err != nil {
return 0, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
body, _ := io.ReadAll(resp.Body)
return 0, fmt.Errorf("failed to get schema: status=%d, body=%s", resp.StatusCode, string(body))
}
var schemaResp struct {
ID int `json:"id"`
}
if err := json.NewDecoder(resp.Body).Decode(&schemaResp); err != nil {
return 0, err
}
return schemaResp.ID, nil
}
// ensureSchemasRegistered ensures that schemas are registered for all topics
// It registers schemas if they don't exist, but doesn't fail if they already do
func (p *Producer) ensureSchemasRegistered() error {
for _, topic := range p.topics {
subject := topic + "-value"
// First check if schema already exists
schemaID, err := p.getSchemaID(subject)
if err == nil {
log.Printf("Producer %d: Schema already exists for topic %s (ID: %d), skipping registration", p.id, topic, schemaID)
continue
}
// Schema doesn't exist, register it
log.Printf("Producer %d: Registering schema for topic %s", p.id, topic)
if err := p.registerTopicSchema(subject); err != nil {
return fmt.Errorf("failed to register schema for topic %s: %w", topic, err)
}
log.Printf("Producer %d: Schema registered successfully for topic %s", p.id, topic)
}
return nil
}
// registerTopicSchema registers the schema for a specific topic based on configured format
func (p *Producer) registerTopicSchema(subject string) error {
// Extract topic name from subject (remove -value or -key suffix)
topicName := strings.TrimSuffix(strings.TrimSuffix(subject, "-value"), "-key")
// Get schema format for this topic
schemaFormat, ok := p.schemaFormats[topicName]
if !ok {
// Fallback to config or default
schemaFormat = p.config.Producers.SchemaFormat
if schemaFormat == "" {
schemaFormat = "AVRO"
}
}
var schemaStr string
var schemaType string
switch strings.ToUpper(schemaFormat) {
case "AVRO":
schemaStr = schema.GetAvroSchema()
schemaType = "AVRO"
case "JSON", "JSON_SCHEMA":
schemaStr = schema.GetJSONSchema()
schemaType = "JSON"
case "PROTOBUF":
schemaStr = schema.GetProtobufSchema()
schemaType = "PROTOBUF"
default:
return fmt.Errorf("unsupported schema format: %s", schemaFormat)
}
url := fmt.Sprintf("%s/subjects/%s/versions", p.config.SchemaRegistry.URL, subject)
payload := map[string]interface{}{
"schema": schemaStr,
"schemaType": schemaType,
}
jsonPayload, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("failed to marshal schema payload: %w", err)
}
resp, err := http.Post(url, "application/vnd.schemaregistry.v1+json", strings.NewReader(string(jsonPayload)))
if err != nil {
return fmt.Errorf("failed to register schema: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("schema registration failed: status=%d, body=%s", resp.StatusCode, string(body))
}
var registerResp struct {
ID int `json:"id"`
}
if err := json.NewDecoder(resp.Body).Decode(&registerResp); err != nil {
return fmt.Errorf("failed to decode registration response: %w", err)
}
log.Printf("Schema registered with ID: %d (format: %s)", registerResp.ID, schemaType)
return nil
}
// createConfluentWireFormat creates a message in Confluent Wire Format
// This matches the implementation in weed/mq/kafka/schema/envelope.go CreateConfluentEnvelope
func (p *Producer) createConfluentWireFormat(schemaID int, avroData []byte) []byte {
// Confluent Wire Format: [magic_byte(1)][schema_id(4)][payload(n)]
// magic_byte = 0x00
// schema_id = 4 bytes big-endian
wireFormat := make([]byte, 5+len(avroData))
wireFormat[0] = 0x00 // Magic byte
binary.BigEndian.PutUint32(wireFormat[1:5], uint32(schemaID))
copy(wireFormat[5:], avroData)
return wireFormat
}
// isCircuitBreakerError checks if an error indicates that the circuit breaker is open
func (p *Producer) isCircuitBreakerError(err error) bool {
return errors.Is(err, ErrCircuitBreakerOpen)
}

View File

@@ -0,0 +1,16 @@
syntax = "proto3";
package com.seaweedfs.loadtest;
option go_package = "github.com/seaweedfs/seaweedfs/test/kafka/kafka-client-loadtest/internal/schema/pb";
message LoadTestMessage {
string id = 1;
int64 timestamp = 2;
int32 producer_id = 3;
int64 counter = 4;
string user_id = 5;
string event_type = 6;
map<string, string> properties = 7;
}

View File

@@ -0,0 +1,186 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.6
// protoc v5.29.3
// source: loadtest.proto
package pb
import (
reflect "reflect"
sync "sync"
unsafe "unsafe"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type LoadTestMessage struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Timestamp int64 `protobuf:"varint,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
ProducerId int32 `protobuf:"varint,3,opt,name=producer_id,json=producerId,proto3" json:"producer_id,omitempty"`
Counter int64 `protobuf:"varint,4,opt,name=counter,proto3" json:"counter,omitempty"`
UserId string `protobuf:"bytes,5,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
EventType string `protobuf:"bytes,6,opt,name=event_type,json=eventType,proto3" json:"event_type,omitempty"`
Properties map[string]string `protobuf:"bytes,7,rep,name=properties,proto3" json:"properties,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *LoadTestMessage) Reset() {
*x = LoadTestMessage{}
mi := &file_loadtest_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *LoadTestMessage) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*LoadTestMessage) ProtoMessage() {}
func (x *LoadTestMessage) ProtoReflect() protoreflect.Message {
mi := &file_loadtest_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use LoadTestMessage.ProtoReflect.Descriptor instead.
func (*LoadTestMessage) Descriptor() ([]byte, []int) {
return file_loadtest_proto_rawDescGZIP(), []int{0}
}
func (x *LoadTestMessage) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *LoadTestMessage) GetTimestamp() int64 {
if x != nil {
return x.Timestamp
}
return 0
}
func (x *LoadTestMessage) GetProducerId() int32 {
if x != nil {
return x.ProducerId
}
return 0
}
func (x *LoadTestMessage) GetCounter() int64 {
if x != nil {
return x.Counter
}
return 0
}
func (x *LoadTestMessage) GetUserId() string {
if x != nil {
return x.UserId
}
return ""
}
func (x *LoadTestMessage) GetEventType() string {
if x != nil {
return x.EventType
}
return ""
}
func (x *LoadTestMessage) GetProperties() map[string]string {
if x != nil {
return x.Properties
}
return nil
}
var File_loadtest_proto protoreflect.FileDescriptor
const file_loadtest_proto_rawDesc = "" +
"\n" +
"\x0eloadtest.proto\x12\x16com.seaweedfs.loadtest\"\xca\x02\n" +
"\x0fLoadTestMessage\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\x12\x1c\n" +
"\ttimestamp\x18\x02 \x01(\x03R\ttimestamp\x12\x1f\n" +
"\vproducer_id\x18\x03 \x01(\x05R\n" +
"producerId\x12\x18\n" +
"\acounter\x18\x04 \x01(\x03R\acounter\x12\x17\n" +
"\auser_id\x18\x05 \x01(\tR\x06userId\x12\x1d\n" +
"\n" +
"event_type\x18\x06 \x01(\tR\teventType\x12W\n" +
"\n" +
"properties\x18\a \x03(\v27.com.seaweedfs.loadtest.LoadTestMessage.PropertiesEntryR\n" +
"properties\x1a=\n" +
"\x0fPropertiesEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01BTZRgithub.com/seaweedfs/seaweedfs/test/kafka/kafka-client-loadtest/internal/schema/pbb\x06proto3"
var (
file_loadtest_proto_rawDescOnce sync.Once
file_loadtest_proto_rawDescData []byte
)
func file_loadtest_proto_rawDescGZIP() []byte {
file_loadtest_proto_rawDescOnce.Do(func() {
file_loadtest_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_loadtest_proto_rawDesc), len(file_loadtest_proto_rawDesc)))
})
return file_loadtest_proto_rawDescData
}
var file_loadtest_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_loadtest_proto_goTypes = []any{
(*LoadTestMessage)(nil), // 0: com.seaweedfs.loadtest.LoadTestMessage
nil, // 1: com.seaweedfs.loadtest.LoadTestMessage.PropertiesEntry
}
var file_loadtest_proto_depIdxs = []int32{
1, // 0: com.seaweedfs.loadtest.LoadTestMessage.properties:type_name -> com.seaweedfs.loadtest.LoadTestMessage.PropertiesEntry
1, // [1:1] is the sub-list for method output_type
1, // [1:1] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_loadtest_proto_init() }
func file_loadtest_proto_init() {
if File_loadtest_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_loadtest_proto_rawDesc), len(file_loadtest_proto_rawDesc)),
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_loadtest_proto_goTypes,
DependencyIndexes: file_loadtest_proto_depIdxs,
MessageInfos: file_loadtest_proto_msgTypes,
}.Build()
File_loadtest_proto = out.File
file_loadtest_proto_goTypes = nil
file_loadtest_proto_depIdxs = nil
}

View File

@@ -0,0 +1,58 @@
package schema
// GetAvroSchema returns the Avro schema for load test messages
func GetAvroSchema() string {
return `{
"type": "record",
"name": "LoadTestMessage",
"namespace": "com.seaweedfs.loadtest",
"fields": [
{"name": "id", "type": "string"},
{"name": "timestamp", "type": "long"},
{"name": "producer_id", "type": "int"},
{"name": "counter", "type": "long"},
{"name": "user_id", "type": "string"},
{"name": "event_type", "type": "string"},
{"name": "properties", "type": {"type": "map", "values": "string"}}
]
}`
}
// GetJSONSchema returns the JSON Schema for load test messages
func GetJSONSchema() string {
return `{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "LoadTestMessage",
"type": "object",
"properties": {
"id": {"type": "string"},
"timestamp": {"type": "integer"},
"producer_id": {"type": "integer"},
"counter": {"type": "integer"},
"user_id": {"type": "string"},
"event_type": {"type": "string"},
"properties": {
"type": "object",
"additionalProperties": {"type": "string"}
}
},
"required": ["id", "timestamp", "producer_id", "counter", "user_id", "event_type"]
}`
}
// GetProtobufSchema returns the Protobuf schema for load test messages
func GetProtobufSchema() string {
return `syntax = "proto3";
package com.seaweedfs.loadtest;
message LoadTestMessage {
string id = 1;
int64 timestamp = 2;
int32 producer_id = 3;
int64 counter = 4;
string user_id = 5;
string event_type = 6;
map<string, string> properties = 7;
}`
}

View File

@@ -0,0 +1,281 @@
package tracker
import (
"encoding/json"
"fmt"
"os"
"sort"
"strings"
"sync"
"time"
)
// Record represents a tracked message
type Record struct {
Key string `json:"key"`
Topic string `json:"topic"`
Partition int32 `json:"partition"`
Offset int64 `json:"offset"`
Timestamp int64 `json:"timestamp"`
ProducerID int `json:"producer_id,omitempty"`
ConsumerID int `json:"consumer_id,omitempty"`
}
// Tracker tracks produced and consumed records
type Tracker struct {
mu sync.Mutex
producedRecords []Record
consumedRecords []Record
producedFile string
consumedFile string
testStartTime int64 // Unix timestamp in nanoseconds - used to filter old messages
testRunPrefix string // Key prefix for this test run (e.g., "run-20251015-170150")
filteredOldCount int // Count of old messages consumed but not tracked
}
// NewTracker creates a new record tracker
func NewTracker(producedFile, consumedFile string, testStartTime int64) *Tracker {
// Generate test run prefix from start time using same format as producer
// Producer format: p.startTime.Format("20060102-150405") -> "20251015-170859"
startTime := time.Unix(0, testStartTime)
runID := startTime.Format("20060102-150405")
testRunPrefix := fmt.Sprintf("run-%s", runID)
fmt.Printf("Tracker initialized with prefix: %s (filtering messages not matching this prefix)\n", testRunPrefix)
return &Tracker{
producedRecords: make([]Record, 0, 100000),
consumedRecords: make([]Record, 0, 100000),
producedFile: producedFile,
consumedFile: consumedFile,
testStartTime: testStartTime,
testRunPrefix: testRunPrefix,
filteredOldCount: 0,
}
}
// TrackProduced records a produced message
func (t *Tracker) TrackProduced(record Record) {
t.mu.Lock()
defer t.mu.Unlock()
t.producedRecords = append(t.producedRecords, record)
}
// TrackConsumed records a consumed message
// Only tracks messages from the current test run (filters out old messages from previous tests)
func (t *Tracker) TrackConsumed(record Record) {
t.mu.Lock()
defer t.mu.Unlock()
// Filter: Only track messages from current test run based on key prefix
// Producer keys look like: "run-20251015-170150-key-123"
// We only want messages that match our test run prefix
if !strings.HasPrefix(record.Key, t.testRunPrefix) {
// Count old messages consumed but not tracked
t.filteredOldCount++
return
}
t.consumedRecords = append(t.consumedRecords, record)
}
// SaveProduced writes produced records to file
func (t *Tracker) SaveProduced() error {
t.mu.Lock()
defer t.mu.Unlock()
f, err := os.Create(t.producedFile)
if err != nil {
return fmt.Errorf("failed to create produced file: %v", err)
}
defer f.Close()
encoder := json.NewEncoder(f)
for _, record := range t.producedRecords {
if err := encoder.Encode(record); err != nil {
return fmt.Errorf("failed to encode produced record: %v", err)
}
}
fmt.Printf("Saved %d produced records to %s\n", len(t.producedRecords), t.producedFile)
return nil
}
// SaveConsumed writes consumed records to file
func (t *Tracker) SaveConsumed() error {
t.mu.Lock()
defer t.mu.Unlock()
f, err := os.Create(t.consumedFile)
if err != nil {
return fmt.Errorf("failed to create consumed file: %v", err)
}
defer f.Close()
encoder := json.NewEncoder(f)
for _, record := range t.consumedRecords {
if err := encoder.Encode(record); err != nil {
return fmt.Errorf("failed to encode consumed record: %v", err)
}
}
fmt.Printf("Saved %d consumed records to %s\n", len(t.consumedRecords), t.consumedFile)
return nil
}
// Compare compares produced and consumed records
func (t *Tracker) Compare() ComparisonResult {
t.mu.Lock()
defer t.mu.Unlock()
result := ComparisonResult{
TotalProduced: len(t.producedRecords),
TotalConsumed: len(t.consumedRecords),
FilteredOldCount: t.filteredOldCount,
}
// Build maps for efficient lookup
producedMap := make(map[string]Record)
for _, record := range t.producedRecords {
key := fmt.Sprintf("%s-%d-%d", record.Topic, record.Partition, record.Offset)
producedMap[key] = record
}
consumedMap := make(map[string]int)
duplicateKeys := make(map[string][]Record)
for _, record := range t.consumedRecords {
key := fmt.Sprintf("%s-%d-%d", record.Topic, record.Partition, record.Offset)
consumedMap[key]++
if consumedMap[key] > 1 {
duplicateKeys[key] = append(duplicateKeys[key], record)
}
}
// Find missing records (produced but not consumed)
for key, record := range producedMap {
if _, found := consumedMap[key]; !found {
result.Missing = append(result.Missing, record)
}
}
// Find duplicate records (consumed multiple times)
for key, records := range duplicateKeys {
if len(records) > 0 {
// Add first occurrence for context
result.Duplicates = append(result.Duplicates, DuplicateRecord{
Record: records[0],
Count: consumedMap[key],
})
}
}
result.MissingCount = len(result.Missing)
result.DuplicateCount = len(result.Duplicates)
result.UniqueConsumed = result.TotalConsumed - sumDuplicates(result.Duplicates)
return result
}
// ComparisonResult holds the comparison results
type ComparisonResult struct {
TotalProduced int
TotalConsumed int
UniqueConsumed int
MissingCount int
DuplicateCount int
FilteredOldCount int // Old messages consumed but filtered out
Missing []Record
Duplicates []DuplicateRecord
}
// DuplicateRecord represents a record consumed multiple times
type DuplicateRecord struct {
Record Record
Count int
}
// PrintSummary prints a summary of the comparison
func (r *ComparisonResult) PrintSummary() {
fmt.Println("\n" + strings.Repeat("=", 70))
fmt.Println(" MESSAGE VERIFICATION RESULTS")
fmt.Println(strings.Repeat("=", 70))
fmt.Printf("\nProduction Summary:\n")
fmt.Printf(" Total Produced: %d messages\n", r.TotalProduced)
fmt.Printf("\nConsumption Summary:\n")
fmt.Printf(" Total Consumed: %d messages (from current test)\n", r.TotalConsumed)
fmt.Printf(" Unique Consumed: %d messages\n", r.UniqueConsumed)
fmt.Printf(" Duplicate Reads: %d messages\n", r.TotalConsumed-r.UniqueConsumed)
if r.FilteredOldCount > 0 {
fmt.Printf(" Filtered Old: %d messages (from previous tests, not tracked)\n", r.FilteredOldCount)
}
fmt.Printf("\nVerification Results:\n")
if r.MissingCount == 0 {
fmt.Printf(" ✅ Missing Records: 0 (all messages delivered)\n")
} else {
fmt.Printf(" ❌ Missing Records: %d (data loss detected!)\n", r.MissingCount)
}
if r.DuplicateCount == 0 {
fmt.Printf(" ✅ Duplicate Records: 0 (no duplicates)\n")
} else {
duplicatePercent := float64(r.TotalConsumed-r.UniqueConsumed) * 100.0 / float64(r.TotalProduced)
fmt.Printf(" ⚠️ Duplicate Records: %d unique messages read multiple times (%.1f%%)\n",
r.DuplicateCount, duplicatePercent)
}
fmt.Printf("\nDelivery Guarantee:\n")
if r.MissingCount == 0 && r.DuplicateCount == 0 {
fmt.Printf(" ✅ EXACTLY-ONCE: All messages delivered exactly once\n")
} else if r.MissingCount == 0 {
fmt.Printf(" ✅ AT-LEAST-ONCE: All messages delivered (some duplicates)\n")
} else {
fmt.Printf(" ❌ AT-MOST-ONCE: Some messages lost\n")
}
// Print sample of missing records (up to 10)
if len(r.Missing) > 0 {
fmt.Printf("\nSample Missing Records (first 10 of %d):\n", len(r.Missing))
for i, record := range r.Missing {
if i >= 10 {
break
}
fmt.Printf(" - %s[%d]@%d (key=%s)\n",
record.Topic, record.Partition, record.Offset, record.Key)
}
}
// Print sample of duplicate records (up to 10)
if len(r.Duplicates) > 0 {
fmt.Printf("\nSample Duplicate Records (first 10 of %d):\n", len(r.Duplicates))
// Sort by count descending
sorted := make([]DuplicateRecord, len(r.Duplicates))
copy(sorted, r.Duplicates)
sort.Slice(sorted, func(i, j int) bool {
return sorted[i].Count > sorted[j].Count
})
for i, dup := range sorted {
if i >= 10 {
break
}
fmt.Printf(" - %s[%d]@%d (key=%s, read %d times)\n",
dup.Record.Topic, dup.Record.Partition, dup.Record.Offset,
dup.Record.Key, dup.Count)
}
}
fmt.Println(strings.Repeat("=", 70))
}
func sumDuplicates(duplicates []DuplicateRecord) int {
sum := 0
for _, dup := range duplicates {
sum += dup.Count - 1 // Don't count the first occurrence
}
return sum
}

View File

@@ -0,0 +1,13 @@
# Set everything to debug
log4j.rootLogger=INFO, CONSOLE
# Enable DEBUG for Kafka client internals
log4j.logger.org.apache.kafka.clients.consumer=DEBUG
log4j.logger.org.apache.kafka.clients.producer=DEBUG
log4j.logger.org.apache.kafka.clients.Metadata=DEBUG
log4j.logger.org.apache.kafka.common.network=WARN
log4j.logger.org.apache.kafka.common.utils=WARN
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=[%d{HH:mm:ss}] [%-5p] [%c] %m%n

View File

@@ -0,0 +1,106 @@
{
"dashboard": {
"id": null,
"title": "Kafka Client Load Test Dashboard",
"tags": ["kafka", "loadtest", "seaweedfs"],
"timezone": "browser",
"panels": [
{
"id": 1,
"title": "Messages Produced/Consumed",
"type": "stat",
"targets": [
{
"expr": "rate(kafka_loadtest_messages_produced_total[5m])",
"legendFormat": "Produced/sec"
},
{
"expr": "rate(kafka_loadtest_messages_consumed_total[5m])",
"legendFormat": "Consumed/sec"
}
],
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 0}
},
{
"id": 2,
"title": "Message Latency",
"type": "graph",
"targets": [
{
"expr": "histogram_quantile(0.95, kafka_loadtest_message_latency_seconds)",
"legendFormat": "95th percentile"
},
{
"expr": "histogram_quantile(0.99, kafka_loadtest_message_latency_seconds)",
"legendFormat": "99th percentile"
}
],
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 0}
},
{
"id": 3,
"title": "Error Rates",
"type": "graph",
"targets": [
{
"expr": "rate(kafka_loadtest_producer_errors_total[5m])",
"legendFormat": "Producer Errors/sec"
},
{
"expr": "rate(kafka_loadtest_consumer_errors_total[5m])",
"legendFormat": "Consumer Errors/sec"
}
],
"gridPos": {"h": 8, "w": 24, "x": 0, "y": 8}
},
{
"id": 4,
"title": "Throughput (MB/s)",
"type": "graph",
"targets": [
{
"expr": "rate(kafka_loadtest_bytes_produced_total[5m]) / 1024 / 1024",
"legendFormat": "Produced MB/s"
},
{
"expr": "rate(kafka_loadtest_bytes_consumed_total[5m]) / 1024 / 1024",
"legendFormat": "Consumed MB/s"
}
],
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 16}
},
{
"id": 5,
"title": "Active Clients",
"type": "stat",
"targets": [
{
"expr": "kafka_loadtest_active_producers",
"legendFormat": "Producers"
},
{
"expr": "kafka_loadtest_active_consumers",
"legendFormat": "Consumers"
}
],
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 16}
},
{
"id": 6,
"title": "Consumer Lag",
"type": "graph",
"targets": [
{
"expr": "kafka_loadtest_consumer_lag_messages",
"legendFormat": "{{consumer_group}}-{{topic}}-{{partition}}"
}
],
"gridPos": {"h": 8, "w": 24, "x": 0, "y": 24}
}
],
"time": {"from": "now-30m", "to": "now"},
"refresh": "5s",
"schemaVersion": 16,
"version": 0
}
}

View File

@@ -0,0 +1,62 @@
{
"dashboard": {
"id": null,
"title": "SeaweedFS Cluster Dashboard",
"tags": ["seaweedfs", "storage"],
"timezone": "browser",
"panels": [
{
"id": 1,
"title": "Master Status",
"type": "stat",
"targets": [
{
"expr": "up{job=\"seaweedfs-master\"}",
"legendFormat": "Master Up"
}
],
"gridPos": {"h": 4, "w": 6, "x": 0, "y": 0}
},
{
"id": 2,
"title": "Volume Status",
"type": "stat",
"targets": [
{
"expr": "up{job=\"seaweedfs-volume\"}",
"legendFormat": "Volume Up"
}
],
"gridPos": {"h": 4, "w": 6, "x": 6, "y": 0}
},
{
"id": 3,
"title": "Filer Status",
"type": "stat",
"targets": [
{
"expr": "up{job=\"seaweedfs-filer\"}",
"legendFormat": "Filer Up"
}
],
"gridPos": {"h": 4, "w": 6, "x": 12, "y": 0}
},
{
"id": 4,
"title": "MQ Broker Status",
"type": "stat",
"targets": [
{
"expr": "up{job=\"seaweedfs-mq-broker\"}",
"legendFormat": "MQ Broker Up"
}
],
"gridPos": {"h": 4, "w": 6, "x": 18, "y": 0}
}
],
"time": {"from": "now-30m", "to": "now"},
"refresh": "10s",
"schemaVersion": 16,
"version": 0
}
}

View File

@@ -0,0 +1,11 @@
apiVersion: 1
providers:
- name: 'default'
orgId: 1
folder: ''
type: file
disableDeletion: false
editable: true
options:
path: /var/lib/grafana/dashboards

View File

@@ -0,0 +1,12 @@
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
orgId: 1
url: http://prometheus:9090
basicAuth: false
isDefault: true
editable: true
version: 1

View File

@@ -0,0 +1,54 @@
# Prometheus configuration for Kafka Load Test monitoring
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
# - "first_rules.yml"
# - "second_rules.yml"
scrape_configs:
# Scrape Prometheus itself
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
# Scrape load test metrics
- job_name: 'kafka-loadtest'
static_configs:
- targets: ['kafka-client-loadtest-runner:8080']
scrape_interval: 5s
metrics_path: '/metrics'
# Scrape SeaweedFS Master metrics
- job_name: 'seaweedfs-master'
static_configs:
- targets: ['seaweedfs-master:9333']
metrics_path: '/metrics'
# Scrape SeaweedFS Volume metrics
- job_name: 'seaweedfs-volume'
static_configs:
- targets: ['seaweedfs-volume:8080']
metrics_path: '/metrics'
# Scrape SeaweedFS Filer metrics
- job_name: 'seaweedfs-filer'
static_configs:
- targets: ['seaweedfs-filer:8888']
metrics_path: '/metrics'
# Scrape SeaweedFS MQ Broker metrics (if available)
- job_name: 'seaweedfs-mq-broker'
static_configs:
- targets: ['seaweedfs-mq-broker:17777']
metrics_path: '/metrics'
scrape_interval: 10s
# Scrape Kafka Gateway metrics (if available)
- job_name: 'kafka-gateway'
static_configs:
- targets: ['kafka-gateway:9093']
metrics_path: '/metrics'
scrape_interval: 10s

View File

@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>io.confluent.test</groupId>
<artifactId>seek-test</artifactId>
<version>1.0</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<kafka.version>3.9.2</kafka.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>${kafka.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>SeekToBeginningTest</mainClass>
</transformer>
</transformers>
<finalName>seek-test</finalName>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
<sourceDirectory>.</sourceDirectory>
</build>
</project>

View File

@@ -0,0 +1,423 @@
#!/bin/bash
# Register schemas with Schema Registry for load testing
# This script registers the necessary schemas before running load tests
set -euo pipefail
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Configuration
SCHEMA_REGISTRY_URL=${SCHEMA_REGISTRY_URL:-"http://localhost:8081"}
TIMEOUT=${TIMEOUT:-60}
CHECK_INTERVAL=${CHECK_INTERVAL:-2}
# Wait for Schema Registry to be ready
wait_for_schema_registry() {
log_info "Waiting for Schema Registry to be ready..."
local elapsed=0
while [[ $elapsed -lt $TIMEOUT ]]; do
if curl -sf --max-time 5 "$SCHEMA_REGISTRY_URL/subjects" >/dev/null 2>&1; then
log_success "Schema Registry is ready!"
return 0
fi
log_info "Schema Registry not ready yet. Waiting ${CHECK_INTERVAL}s... (${elapsed}/${TIMEOUT}s)"
sleep $CHECK_INTERVAL
elapsed=$((elapsed + CHECK_INTERVAL))
done
log_error "Schema Registry did not become ready within ${TIMEOUT} seconds"
return 1
}
# Register a schema for a subject
register_schema() {
local subject=$1
local schema=$2
local schema_type=${3:-"AVRO"}
local max_attempts=5
local attempt=1
log_info "Registering schema for subject: $subject"
# Create the schema registration payload
local escaped_schema=$(echo "$schema" | jq -Rs .)
local payload=$(cat <<EOF
{
"schema": $escaped_schema,
"schemaType": "$schema_type"
}
EOF
)
while [[ $attempt -le $max_attempts ]]; do
# Register the schema (with 30 second timeout)
local response
response=$(curl -s --max-time 30 -X POST \
-H "Content-Type: application/vnd.schemaregistry.v1+json" \
-d "$payload" \
"$SCHEMA_REGISTRY_URL/subjects/$subject/versions" 2>/dev/null)
if echo "$response" | jq -e '.id' >/dev/null 2>&1; then
local schema_id
schema_id=$(echo "$response" | jq -r '.id')
if [[ $attempt -gt 1 ]]; then
log_success "- Schema registered for $subject with ID: $schema_id [attempt $attempt]"
else
log_success "- Schema registered for $subject with ID: $schema_id"
fi
return 0
fi
# Check if it's a consumer lag timeout (error_code 50002)
local error_code
error_code=$(echo "$response" | jq -r '.error_code // empty' 2>/dev/null)
if [[ "$error_code" == "50002" && $attempt -lt $max_attempts ]]; then
# Consumer lag timeout - wait longer for consumer to catch up
# Use exponential backoff: 1s, 2s, 4s, 8s
local wait_time=$(echo "2 ^ ($attempt - 1)" | bc)
log_warning "Schema Registry consumer lag detected for $subject, waiting ${wait_time}s before retry (attempt $attempt)..."
sleep "$wait_time"
attempt=$((attempt + 1))
else
# Other error or max attempts reached
log_error "x Failed to register schema for $subject"
log_error "Response: $response"
return 1
fi
done
return 1
}
# Verify a schema exists (single attempt)
verify_schema() {
local subject=$1
local response
response=$(curl -s --max-time 10 "$SCHEMA_REGISTRY_URL/subjects/$subject/versions/latest" 2>/dev/null)
if echo "$response" | jq -e '.id' >/dev/null 2>&1; then
local schema_id
local version
schema_id=$(echo "$response" | jq -r '.id')
version=$(echo "$response" | jq -r '.version')
log_success "- Schema verified for $subject (ID: $schema_id, Version: $version)"
return 0
else
return 1
fi
}
# Verify a schema exists with retry logic (handles Schema Registry consumer lag)
verify_schema_with_retry() {
local subject=$1
local max_attempts=10
local attempt=1
log_info "Verifying schema for subject: $subject"
while [[ $attempt -le $max_attempts ]]; do
local response
response=$(curl -s --max-time 10 "$SCHEMA_REGISTRY_URL/subjects/$subject/versions/latest" 2>/dev/null)
if echo "$response" | jq -e '.id' >/dev/null 2>&1; then
local schema_id
local version
schema_id=$(echo "$response" | jq -r '.id')
version=$(echo "$response" | jq -r '.version')
if [[ $attempt -gt 1 ]]; then
log_success "- Schema verified for $subject (ID: $schema_id, Version: $version) [attempt $attempt]"
else
log_success "- Schema verified for $subject (ID: $schema_id, Version: $version)"
fi
return 0
fi
# Schema not found, wait and retry (handles Schema Registry consumer lag)
if [[ $attempt -lt $max_attempts ]]; then
# Longer exponential backoff for Schema Registry consumer lag: 0.5s, 1s, 2s, 3s, 4s...
local wait_time=$(echo "scale=1; 0.5 * $attempt" | bc)
sleep "$wait_time"
attempt=$((attempt + 1))
else
log_error "x Schema not found for $subject (tried $max_attempts times)"
return 1
fi
done
return 1
}
# Register load test schemas (optimized for batch registration)
register_loadtest_schemas() {
log_info "Registering load test schemas with multiple formats..."
# Define the Avro schema for load test messages
local avro_value_schema='{
"type": "record",
"name": "LoadTestMessage",
"namespace": "com.seaweedfs.loadtest",
"fields": [
{"name": "id", "type": "string"},
{"name": "timestamp", "type": "long"},
{"name": "producer_id", "type": "int"},
{"name": "counter", "type": "long"},
{"name": "user_id", "type": "string"},
{"name": "event_type", "type": "string"},
{"name": "properties", "type": {"type": "map", "values": "string"}}
]
}'
# Define the JSON schema for load test messages
local json_value_schema='{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "LoadTestMessage",
"type": "object",
"properties": {
"id": {"type": "string"},
"timestamp": {"type": "integer"},
"producer_id": {"type": "integer"},
"counter": {"type": "integer"},
"user_id": {"type": "string"},
"event_type": {"type": "string"},
"properties": {
"type": "object",
"additionalProperties": {"type": "string"}
}
},
"required": ["id", "timestamp", "producer_id", "counter", "user_id", "event_type"]
}'
# Define the Protobuf schema for load test messages
local protobuf_value_schema='syntax = "proto3";
package com.seaweedfs.loadtest;
message LoadTestMessage {
string id = 1;
int64 timestamp = 2;
int32 producer_id = 3;
int64 counter = 4;
string user_id = 5;
string event_type = 6;
map<string, string> properties = 7;
}'
# Define the key schema (simple string)
local avro_key_schema='{"type": "string"}'
local json_key_schema='{"type": "string"}'
local protobuf_key_schema='syntax = "proto3"; message Key { string key = 1; }'
# Register schemas for all load test topics with different formats
local topics=("loadtest-topic-0" "loadtest-topic-1" "loadtest-topic-2" "loadtest-topic-3" "loadtest-topic-4")
local success_count=0
local total_schemas=0
# Distribute formats: topic-0=AVRO, topic-1=JSON, topic-2=PROTOBUF, topic-3=AVRO, topic-4=JSON
local idx=0
for topic in "${topics[@]}"; do
local format
local value_schema
local key_schema
# Determine format based on topic index (same as producer logic)
case $((idx % 3)) in
0)
format="AVRO"
value_schema="$avro_value_schema"
key_schema="$avro_key_schema"
;;
1)
format="JSON"
value_schema="$json_value_schema"
key_schema="$json_key_schema"
;;
2)
format="PROTOBUF"
value_schema="$protobuf_value_schema"
key_schema="$protobuf_key_schema"
;;
esac
log_info "Registering $topic with $format schema..."
# Register value schema
if register_schema "${topic}-value" "$value_schema" "$format"; then
success_count=$((success_count + 1))
fi
total_schemas=$((total_schemas + 1))
# Small delay to let Schema Registry consumer process (prevents consumer lag)
sleep 0.2
# Register key schema
if register_schema "${topic}-key" "$key_schema" "$format"; then
success_count=$((success_count + 1))
fi
total_schemas=$((total_schemas + 1))
# Small delay to let Schema Registry consumer process (prevents consumer lag)
sleep 0.2
idx=$((idx + 1))
done
log_info "Schema registration summary: $success_count/$total_schemas schemas registered successfully"
log_info "Format distribution: topic-0=AVRO, topic-1=JSON, topic-2=PROTOBUF, topic-3=AVRO, topic-4=JSON"
if [[ $success_count -eq $total_schemas ]]; then
log_success "All load test schemas registered successfully with multiple formats!"
return 0
else
log_error "Some schemas failed to register"
return 1
fi
}
# Verify all schemas are registered
verify_loadtest_schemas() {
log_info "Verifying load test schemas..."
local topics=("loadtest-topic-0" "loadtest-topic-1" "loadtest-topic-2" "loadtest-topic-3" "loadtest-topic-4")
local success_count=0
local total_schemas=0
for topic in "${topics[@]}"; do
# Verify value schema with retry (handles Schema Registry consumer lag)
if verify_schema_with_retry "${topic}-value"; then
success_count=$((success_count + 1))
fi
total_schemas=$((total_schemas + 1))
# Verify key schema with retry (handles Schema Registry consumer lag)
if verify_schema_with_retry "${topic}-key"; then
success_count=$((success_count + 1))
fi
total_schemas=$((total_schemas + 1))
done
log_info "Schema verification summary: $success_count/$total_schemas schemas verified"
if [[ $success_count -eq $total_schemas ]]; then
log_success "All load test schemas verified successfully!"
return 0
else
log_error "Some schemas are missing or invalid"
return 1
fi
}
# List all registered subjects
list_subjects() {
log_info "Listing all registered subjects..."
local subjects
subjects=$(curl -s --max-time 10 "$SCHEMA_REGISTRY_URL/subjects" 2>/dev/null)
if echo "$subjects" | jq -e '.[]' >/dev/null 2>&1; then
# Use process substitution instead of pipeline to avoid subshell exit code issues
while IFS= read -r subject; do
log_info " - $subject"
done < <(echo "$subjects" | jq -r '.[]')
else
log_warning "No subjects found or Schema Registry not accessible"
fi
return 0
}
# Clean up schemas (for testing)
cleanup_schemas() {
log_warning "Cleaning up load test schemas..."
local topics=("loadtest-topic-0" "loadtest-topic-1" "loadtest-topic-2" "loadtest-topic-3" "loadtest-topic-4")
for topic in "${topics[@]}"; do
# Delete value schema (with timeout)
curl -s --max-time 10 -X DELETE "$SCHEMA_REGISTRY_URL/subjects/${topic}-value" >/dev/null 2>&1 || true
curl -s --max-time 10 -X DELETE "$SCHEMA_REGISTRY_URL/subjects/${topic}-value?permanent=true" >/dev/null 2>&1 || true
# Delete key schema (with timeout)
curl -s --max-time 10 -X DELETE "$SCHEMA_REGISTRY_URL/subjects/${topic}-key" >/dev/null 2>&1 || true
curl -s --max-time 10 -X DELETE "$SCHEMA_REGISTRY_URL/subjects/${topic}-key?permanent=true" >/dev/null 2>&1 || true
done
log_success "Schema cleanup completed"
}
# Main function
main() {
case "${1:-register}" in
"register")
wait_for_schema_registry
register_loadtest_schemas
;;
"verify")
wait_for_schema_registry
verify_loadtest_schemas
;;
"list")
wait_for_schema_registry
list_subjects
;;
"cleanup")
wait_for_schema_registry
cleanup_schemas
;;
"full")
wait_for_schema_registry
register_loadtest_schemas
# Wait for Schema Registry consumer to catch up before verification
log_info "Waiting 3 seconds for Schema Registry consumer to process all schemas..."
sleep 3
verify_loadtest_schemas
list_subjects
;;
*)
echo "Usage: $0 [register|verify|list|cleanup|full]"
echo ""
echo "Commands:"
echo " register - Register load test schemas (default)"
echo " verify - Verify schemas are registered"
echo " list - List all registered subjects"
echo " cleanup - Clean up load test schemas"
echo " full - Register, verify, and list schemas"
echo ""
echo "Environment variables:"
echo " SCHEMA_REGISTRY_URL - Schema Registry URL (default: http://localhost:8081)"
echo " TIMEOUT - Maximum time to wait for Schema Registry (default: 60)"
echo " CHECK_INTERVAL - Check interval in seconds (default: 2)"
exit 1
;;
esac
return 0
}
main "$@"

View File

@@ -0,0 +1,480 @@
#!/bin/bash
# Kafka Client Load Test Runner Script
# This script helps run various load test scenarios against SeaweedFS Kafka Gateway
set -euo pipefail
# Default configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
DOCKER_COMPOSE_FILE="$PROJECT_DIR/docker-compose.yml"
CONFIG_FILE="$PROJECT_DIR/config/loadtest.yaml"
# Default test parameters
TEST_MODE="comprehensive"
TEST_DURATION="300s"
PRODUCER_COUNT=10
CONSUMER_COUNT=5
MESSAGE_RATE=1000
MESSAGE_SIZE=1024
TOPIC_COUNT=5
PARTITIONS_PER_TOPIC=3
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Function to print colored output
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Function to show usage
show_usage() {
cat << EOF
Kafka Client Load Test Runner
Usage: $0 [OPTIONS] [COMMAND]
Commands:
start Start the load test infrastructure and run tests
stop Stop all services
restart Restart all services
status Show service status
logs Show logs from all services
clean Clean up all resources (volumes, networks, etc.)
monitor Start monitoring stack (Prometheus + Grafana)
scenarios Run predefined test scenarios
Options:
-m, --mode MODE Test mode: producer, consumer, comprehensive (default: comprehensive)
-d, --duration DURATION Test duration (default: 300s)
-p, --producers COUNT Number of producers (default: 10)
-c, --consumers COUNT Number of consumers (default: 5)
-r, --rate RATE Messages per second per producer (default: 1000)
-s, --size SIZE Message size in bytes (default: 1024)
-t, --topics COUNT Number of topics (default: 5)
--partitions COUNT Partitions per topic (default: 3)
--config FILE Configuration file (default: config/loadtest.yaml)
--monitoring Enable monitoring stack
--wait-ready Wait for services to be ready before starting tests
-v, --verbose Verbose output
-h, --help Show this help message
Examples:
# Run comprehensive test for 5 minutes
$0 start -m comprehensive -d 5m
# Run producer-only test with high throughput
$0 start -m producer -p 20 -r 2000 -d 10m
# Run consumer-only test
$0 start -m consumer -c 10
# Run with monitoring
$0 start --monitoring -d 15m
# Clean up everything
$0 clean
Predefined Scenarios:
quick Quick smoke test (1 min, low load)
standard Standard load test (5 min, medium load)
stress Stress test (10 min, high load)
endurance Endurance test (30 min, sustained load)
burst Burst test (variable load)
EOF
}
# Parse command line arguments
parse_args() {
while [[ $# -gt 0 ]]; do
case $1 in
-m|--mode)
TEST_MODE="$2"
shift 2
;;
-d|--duration)
TEST_DURATION="$2"
shift 2
;;
-p|--producers)
PRODUCER_COUNT="$2"
shift 2
;;
-c|--consumers)
CONSUMER_COUNT="$2"
shift 2
;;
-r|--rate)
MESSAGE_RATE="$2"
shift 2
;;
-s|--size)
MESSAGE_SIZE="$2"
shift 2
;;
-t|--topics)
TOPIC_COUNT="$2"
shift 2
;;
--partitions)
PARTITIONS_PER_TOPIC="$2"
shift 2
;;
--config)
CONFIG_FILE="$2"
shift 2
;;
--monitoring)
ENABLE_MONITORING=1
shift
;;
--wait-ready)
WAIT_READY=1
shift
;;
-v|--verbose)
VERBOSE=1
shift
;;
-h|--help)
show_usage
exit 0
;;
-*)
log_error "Unknown option: $1"
show_usage
exit 1
;;
*)
if [[ -z "${COMMAND:-}" ]]; then
COMMAND="$1"
else
log_error "Multiple commands specified"
show_usage
exit 1
fi
shift
;;
esac
done
}
# Check if Docker and Docker Compose are available
check_dependencies() {
if ! command -v docker &> /dev/null; then
log_error "Docker is not installed or not in PATH"
exit 1
fi
if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then
log_error "Docker Compose is not installed or not in PATH"
exit 1
fi
# Use docker compose if available, otherwise docker-compose
if docker compose version &> /dev/null; then
DOCKER_COMPOSE="docker compose"
else
DOCKER_COMPOSE="docker-compose"
fi
}
# Wait for services to be ready
wait_for_services() {
log_info "Waiting for services to be ready..."
local timeout=300 # 5 minutes timeout
local elapsed=0
local check_interval=5
while [[ $elapsed -lt $timeout ]]; do
if $DOCKER_COMPOSE -f "$DOCKER_COMPOSE_FILE" ps --format table | grep -q "healthy"; then
if check_service_health; then
log_success "All services are ready!"
return 0
fi
fi
sleep $check_interval
elapsed=$((elapsed + check_interval))
log_info "Waiting... ($elapsed/${timeout}s)"
done
log_error "Services did not become ready within $timeout seconds"
return 1
}
# Check health of critical services
check_service_health() {
# Check Kafka Gateway
if ! curl -s http://localhost:9093 >/dev/null 2>&1; then
return 1
fi
# Check Schema Registry
if ! curl -s http://localhost:8081/subjects >/dev/null 2>&1; then
return 1
fi
return 0
}
# Start the load test infrastructure
start_services() {
log_info "Starting SeaweedFS Kafka load test infrastructure..."
# Set environment variables
export TEST_MODE="$TEST_MODE"
export TEST_DURATION="$TEST_DURATION"
export PRODUCER_COUNT="$PRODUCER_COUNT"
export CONSUMER_COUNT="$CONSUMER_COUNT"
export MESSAGE_RATE="$MESSAGE_RATE"
export MESSAGE_SIZE="$MESSAGE_SIZE"
export TOPIC_COUNT="$TOPIC_COUNT"
export PARTITIONS_PER_TOPIC="$PARTITIONS_PER_TOPIC"
# Start core services
$DOCKER_COMPOSE -f "$DOCKER_COMPOSE_FILE" up -d \
seaweedfs-master \
seaweedfs-volume \
seaweedfs-filer \
seaweedfs-mq-broker \
kafka-gateway \
schema-registry
# Start monitoring if enabled
if [[ "${ENABLE_MONITORING:-0}" == "1" ]]; then
log_info "Starting monitoring stack..."
$DOCKER_COMPOSE -f "$DOCKER_COMPOSE_FILE" --profile monitoring up -d
fi
# Wait for services to be ready if requested
if [[ "${WAIT_READY:-0}" == "1" ]]; then
wait_for_services
fi
log_success "Infrastructure started successfully"
}
# Run the load test
run_loadtest() {
log_info "Starting Kafka client load test..."
log_info "Mode: $TEST_MODE, Duration: $TEST_DURATION"
log_info "Producers: $PRODUCER_COUNT, Consumers: $CONSUMER_COUNT"
log_info "Message Rate: $MESSAGE_RATE msgs/sec, Size: $MESSAGE_SIZE bytes"
# Run the load test
$DOCKER_COMPOSE -f "$DOCKER_COMPOSE_FILE" --profile loadtest up --abort-on-container-exit kafka-client-loadtest
# Show test results
show_results
}
# Show test results
show_results() {
log_info "Load test completed! Gathering results..."
# Get final metrics from the load test container
if $DOCKER_COMPOSE -f "$DOCKER_COMPOSE_FILE" ps kafka-client-loadtest-runner &>/dev/null; then
log_info "Final test statistics:"
$DOCKER_COMPOSE -f "$DOCKER_COMPOSE_FILE" exec -T kafka-client-loadtest-runner curl -s http://localhost:8080/stats || true
fi
# Show Prometheus metrics if monitoring is enabled
if [[ "${ENABLE_MONITORING:-0}" == "1" ]]; then
log_info "Monitoring dashboards available at:"
log_info " Prometheus: http://localhost:9090"
log_info " Grafana: http://localhost:3000 (admin/admin)"
fi
# Show where results are stored
if [[ -d "$PROJECT_DIR/test-results" ]]; then
log_info "Test results saved to: $PROJECT_DIR/test-results/"
fi
}
# Stop services
stop_services() {
log_info "Stopping all services..."
$DOCKER_COMPOSE -f "$DOCKER_COMPOSE_FILE" --profile loadtest --profile monitoring down
log_success "Services stopped"
}
# Show service status
show_status() {
log_info "Service status:"
$DOCKER_COMPOSE -f "$DOCKER_COMPOSE_FILE" ps
}
# Show logs
show_logs() {
$DOCKER_COMPOSE -f "$DOCKER_COMPOSE_FILE" logs -f "${1:-}"
}
# Clean up all resources
clean_all() {
log_warning "This will remove all volumes, networks, and containers. Are you sure? (y/N)"
read -r response
if [[ "$response" =~ ^[Yy]$ ]]; then
log_info "Cleaning up all resources..."
$DOCKER_COMPOSE -f "$DOCKER_COMPOSE_FILE" --profile loadtest --profile monitoring down -v --remove-orphans
# Remove any remaining volumes
docker volume ls -q | grep -E "(kafka-client-loadtest|seaweedfs)" | xargs -r docker volume rm
# Remove networks
docker network ls -q | grep -E "kafka-client-loadtest" | xargs -r docker network rm
log_success "Cleanup completed"
else
log_info "Cleanup cancelled"
fi
}
# Run predefined scenarios
run_scenario() {
local scenario="$1"
case "$scenario" in
quick)
TEST_MODE="comprehensive"
TEST_DURATION="1m"
PRODUCER_COUNT=2
CONSUMER_COUNT=2
MESSAGE_RATE=100
MESSAGE_SIZE=512
TOPIC_COUNT=2
;;
standard)
TEST_MODE="comprehensive"
TEST_DURATION="5m"
PRODUCER_COUNT=5
CONSUMER_COUNT=3
MESSAGE_RATE=500
MESSAGE_SIZE=1024
TOPIC_COUNT=3
;;
stress)
TEST_MODE="comprehensive"
TEST_DURATION="10m"
PRODUCER_COUNT=20
CONSUMER_COUNT=10
MESSAGE_RATE=2000
MESSAGE_SIZE=2048
TOPIC_COUNT=10
;;
endurance)
TEST_MODE="comprehensive"
TEST_DURATION="30m"
PRODUCER_COUNT=10
CONSUMER_COUNT=5
MESSAGE_RATE=1000
MESSAGE_SIZE=1024
TOPIC_COUNT=5
;;
burst)
TEST_MODE="comprehensive"
TEST_DURATION="10m"
PRODUCER_COUNT=10
CONSUMER_COUNT=5
MESSAGE_RATE=1000
MESSAGE_SIZE=1024
TOPIC_COUNT=5
# Note: Burst behavior would be configured in the load test config
;;
*)
log_error "Unknown scenario: $scenario"
log_info "Available scenarios: quick, standard, stress, endurance, burst"
exit 1
;;
esac
log_info "Running $scenario scenario..."
start_services
if [[ "${WAIT_READY:-0}" == "1" ]]; then
wait_for_services
fi
run_loadtest
}
# Main execution
main() {
if [[ $# -eq 0 ]]; then
show_usage
exit 0
fi
parse_args "$@"
check_dependencies
case "${COMMAND:-}" in
start)
start_services
run_loadtest
;;
stop)
stop_services
;;
restart)
stop_services
start_services
;;
status)
show_status
;;
logs)
show_logs
;;
clean)
clean_all
;;
monitor)
ENABLE_MONITORING=1
$DOCKER_COMPOSE -f "$DOCKER_COMPOSE_FILE" --profile monitoring up -d
log_success "Monitoring stack started"
log_info "Prometheus: http://localhost:9090"
log_info "Grafana: http://localhost:3000 (admin/admin)"
;;
scenarios)
if [[ -n "${2:-}" ]]; then
run_scenario "$2"
else
log_error "Please specify a scenario"
log_info "Available scenarios: quick, standard, stress, endurance, burst"
exit 1
fi
;;
*)
log_error "Unknown command: ${COMMAND:-}"
show_usage
exit 1
;;
esac
}
# Set default values
ENABLE_MONITORING=0
WAIT_READY=0
VERBOSE=0
# Run main function
main "$@"

View File

@@ -0,0 +1,352 @@
#!/bin/bash
# Setup monitoring for Kafka Client Load Test
# This script sets up Prometheus and Grafana configurations
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
MONITORING_DIR="$PROJECT_DIR/monitoring"
# Colors
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m'
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
# Create monitoring directory structure
setup_directories() {
log_info "Setting up monitoring directories..."
mkdir -p "$MONITORING_DIR/prometheus"
mkdir -p "$MONITORING_DIR/grafana/dashboards"
mkdir -p "$MONITORING_DIR/grafana/provisioning/dashboards"
mkdir -p "$MONITORING_DIR/grafana/provisioning/datasources"
log_success "Directories created"
}
# Create Prometheus configuration
create_prometheus_config() {
log_info "Creating Prometheus configuration..."
cat > "$MONITORING_DIR/prometheus/prometheus.yml" << 'EOF'
# Prometheus configuration for Kafka Load Test monitoring
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
# - "first_rules.yml"
# - "second_rules.yml"
scrape_configs:
# Scrape Prometheus itself
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
# Scrape load test metrics
- job_name: 'kafka-loadtest'
static_configs:
- targets: ['kafka-client-loadtest-runner:8080']
scrape_interval: 5s
metrics_path: '/metrics'
# Scrape SeaweedFS Master metrics
- job_name: 'seaweedfs-master'
static_configs:
- targets: ['seaweedfs-master:9333']
metrics_path: '/metrics'
# Scrape SeaweedFS Volume metrics
- job_name: 'seaweedfs-volume'
static_configs:
- targets: ['seaweedfs-volume:8080']
metrics_path: '/metrics'
# Scrape SeaweedFS Filer metrics
- job_name: 'seaweedfs-filer'
static_configs:
- targets: ['seaweedfs-filer:8888']
metrics_path: '/metrics'
# Scrape SeaweedFS MQ Broker metrics (if available)
- job_name: 'seaweedfs-mq-broker'
static_configs:
- targets: ['seaweedfs-mq-broker:17777']
metrics_path: '/metrics'
scrape_interval: 10s
# Scrape Kafka Gateway metrics (if available)
- job_name: 'kafka-gateway'
static_configs:
- targets: ['kafka-gateway:9093']
metrics_path: '/metrics'
scrape_interval: 10s
EOF
log_success "Prometheus configuration created"
}
# Create Grafana datasource configuration
create_grafana_datasource() {
log_info "Creating Grafana datasource configuration..."
cat > "$MONITORING_DIR/grafana/provisioning/datasources/datasource.yml" << 'EOF'
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
orgId: 1
url: http://prometheus:9090
basicAuth: false
isDefault: true
editable: true
version: 1
EOF
log_success "Grafana datasource configuration created"
}
# Create Grafana dashboard provisioning
create_grafana_dashboard_provisioning() {
log_info "Creating Grafana dashboard provisioning..."
cat > "$MONITORING_DIR/grafana/provisioning/dashboards/dashboard.yml" << 'EOF'
apiVersion: 1
providers:
- name: 'default'
orgId: 1
folder: ''
type: file
disableDeletion: false
editable: true
options:
path: /var/lib/grafana/dashboards
EOF
log_success "Grafana dashboard provisioning created"
}
# Create Kafka Load Test dashboard
create_loadtest_dashboard() {
log_info "Creating Kafka Load Test Grafana dashboard..."
cat > "$MONITORING_DIR/grafana/dashboards/kafka-loadtest.json" << 'EOF'
{
"dashboard": {
"id": null,
"title": "Kafka Client Load Test Dashboard",
"tags": ["kafka", "loadtest", "seaweedfs"],
"timezone": "browser",
"panels": [
{
"id": 1,
"title": "Messages Produced/Consumed",
"type": "stat",
"targets": [
{
"expr": "rate(kafka_loadtest_messages_produced_total[5m])",
"legendFormat": "Produced/sec"
},
{
"expr": "rate(kafka_loadtest_messages_consumed_total[5m])",
"legendFormat": "Consumed/sec"
}
],
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 0}
},
{
"id": 2,
"title": "Message Latency",
"type": "graph",
"targets": [
{
"expr": "histogram_quantile(0.95, kafka_loadtest_message_latency_seconds)",
"legendFormat": "95th percentile"
},
{
"expr": "histogram_quantile(0.99, kafka_loadtest_message_latency_seconds)",
"legendFormat": "99th percentile"
}
],
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 0}
},
{
"id": 3,
"title": "Error Rates",
"type": "graph",
"targets": [
{
"expr": "rate(kafka_loadtest_producer_errors_total[5m])",
"legendFormat": "Producer Errors/sec"
},
{
"expr": "rate(kafka_loadtest_consumer_errors_total[5m])",
"legendFormat": "Consumer Errors/sec"
}
],
"gridPos": {"h": 8, "w": 24, "x": 0, "y": 8}
},
{
"id": 4,
"title": "Throughput (MB/s)",
"type": "graph",
"targets": [
{
"expr": "rate(kafka_loadtest_bytes_produced_total[5m]) / 1024 / 1024",
"legendFormat": "Produced MB/s"
},
{
"expr": "rate(kafka_loadtest_bytes_consumed_total[5m]) / 1024 / 1024",
"legendFormat": "Consumed MB/s"
}
],
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 16}
},
{
"id": 5,
"title": "Active Clients",
"type": "stat",
"targets": [
{
"expr": "kafka_loadtest_active_producers",
"legendFormat": "Producers"
},
{
"expr": "kafka_loadtest_active_consumers",
"legendFormat": "Consumers"
}
],
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 16}
},
{
"id": 6,
"title": "Consumer Lag",
"type": "graph",
"targets": [
{
"expr": "kafka_loadtest_consumer_lag_messages",
"legendFormat": "{{consumer_group}}-{{topic}}-{{partition}}"
}
],
"gridPos": {"h": 8, "w": 24, "x": 0, "y": 24}
}
],
"time": {"from": "now-30m", "to": "now"},
"refresh": "5s",
"schemaVersion": 16,
"version": 0
}
}
EOF
log_success "Kafka Load Test dashboard created"
}
# Create SeaweedFS dashboard
create_seaweedfs_dashboard() {
log_info "Creating SeaweedFS Grafana dashboard..."
cat > "$MONITORING_DIR/grafana/dashboards/seaweedfs.json" << 'EOF'
{
"dashboard": {
"id": null,
"title": "SeaweedFS Cluster Dashboard",
"tags": ["seaweedfs", "storage"],
"timezone": "browser",
"panels": [
{
"id": 1,
"title": "Master Status",
"type": "stat",
"targets": [
{
"expr": "up{job=\"seaweedfs-master\"}",
"legendFormat": "Master Up"
}
],
"gridPos": {"h": 4, "w": 6, "x": 0, "y": 0}
},
{
"id": 2,
"title": "Volume Status",
"type": "stat",
"targets": [
{
"expr": "up{job=\"seaweedfs-volume\"}",
"legendFormat": "Volume Up"
}
],
"gridPos": {"h": 4, "w": 6, "x": 6, "y": 0}
},
{
"id": 3,
"title": "Filer Status",
"type": "stat",
"targets": [
{
"expr": "up{job=\"seaweedfs-filer\"}",
"legendFormat": "Filer Up"
}
],
"gridPos": {"h": 4, "w": 6, "x": 12, "y": 0}
},
{
"id": 4,
"title": "MQ Broker Status",
"type": "stat",
"targets": [
{
"expr": "up{job=\"seaweedfs-mq-broker\"}",
"legendFormat": "MQ Broker Up"
}
],
"gridPos": {"h": 4, "w": 6, "x": 18, "y": 0}
}
],
"time": {"from": "now-30m", "to": "now"},
"refresh": "10s",
"schemaVersion": 16,
"version": 0
}
}
EOF
log_success "SeaweedFS dashboard created"
}
# Main setup function
main() {
log_info "Setting up monitoring for Kafka Client Load Test..."
setup_directories
create_prometheus_config
create_grafana_datasource
create_grafana_dashboard_provisioning
create_loadtest_dashboard
create_seaweedfs_dashboard
log_success "Monitoring setup completed!"
log_info "You can now start the monitoring stack with:"
log_info " ./scripts/run-loadtest.sh monitor"
log_info ""
log_info "After starting, access:"
log_info " Prometheus: http://localhost:9090"
log_info " Grafana: http://localhost:3000 (admin/admin)"
}
main "$@"

View File

@@ -0,0 +1,151 @@
#!/bin/bash
# Test script to verify the retry logic works correctly
# Simulates Schema Registry eventual consistency behavior
set -euo pipefail
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log_info() {
echo -e "${BLUE}[TEST]${NC} $1"
}
log_success() {
echo -e "${GREEN}[PASS]${NC} $1"
}
log_error() {
echo -e "${RED}[FAIL]${NC} $1"
}
# Mock function that simulates Schema Registry eventual consistency
# First N attempts fail, then succeeds
mock_schema_registry_query() {
local subject=$1
local min_attempts_to_succeed=$2
local current_attempt=$3
if [[ $current_attempt -ge $min_attempts_to_succeed ]]; then
# Simulate successful response
echo '{"id":1,"version":1,"schema":"test"}'
return 0
else
# Simulate 404 Not Found
echo '{"error_code":40401,"message":"Subject not found"}'
return 1
fi
}
# Simulate verify_schema_with_retry logic
test_verify_with_retry() {
local subject=$1
local min_attempts_to_succeed=$2
local max_attempts=5
local attempt=1
log_info "Testing $subject (should succeed after $min_attempts_to_succeed attempts)"
while [[ $attempt -le $max_attempts ]]; do
local response
if response=$(mock_schema_registry_query "$subject" "$min_attempts_to_succeed" "$attempt"); then
if echo "$response" | grep -q '"id"'; then
if [[ $attempt -gt 1 ]]; then
log_success "$subject verified after $attempt attempts"
else
log_success "$subject verified on first attempt"
fi
return 0
fi
fi
# Schema not found, wait and retry
if [[ $attempt -lt $max_attempts ]]; then
# Exponential backoff: 0.1s, 0.2s, 0.4s, 0.8s
local wait_time=$(echo "scale=3; 0.1 * (2 ^ ($attempt - 1))" | bc)
log_info " Attempt $attempt failed, waiting ${wait_time}s before retry..."
sleep "$wait_time"
attempt=$((attempt + 1))
else
log_error "$subject verification failed after $max_attempts attempts"
return 1
fi
done
return 1
}
# Run tests
log_info "=========================================="
log_info "Testing Schema Registry Retry Logic"
log_info "=========================================="
echo ""
# Test 1: Schema available immediately
log_info "Test 1: Schema available immediately"
if test_verify_with_retry "immediate-schema" 1; then
log_success "✓ Test 1 passed"
else
log_error "✗ Test 1 failed"
exit 1
fi
echo ""
# Test 2: Schema available after 2 attempts (200ms delay)
log_info "Test 2: Schema available after 2 attempts"
if test_verify_with_retry "delayed-schema-2" 2; then
log_success "✓ Test 2 passed"
else
log_error "✗ Test 2 failed"
exit 1
fi
echo ""
# Test 3: Schema available after 3 attempts (600ms delay)
log_info "Test 3: Schema available after 3 attempts"
if test_verify_with_retry "delayed-schema-3" 3; then
log_success "✓ Test 3 passed"
else
log_error "✗ Test 3 failed"
exit 1
fi
echo ""
# Test 4: Schema available after 4 attempts (1400ms delay)
log_info "Test 4: Schema available after 4 attempts"
if test_verify_with_retry "delayed-schema-4" 4; then
log_success "✓ Test 4 passed"
else
log_error "✗ Test 4 failed"
exit 1
fi
echo ""
# Test 5: Schema never available (should fail)
log_info "Test 5: Schema never available (should fail gracefully)"
if test_verify_with_retry "missing-schema" 10; then
log_error "✗ Test 5 failed (should have failed but passed)"
exit 1
else
log_success "✓ Test 5 passed (correctly failed after max attempts)"
fi
echo ""
log_success "=========================================="
log_success "All tests passed! ✓"
log_success "=========================================="
log_info ""
log_info "Summary:"
log_info "- Immediate availability: works ✓"
log_info "- 2-4 retry attempts: works ✓"
log_info "- Max attempts handling: works ✓"
log_info "- Exponential backoff: works ✓"
log_info ""
log_info "Total retry time budget: ~1.5 seconds (0.1+0.2+0.4+0.8)"
log_info "This should handle Schema Registry consumer lag gracefully."

View File

@@ -0,0 +1,291 @@
#!/bin/bash
# Wait for SeaweedFS and Kafka Gateway services to be ready
# This script checks service health and waits until all services are operational
set -euo pipefail
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Configuration
TIMEOUT=${TIMEOUT:-300} # 5 minutes default timeout
CHECK_INTERVAL=${CHECK_INTERVAL:-5} # Check every 5 seconds
SEAWEEDFS_MASTER_URL=${SEAWEEDFS_MASTER_URL:-"http://localhost:9333"}
KAFKA_GATEWAY_URL=${KAFKA_GATEWAY_URL:-"localhost:9093"}
SCHEMA_REGISTRY_URL=${SCHEMA_REGISTRY_URL:-"http://localhost:8081"}
SEAWEEDFS_FILER_URL=${SEAWEEDFS_FILER_URL:-"http://localhost:8888"}
# Check if a service is reachable
check_http_service() {
local url=$1
local name=$2
if curl -sf "$url" >/dev/null 2>&1; then
return 0
else
return 1
fi
}
# Check TCP port
check_tcp_service() {
local host=$1
local port=$2
local name=$3
if timeout 3 bash -c "</dev/tcp/$host/$port" 2>/dev/null; then
return 0
else
return 1
fi
}
# Check SeaweedFS Master
check_seaweedfs_master() {
if check_http_service "$SEAWEEDFS_MASTER_URL/cluster/status" "SeaweedFS Master"; then
# Additional check: ensure cluster has volumes
local status_json
status_json=$(curl -s "$SEAWEEDFS_MASTER_URL/cluster/status" 2>/dev/null || echo "{}")
# Check if we have at least one volume server
if echo "$status_json" | grep -q '"Max":0'; then
log_warning "SeaweedFS Master is running but no volumes are available"
return 1
fi
return 0
fi
return 1
}
# Check SeaweedFS Filer
check_seaweedfs_filer() {
check_http_service "$SEAWEEDFS_FILER_URL/" "SeaweedFS Filer"
}
# Check Kafka Gateway
check_kafka_gateway() {
local host="localhost"
local port="9093"
check_tcp_service "$host" "$port" "Kafka Gateway"
}
# Check Schema Registry
check_schema_registry() {
# Check if Schema Registry container is running first
if ! docker compose ps schema-registry | grep -q "Up"; then
# Schema Registry is not running, which is okay for basic tests
return 0
fi
# FIXED: Wait for Docker healthcheck to report "healthy", not just "Up"
# Schema Registry has a 30s start_period, so we need to wait for the actual healthcheck
local health_status
health_status=$(docker inspect loadtest-schema-registry --format='{{.State.Health.Status}}' 2>/dev/null || echo "none")
# If container has no healthcheck or healthcheck is not yet healthy, check HTTP directly
if [[ "$health_status" == "healthy" ]]; then
# Container reports healthy, do a final verification
if check_http_service "$SCHEMA_REGISTRY_URL/subjects" "Schema Registry"; then
return 0
fi
elif [[ "$health_status" == "starting" ]]; then
# Still in startup period, wait longer
return 1
elif [[ "$health_status" == "none" ]]; then
# No healthcheck defined (shouldn't happen), fall back to HTTP check
if check_http_service "$SCHEMA_REGISTRY_URL/subjects" "Schema Registry"; then
local subjects
subjects=$(curl -s "$SCHEMA_REGISTRY_URL/subjects" 2>/dev/null || echo "[]")
# Schema registry should at least return an empty array
if [[ "$subjects" == "[]" ]]; then
return 0
elif echo "$subjects" | grep -q '\['; then
return 0
else
log_warning "Schema Registry is not properly connected"
return 1
fi
fi
fi
return 1
}
# Check MQ Broker
check_mq_broker() {
check_tcp_service "localhost" "17777" "SeaweedFS MQ Broker"
}
# Main health check function
check_all_services() {
local all_healthy=true
log_info "Checking service health..."
# Check SeaweedFS Master
if check_seaweedfs_master; then
log_success "✓ SeaweedFS Master is healthy"
else
log_error "✗ SeaweedFS Master is not ready"
all_healthy=false
fi
# Check SeaweedFS Filer
if check_seaweedfs_filer; then
log_success "✓ SeaweedFS Filer is healthy"
else
log_error "✗ SeaweedFS Filer is not ready"
all_healthy=false
fi
# Check MQ Broker
if check_mq_broker; then
log_success "✓ SeaweedFS MQ Broker is healthy"
else
log_error "✗ SeaweedFS MQ Broker is not ready"
all_healthy=false
fi
# Check Kafka Gateway
if check_kafka_gateway; then
log_success "✓ Kafka Gateway is healthy"
else
log_error "✗ Kafka Gateway is not ready"
all_healthy=false
fi
# Check Schema Registry
if ! docker compose ps schema-registry | grep -q "Up"; then
log_warning "⚠ Schema Registry is stopped (skipping)"
elif check_schema_registry; then
log_success "✓ Schema Registry is healthy"
else
# Check if it's still starting up (healthcheck start_period)
local health_status
health_status=$(docker inspect loadtest-schema-registry --format='{{.State.Health.Status}}' 2>/dev/null || echo "unknown")
if [[ "$health_status" == "starting" ]]; then
log_warning "⏳ Schema Registry is starting (waiting for healthcheck...)"
else
log_error "✗ Schema Registry is not ready (status: $health_status)"
fi
all_healthy=false
fi
$all_healthy
}
# Wait for all services to be ready
wait_for_services() {
log_info "Waiting for all services to be ready (timeout: ${TIMEOUT}s)..."
local elapsed=0
while [[ $elapsed -lt $TIMEOUT ]]; do
if check_all_services; then
log_success "All services are ready! (took ${elapsed}s)"
return 0
fi
log_info "Some services are not ready yet. Waiting ${CHECK_INTERVAL}s... (${elapsed}/${TIMEOUT}s)"
sleep $CHECK_INTERVAL
elapsed=$((elapsed + CHECK_INTERVAL))
done
log_error "Services did not become ready within ${TIMEOUT} seconds"
log_error "Final service status:"
check_all_services
# Always dump Schema Registry diagnostics on timeout since it's the problematic service
log_error "==========================================="
log_error "Schema Registry Container Status:"
log_error "==========================================="
docker compose ps schema-registry 2>&1 || echo "Failed to get container status"
docker inspect loadtest-schema-registry --format='Health: {{.State.Health.Status}} ({{len .State.Health.Log}} checks)' 2>&1 || echo "Failed to inspect container"
log_error "==========================================="
log_error "Network Connectivity Check:"
log_error "==========================================="
log_error "Can Schema Registry reach Kafka Gateway?"
docker compose exec -T schema-registry ping -c 3 kafka-gateway 2>&1 || echo "Ping failed"
docker compose exec -T schema-registry nc -zv kafka-gateway 9093 2>&1 || echo "Port 9093 unreachable"
log_error "==========================================="
log_error "Schema Registry Logs (last 100 lines):"
log_error "==========================================="
docker compose logs --tail=100 schema-registry 2>&1 || echo "Failed to get Schema Registry logs"
log_error "==========================================="
log_error "Kafka Gateway Logs (last 50 lines with 'SR' prefix):"
log_error "==========================================="
docker compose logs --tail=200 kafka-gateway 2>&1 | grep -i "SR" | tail -50 || echo "No SR-related logs found in Kafka Gateway"
log_error "==========================================="
log_error "MQ Broker Logs (last 30 lines):"
log_error "==========================================="
docker compose logs --tail=30 seaweedfs-mq-broker 2>&1 || echo "Failed to get MQ Broker logs"
log_error "==========================================="
return 1
}
# Show current service status
show_status() {
log_info "Current service status:"
check_all_services
}
# Main function
main() {
case "${1:-wait}" in
"wait")
wait_for_services
;;
"check")
show_status
;;
"status")
show_status
;;
*)
echo "Usage: $0 [wait|check|status]"
echo ""
echo "Commands:"
echo " wait - Wait for all services to be ready (default)"
echo " check - Check current service status"
echo " status - Same as check"
echo ""
echo "Environment variables:"
echo " TIMEOUT - Maximum time to wait in seconds (default: 300)"
echo " CHECK_INTERVAL - Check interval in seconds (default: 5)"
echo " SEAWEEDFS_MASTER_URL - Master URL (default: http://localhost:9333)"
echo " KAFKA_GATEWAY_URL - Gateway URL (default: localhost:9093)"
echo " SCHEMA_REGISTRY_URL - Schema Registry URL (default: http://localhost:8081)"
echo " SEAWEEDFS_FILER_URL - Filer URL (default: http://localhost:8888)"
exit 1
;;
esac
}
main "$@"

View File

@@ -0,0 +1,36 @@
#!/bin/bash
# Single partition test - produce and consume from ONE topic, ONE partition
set -e
echo "================================================================"
echo " Single Partition Test - Isolate Missing Messages"
echo " - Topic: single-test-topic (1 partition only)"
echo " - Duration: 2 minutes"
echo " - Producer: 1 (50 msgs/sec)"
echo " - Consumer: 1 (reading from partition 0 only)"
echo "================================================================"
# Clean up
make clean
make start
# Run test with single topic, single partition
TEST_MODE=comprehensive \
TEST_DURATION=2m \
PRODUCER_COUNT=1 \
CONSUMER_COUNT=1 \
MESSAGE_RATE=50 \
MESSAGE_SIZE=512 \
TOPIC_COUNT=1 \
PARTITIONS_PER_TOPIC=1 \
VALUE_TYPE=avro \
docker compose --profile loadtest up --abort-on-container-exit kafka-client-loadtest
echo ""
echo "================================================================"
echo " Single Partition Test Complete!"
echo "================================================================"
echo ""
echo "Analyzing results..."
cd test-results && python3 analyze_missing.py

View File

@@ -0,0 +1,43 @@
#!/bin/bash
# Test without schema registry to isolate missing messages issue
# Clean old data
find test-results -name "*.jsonl" -delete 2>/dev/null || true
# Run test without schemas
TEST_MODE=comprehensive \
TEST_DURATION=1m \
PRODUCER_COUNT=2 \
CONSUMER_COUNT=2 \
MESSAGE_RATE=50 \
MESSAGE_SIZE=512 \
VALUE_TYPE=json \
SCHEMAS_ENABLED=false \
docker compose --profile loadtest up --abort-on-container-exit kafka-client-loadtest
echo ""
echo "═══════════════════════════════════════════════════════"
echo "Analyzing results..."
if [ -f test-results/produced.jsonl ] && [ -f test-results/consumed.jsonl ]; then
produced=$(wc -l < test-results/produced.jsonl)
consumed=$(wc -l < test-results/consumed.jsonl)
echo "Produced: $produced"
echo "Consumed: $consumed"
# Check for missing messages
jq -r '"\(.topic)[\(.partition)]@\(.offset)"' test-results/produced.jsonl | sort > /tmp/produced.txt
jq -r '"\(.topic)[\(.partition)]@\(.offset)"' test-results/consumed.jsonl | sort > /tmp/consumed.txt
missing=$(comm -23 /tmp/produced.txt /tmp/consumed.txt | wc -l)
echo "Missing: $missing"
if [ $missing -eq 0 ]; then
echo "✓ NO MISSING MESSAGES!"
else
echo "✗ Still have missing messages"
echo "Sample missing:"
comm -23 /tmp/produced.txt /tmp/consumed.txt | head -10
fi
else
echo "✗ Result files not found"
fi
echo "═══════════════════════════════════════════════════════"

View File

@@ -0,0 +1,86 @@
package main
import (
"context"
"log"
"time"
"github.com/IBM/sarama"
)
func main() {
log.Println("=== Testing OffsetFetch with Debug Sarama ===")
config := sarama.NewConfig()
config.Version = sarama.V2_8_0_0
config.Consumer.Return.Errors = true
config.Consumer.Offsets.Initial = sarama.OffsetOldest
config.Consumer.Offsets.AutoCommit.Enable = true
config.Consumer.Offsets.AutoCommit.Interval = 100 * time.Millisecond
config.Consumer.Group.Session.Timeout = 30 * time.Second
config.Consumer.Group.Heartbeat.Interval = 3 * time.Second
brokers := []string{"localhost:9093"}
group := "test-offset-fetch-group"
topics := []string{"loadtest-topic-0"}
log.Printf("Creating consumer group: group=%s brokers=%v topics=%v", group, brokers, topics)
consumerGroup, err := sarama.NewConsumerGroup(brokers, group, config)
if err != nil {
log.Fatalf("Failed to create consumer group: %v", err)
}
defer consumerGroup.Close()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
handler := &testHandler{}
log.Println("Starting consumer group session...")
log.Println("Watch for 🔍 [SARAMA-DEBUG] logs to trace OffsetFetch calls")
go func() {
for {
if err := consumerGroup.Consume(ctx, topics, handler); err != nil {
log.Printf("Error from consumer: %v", err)
}
if ctx.Err() != nil {
return
}
}
}()
// Wait for context to be done
<-ctx.Done()
log.Println("Test completed")
}
type testHandler struct{}
func (h *testHandler) Setup(session sarama.ConsumerGroupSession) error {
log.Printf("✓ Consumer group session setup: generation=%d memberID=%s", session.GenerationID(), session.MemberID())
return nil
}
func (h *testHandler) Cleanup(session sarama.ConsumerGroupSession) error {
log.Println("Consumer group session cleanup")
return nil
}
func (h *testHandler) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
log.Printf("✓ Started consuming: topic=%s partition=%d offset=%d", claim.Topic(), claim.Partition(), claim.InitialOffset())
count := 0
for message := range claim.Messages() {
count++
log.Printf(" Received message #%d: offset=%d", count, message.Offset)
session.MarkMessage(message, "")
if count >= 5 {
log.Println("Received 5 messages, stopping")
return nil
}
}
return nil
}

View File

@@ -0,0 +1,290 @@
import org.apache.kafka.clients.admin.AdminClient;
import org.apache.kafka.clients.admin.AdminClientConfig;
import org.apache.kafka.clients.admin.DescribeClusterResult;
import org.apache.kafka.common.Node;
import java.io.*;
import java.net.*;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.concurrent.ExecutionException;
public class AdminClientDebugger {
public static void main(String[] args) throws Exception {
String broker = args.length > 0 ? args[0] : "localhost:9093";
System.out.println("=".repeat(80));
System.out.println("KAFKA ADMINCLIENT DEBUGGER");
System.out.println("=".repeat(80));
System.out.println("Target broker: " + broker);
// Test 1: Raw socket - capture exact bytes
System.out.println("\n" + "=".repeat(80));
System.out.println("TEST 1: Raw Socket - Capture ApiVersions Exchange");
System.out.println("=".repeat(80));
testRawSocket(broker);
// Test 2: AdminClient with detailed logging
System.out.println("\n" + "=".repeat(80));
System.out.println("TEST 2: AdminClient with Logging");
System.out.println("=".repeat(80));
testAdminClient(broker);
}
private static void testRawSocket(String broker) {
String[] parts = broker.split(":");
String host = parts[0];
int port = Integer.parseInt(parts[1]);
try (Socket socket = new Socket(host, port)) {
socket.setSoTimeout(10000);
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
System.out.println("Connected to " + broker);
// Build ApiVersions request (v4)
// Format:
// [Size][ApiKey=18][ApiVersion=4][CorrelationId=0][ClientId][TaggedFields]
ByteArrayOutputStream requestBody = new ByteArrayOutputStream();
// ApiKey (2 bytes) = 18
requestBody.write(0);
requestBody.write(18);
// ApiVersion (2 bytes) = 4
requestBody.write(0);
requestBody.write(4);
// CorrelationId (4 bytes) = 0
requestBody.write(new byte[] { 0, 0, 0, 0 });
// ClientId (compact string) = "debug-client"
String clientId = "debug-client";
writeCompactString(requestBody, clientId);
// Tagged fields (empty)
requestBody.write(0x00);
byte[] request = requestBody.toByteArray();
// Write size
ByteBuffer sizeBuffer = ByteBuffer.allocate(4);
sizeBuffer.putInt(request.length);
out.write(sizeBuffer.array());
// Write request
out.write(request);
out.flush();
System.out.println("\nSENT ApiVersions v4 Request:");
System.out.println(" Size: " + request.length + " bytes");
hexDump(" Request", request, Math.min(64, request.length));
// Read response size
byte[] sizeBytes = new byte[4];
int read = in.read(sizeBytes);
if (read != 4) {
System.out.println("Failed to read response size (got " + read + " bytes)");
return;
}
int responseSize = ByteBuffer.wrap(sizeBytes).getInt();
System.out.println("\nRECEIVED Response:");
System.out.println(" Size: " + responseSize + " bytes");
// Read response body
byte[] responseBytes = new byte[responseSize];
int totalRead = 0;
while (totalRead < responseSize) {
int n = in.read(responseBytes, totalRead, responseSize - totalRead);
if (n == -1) {
System.out.println("Unexpected EOF after " + totalRead + " bytes");
return;
}
totalRead += n;
}
System.out.println(" Read complete response: " + totalRead + " bytes");
// Decode response
System.out.println("\nRESPONSE STRUCTURE:");
decodeApiVersionsResponse(responseBytes);
// Try to read more (should timeout or get EOF)
System.out.println("\n⏱️ Waiting for any additional data (10s timeout)...");
socket.setSoTimeout(10000);
try {
int nextByte = in.read();
if (nextByte == -1) {
System.out.println(" Server closed connection (EOF)");
} else {
System.out.println(" Unexpected data: " + nextByte);
}
} catch (SocketTimeoutException e) {
System.out.println(" Timeout - no additional data");
}
} catch (Exception e) {
System.out.println("Error: " + e.getMessage());
e.printStackTrace();
}
}
private static void testAdminClient(String broker) {
Properties props = new Properties();
props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, broker);
props.put(AdminClientConfig.CLIENT_ID_CONFIG, "admin-client-debugger");
props.put(AdminClientConfig.REQUEST_TIMEOUT_MS_CONFIG, 10000);
props.put(AdminClientConfig.DEFAULT_API_TIMEOUT_MS_CONFIG, 10000);
System.out.println("Creating AdminClient with config:");
props.forEach((k, v) -> System.out.println(" " + k + " = " + v));
try (AdminClient adminClient = AdminClient.create(props)) {
System.out.println("AdminClient created");
// Give the thread time to start
Thread.sleep(1000);
System.out.println("\nCalling describeCluster()...");
DescribeClusterResult result = adminClient.describeCluster();
System.out.println(" Waiting for nodes...");
Collection<Node> nodes = result.nodes().get();
System.out.println("Cluster description retrieved:");
System.out.println(" Nodes: " + nodes.size());
for (Node node : nodes) {
System.out.println(" - Node " + node.id() + ": " + node.host() + ":" + node.port());
}
System.out.println("\n Cluster ID: " + result.clusterId().get());
Node controller = result.controller().get();
if (controller != null) {
System.out.println(" Controller: Node " + controller.id());
}
} catch (ExecutionException e) {
System.out.println("Execution error: " + e.getCause().getMessage());
e.getCause().printStackTrace();
} catch (Exception e) {
System.out.println("Error: " + e.getMessage());
e.printStackTrace();
}
}
private static void decodeApiVersionsResponse(byte[] data) {
int offset = 0;
try {
// Correlation ID (4 bytes)
int correlationId = ByteBuffer.wrap(data, offset, 4).getInt();
System.out.println(" [Offset " + offset + "] Correlation ID: " + correlationId);
offset += 4;
// Header tagged fields (varint - should be 0x00 for flexible v3+)
int taggedFieldsLength = readUnsignedVarint(data, offset);
System.out.println(" [Offset " + offset + "] Header Tagged Fields Length: " + taggedFieldsLength);
offset += varintSize(data[offset]);
// Error code (2 bytes)
short errorCode = ByteBuffer.wrap(data, offset, 2).getShort();
System.out.println(" [Offset " + offset + "] Error Code: " + errorCode);
offset += 2;
// API Keys array (compact array - varint length)
int apiKeysLength = readUnsignedVarint(data, offset) - 1; // Compact array: length+1
System.out.println(" [Offset " + offset + "] API Keys Count: " + apiKeysLength);
offset += varintSize(data[offset]);
// Show first few API keys
System.out.println(" First 5 API Keys:");
for (int i = 0; i < Math.min(5, apiKeysLength); i++) {
short apiKey = ByteBuffer.wrap(data, offset, 2).getShort();
offset += 2;
short minVersion = ByteBuffer.wrap(data, offset, 2).getShort();
offset += 2;
short maxVersion = ByteBuffer.wrap(data, offset, 2).getShort();
offset += 2;
// Per-element tagged fields
int perElementTagged = readUnsignedVarint(data, offset);
offset += varintSize(data[offset]);
System.out.println(" " + (i + 1) + ". API " + apiKey + ": v" + minVersion + "-v" + maxVersion);
}
System.out.println(" ... (showing first 5 of " + apiKeysLength + " APIs)");
System.out.println(" Response structure is valid!");
// Hex dump of first 64 bytes
hexDump("\n First 64 bytes", data, Math.min(64, data.length));
} catch (Exception e) {
System.out.println(" Failed to decode at offset " + offset + ": " + e.getMessage());
hexDump(" Raw bytes", data, Math.min(128, data.length));
}
}
private static int readUnsignedVarint(byte[] data, int offset) {
int value = 0;
int shift = 0;
while (true) {
byte b = data[offset++];
value |= (b & 0x7F) << shift;
if ((b & 0x80) == 0)
break;
shift += 7;
}
return value;
}
private static int varintSize(byte firstByte) {
int size = 1;
byte b = firstByte;
while ((b & 0x80) != 0) {
size++;
b = (byte) (b << 1);
}
return size;
}
private static void writeCompactString(ByteArrayOutputStream out, String str) {
byte[] bytes = str.getBytes();
writeUnsignedVarint(out, bytes.length + 1); // Compact string: length+1
out.write(bytes, 0, bytes.length);
}
private static void writeUnsignedVarint(ByteArrayOutputStream out, int value) {
while ((value & ~0x7F) != 0) {
out.write((byte) ((value & 0x7F) | 0x80));
value >>>= 7;
}
out.write((byte) value);
}
private static void hexDump(String label, byte[] data, int length) {
System.out.println(label + " (hex dump):");
for (int i = 0; i < length; i += 16) {
System.out.printf(" %04x ", i);
for (int j = 0; j < 16; j++) {
if (i + j < length) {
System.out.printf("%02x ", data[i + j] & 0xFF);
} else {
System.out.print(" ");
}
if (j == 7)
System.out.print(" ");
}
System.out.print(" |");
for (int j = 0; j < 16 && i + j < length; j++) {
byte b = data[i + j];
System.out.print((b >= 32 && b < 127) ? (char) b : '.');
}
System.out.println("|");
}
}
}

View File

@@ -0,0 +1,72 @@
import org.apache.kafka.clients.admin.AdminClient;
import org.apache.kafka.clients.admin.AdminClientConfig;
import org.apache.kafka.clients.admin.DescribeClusterResult;
import org.apache.kafka.clients.admin.ListTopicsResult;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
public class JavaAdminClientTest {
public static void main(String[] args) {
// Set uncaught exception handler to catch AdminClient thread errors
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
System.err.println("UNCAUGHT EXCEPTION in thread " + t.getName() + ":");
e.printStackTrace();
});
String bootstrapServers = args.length > 0 ? args[0] : "localhost:9093";
System.out.println("Testing Kafka wire protocol with broker: " + bootstrapServers);
Properties props = new Properties();
props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
props.put(AdminClientConfig.REQUEST_TIMEOUT_MS_CONFIG, 10000);
props.put(AdminClientConfig.DEFAULT_API_TIMEOUT_MS_CONFIG, 10000);
props.put(AdminClientConfig.CLIENT_ID_CONFIG, "java-admin-test");
props.put(AdminClientConfig.CONNECTIONS_MAX_IDLE_MS_CONFIG, 120000);
props.put(AdminClientConfig.SOCKET_CONNECTION_SETUP_TIMEOUT_MS_CONFIG, 10000);
props.put(AdminClientConfig.SOCKET_CONNECTION_SETUP_TIMEOUT_MAX_MS_CONFIG, 30000);
props.put(AdminClientConfig.SECURITY_PROTOCOL_CONFIG, "PLAINTEXT");
props.put(AdminClientConfig.RECONNECT_BACKOFF_MS_CONFIG, 50);
props.put(AdminClientConfig.RECONNECT_BACKOFF_MAX_MS_CONFIG, 1000);
System.out.println("Creating AdminClient with config:");
props.forEach((k, v) -> System.out.println(" " + k + " = " + v));
try (AdminClient adminClient = AdminClient.create(props)) {
System.out.println("AdminClient created successfully");
Thread.sleep(2000); // Give it time to initialize
// Test 1: Describe Cluster (uses Metadata API internally)
System.out.println("\n=== Test 1: Describe Cluster ===");
try {
DescribeClusterResult clusterResult = adminClient.describeCluster();
String clusterId = clusterResult.clusterId().get(10, TimeUnit.SECONDS);
int nodeCount = clusterResult.nodes().get(10, TimeUnit.SECONDS).size();
System.out.println("Cluster ID: " + clusterId);
System.out.println("Nodes: " + nodeCount);
} catch (Exception e) {
System.err.println("Describe Cluster failed: " + e.getMessage());
e.printStackTrace();
}
// Test 2: List Topics
System.out.println("\n=== Test 2: List Topics ===");
try {
ListTopicsResult topicsResult = adminClient.listTopics();
int topicCount = topicsResult.names().get(10, TimeUnit.SECONDS).size();
System.out.println("Topics: " + topicCount);
} catch (Exception e) {
System.err.println("List Topics failed: " + e.getMessage());
e.printStackTrace();
}
System.out.println("\nAll tests completed!");
} catch (Exception e) {
System.err.println("AdminClient creation failed: " + e.getMessage());
e.printStackTrace();
System.exit(1);
}
}
}

View File

@@ -0,0 +1,82 @@
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.serialization.StringDeserializer;
import java.time.Duration;
import java.util.Collections;
import java.util.Properties;
public class JavaKafkaConsumer {
public static void main(String[] args) {
if (args.length < 2) {
System.err.println("Usage: java JavaKafkaConsumer <broker> <topic>");
System.exit(1);
}
String broker = args[0];
String topic = args[1];
System.out.println("Connecting to Kafka broker: " + broker);
System.out.println("Topic: " + topic);
Properties props = new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, broker);
props.put(ConsumerConfig.GROUP_ID_CONFIG, "java-test-group");
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");
props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, "10");
props.put(ConsumerConfig.FETCH_MIN_BYTES_CONFIG, "1");
props.put(ConsumerConfig.FETCH_MAX_WAIT_MS_CONFIG, "1000");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Collections.singletonList(topic));
System.out.println("Starting to consume messages...");
int messageCount = 0;
int errorCount = 0;
long startTime = System.currentTimeMillis();
try {
while (true) {
try {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String, String> record : records) {
messageCount++;
System.out.printf("Message #%d: topic=%s partition=%d offset=%d key=%s value=%s%n",
messageCount, record.topic(), record.partition(), record.offset(),
record.key(), record.value());
}
// Stop after 100 messages or 60 seconds
if (messageCount >= 100 || (System.currentTimeMillis() - startTime) > 60000) {
long duration = System.currentTimeMillis() - startTime;
System.out.printf("%nSuccessfully consumed %d messages in %dms%n", messageCount, duration);
System.out.printf("Success rate: %.1f%% (%d/%d including errors)%n",
(double) messageCount / (messageCount + errorCount) * 100, messageCount,
messageCount + errorCount);
break;
}
} catch (Exception e) {
errorCount++;
System.err.printf("Error during poll #%d: %s%n", errorCount, e.getMessage());
e.printStackTrace();
// Stop after 10 consecutive errors or 60 seconds
if (errorCount > 10 || (System.currentTimeMillis() - startTime) > 60000) {
long duration = System.currentTimeMillis() - startTime;
System.err.printf("%nStopping after %d errors in %dms%n", errorCount, duration);
break;
}
}
}
} finally {
consumer.close();
}
}
}

View File

@@ -0,0 +1,68 @@
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
import java.util.concurrent.Future;
public class JavaProducerTest {
public static void main(String[] args) {
String bootstrapServers = args.length > 0 ? args[0] : "localhost:9093";
String topicName = args.length > 1 ? args[1] : "test-topic";
System.out.println("Testing Kafka Producer with broker: " + bootstrapServers);
System.out.println(" Topic: " + topicName);
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.CLIENT_ID_CONFIG, "java-producer-test");
props.put(ProducerConfig.ACKS_CONFIG, "1");
props.put(ProducerConfig.RETRIES_CONFIG, 0);
props.put(ProducerConfig.MAX_BLOCK_MS_CONFIG, 10000);
System.out.println("Creating Producer with config:");
props.forEach((k, v) -> System.out.println(" " + k + " = " + v));
try (KafkaProducer<String, String> producer = new KafkaProducer<>(props)) {
System.out.println("Producer created successfully");
// Try to send a test message
System.out.println("\n=== Test: Send Message ===");
try {
ProducerRecord<String, String> record = new ProducerRecord<>(topicName, "key1", "value1");
System.out.println("Sending record to topic: " + topicName);
Future<RecordMetadata> future = producer.send(record);
RecordMetadata metadata = future.get(); // This will block and wait for response
System.out.println("Message sent successfully!");
System.out.println(" Topic: " + metadata.topic());
System.out.println(" Partition: " + metadata.partition());
System.out.println(" Offset: " + metadata.offset());
} catch (Exception e) {
System.err.println("Send failed: " + e.getMessage());
e.printStackTrace();
// Print cause chain
Throwable cause = e.getCause();
int depth = 1;
while (cause != null && depth < 5) {
System.err.println(
" Cause " + depth + ": " + cause.getClass().getName() + ": " + cause.getMessage());
cause = cause.getCause();
depth++;
}
}
System.out.println("\nTest completed!");
} catch (Exception e) {
System.err.println("Producer creation or operation failed: " + e.getMessage());
e.printStackTrace();
System.exit(1);
}
}
}

View File

@@ -0,0 +1,124 @@
package tools;
import io.confluent.kafka.schemaregistry.client.CachedSchemaRegistryClient;
import io.confluent.kafka.schemaregistry.client.SchemaRegistryClient;
import org.apache.avro.Schema;
import org.apache.avro.SchemaBuilder;
public class SchemaRegistryTest {
private static final String SCHEMA_REGISTRY_URL = "http://localhost:8081";
public static void main(String[] args) {
System.out.println("================================================================================");
System.out.println("Schema Registry Test - Verifying In-Memory Read Optimization");
System.out.println("================================================================================\n");
SchemaRegistryClient schemaRegistry = new CachedSchemaRegistryClient(SCHEMA_REGISTRY_URL, 100);
boolean allTestsPassed = true;
try {
// Test 1: Register first schema
System.out.println("Test 1: Registering first schema (user-value)...");
Schema userValueSchema = SchemaBuilder
.record("User").fields()
.requiredString("name")
.requiredInt("age")
.endRecord();
long startTime = System.currentTimeMillis();
int schema1Id = schemaRegistry.register("user-value", userValueSchema);
long elapsedTime = System.currentTimeMillis() - startTime;
System.out.println("✓ SUCCESS: Schema registered with ID: " + schema1Id + " (took " + elapsedTime + "ms)");
// Test 2: Register second schema immediately (tests read-after-write)
System.out.println("\nTest 2: Registering second schema immediately (user-key)...");
Schema userKeySchema = SchemaBuilder
.record("UserKey").fields()
.requiredString("userId")
.endRecord();
startTime = System.currentTimeMillis();
int schema2Id = schemaRegistry.register("user-key", userKeySchema);
elapsedTime = System.currentTimeMillis() - startTime;
System.out.println("✓ SUCCESS: Schema registered with ID: " + schema2Id + " (took " + elapsedTime + "ms)");
// Test 3: Rapid fire registrations (tests concurrent writes)
System.out.println("\nTest 3: Rapid fire registrations (10 schemas in parallel)...");
startTime = System.currentTimeMillis();
Thread[] threads = new Thread[10];
final boolean[] results = new boolean[10];
for (int i = 0; i < 10; i++) {
final int index = i;
threads[i] = new Thread(() -> {
try {
Schema schema = SchemaBuilder
.record("Test" + index).fields()
.requiredString("field" + index)
.endRecord();
schemaRegistry.register("test-" + index + "-value", schema);
results[index] = true;
} catch (Exception e) {
System.err.println("✗ ERROR in thread " + index + ": " + e.getMessage());
results[index] = false;
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
elapsedTime = System.currentTimeMillis() - startTime;
int successCount = 0;
for (boolean result : results) {
if (result) successCount++;
}
if (successCount == 10) {
System.out.println("✓ SUCCESS: All 10 schemas registered (took " + elapsedTime + "ms total, ~" + (elapsedTime / 10) + "ms per schema)");
} else {
System.out.println("✗ PARTIAL FAILURE: Only " + successCount + "/10 schemas registered");
allTestsPassed = false;
}
// Test 4: Verify we can retrieve all schemas
System.out.println("\nTest 4: Verifying all schemas are retrievable...");
startTime = System.currentTimeMillis();
Schema retrieved1 = schemaRegistry.getById(schema1Id);
Schema retrieved2 = schemaRegistry.getById(schema2Id);
elapsedTime = System.currentTimeMillis() - startTime;
if (retrieved1.equals(userValueSchema) && retrieved2.equals(userKeySchema)) {
System.out.println("✓ SUCCESS: All schemas retrieved correctly (took " + elapsedTime + "ms)");
} else {
System.out.println("✗ FAILURE: Schema mismatch");
allTestsPassed = false;
}
// Summary
System.out.println("\n===============================================================================");
if (allTestsPassed) {
System.out.println("✓ ALL TESTS PASSED!");
System.out.println("===============================================================================");
System.out.println("\nOptimization verified:");
System.out.println("- ForceFlush is NO LONGER NEEDED");
System.out.println("- Subscribers read from in-memory buffer using IsOffsetInMemory()");
System.out.println("- Per-subscriber notification channels provide instant wake-up");
System.out.println("- True concurrent writes without serialization");
System.exit(0);
} else {
System.out.println("✗ SOME TESTS FAILED");
System.out.println("===============================================================================");
System.exit(1);
}
} catch (Exception e) {
System.err.println("\n✗ FATAL ERROR: " + e.getMessage());
e.printStackTrace();
System.exit(1);
}
}
}

View File

@@ -0,0 +1,78 @@
import java.net.*;
import java.nio.*;
import java.nio.channels.*;
public class TestSocketReadiness {
public static void main(String[] args) throws Exception {
String host = args.length > 0 ? args[0] : "localhost";
int port = args.length > 1 ? Integer.parseInt(args[1]) : 9093;
System.out.println("Testing socket readiness with " + host + ":" + port);
// Test 1: Simple blocking connect
System.out.println("\n=== Test 1: Blocking Socket ===");
try (Socket socket = new Socket()) {
socket.connect(new InetSocketAddress(host, port), 5000);
System.out.println("Blocking socket connected");
System.out.println(" Available bytes: " + socket.getInputStream().available());
Thread.sleep(100);
System.out.println(" Available bytes after 100ms: " + socket.getInputStream().available());
} catch (Exception e) {
System.err.println("Blocking socket failed: " + e.getMessage());
}
// Test 2: Non-blocking NIO socket (like Kafka client uses)
System.out.println("\n=== Test 2: Non-blocking NIO Socket ===");
Selector selector = Selector.open();
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
try {
boolean connected = channel.connect(new InetSocketAddress(host, port));
System.out.println(" connect() returned: " + connected);
SelectionKey key = channel.register(selector, SelectionKey.OP_CONNECT);
int ready = selector.select(5000);
System.out.println(" selector.select() returned: " + ready);
if (ready > 0) {
for (SelectionKey k : selector.selectedKeys()) {
if (k.isConnectable()) {
System.out.println(" isConnectable: true");
boolean finished = channel.finishConnect();
System.out.println(" finishConnect() returned: " + finished);
if (finished) {
k.interestOps(SelectionKey.OP_READ);
// Now check if immediately readable (THIS is what might be wrong)
selector.selectedKeys().clear();
int readReady = selector.selectNow();
System.out.println(" Immediately after connect, selectNow() = " + readReady);
if (readReady > 0) {
System.out.println(" Socket is IMMEDIATELY readable (unexpected!)");
ByteBuffer buf = ByteBuffer.allocate(1);
int bytesRead = channel.read(buf);
System.out.println(" read() returned: " + bytesRead);
} else {
System.out.println(" Socket is NOT immediately readable (correct)");
}
}
}
}
}
System.out.println("NIO socket test completed");
} catch (Exception e) {
System.err.println("NIO socket failed: " + e.getMessage());
e.printStackTrace();
} finally {
channel.close();
selector.close();
}
System.out.println("\nAll tests completed");
}
}

View File

@@ -0,0 +1,10 @@
module simple-test
go 1.24.7
require github.com/segmentio/kafka-go v0.4.49
require (
github.com/klauspost/compress v1.15.9 // indirect
github.com/pierrec/lz4/v4 v4.1.15 // indirect
)

View File

@@ -0,0 +1,24 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY=
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0=
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/segmentio/kafka-go v0.4.49 h1:GJiNX1d/g+kG6ljyJEoi9++PUMdXGAxb7JGPiDCuNmk=
github.com/segmentio/kafka-go v0.4.49/go.mod h1:Y1gn60kzLEEaW28YshXyk2+VCUKbJ3Qr6DrnT3i4+9E=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -0,0 +1,69 @@
package main
import (
"context"
"log"
"os"
"time"
"github.com/segmentio/kafka-go"
)
func main() {
if len(os.Args) < 3 {
log.Fatal("Usage: kafka-go-consumer <broker> <topic>")
}
broker := os.Args[1]
topic := os.Args[2]
log.Printf("Connecting to Kafka broker: %s", broker)
log.Printf("Topic: %s", topic)
// Create a new reader
r := kafka.NewReader(kafka.ReaderConfig{
Brokers: []string{broker},
Topic: topic,
GroupID: "kafka-go-test-group",
MinBytes: 1,
MaxBytes: 10e6, // 10MB
MaxWait: 1 * time.Second,
})
defer r.Close()
log.Printf("Starting to consume messages...")
ctx := context.Background()
messageCount := 0
errorCount := 0
startTime := time.Now()
for {
m, err := r.ReadMessage(ctx)
if err != nil {
errorCount++
log.Printf("Error reading message #%d: %v", messageCount+1, err)
// Stop after 10 consecutive errors or 60 seconds
if errorCount > 10 || time.Since(startTime) > 60*time.Second {
log.Printf("\nStopping after %d errors in %v", errorCount, time.Since(startTime))
break
}
continue
}
// Reset error count on successful read
errorCount = 0
messageCount++
log.Printf("Message #%d: topic=%s partition=%d offset=%d key=%s value=%s",
messageCount, m.Topic, m.Partition, m.Offset, string(m.Key), string(m.Value))
// Stop after 100 messages or 60 seconds
if messageCount >= 100 || time.Since(startTime) > 60*time.Second {
log.Printf("\nSuccessfully consumed %d messages in %v", messageCount, time.Since(startTime))
log.Printf("Success rate: %.1f%% (%d/%d including errors)",
float64(messageCount)/float64(messageCount+errorCount)*100, messageCount, messageCount+errorCount)
break
}
}
}

View File

@@ -0,0 +1,12 @@
log4j.rootLogger=DEBUG, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ISO8601} %-5p [%t] %c: %m%n
# More verbose for Kafka client
log4j.logger.org.apache.kafka=DEBUG
log4j.logger.org.apache.kafka.clients=TRACE
log4j.logger.org.apache.kafka.clients.NetworkClient=TRACE

View File

@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.seaweedfs.test</groupId>
<artifactId>kafka-consumer-test</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<kafka.version>3.9.2</kafka.version>
<confluent.version>7.6.0</confluent.version>
</properties>
<repositories>
<repository>
<id>confluent</id>
<url>https://packages.confluent.io/maven/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>${kafka.version}</version>
</dependency>
<dependency>
<groupId>io.confluent</groupId>
<artifactId>kafka-schema-registry-client</artifactId>
<version>${confluent.version}</version>
</dependency>
<dependency>
<groupId>io.confluent</groupId>
<artifactId>kafka-avro-serializer</artifactId>
<version>${confluent.version}</version>
</dependency>
<dependency>
<groupId>org.apache.avro</groupId>
<artifactId>avro</artifactId>
<version>1.11.5</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.9</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<mainClass>tools.SchemaRegistryTest</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,63 @@
#!/bin/bash
# Verify schema format distribution across topics
set -e
SCHEMA_REGISTRY_URL="${SCHEMA_REGISTRY_URL:-http://localhost:8081}"
TOPIC_PREFIX="${TOPIC_PREFIX:-loadtest-topic}"
TOPIC_COUNT="${TOPIC_COUNT:-5}"
echo "================================"
echo "Schema Format Verification"
echo "================================"
echo ""
echo "Schema Registry: $SCHEMA_REGISTRY_URL"
echo "Topic Prefix: $TOPIC_PREFIX"
echo "Topic Count: $TOPIC_COUNT"
echo ""
echo "Registered Schemas:"
echo "-------------------"
for i in $(seq 0 $((TOPIC_COUNT-1))); do
topic="${TOPIC_PREFIX}-${i}"
subject="${topic}-value"
echo -n "Topic $i ($topic): "
# Try to get schema
response=$(curl -s "${SCHEMA_REGISTRY_URL}/subjects/${subject}/versions/latest" 2>/dev/null || echo '{"error":"not found"}')
if echo "$response" | grep -q "error"; then
echo "❌ NOT REGISTERED"
else
schema_type=$(echo "$response" | grep -o '"schemaType":"[^"]*"' | cut -d'"' -f4)
schema_id=$(echo "$response" | grep -o '"id":[0-9]*' | cut -d':' -f2)
if [ -z "$schema_type" ]; then
schema_type="AVRO" # Default if not specified
fi
# Expected format based on index
if [ $((i % 2)) -eq 0 ]; then
expected="AVRO"
else
expected="JSON"
fi
if [ "$schema_type" = "$expected" ]; then
echo "✅ $schema_type (ID: $schema_id) - matches expected"
else
echo "⚠️ $schema_type (ID: $schema_id) - expected $expected"
fi
fi
done
echo ""
echo "Expected Distribution:"
echo "----------------------"
echo "Even indices (0, 2, 4, ...): AVRO"
echo "Odd indices (1, 3, 5, ...): JSON"
echo ""

View File

@@ -0,0 +1,630 @@
package integration
import (
"context"
"fmt"
"math/rand"
"strconv"
"sync"
"sync/atomic"
"testing"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/keepalive"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb/mq_pb"
"github.com/seaweedfs/seaweedfs/weed/pb/schema_pb"
)
// TestRecord represents a record with reasonable fields for integration testing
type MockTestRecord struct {
ID string
UserID int64
Timestamp int64
Event string
Data map[string]interface{}
Metadata map[string]string
}
// GenerateTestRecord creates a realistic test record
func GenerateMockTestRecord(id int) MockTestRecord {
events := []string{"user_login", "user_logout", "page_view", "purchase", "signup", "profile_update", "search"}
metadata := map[string]string{
"source": "web",
"version": "1.0.0",
"region": "us-west-2",
"client_ip": fmt.Sprintf("192.168.%d.%d", rand.Intn(255), rand.Intn(255)),
}
data := map[string]interface{}{
"session_id": fmt.Sprintf("sess_%d_%d", id, time.Now().Unix()),
"user_agent": "Mozilla/5.0 (compatible; SeaweedFS-Test/1.0)",
"referrer": "https://example.com/page" + strconv.Itoa(rand.Intn(100)),
"duration": rand.Intn(3600), // seconds
"score": rand.Float64() * 100,
}
return MockTestRecord{
ID: fmt.Sprintf("record_%d", id),
UserID: int64(rand.Intn(10000) + 1),
Timestamp: time.Now().UnixNano(),
Event: events[rand.Intn(len(events))],
Data: data,
Metadata: metadata,
}
}
// SerializeTestRecord converts TestRecord to key-value pair for Kafka
func SerializeMockTestRecord(record MockTestRecord) ([]byte, []byte) {
key := fmt.Sprintf("user_%d:%s", record.UserID, record.ID)
// Create a realistic JSON-like value with reasonable size (200-500 bytes)
value := fmt.Sprintf(`{
"id": "%s",
"user_id": %d,
"timestamp": %d,
"event": "%s",
"session_id": "%v",
"user_agent": "%v",
"referrer": "%v",
"duration": %v,
"score": %.2f,
"source": "%s",
"version": "%s",
"region": "%s",
"client_ip": "%s",
"batch_info": "This is additional data to make the record size more realistic for testing purposes. It simulates the kind of metadata and context that would typically be included in real-world event data."
}`,
record.ID,
record.UserID,
record.Timestamp,
record.Event,
record.Data["session_id"],
record.Data["user_agent"],
record.Data["referrer"],
record.Data["duration"],
record.Data["score"],
record.Metadata["source"],
record.Metadata["version"],
record.Metadata["region"],
record.Metadata["client_ip"],
)
return []byte(key), []byte(value)
}
// DirectBrokerClient connects directly to the broker without discovery
type DirectBrokerClient struct {
brokerAddress string
conn *grpc.ClientConn
client mq_pb.SeaweedMessagingClient
// Publisher streams: topic-partition -> stream info
publishersLock sync.RWMutex
publishers map[string]*PublisherSession
ctx context.Context
cancel context.CancelFunc
}
// PublisherSession tracks a publishing stream to SeaweedMQ broker
type PublisherSession struct {
Topic string
Partition int32
Stream mq_pb.SeaweedMessaging_PublishMessageClient
MessageCount int64 // Track messages sent for batch ack handling
}
func NewDirectBrokerClient(brokerAddr string) (*DirectBrokerClient, error) {
// Use a short-lived context for dialing so we don't store a canceled
// context in the returned client. The client's operational context
// (used by methods) should be cancellable independently.
dialCtx, dialCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer dialCancel()
// Add keepalive settings; use exported server constants to keep values in sync.
conn, err := grpc.DialContext(dialCtx, brokerAddr,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithKeepaliveParams(keepalive.ClientParameters{
Time: pb.GrpcKeepAliveTime, // align with server MinTime
Timeout: pb.GrpcKeepAliveTimeout, // align with server timeout
PermitWithoutStream: false, // reduce pings when idle
}))
if err != nil {
return nil, fmt.Errorf("failed to connect to broker: %v", err)
}
// Create a long-lived context for the client's lifetime and store it
// in the returned DirectBrokerClient so callers can cancel when done.
clientCtx, clientCancel := context.WithCancel(context.Background())
client := mq_pb.NewSeaweedMessagingClient(conn)
return &DirectBrokerClient{
brokerAddress: brokerAddr,
conn: conn,
client: client,
publishers: make(map[string]*PublisherSession),
ctx: clientCtx,
cancel: clientCancel,
}, nil
}
func (c *DirectBrokerClient) Close() {
c.cancel()
// Close all publisher streams
c.publishersLock.Lock()
for key := range c.publishers {
delete(c.publishers, key)
}
c.publishersLock.Unlock()
c.conn.Close()
}
func (c *DirectBrokerClient) ConfigureTopic(topicName string, partitions int32) error {
topic := &schema_pb.Topic{
Namespace: "kafka",
Name: topicName,
}
// Create schema for MockTestRecord
recordType := &schema_pb.RecordType{
Fields: []*schema_pb.Field{
{
Name: "id",
FieldIndex: 0,
Type: &schema_pb.Type{
Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING},
},
},
{
Name: "user_id",
FieldIndex: 1,
Type: &schema_pb.Type{
Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_INT64},
},
},
{
Name: "timestamp",
FieldIndex: 2,
Type: &schema_pb.Type{
Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_INT64},
},
},
{
Name: "event",
FieldIndex: 3,
Type: &schema_pb.Type{
Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING},
},
},
{
Name: "data",
FieldIndex: 4,
Type: &schema_pb.Type{
Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING}, // JSON string
},
},
{
Name: "metadata",
FieldIndex: 5,
Type: &schema_pb.Type{
Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING}, // JSON string
},
},
},
}
// Use user_id as the key column for partitioning
keyColumns := []string{"user_id"}
_, err := c.client.ConfigureTopic(c.ctx, &mq_pb.ConfigureTopicRequest{
Topic: topic,
PartitionCount: partitions,
MessageRecordType: recordType,
KeyColumns: keyColumns,
})
return err
}
func (c *DirectBrokerClient) PublishRecord(topicName string, partition int32, key, value []byte) error {
session, err := c.getOrCreatePublisher(topicName, partition)
if err != nil {
return err
}
// Send data message using broker API format
dataMsg := &mq_pb.DataMessage{
Key: key,
Value: value,
TsNs: time.Now().UnixNano(),
}
if err := session.Stream.Send(&mq_pb.PublishMessageRequest{
Message: &mq_pb.PublishMessageRequest_Data{
Data: dataMsg,
},
}); err != nil {
return fmt.Errorf("failed to send data: %v", err)
}
// Don't wait for individual acks! AckInterval=100 means acks come in batches
// The broker will handle acknowledgments asynchronously
return nil
}
// getOrCreatePublisher gets or creates a publisher stream for a topic-partition
func (c *DirectBrokerClient) getOrCreatePublisher(topic string, partition int32) (*PublisherSession, error) {
key := fmt.Sprintf("%s-%d", topic, partition)
// Try to get existing publisher
c.publishersLock.RLock()
if session, exists := c.publishers[key]; exists {
c.publishersLock.RUnlock()
return session, nil
}
c.publishersLock.RUnlock()
// Create new publisher stream
c.publishersLock.Lock()
defer c.publishersLock.Unlock()
// Double-check after acquiring write lock
if session, exists := c.publishers[key]; exists {
return session, nil
}
// Create the stream
stream, err := c.client.PublishMessage(c.ctx)
if err != nil {
return nil, fmt.Errorf("failed to create publish stream: %v", err)
}
// Get the actual partition assignment from the broker
actualPartition, err := c.getActualPartitionAssignment(topic, partition)
if err != nil {
return nil, fmt.Errorf("failed to get actual partition assignment: %v", err)
}
// Send init message using the actual partition structure that the broker allocated
if err := stream.Send(&mq_pb.PublishMessageRequest{
Message: &mq_pb.PublishMessageRequest_Init{
Init: &mq_pb.PublishMessageRequest_InitMessage{
Topic: &schema_pb.Topic{
Namespace: "kafka",
Name: topic,
},
Partition: actualPartition,
AckInterval: 200, // Ack every 200 messages for better balance
PublisherName: "direct-test",
},
},
}); err != nil {
return nil, fmt.Errorf("failed to send init message: %v", err)
}
session := &PublisherSession{
Topic: topic,
Partition: partition,
Stream: stream,
MessageCount: 0,
}
c.publishers[key] = session
return session, nil
}
// getActualPartitionAssignment looks up the actual partition assignment from the broker configuration
func (c *DirectBrokerClient) getActualPartitionAssignment(topic string, kafkaPartition int32) (*schema_pb.Partition, error) {
// Look up the topic configuration from the broker to get the actual partition assignments
lookupResp, err := c.client.LookupTopicBrokers(c.ctx, &mq_pb.LookupTopicBrokersRequest{
Topic: &schema_pb.Topic{
Namespace: "kafka",
Name: topic,
},
})
if err != nil {
return nil, fmt.Errorf("failed to lookup topic brokers: %v", err)
}
if len(lookupResp.BrokerPartitionAssignments) == 0 {
return nil, fmt.Errorf("no partition assignments found for topic %s", topic)
}
totalPartitions := int32(len(lookupResp.BrokerPartitionAssignments))
if kafkaPartition >= totalPartitions {
return nil, fmt.Errorf("kafka partition %d out of range, topic %s has %d partitions",
kafkaPartition, topic, totalPartitions)
}
// Calculate expected range for this Kafka partition
// Ring is divided equally among partitions, with last partition getting any remainder
const ringSize = int32(2520) // MaxPartitionCount constant
rangeSize := ringSize / totalPartitions
expectedRangeStart := kafkaPartition * rangeSize
var expectedRangeStop int32
if kafkaPartition == totalPartitions-1 {
// Last partition gets the remainder to fill the entire ring
expectedRangeStop = ringSize
} else {
expectedRangeStop = (kafkaPartition + 1) * rangeSize
}
// Find the broker assignment that matches this range
for _, assignment := range lookupResp.BrokerPartitionAssignments {
if assignment.Partition == nil {
continue
}
// Check if this assignment's range matches our expected range
if assignment.Partition.RangeStart == expectedRangeStart && assignment.Partition.RangeStop == expectedRangeStop {
return assignment.Partition, nil
}
}
return nil, fmt.Errorf("no broker assignment found for Kafka partition %d with expected range [%d, %d]",
kafkaPartition, expectedRangeStart, expectedRangeStop)
}
// TestDirectBroker_MillionRecordsIntegration tests the broker directly without discovery
func TestDirectBroker_MillionRecordsIntegration(t *testing.T) {
// Skip by default - this is a large integration test
if testing.Short() {
t.Skip("Skipping million-record integration test in short mode")
}
// Configuration
const (
totalRecords = 1000000
numPartitions = int32(8) // Use multiple partitions for better performance
numProducers = 4 // Concurrent producers
brokerAddr = "localhost:17777"
)
// Create direct broker client for topic configuration
configClient, err := NewDirectBrokerClient(brokerAddr)
if err != nil {
t.Fatalf("Failed to create direct broker client: %v", err)
}
defer configClient.Close()
topicName := fmt.Sprintf("million-records-direct-test-%d", time.Now().Unix())
// Create topic
glog.Infof("Creating topic %s with %d partitions", topicName, numPartitions)
err = configClient.ConfigureTopic(topicName, numPartitions)
if err != nil {
t.Fatalf("Failed to configure topic: %v", err)
}
// Performance tracking
var totalProduced int64
var totalErrors int64
startTime := time.Now()
// Progress tracking
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
for {
select {
case <-ticker.C:
produced := atomic.LoadInt64(&totalProduced)
errors := atomic.LoadInt64(&totalErrors)
elapsed := time.Since(startTime)
rate := float64(produced) / elapsed.Seconds()
glog.Infof("Progress: %d/%d records (%.1f%%), rate: %.0f records/sec, errors: %d",
produced, totalRecords, float64(produced)/float64(totalRecords)*100, rate, errors)
case <-ctx.Done():
return
}
}
}()
// Producer function
producer := func(producerID int, recordsPerProducer int) error {
defer func() {
glog.Infof("Producer %d finished", producerID)
}()
// Create dedicated client for this producer
producerClient, err := NewDirectBrokerClient(brokerAddr)
if err != nil {
return fmt.Errorf("Producer %d failed to create client: %v", producerID, err)
}
defer producerClient.Close()
// Add timeout context for each producer
producerCtx, producerCancel := context.WithTimeout(ctx, 10*time.Minute)
defer producerCancel()
glog.Infof("Producer %d: About to start producing %d records with dedicated client", producerID, recordsPerProducer)
for i := 0; i < recordsPerProducer; i++ {
// Check if context is cancelled or timed out
select {
case <-producerCtx.Done():
glog.Errorf("Producer %d timed out or cancelled after %d records", producerID, i)
return producerCtx.Err()
default:
}
// Debug progress for all producers every 50k records
if i > 0 && i%50000 == 0 {
glog.Infof("Producer %d: Progress %d/%d records (%.1f%%)", producerID, i, recordsPerProducer, float64(i)/float64(recordsPerProducer)*100)
}
// Calculate global record ID
recordID := producerID*recordsPerProducer + i
// Generate test record
testRecord := GenerateMockTestRecord(recordID)
key, value := SerializeMockTestRecord(testRecord)
// Distribute across partitions based on user ID
partition := int32(testRecord.UserID % int64(numPartitions))
// Debug first few records for each producer
if i < 3 {
glog.Infof("Producer %d: Record %d -> UserID %d -> Partition %d", producerID, i, testRecord.UserID, partition)
}
// Produce the record with retry logic
var err error
maxRetries := 3
for retry := 0; retry < maxRetries; retry++ {
err = producerClient.PublishRecord(topicName, partition, key, value)
if err == nil {
break // Success
}
// If it's an EOF error, wait a bit before retrying
if err.Error() == "failed to send data: EOF" {
time.Sleep(time.Duration(retry+1) * 100 * time.Millisecond)
continue
}
// For other errors, don't retry
break
}
if err != nil {
atomic.AddInt64(&totalErrors, 1)
errorCount := atomic.LoadInt64(&totalErrors)
if errorCount < 20 { // Log first 20 errors to get more insight
glog.Errorf("Producer %d failed to produce record %d (i=%d) after %d retries: %v", producerID, recordID, i, maxRetries, err)
}
// Don't continue - this might be causing producers to exit early
// Let's see what happens if we return the error instead
if errorCount > 1000 { // If too many errors, give up
glog.Errorf("Producer %d giving up after %d errors", producerID, errorCount)
return fmt.Errorf("too many errors: %d", errorCount)
}
continue
}
atomic.AddInt64(&totalProduced, 1)
// Log progress for first producer
if producerID == 0 && (i+1)%10000 == 0 {
glog.Infof("Producer %d: produced %d records", producerID, i+1)
}
}
glog.Infof("Producer %d: Completed loop, produced %d records successfully", producerID, recordsPerProducer)
return nil
}
// Start concurrent producers
glog.Infof("Starting %d concurrent producers to produce %d records", numProducers, totalRecords)
var wg sync.WaitGroup
recordsPerProducer := totalRecords / numProducers
for i := 0; i < numProducers; i++ {
wg.Add(1)
go func(producerID int) {
defer wg.Done()
glog.Infof("Producer %d starting with %d records to produce", producerID, recordsPerProducer)
if err := producer(producerID, recordsPerProducer); err != nil {
glog.Errorf("Producer %d failed: %v", producerID, err)
}
}(i)
}
// Wait for all producers to complete
wg.Wait()
cancel() // Stop progress reporting
produceTime := time.Since(startTime)
finalProduced := atomic.LoadInt64(&totalProduced)
finalErrors := atomic.LoadInt64(&totalErrors)
glog.Infof("Production completed: %d records in %v (%.0f records/sec), errors: %d",
finalProduced, produceTime, float64(finalProduced)/produceTime.Seconds(), finalErrors)
// Performance summary
if finalProduced > 0 {
glog.Infof("\n"+
"=== PERFORMANCE SUMMARY ===\n"+
"Records produced: %d\n"+
"Production time: %v\n"+
"Production rate: %.0f records/sec\n"+
"Errors: %d (%.2f%%)\n"+
"Partitions: %d\n"+
"Concurrent producers: %d\n"+
"Average record size: ~300 bytes\n"+
"Total data: ~%.1f MB\n"+
"Throughput: ~%.1f MB/sec\n",
finalProduced,
produceTime,
float64(finalProduced)/produceTime.Seconds(),
finalErrors,
float64(finalErrors)/float64(totalRecords)*100,
numPartitions,
numProducers,
float64(finalProduced)*300/(1024*1024),
float64(finalProduced)*300/(1024*1024)/produceTime.Seconds(),
)
}
// Test assertions
if finalProduced < int64(totalRecords*0.95) { // Allow 5% tolerance for errors
t.Errorf("Too few records produced: %d < %d (95%% of target)", finalProduced, int64(float64(totalRecords)*0.95))
}
if finalErrors > int64(totalRecords*0.05) { // Error rate should be < 5%
t.Errorf("Too many errors: %d > %d (5%% of target)", finalErrors, int64(float64(totalRecords)*0.05))
}
glog.Infof("Direct broker million-record integration test completed successfully!")
}
// BenchmarkDirectBroker_ProduceThroughput benchmarks the production throughput
func BenchmarkDirectBroker_ProduceThroughput(b *testing.B) {
if testing.Short() {
b.Skip("Skipping benchmark in short mode")
}
client, err := NewDirectBrokerClient("localhost:17777")
if err != nil {
b.Fatalf("Failed to create client: %v", err)
}
defer client.Close()
topicName := fmt.Sprintf("benchmark-topic-%d", time.Now().Unix())
err = client.ConfigureTopic(topicName, 1)
if err != nil {
b.Fatalf("Failed to configure topic: %v", err)
}
// Pre-generate test data
records := make([]MockTestRecord, b.N)
for i := 0; i < b.N; i++ {
records[i] = GenerateMockTestRecord(i)
}
b.ResetTimer()
b.StartTimer()
for i := 0; i < b.N; i++ {
key, value := SerializeMockTestRecord(records[i])
err := client.PublishRecord(topicName, 0, key, value)
if err != nil {
b.Fatalf("Failed to produce record %d: %v", i, err)
}
}
b.StopTimer()
}

View File

@@ -0,0 +1,139 @@
package integration
import (
"fmt"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/seaweedfs/seaweedfs/weed/glog"
)
// TestQuickPerformance_10K tests the fixed broker with 10K records
func TestQuickPerformance_10K(t *testing.T) {
const (
totalRecords = 10000 // 10K records for quick test
numPartitions = int32(4)
numProducers = 4
brokerAddr = "localhost:17777"
)
// Create direct broker client
client, err := NewDirectBrokerClient(brokerAddr)
if err != nil {
t.Fatalf("Failed to create direct broker client: %v", err)
}
defer client.Close()
topicName := fmt.Sprintf("quick-test-%d", time.Now().Unix())
// Create topic
glog.Infof("Creating topic %s with %d partitions", topicName, numPartitions)
err = client.ConfigureTopic(topicName, numPartitions)
if err != nil {
t.Fatalf("Failed to configure topic: %v", err)
}
// Performance tracking
var totalProduced int64
var totalErrors int64
startTime := time.Now()
// Producer function
producer := func(producerID int, recordsPerProducer int) error {
for i := 0; i < recordsPerProducer; i++ {
recordID := producerID*recordsPerProducer + i
// Generate test record
testRecord := GenerateMockTestRecord(recordID)
key, value := SerializeMockTestRecord(testRecord)
partition := int32(testRecord.UserID % int64(numPartitions))
// Produce the record (now async!)
err := client.PublishRecord(topicName, partition, key, value)
if err != nil {
atomic.AddInt64(&totalErrors, 1)
if atomic.LoadInt64(&totalErrors) < 5 {
glog.Errorf("Producer %d failed to produce record %d: %v", producerID, recordID, err)
}
continue
}
atomic.AddInt64(&totalProduced, 1)
// Log progress
if (i+1)%1000 == 0 {
elapsed := time.Since(startTime)
rate := float64(atomic.LoadInt64(&totalProduced)) / elapsed.Seconds()
glog.Infof("Producer %d: %d records, current rate: %.0f records/sec",
producerID, i+1, rate)
}
}
return nil
}
// Start concurrent producers
glog.Infof("Starting %d producers for %d records total", numProducers, totalRecords)
var wg sync.WaitGroup
recordsPerProducer := totalRecords / numProducers
for i := 0; i < numProducers; i++ {
wg.Add(1)
go func(producerID int) {
defer wg.Done()
if err := producer(producerID, recordsPerProducer); err != nil {
glog.Errorf("Producer %d failed: %v", producerID, err)
}
}(i)
}
// Wait for completion
wg.Wait()
produceTime := time.Since(startTime)
finalProduced := atomic.LoadInt64(&totalProduced)
finalErrors := atomic.LoadInt64(&totalErrors)
// Performance results
throughputPerSec := float64(finalProduced) / produceTime.Seconds()
dataVolumeMB := float64(finalProduced) * 300 / (1024 * 1024) // ~300 bytes per record
throughputMBPerSec := dataVolumeMB / produceTime.Seconds()
glog.Infof("\n"+
"QUICK PERFORMANCE TEST RESULTS\n"+
"=====================================\n"+
"Records produced: %d / %d\n"+
"Production time: %v\n"+
"Throughput: %.0f records/sec\n"+
"Data volume: %.1f MB\n"+
"Bandwidth: %.1f MB/sec\n"+
"Errors: %d (%.2f%%)\n"+
"Success rate: %.1f%%\n",
finalProduced, totalRecords,
produceTime,
throughputPerSec,
dataVolumeMB,
throughputMBPerSec,
finalErrors,
float64(finalErrors)/float64(totalRecords)*100,
float64(finalProduced)/float64(totalRecords)*100,
)
// Assertions
if finalProduced < int64(totalRecords*0.90) { // Allow 10% tolerance
t.Errorf("Too few records produced: %d < %d (90%% of target)", finalProduced, int64(float64(totalRecords)*0.90))
}
if throughputPerSec < 100 { // Should be much higher than 1 record/sec now!
t.Errorf("Throughput too low: %.0f records/sec (expected > 100)", throughputPerSec)
}
if finalErrors > int64(totalRecords*0.10) { // Error rate should be < 10%
t.Errorf("Too many errors: %d > %d (10%% of target)", finalErrors, int64(float64(totalRecords)*0.10))
}
glog.Infof("Performance test passed! Ready for million-record test.")
}

View File

@@ -0,0 +1,207 @@
package integration
import (
"fmt"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/seaweedfs/seaweedfs/weed/glog"
)
// TestResumeMillionRecords_Fixed - Fixed version with better concurrency handling
func TestResumeMillionRecords_Fixed(t *testing.T) {
const (
totalRecords = 1000000
numPartitions = int32(8)
numProducers = 4
brokerAddr = "localhost:17777"
batchSize = 100 // Process in smaller batches to avoid overwhelming
)
// Create direct broker client
client, err := NewDirectBrokerClient(brokerAddr)
if err != nil {
t.Fatalf("Failed to create direct broker client: %v", err)
}
defer client.Close()
topicName := fmt.Sprintf("resume-million-test-%d", time.Now().Unix())
// Create topic
glog.Infof("Creating topic %s with %d partitions for RESUMED test", topicName, numPartitions)
err = client.ConfigureTopic(topicName, numPartitions)
if err != nil {
t.Fatalf("Failed to configure topic: %v", err)
}
// Performance tracking
var totalProduced int64
var totalErrors int64
startTime := time.Now()
// Progress tracking
ticker := time.NewTicker(5 * time.Second) // More frequent updates
defer ticker.Stop()
go func() {
for range ticker.C {
produced := atomic.LoadInt64(&totalProduced)
errors := atomic.LoadInt64(&totalErrors)
elapsed := time.Since(startTime)
rate := float64(produced) / elapsed.Seconds()
progressPercent := float64(produced) / float64(totalRecords) * 100
glog.Infof("PROGRESS: %d/%d records (%.1f%%), rate: %.0f records/sec, errors: %d",
produced, totalRecords, progressPercent, rate, errors)
if produced >= totalRecords {
return
}
}
}()
// Fixed producer function with better error handling
producer := func(producerID int, recordsPerProducer int) error {
defer glog.Infof("Producer %d FINISHED", producerID)
// Create dedicated clients per producer to avoid contention
producerClient, err := NewDirectBrokerClient(brokerAddr)
if err != nil {
return fmt.Errorf("producer %d failed to create client: %v", producerID, err)
}
defer producerClient.Close()
successCount := 0
for i := 0; i < recordsPerProducer; i++ {
recordID := producerID*recordsPerProducer + i
// Generate test record
testRecord := GenerateMockTestRecord(recordID)
key, value := SerializeMockTestRecord(testRecord)
partition := int32(testRecord.UserID % int64(numPartitions))
// Produce with retry logic
maxRetries := 3
var lastErr error
success := false
for retry := 0; retry < maxRetries; retry++ {
err := producerClient.PublishRecord(topicName, partition, key, value)
if err == nil {
success = true
break
}
lastErr = err
time.Sleep(time.Duration(retry+1) * 100 * time.Millisecond) // Exponential backoff
}
if success {
atomic.AddInt64(&totalProduced, 1)
successCount++
} else {
atomic.AddInt64(&totalErrors, 1)
if atomic.LoadInt64(&totalErrors) < 10 {
glog.Errorf("Producer %d failed record %d after retries: %v", producerID, recordID, lastErr)
}
}
// Batch progress logging
if successCount > 0 && successCount%10000 == 0 {
glog.Infof("Producer %d: %d/%d records completed", producerID, successCount, recordsPerProducer)
}
// Small delay to prevent overwhelming the broker
if i > 0 && i%batchSize == 0 {
time.Sleep(10 * time.Millisecond)
}
}
glog.Infof("Producer %d completed: %d successful, %d errors",
producerID, successCount, recordsPerProducer-successCount)
return nil
}
// Start concurrent producers
glog.Infof("Starting FIXED %d producers for %d records total", numProducers, totalRecords)
var wg sync.WaitGroup
recordsPerProducer := totalRecords / numProducers
for i := 0; i < numProducers; i++ {
wg.Add(1)
go func(producerID int) {
defer wg.Done()
if err := producer(producerID, recordsPerProducer); err != nil {
glog.Errorf("Producer %d FAILED: %v", producerID, err)
}
}(i)
}
// Wait for completion with timeout
done := make(chan bool)
go func() {
wg.Wait()
done <- true
}()
select {
case <-done:
glog.Infof("All producers completed normally")
case <-time.After(30 * time.Minute): // 30-minute timeout
glog.Errorf("Test timed out after 30 minutes")
t.Errorf("Test timed out")
return
}
produceTime := time.Since(startTime)
finalProduced := atomic.LoadInt64(&totalProduced)
finalErrors := atomic.LoadInt64(&totalErrors)
// Performance results
throughputPerSec := float64(finalProduced) / produceTime.Seconds()
dataVolumeMB := float64(finalProduced) * 300 / (1024 * 1024)
throughputMBPerSec := dataVolumeMB / produceTime.Seconds()
successRate := float64(finalProduced) / float64(totalRecords) * 100
glog.Infof("\n"+
"=== FINAL MILLION RECORD TEST RESULTS ===\n"+
"==========================================\n"+
"Records produced: %d / %d\n"+
"Production time: %v\n"+
"Average throughput: %.0f records/sec\n"+
"Data volume: %.1f MB\n"+
"Bandwidth: %.1f MB/sec\n"+
"Errors: %d (%.2f%%)\n"+
"Success rate: %.1f%%\n"+
"Partitions used: %d\n"+
"Concurrent producers: %d\n",
finalProduced, totalRecords,
produceTime,
throughputPerSec,
dataVolumeMB,
throughputMBPerSec,
finalErrors,
float64(finalErrors)/float64(totalRecords)*100,
successRate,
numPartitions,
numProducers,
)
// Test assertions
if finalProduced < int64(totalRecords*0.95) { // Allow 5% tolerance
t.Errorf("Too few records produced: %d < %d (95%% of target)", finalProduced, int64(float64(totalRecords)*0.95))
}
if finalErrors > int64(totalRecords*0.05) { // Error rate should be < 5%
t.Errorf("Too many errors: %d > %d (5%% of target)", finalErrors, int64(float64(totalRecords)*0.05))
}
if throughputPerSec < 100 {
t.Errorf("Throughput too low: %.0f records/sec (expected > 100)", throughputPerSec)
}
glog.Infof("🏆 MILLION RECORD KAFKA INTEGRATION TEST COMPLETED SUCCESSFULLY!")
}

View File

@@ -0,0 +1,115 @@
#!/bin/bash
# Script to run the Kafka Gateway Million Record Integration Test
# This test requires a running SeaweedFS infrastructure (Master, Filer, MQ Broker)
set -e
echo "=== SeaweedFS Kafka Gateway Million Record Integration Test ==="
echo "Test Date: $(date)"
echo "Hostname: $(hostname)"
echo ""
# Configuration
MASTERS=${SEAWEED_MASTERS:-"localhost:9333"}
FILER_GROUP=${SEAWEED_FILER_GROUP:-"default"}
TEST_DIR="."
TEST_NAME="TestDirectBroker_MillionRecordsIntegration"
echo "Configuration:"
echo " Masters: $MASTERS"
echo " Filer Group: $FILER_GROUP"
echo " Test Directory: $TEST_DIR"
echo ""
# Check if SeaweedFS infrastructure is running
echo "=== Checking Infrastructure ==="
# Function to check if a service is running
check_service() {
local host_port=$1
local service_name=$2
if timeout 3 bash -c "</dev/tcp/${host_port//://}" 2>/dev/null; then
echo "✓ $service_name is running on $host_port"
return 0
else
echo "✗ $service_name is NOT running on $host_port"
return 1
fi
}
# Check each master
IFS=',' read -ra MASTER_ARRAY <<< "$MASTERS"
MASTERS_OK=true
for master in "${MASTER_ARRAY[@]}"; do
if ! check_service "$master" "SeaweedFS Master"; then
MASTERS_OK=false
fi
done
if [ "$MASTERS_OK" = false ]; then
echo ""
echo "ERROR: One or more SeaweedFS Masters are not running."
echo "Please start your SeaweedFS infrastructure before running this test."
echo ""
echo "Example commands to start SeaweedFS:"
echo " # Terminal 1: Start Master"
echo " weed master -defaultReplication=001 -mdir=/tmp/seaweedfs/master -peers=none"
echo ""
echo " # Terminal 2: Start Filer"
echo " weed filer -master=localhost:9333 -filer.dir=/tmp/seaweedfs/filer"
echo ""
echo " # Terminal 3: Start MQ Broker"
echo " weed mq.broker -filer=localhost:8888 -master=localhost:9333"
echo ""
exit 1
fi
echo ""
echo "=== Infrastructure Check Passed ==="
echo ""
# Change to the correct directory
cd "$TEST_DIR"
# Set environment variables for the test
export SEAWEED_MASTERS="$MASTERS"
export SEAWEED_FILER_GROUP="$FILER_GROUP"
# Run the test with verbose output
echo "=== Running Million Record Integration Test ==="
echo "This may take several minutes..."
echo ""
# Run the specific test with timeout and verbose output
timeout 1800 go test -v -run "$TEST_NAME" -timeout=30m 2>&1 | tee /tmp/seaweed_million_record_test.log
TEST_EXIT_CODE=${PIPESTATUS[0]}
echo ""
echo "=== Test Completed ==="
echo "Exit Code: $TEST_EXIT_CODE"
echo "Full log available at: /tmp/seaweed_million_record_test.log"
echo ""
# Show summary from the log
echo "=== Performance Summary ==="
if grep -q "PERFORMANCE SUMMARY" /tmp/seaweed_million_record_test.log; then
grep -A 15 "PERFORMANCE SUMMARY" /tmp/seaweed_million_record_test.log
else
echo "Performance summary not found in log"
fi
echo ""
if [ $TEST_EXIT_CODE -eq 0 ]; then
echo "🎉 TEST PASSED: Million record integration test completed successfully!"
else
echo "❌ TEST FAILED: Million record integration test failed with exit code $TEST_EXIT_CODE"
echo "Check the log file for details: /tmp/seaweed_million_record_test.log"
fi
echo ""
echo "=== Test Run Complete ==="
exit $TEST_EXIT_CODE

View File

@@ -0,0 +1,131 @@
#!/bin/bash
# Script to set up SeaweedFS infrastructure for Kafka Gateway testing
# This script will start Master, Filer, and MQ Broker components
set -e
BASE_DIR="/tmp/seaweedfs"
LOG_DIR="$BASE_DIR/logs"
DATA_DIR="$BASE_DIR/data"
echo "=== SeaweedFS Infrastructure Setup ==="
echo "Setup Date: $(date)"
echo "Base Directory: $BASE_DIR"
echo ""
# Create directories
mkdir -p "$BASE_DIR/master" "$BASE_DIR/filer" "$BASE_DIR/broker" "$LOG_DIR"
# Function to check if a service is running
check_service() {
local host_port=$1
local service_name=$2
if timeout 3 bash -c "</dev/tcp/${host_port//://}" 2>/dev/null; then
echo "✓ $service_name is already running on $host_port"
return 0
else
echo "✗ $service_name is NOT running on $host_port"
return 1
fi
}
# Function to start a service in background
start_service() {
local cmd="$1"
local service_name="$2"
local log_file="$3"
local check_port="$4"
echo "Starting $service_name..."
echo "Command: $cmd"
echo "Log: $log_file"
# Start in background
nohup $cmd > "$log_file" 2>&1 &
local pid=$!
echo "PID: $pid"
# Wait for service to be ready
local retries=30
while [ $retries -gt 0 ]; do
if check_service "$check_port" "$service_name" 2>/dev/null; then
echo "✓ $service_name is ready"
return 0
fi
retries=$((retries - 1))
sleep 1
echo -n "."
done
echo ""
echo "❌ $service_name failed to start within 30 seconds"
return 1
}
# Stop any existing processes
echo "=== Cleaning up existing processes ==="
pkill -f "weed master" || true
pkill -f "weed filer" || true
pkill -f "weed mq.broker" || true
sleep 2
echo ""
echo "=== Starting SeaweedFS Components ==="
# Start Master
if ! check_service "localhost:9333" "SeaweedFS Master"; then
start_service \
"weed master -defaultReplication=001 -mdir=$BASE_DIR/master -peers=none" \
"SeaweedFS Master" \
"$LOG_DIR/master.log" \
"localhost:9333"
echo ""
fi
# Start Filer
if ! check_service "localhost:8888" "SeaweedFS Filer"; then
start_service \
"weed filer -master=localhost:9333 -filer.dir=$BASE_DIR/filer" \
"SeaweedFS Filer" \
"$LOG_DIR/filer.log" \
"localhost:8888"
echo ""
fi
# Start MQ Broker
if ! check_service "localhost:17777" "SeaweedFS MQ Broker"; then
start_service \
"weed mq.broker -filer=localhost:8888 -master=localhost:9333" \
"SeaweedFS MQ Broker" \
"$LOG_DIR/broker.log" \
"localhost:17777"
echo ""
fi
echo "=== Infrastructure Status ==="
check_service "localhost:9333" "Master (gRPC)"
check_service "localhost:9334" "Master (HTTP)"
check_service "localhost:8888" "Filer (HTTP)"
check_service "localhost:18888" "Filer (gRPC)"
check_service "localhost:17777" "MQ Broker"
echo ""
echo "=== Infrastructure Ready ==="
echo "Log files:"
echo " Master: $LOG_DIR/master.log"
echo " Filer: $LOG_DIR/filer.log"
echo " Broker: $LOG_DIR/broker.log"
echo ""
echo "To view logs in real-time:"
echo " tail -f $LOG_DIR/master.log"
echo " tail -f $LOG_DIR/filer.log"
echo " tail -f $LOG_DIR/broker.log"
echo ""
echo "To stop all services:"
echo " pkill -f \"weed master\""
echo " pkill -f \"weed filer\""
echo " pkill -f \"weed mq.broker\""
echo ""
echo "[OK] SeaweedFS infrastructure is ready for testing!"

View File

@@ -0,0 +1,54 @@
#!/bin/sh
# Kafka Gateway Startup Script for Integration Testing
set -e
echo "Starting Kafka Gateway..."
SEAWEEDFS_MASTERS=${SEAWEEDFS_MASTERS:-seaweedfs-master:9333}
SEAWEEDFS_FILER=${SEAWEEDFS_FILER:-seaweedfs-filer:8888}
SEAWEEDFS_MQ_BROKER=${SEAWEEDFS_MQ_BROKER:-seaweedfs-mq-broker:17777}
SEAWEEDFS_FILER_GROUP=${SEAWEEDFS_FILER_GROUP:-}
# Wait for dependencies
echo "Waiting for SeaweedFS master(s)..."
OLD_IFS="$IFS"
IFS=','
for MASTER in $SEAWEEDFS_MASTERS; do
MASTER_HOST=${MASTER%:*}
MASTER_PORT=${MASTER#*:}
while ! nc -z "$MASTER_HOST" "$MASTER_PORT"; do
sleep 1
done
echo "SeaweedFS master $MASTER is ready"
done
IFS="$OLD_IFS"
echo "Waiting for SeaweedFS Filer..."
while ! nc -z "${SEAWEEDFS_FILER%:*}" "${SEAWEEDFS_FILER#*:}"; do
sleep 1
done
echo "SeaweedFS Filer is ready"
echo "Waiting for SeaweedFS MQ Broker..."
while ! nc -z "${SEAWEEDFS_MQ_BROKER%:*}" "${SEAWEEDFS_MQ_BROKER#*:}"; do
sleep 1
done
echo "SeaweedFS MQ Broker is ready"
echo "Waiting for Schema Registry..."
while ! curl -f "${SCHEMA_REGISTRY_URL}/subjects" > /dev/null 2>&1; do
sleep 1
done
echo "Schema Registry is ready"
# Start Kafka Gateway
echo "Starting Kafka Gateway on port ${KAFKA_PORT:-9093}..."
exec /usr/bin/weed mq.kafka.gateway \
-master=${SEAWEEDFS_MASTERS} \
-filerGroup=${SEAWEEDFS_FILER_GROUP} \
-port=${KAFKA_PORT:-9093} \
-port.pprof=${PPROF_PORT:-10093} \
-schema-registry-url=${SCHEMA_REGISTRY_URL} \
-ip=0.0.0.0

View File

@@ -0,0 +1,129 @@
#!/bin/bash
# Test script to verify broker discovery works end-to-end
set -e
echo "=== Testing SeaweedFS Broker Discovery ==="
cd /Users/chrislu/go/src/github.com/seaweedfs/seaweedfs
# Build weed binary
echo "Building weed binary..."
go build -o /tmp/weed-discovery ./weed
# Setup data directory
WEED_DATA_DIR="/tmp/seaweedfs-discovery-test-$$"
mkdir -p "$WEED_DATA_DIR"
echo "Using data directory: $WEED_DATA_DIR"
# Cleanup function
cleanup() {
echo "Cleaning up..."
pkill -f "weed.*server" || true
pkill -f "weed.*mq.broker" || true
sleep 2
rm -rf "$WEED_DATA_DIR"
rm -f /tmp/weed-discovery* /tmp/broker-discovery-test*
}
trap cleanup EXIT
# Start SeaweedFS server with consistent IP configuration
echo "Starting SeaweedFS server..."
/tmp/weed-discovery -v 1 server \
-ip="127.0.0.1" \
-ip.bind="127.0.0.1" \
-dir="$WEED_DATA_DIR" \
-master.raftHashicorp \
-master.port=9333 \
-volume.port=8081 \
-filer.port=8888 \
-filer=true \
-metricsPort=9325 \
> /tmp/weed-discovery-server.log 2>&1 &
SERVER_PID=$!
echo "Server PID: $SERVER_PID"
# Wait for master
echo "Waiting for master..."
for i in $(seq 1 30); do
if curl -s http://127.0.0.1:9333/cluster/status >/dev/null; then
echo "✓ Master is up"
break
fi
echo " Waiting for master... ($i/30)"
sleep 1
done
# Give components time to initialize
echo "Waiting for components to initialize..."
sleep 10
# Start MQ broker
echo "Starting MQ broker..."
/tmp/weed-discovery -v 2 mq.broker \
-master="127.0.0.1:9333" \
-port=17777 \
> /tmp/weed-discovery-broker.log 2>&1 &
BROKER_PID=$!
echo "Broker PID: $BROKER_PID"
# Wait for broker
echo "Waiting for broker to register..."
sleep 15
broker_ready=false
for i in $(seq 1 20); do
if nc -z 127.0.0.1 17777; then
echo "✓ MQ broker is accepting connections"
broker_ready=true
break
fi
echo " Waiting for MQ broker... ($i/20)"
sleep 1
done
if [ "$broker_ready" = false ]; then
echo "[FAIL] MQ broker failed to start"
echo "Server logs:"
cat /tmp/weed-discovery-server.log
echo "Broker logs:"
cat /tmp/weed-discovery-broker.log
exit 1
fi
# Additional wait for broker registration
echo "Allowing broker to register with master..."
sleep 15
# Check cluster status
echo "Checking cluster status..."
CLUSTER_STATUS=$(curl -s "http://127.0.0.1:9333/cluster/status")
echo "Cluster status: $CLUSTER_STATUS"
# Now test broker discovery using the same approach as the Kafka gateway
echo "Testing broker discovery..."
cd test/kafka
SEAWEEDFS_MASTERS=127.0.0.1:9333 timeout 30s go test -v -run "TestOffsetManagement" -timeout 25s ./e2e/... > /tmp/broker-discovery-test.log 2>&1 && discovery_success=true || discovery_success=false
if [ "$discovery_success" = true ]; then
echo "[OK] Broker discovery test PASSED!"
echo "Gateway was able to discover and connect to MQ brokers"
else
echo "[FAIL] Broker discovery test FAILED"
echo "Last few lines of test output:"
tail -20 /tmp/broker-discovery-test.log || echo "No test logs available"
fi
echo
echo "📊 Test Results:"
echo " Broker startup: ✅"
echo " Broker registration: ✅"
echo " Gateway discovery: $([ "$discovery_success" = true ] && echo "✅" || echo "❌")"
echo
echo "📁 Logs available:"
echo " Server: /tmp/weed-discovery-server.log"
echo " Broker: /tmp/weed-discovery-broker.log"
echo " Discovery test: /tmp/broker-discovery-test.log"

View File

@@ -0,0 +1,111 @@
#!/bin/bash
# Script to test SeaweedFS MQ broker startup locally
# This helps debug broker startup issues before running CI
set -e
echo "=== Testing SeaweedFS MQ Broker Startup ==="
# Build weed binary
echo "Building weed binary..."
cd "$(dirname "$0")/../../.."
go build -o /tmp/weed ./weed
# Setup data directory
WEED_DATA_DIR="/tmp/seaweedfs-broker-test-$$"
mkdir -p "$WEED_DATA_DIR"
echo "Using data directory: $WEED_DATA_DIR"
# Cleanup function
cleanup() {
echo "Cleaning up..."
pkill -f "weed.*server" || true
pkill -f "weed.*mq.broker" || true
sleep 2
rm -rf "$WEED_DATA_DIR"
rm -f /tmp/weed-*.log
}
trap cleanup EXIT
# Start SeaweedFS server
echo "Starting SeaweedFS server..."
/tmp/weed -v 1 server \
-ip="127.0.0.1" \
-ip.bind="0.0.0.0" \
-dir="$WEED_DATA_DIR" \
-master.raftHashicorp \
-master.port=9333 \
-volume.port=8081 \
-filer.port=8888 \
-filer=true \
-metricsPort=9325 \
> /tmp/weed-server-test.log 2>&1 &
SERVER_PID=$!
echo "Server PID: $SERVER_PID"
# Wait for master
echo "Waiting for master..."
for i in $(seq 1 30); do
if curl -s http://127.0.0.1:9333/cluster/status >/dev/null; then
echo "✓ Master is up"
break
fi
echo " Waiting for master... ($i/30)"
sleep 1
done
# Wait for filer
echo "Waiting for filer..."
for i in $(seq 1 30); do
if nc -z 127.0.0.1 8888; then
echo "✓ Filer is up"
break
fi
echo " Waiting for filer... ($i/30)"
sleep 1
done
# Start MQ broker
echo "Starting MQ broker..."
/tmp/weed -v 2 mq.broker \
-master="127.0.0.1:9333" \
-ip="127.0.0.1" \
-port=17777 \
> /tmp/weed-mq-broker-test.log 2>&1 &
BROKER_PID=$!
echo "Broker PID: $BROKER_PID"
# Wait for broker
echo "Waiting for broker..."
broker_ready=false
for i in $(seq 1 30); do
if nc -z 127.0.0.1 17777; then
echo "✓ MQ broker is up"
broker_ready=true
break
fi
echo " Waiting for MQ broker... ($i/30)"
sleep 1
done
if [ "$broker_ready" = false ]; then
echo "❌ MQ broker failed to start"
echo
echo "=== Server logs ==="
cat /tmp/weed-server-test.log
echo
echo "=== Broker logs ==="
cat /tmp/weed-mq-broker-test.log
exit 1
fi
# Broker started successfully - discovery will be tested by Kafka gateway
echo "✓ Broker started successfully and accepting connections"
echo
echo "[OK] All tests passed!"
echo "Server logs: /tmp/weed-server-test.log"
echo "Broker logs: /tmp/weed-mq-broker-test.log"

View File

@@ -0,0 +1,77 @@
#!/bin/bash
# Test script for schema registry E2E testing
# This script sets up a mock schema registry and runs the E2E tests
set -e
echo "🚀 Starting Schema Registry E2E Test"
# Check if we have a real schema registry URL
if [ -n "$SCHEMA_REGISTRY_URL" ]; then
echo "📡 Using real Schema Registry: $SCHEMA_REGISTRY_URL"
else
echo "🔧 No SCHEMA_REGISTRY_URL set, using mock registry"
# For now, we'll skip the test if no real registry is available
# In the future, we could start a mock registry here
export SCHEMA_REGISTRY_URL="http://localhost:8081"
echo "⚠️ Mock registry not implemented yet, test will be skipped"
fi
# Start SeaweedFS infrastructure
echo "🌱 Starting SeaweedFS infrastructure..."
cd /Users/chrislu/go/src/github.com/seaweedfs/seaweedfs
# Clean up any existing processes
pkill -f "weed server" || true
pkill -f "weed mq.broker" || true
sleep 2
# Start SeaweedFS server
echo "🗄️ Starting SeaweedFS server..."
/tmp/weed server -dir=/tmp/seaweedfs-test -master.port=9333 -volume.port=8080 -filer.port=8888 -ip=localhost -master.peers=none > /tmp/seaweed-server.log 2>&1 &
SERVER_PID=$!
# Wait for server to be ready
sleep 5
# Start MQ broker
echo "📨 Starting SeaweedMQ broker..."
/tmp/weed mq.broker -master=localhost:9333 -port=17777 > /tmp/seaweed-broker.log 2>&1 &
BROKER_PID=$!
# Wait for broker to be ready
sleep 3
# Check if services are running
if ! curl -s http://localhost:9333/cluster/status > /dev/null; then
echo "[FAIL] SeaweedFS server not ready"
exit 1
fi
echo "[OK] SeaweedFS infrastructure ready"
# Run the schema registry E2E tests
echo "🧪 Running Schema Registry E2E tests..."
cd /Users/chrislu/go/src/github.com/seaweedfs/seaweedfs/test/kafka
export SEAWEEDFS_MASTERS=127.0.0.1:9333
# Run the tests
if go test -v ./integration -run TestSchemaRegistryE2E -timeout 5m; then
echo "[OK] Schema Registry E2E tests PASSED!"
TEST_RESULT=0
else
echo "[FAIL] Schema Registry E2E tests FAILED!"
TEST_RESULT=1
fi
# Cleanup
echo "🧹 Cleaning up..."
kill $BROKER_PID $SERVER_PID 2>/dev/null || true
sleep 2
pkill -f "weed server" || true
pkill -f "weed mq.broker" || true
echo "🏁 Schema Registry E2E Test completed"
exit $TEST_RESULT

View File

@@ -0,0 +1,135 @@
#!/bin/bash
# Wait for Services Script for Kafka Integration Tests
set -e
echo "Waiting for services to be ready..."
# Configuration
KAFKA_HOST=${KAFKA_HOST:-localhost}
KAFKA_PORT=${KAFKA_PORT:-9092}
SCHEMA_REGISTRY_URL=${SCHEMA_REGISTRY_URL:-http://localhost:8081}
KAFKA_GATEWAY_HOST=${KAFKA_GATEWAY_HOST:-localhost}
KAFKA_GATEWAY_PORT=${KAFKA_GATEWAY_PORT:-9093}
SEAWEEDFS_MASTER_URL=${SEAWEEDFS_MASTER_URL:-http://localhost:9333}
MAX_WAIT=${MAX_WAIT:-300} # 5 minutes
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Helper function to wait for a service
wait_for_service() {
local service_name=$1
local check_command=$2
local timeout=${3:-60}
echo -e "${BLUE}Waiting for ${service_name}...${NC}"
local count=0
while [ $count -lt $timeout ]; do
if eval "$check_command" > /dev/null 2>&1; then
echo -e "${GREEN}[OK] ${service_name} is ready${NC}"
return 0
fi
if [ $((count % 10)) -eq 0 ]; then
echo -e "${YELLOW}Still waiting for ${service_name}... (${count}s)${NC}"
fi
sleep 1
count=$((count + 1))
done
echo -e "${RED}[FAIL] ${service_name} failed to start within ${timeout} seconds${NC}"
return 1
}
# Wait for Zookeeper
echo "=== Checking Zookeeper ==="
wait_for_service "Zookeeper" "nc -z localhost 2181" 30
# Wait for Kafka
echo "=== Checking Kafka ==="
wait_for_service "Kafka" "nc -z ${KAFKA_HOST} ${KAFKA_PORT}" 60
# Test Kafka broker API
echo "=== Testing Kafka API ==="
wait_for_service "Kafka API" "timeout 5 kafka-broker-api-versions --bootstrap-server ${KAFKA_HOST}:${KAFKA_PORT}" 30
# Wait for Schema Registry
echo "=== Checking Schema Registry ==="
wait_for_service "Schema Registry" "curl -f ${SCHEMA_REGISTRY_URL}/subjects" 60
# Wait for SeaweedFS Master
echo "=== Checking SeaweedFS Master ==="
wait_for_service "SeaweedFS Master" "curl -f ${SEAWEEDFS_MASTER_URL}/cluster/status" 30
# Wait for SeaweedFS Volume
echo "=== Checking SeaweedFS Volume ==="
wait_for_service "SeaweedFS Volume" "curl -f http://localhost:8080/status" 30
# Wait for SeaweedFS Filer
echo "=== Checking SeaweedFS Filer ==="
wait_for_service "SeaweedFS Filer" "curl -f http://localhost:8888/" 30
# Wait for SeaweedFS MQ Broker
echo "=== Checking SeaweedFS MQ Broker ==="
wait_for_service "SeaweedFS MQ Broker" "nc -z localhost 17777" 30
# Wait for SeaweedFS MQ Agent
echo "=== Checking SeaweedFS MQ Agent ==="
wait_for_service "SeaweedFS MQ Agent" "nc -z localhost 16777" 30
# Wait for Kafka Gateway
echo "=== Checking Kafka Gateway ==="
wait_for_service "Kafka Gateway" "nc -z ${KAFKA_GATEWAY_HOST} ${KAFKA_GATEWAY_PORT}" 60
# Final verification
echo "=== Final Verification ==="
# Test Kafka topic creation
echo "Testing Kafka topic operations..."
TEST_TOPIC="health-check-$(date +%s)"
if kafka-topics --create --topic "$TEST_TOPIC" --bootstrap-server "${KAFKA_HOST}:${KAFKA_PORT}" --partitions 1 --replication-factor 1 > /dev/null 2>&1; then
echo -e "${GREEN}[OK] Kafka topic creation works${NC}"
kafka-topics --delete --topic "$TEST_TOPIC" --bootstrap-server "${KAFKA_HOST}:${KAFKA_PORT}" > /dev/null 2>&1 || true
else
echo -e "${RED}[FAIL] Kafka topic creation failed${NC}"
exit 1
fi
# Test Schema Registry
echo "Testing Schema Registry..."
if curl -f "${SCHEMA_REGISTRY_URL}/subjects" > /dev/null 2>&1; then
echo -e "${GREEN}[OK] Schema Registry is accessible${NC}"
else
echo -e "${RED}[FAIL] Schema Registry is not accessible${NC}"
exit 1
fi
# Test Kafka Gateway connectivity
echo "Testing Kafka Gateway..."
if nc -z "${KAFKA_GATEWAY_HOST}" "${KAFKA_GATEWAY_PORT}"; then
echo -e "${GREEN}[OK] Kafka Gateway is accessible${NC}"
else
echo -e "${RED}[FAIL] Kafka Gateway is not accessible${NC}"
exit 1
fi
echo -e "${GREEN}All services are ready!${NC}"
echo ""
echo "Service endpoints:"
echo " Kafka: ${KAFKA_HOST}:${KAFKA_PORT}"
echo " Schema Registry: ${SCHEMA_REGISTRY_URL}"
echo " Kafka Gateway: ${KAFKA_GATEWAY_HOST}:${KAFKA_GATEWAY_PORT}"
echo " SeaweedFS Master: ${SEAWEEDFS_MASTER_URL}"
echo " SeaweedFS Filer: http://localhost:8888"
echo " SeaweedFS MQ Broker: localhost:17777"
echo " SeaweedFS MQ Agent: localhost:16777"
echo ""
echo "Ready to run integration tests!"

View File

@@ -0,0 +1,10 @@
module simple-consumer
go 1.21
require github.com/segmentio/kafka-go v0.4.47
require (
github.com/klauspost/compress v1.17.0 // indirect
github.com/pierrec/lz4/v4 v4.1.15 // indirect
)

View File

@@ -0,0 +1,69 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0=
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/segmentio/kafka-go v0.4.47 h1:IqziR4pA3vrZq7YdRxaT3w1/5fvIH5qpCwstUanQQB0=
github.com/segmentio/kafka-go v0.4.47/go.mod h1:HjF6XbOKh0Pjlkr5GVZxt6CsjjwnmhVOfURM5KMd8qg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -0,0 +1,123 @@
package main
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"time"
"github.com/segmentio/kafka-go"
)
func main() {
// Configuration
brokerAddress := "localhost:9093" // Kafka gateway port (not SeaweedMQ broker port 17777)
topicName := "_raw_messages" // Topic with "_" prefix - should skip schema validation
groupID := "raw-message-consumer"
fmt.Printf("Consuming messages from topic '%s' on broker '%s'\n", topicName, brokerAddress)
// Create a new reader
reader := kafka.NewReader(kafka.ReaderConfig{
Brokers: []string{brokerAddress},
Topic: topicName,
GroupID: groupID,
// Start reading from the beginning for testing
StartOffset: kafka.FirstOffset,
// Configure for quick consumption
MinBytes: 1,
MaxBytes: 10e6, // 10MB
})
defer reader.Close()
// Set up signal handling for graceful shutdown
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigChan
fmt.Println("\nReceived shutdown signal, stopping consumer...")
cancel()
}()
fmt.Println("Starting to consume messages (Press Ctrl+C to stop)...")
fmt.Println("=" + fmt.Sprintf("%60s", "="))
messageCount := 0
for {
select {
case <-ctx.Done():
fmt.Printf("\nStopped consuming. Total messages processed: %d\n", messageCount)
return
default:
// Set a timeout for reading messages
msgCtx, msgCancel := context.WithTimeout(ctx, 5*time.Second)
message, err := reader.ReadMessage(msgCtx)
msgCancel()
if err != nil {
if err == context.DeadlineExceeded {
fmt.Print(".")
continue
}
log.Printf("Error reading message: %v", err)
continue
}
messageCount++
// Display message details
fmt.Printf("\nMessage #%d:\n", messageCount)
fmt.Printf(" Partition: %d, Offset: %d\n", message.Partition, message.Offset)
fmt.Printf(" Key: %s\n", string(message.Key))
fmt.Printf(" Value: %s\n", string(message.Value))
fmt.Printf(" Timestamp: %s\n", message.Time.Format(time.RFC3339))
// Display headers if present
if len(message.Headers) > 0 {
fmt.Printf(" Headers:\n")
for _, header := range message.Headers {
fmt.Printf(" %s: %s\n", header.Key, string(header.Value))
}
}
// Try to detect content type
contentType := detectContentType(message.Value)
fmt.Printf(" Content Type: %s\n", contentType)
fmt.Printf(" Raw Size: %d bytes\n", len(message.Value))
fmt.Println(" " + fmt.Sprintf("%50s", "-"))
}
}
}
// detectContentType tries to determine the content type of the message
func detectContentType(data []byte) string {
if len(data) == 0 {
return "empty"
}
// Check if it looks like JSON
trimmed := string(data)
if (trimmed[0] == '{' && trimmed[len(trimmed)-1] == '}') ||
(trimmed[0] == '[' && trimmed[len(trimmed)-1] == ']') {
return "JSON"
}
// Check if it's printable text
for _, b := range data {
if b < 32 && b != 9 && b != 10 && b != 13 { // Allow tab, LF, CR
return "binary"
}
}
return "text"
}

View File

@@ -0,0 +1,77 @@
# Simple Kafka-Go Publisher for SeaweedMQ
This is a simple publisher client that demonstrates publishing raw messages to SeaweedMQ topics with "_" prefix, which bypass schema validation.
## Features
- **Schema-Free Publishing**: Topics with "_" prefix don't require schema validation
- **Raw Message Storage**: Messages are stored in a "value" field as raw bytes
- **Multiple Message Formats**: Supports JSON, binary, and empty messages
- **Kafka-Go Compatible**: Uses the popular kafka-go library
## Prerequisites
1. **SeaweedMQ Running**: Make sure SeaweedMQ is running on `localhost:17777` (default Kafka port)
2. **Go Modules**: The project uses Go modules for dependency management
## Setup and Run
```bash
# Navigate to the publisher directory
cd test/kafka/simple-publisher
# Download dependencies
go mod tidy
# Run the publisher
go run main.go
```
## Expected Output
```
Publishing messages to topic '_raw_messages' on broker 'localhost:17777'
Publishing messages...
- Published message 1: {"id":1,"message":"Hello from kafka-go client",...}
- Published message 2: {"id":2,"message":"Raw message without schema validation",...}
- Published message 3: {"id":3,"message":"Testing SMQ with underscore prefix topic",...}
Publishing different raw message formats...
- Published raw message 1: key=binary_key, value=Simple string message
- Published raw message 2: key=json_key, value={"raw_field": "raw_value", "number": 42}
- Published raw message 3: key=empty_key, value=
- Published raw message 4: key=, value=Message with no key
All test messages published to topic with '_' prefix!
These messages should be stored as raw bytes without schema validation.
```
## Topic Naming Convention
- **Schema-Required Topics**: `user-events`, `orders`, `payments` (require schema validation)
- **Schema-Free Topics**: `_raw_messages`, `_logs`, `_metrics` (bypass schema validation)
The "_" prefix tells SeaweedMQ to treat the topic as a system topic and skip schema processing entirely.
## Message Storage
For topics with "_" prefix:
- Messages are stored as raw bytes without schema validation
- No Confluent Schema Registry envelope is required
- Any binary data or text can be published
- SMQ assumes raw messages are stored in a "value" field internally
## Integration with SeaweedMQ
This client works with SeaweedMQ's existing schema bypass logic:
1. **`isSystemTopic()`** function identifies "_" prefix topics as system topics
2. **`produceSchemaBasedRecord()`** bypasses schema processing for system topics
3. **Raw storage** via `seaweedMQHandler.ProduceRecord()` stores messages as-is
## Use Cases
- **Log ingestion**: Store application logs without predefined schema
- **Metrics collection**: Publish time-series data in various formats
- **Raw data pipelines**: Process unstructured data before applying schemas
- **Development/testing**: Quickly publish test data without schema setup

View File

@@ -0,0 +1,10 @@
module simple-publisher
go 1.21
require github.com/segmentio/kafka-go v0.4.47
require (
github.com/klauspost/compress v1.17.0 // indirect
github.com/pierrec/lz4/v4 v4.1.15 // indirect
)

View File

@@ -0,0 +1,69 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0=
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/segmentio/kafka-go v0.4.47 h1:IqziR4pA3vrZq7YdRxaT3w1/5fvIH5qpCwstUanQQB0=
github.com/segmentio/kafka-go v0.4.47/go.mod h1:HjF6XbOKh0Pjlkr5GVZxt6CsjjwnmhVOfURM5KMd8qg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -0,0 +1,127 @@
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"time"
"github.com/segmentio/kafka-go"
)
func main() {
// Configuration
brokerAddress := "localhost:9093" // Kafka gateway port (not SeaweedMQ broker port 17777)
topicName := "_raw_messages" // Topic with "_" prefix - should skip schema validation
fmt.Printf("Publishing messages to topic '%s' on broker '%s'\n", topicName, brokerAddress)
// Create a new writer
writer := &kafka.Writer{
Addr: kafka.TCP(brokerAddress),
Topic: topicName,
Balancer: &kafka.LeastBytes{},
// Configure for immediate delivery (useful for testing)
BatchTimeout: 10 * time.Millisecond,
BatchSize: 1,
}
defer writer.Close()
// Sample data to publish
messages := []map[string]interface{}{
{
"id": 1,
"message": "Hello from kafka-go client",
"timestamp": time.Now().Unix(),
"user_id": "user123",
},
{
"id": 2,
"message": "Raw message without schema validation",
"timestamp": time.Now().Unix(),
"user_id": "user456",
"metadata": map[string]string{
"source": "test-client",
"type": "raw",
},
},
{
"id": 3,
"message": "Testing SMQ with underscore prefix topic",
"timestamp": time.Now().Unix(),
"user_id": "user789",
"data": []byte("Some binary data here"),
},
}
ctx := context.Background()
fmt.Println("Publishing messages...")
for i, msgData := range messages {
// Convert message to JSON (simulating raw messages stored in "value" field)
valueBytes, err := json.Marshal(msgData)
if err != nil {
log.Fatalf("Failed to marshal message %d: %v", i+1, err)
}
// Create Kafka message
msg := kafka.Message{
Key: []byte(fmt.Sprintf("key_%d", msgData["id"])),
Value: valueBytes,
Headers: []kafka.Header{
{Key: "source", Value: []byte("kafka-go-client")},
{Key: "content-type", Value: []byte("application/json")},
},
}
// Write message
err = writer.WriteMessages(ctx, msg)
if err != nil {
log.Printf("Failed to write message %d: %v", i+1, err)
continue
}
fmt.Printf("-Published message %d: %s\n", i+1, string(valueBytes))
// Small delay between messages
time.Sleep(100 * time.Millisecond)
}
fmt.Println("\nAll messages published successfully!")
// Test with different raw message types
fmt.Println("\nPublishing different raw message formats...")
rawMessages := []kafka.Message{
{
Key: []byte("binary_key"),
Value: []byte("Simple string message"),
},
{
Key: []byte("json_key"),
Value: []byte(`{"raw_field": "raw_value", "number": 42}`),
},
{
Key: []byte("empty_key"),
Value: []byte{}, // Empty value
},
{
Key: nil, // No key
Value: []byte("Message with no key"),
},
}
for i, msg := range rawMessages {
err := writer.WriteMessages(ctx, msg)
if err != nil {
log.Printf("Failed to write raw message %d: %v", i+1, err)
continue
}
fmt.Printf("-Published raw message %d: key=%s, value=%s\n",
i+1, string(msg.Key), string(msg.Value))
}
fmt.Println("\nAll test messages published to topic with '_' prefix!")
fmt.Println("These messages should be stored as raw bytes without schema validation.")
}

View File

@@ -0,0 +1,75 @@
#!/bin/bash
# Test script for SMQ schema bypass functionality
# This script tests publishing to topics with "_" prefix which should bypass schema validation
set -e
echo "🧪 Testing SMQ Schema Bypass for Topics with '_' Prefix"
echo "========================================================="
# Check if Kafka gateway is running
echo "Checking if Kafka gateway is running on localhost:9093..."
if ! nc -z localhost 9093 2>/dev/null; then
echo "[FAIL] Kafka gateway is not running on localhost:9093"
echo "Please start SeaweedMQ with Kafka gateway enabled first"
exit 1
fi
echo "[OK] Kafka gateway is running"
# Test with schema-required topic (should require schema)
echo
echo "Testing schema-required topic (should require schema validation)..."
SCHEMA_TOPIC="user-events"
echo "Topic: $SCHEMA_TOPIC (regular topic, requires schema)"
# Test with underscore prefix topic (should bypass schema)
echo
echo "Testing schema-bypass topic (should skip schema validation)..."
BYPASS_TOPIC="_raw_messages"
echo "Topic: $BYPASS_TOPIC (underscore prefix, bypasses schema)"
# Build and test the publisher
echo
echo "Building publisher..."
cd simple-publisher
go mod tidy
echo "[OK] Publisher dependencies ready"
echo
echo "Running publisher test..."
timeout 30s go run main.go || {
echo "[FAIL] Publisher test failed or timed out"
exit 1
}
echo "[OK] Publisher test completed"
# Build consumer
echo
echo "Building consumer..."
cd ../simple-consumer
go mod tidy
echo "[OK] Consumer dependencies ready"
echo
echo "Testing consumer (will run for 10 seconds)..."
timeout 10s go run main.go || {
if [ $? -eq 124 ]; then
echo "[OK] Consumer test completed (timed out as expected)"
else
echo "[FAIL] Consumer test failed"
exit 1
fi
}
echo
echo "All tests completed successfully!"
echo
echo "Summary:"
echo "- [OK] Topics with '_' prefix bypass schema validation"
echo "- [OK] Raw messages are stored as bytes in the 'value' field"
echo "- [OK] kafka-go client works with SeaweedMQ"
echo "- [OK] No schema validation errors for '_raw_messages' topic"
echo
echo "The SMQ schema bypass functionality is working correctly!"
echo "Topics with '_' prefix are treated as system topics and bypass all schema processing."

View File

@@ -0,0 +1,20 @@
#!/bin/bash
# Test script to produce JSON messages and check timestamp field
# Produce 3 JSON messages
for i in 1 2 3; do
TS=$(date +%s%N)
echo "{\"id\":\"test-msg-$i\",\"timestamp\":$TS,\"producer_id\":999,\"counter\":$i,\"user_id\":\"user-test\",\"event_type\":\"test\"}"
done | docker run --rm -i --network kafka-client-loadtest \
edenhill/kcat:1.7.1 \
-P -b kafka-gateway:9093 -t test-json-topic
echo "Messages produced. Waiting 2 seconds for processing..."
sleep 2
echo "Querying messages..."
cd /Users/chrislu/go/src/github.com/seaweedfs/seaweedfs/test/kafka/kafka-client-loadtest
docker compose exec kafka-gateway /usr/bin/weed-sql \
-master=seaweedfs-master:9333 \
-database=kafka \
-query="SELECT id, timestamp, producer_id, counter, user_id, event_type FROM \"test-json-topic\" LIMIT 5;"

View File

@@ -0,0 +1,79 @@
package unit
import (
"fmt"
"net"
"strings"
"testing"
"time"
"github.com/seaweedfs/seaweedfs/test/kafka/internal/testutil"
)
// TestGatewayBasicFunctionality tests basic gateway operations
func TestGatewayBasicFunctionality(t *testing.T) {
gateway := testutil.NewGatewayTestServer(t, testutil.GatewayOptions{})
defer gateway.CleanupAndClose()
addr := gateway.StartAndWait()
// Give the gateway a bit more time to be fully ready
time.Sleep(200 * time.Millisecond)
t.Run("AcceptsConnections", func(t *testing.T) {
testGatewayAcceptsConnections(t, addr)
})
t.Run("RefusesAfterClose", func(t *testing.T) {
testGatewayRefusesAfterClose(t, gateway)
})
}
func testGatewayAcceptsConnections(t *testing.T, addr string) {
// Test basic TCP connection to gateway
t.Logf("Testing connection to gateway at %s", addr)
conn, err := net.DialTimeout("tcp", addr, 5*time.Second)
if err != nil {
t.Fatalf("Failed to connect to gateway: %v", err)
}
defer conn.Close()
// Test that we can establish a connection and the gateway is listening
// We don't need to send a full Kafka request for this basic test
t.Logf("Successfully connected to gateway at %s", addr)
// Optional: Test that we can write some data without error
testData := []byte("test")
conn.SetWriteDeadline(time.Now().Add(1 * time.Second))
if _, err := conn.Write(testData); err != nil {
t.Logf("Write test failed (expected for basic connectivity test): %v", err)
} else {
t.Logf("Write test succeeded")
}
}
func testGatewayRefusesAfterClose(t *testing.T, gateway *testutil.GatewayTestServer) {
// Get the address from the gateway's listener
host, port := gateway.GetListenerAddr()
addr := fmt.Sprintf("%s:%d", host, port)
// Close the gateway
gateway.CleanupAndClose()
t.Log("Testing that gateway refuses connections after close")
// Attempt to connect - should fail
conn, err := net.DialTimeout("tcp", addr, 2*time.Second)
if err == nil {
conn.Close()
t.Fatal("Expected connection to fail after gateway close, but it succeeded")
}
// Verify it's a connection refused error
if !strings.Contains(err.Error(), "connection refused") && !strings.Contains(err.Error(), "connect: connection refused") {
t.Logf("Connection failed as expected with error: %v", err)
} else {
t.Logf("Connection properly refused: %v", err)
}
}