diff --git a/cmd/debug/main.go b/cmd/debug/main.go new file mode 100644 index 0000000..5329cbd --- /dev/null +++ b/cmd/debug/main.go @@ -0,0 +1,175 @@ +package main + +import ( + "context" + "crypto/rand" + "flag" + "fmt" + "log" + "time" + + "github.com/nomasters/haystack/client" + "github.com/nomasters/haystack/needle" +) + +func main() { + endpoint := flag.String("endpoint", "localhost:1337", "Haystack server endpoint") + flag.Parse() + + fmt.Printf("šŸ” Haystack Debug Test\n") + fmt.Printf("======================\n") + fmt.Printf("Endpoint: %s\n\n", *endpoint) + + // Create client + cfg := &client.Config{ + Address: *endpoint, + MaxConnections: 1, + ReadTimeout: 5 * time.Second, + WriteTimeout: 5 * time.Second, + } + + c, err := client.New(cfg) + if err != nil { + log.Fatalf("Failed to create client: %v", err) + } + defer c.Close() + + ctx := context.Background() + + // Test 1: Simple SET and immediate GET + fmt.Println("Test 1: SET and immediate GET") + fmt.Println("------------------------------") + + // Create a test needle with known data + testData := []byte("Hello Haystack! This is a test message.") + // Pad to 160 bytes + paddedData := make([]byte, 160) + copy(paddedData, testData) + + testNeedle, err := needle.New(paddedData) + if err != nil { + log.Fatalf("Failed to create needle: %v", err) + } + + hash := testNeedle.Hash() + fmt.Printf("Created needle with hash: %x\n", hash) + fmt.Printf("Needle data (first 40 bytes): %s\n", paddedData[:40]) + + // SET the needle + fmt.Print("\nSetting needle... ") + start := time.Now() + err = c.Set(ctx, testNeedle) + setDuration := time.Since(start) + if err != nil { + fmt.Printf("āŒ Error: %v\n", err) + } else { + fmt.Printf("āœ… (took %v)\n", setDuration) + } + + // Small delay to ensure it's processed + fmt.Println("Waiting 100ms for server to process...") + time.Sleep(100 * time.Millisecond) + + // GET the needle back + fmt.Print("Getting needle... ") + start = time.Now() + gotNeedle, err := c.Get(ctx, hash) + getDuration := time.Since(start) + if err != nil { + fmt.Printf("āŒ Error: %v\n", err) + } else { + fmt.Printf("āœ… (took %v)\n", getDuration) + + // Verify the data matches + if gotNeedle != nil { + gotPayload := gotNeedle.Payload() + matches := true + for i := range paddedData { + if paddedData[i] != gotPayload[i] { + matches = false + break + } + } + if matches { + fmt.Println("āœ… Data matches!") + } else { + fmt.Println("āŒ Data mismatch!") + fmt.Printf("Expected: %x\n", paddedData[:40]) + fmt.Printf("Got: %x\n", gotPayload[:40]) + } + } + } + + // Test 2: GET non-existent needle + fmt.Println("\n\nTest 2: GET non-existent needle") + fmt.Println("--------------------------------") + + randomData := make([]byte, 160) + rand.Read(randomData) + randomNeedle, _ := needle.New(randomData) + randomHash := randomNeedle.Hash() + + fmt.Printf("Trying to get random hash: %x\n", randomHash) + fmt.Print("Getting non-existent needle... ") + start = time.Now() + _, err = c.Get(ctx, randomHash) + getDuration = time.Since(start) + if err != nil { + fmt.Printf("āŒ Error (expected): %v (took %v)\n", err, getDuration) + } else { + fmt.Printf("āš ļø Unexpectedly found data! (took %v)\n", getDuration) + } + + // Test 3: Rapid SET/GET cycle + fmt.Println("\n\nTest 3: Rapid SET/GET cycle") + fmt.Println("---------------------------") + + successCount := 0 + errorCount := 0 + + for i := 0; i < 10; i++ { + // Create unique data for each iteration + iterData := make([]byte, 160) + copy(iterData, fmt.Sprintf("Test iteration %d", i)) + + iterNeedle, _ := needle.New(iterData) + iterHash := iterNeedle.Hash() + + // SET + if err := c.Set(ctx, iterNeedle); err != nil { + fmt.Printf(" SET %d: āŒ %v\n", i, err) + errorCount++ + continue + } + + // Small delay + time.Sleep(50 * time.Millisecond) + + // GET + if _, err := c.Get(ctx, iterHash); err != nil { + fmt.Printf(" Iteration %d: āŒ GET failed: %v\n", i, err) + errorCount++ + } else { + fmt.Printf(" Iteration %d: āœ…\n", i) + successCount++ + } + } + + fmt.Printf("\nResults: %d successful, %d failed\n", successCount, errorCount) + + // Test 4: Check if original needle still exists + fmt.Println("\n\nTest 4: Check if first needle still exists") + fmt.Println("------------------------------------------") + + fmt.Printf("Getting original hash: %x\n", hash) + fmt.Print("Getting original needle... ") + start = time.Now() + _, err = c.Get(ctx, hash) + getDuration = time.Since(start) + if err != nil { + fmt.Printf("āŒ Error: %v (took %v)\n", err, getDuration) + fmt.Println("Original data may have expired or been evicted") + } else { + fmt.Printf("āœ… Still exists! (took %v)\n", getDuration) + } +} diff --git a/cmd/stress/main.go b/cmd/stress/main.go new file mode 100644 index 0000000..fc94dbe --- /dev/null +++ b/cmd/stress/main.go @@ -0,0 +1,461 @@ +package main + +import ( + "context" + "crypto/rand" + "flag" + "fmt" + "log" + "os" + "os/signal" + "sync" + "sync/atomic" + "syscall" + "time" + + "github.com/nomasters/haystack/client" + "github.com/nomasters/haystack/needle" +) + +type Stats struct { + setOps atomic.Int64 + getOps atomic.Int64 + setErrors atomic.Int64 + getErrors atomic.Int64 + setLatency atomic.Int64 // cumulative microseconds + getLatency atomic.Int64 // cumulative microseconds +} + +func (s *Stats) RecordSet(duration time.Duration, err error) { + s.setOps.Add(1) + s.setLatency.Add(duration.Microseconds()) + if err != nil { + s.setErrors.Add(1) + } +} + +func (s *Stats) RecordGet(duration time.Duration, err error) { + s.getOps.Add(1) + s.getLatency.Add(duration.Microseconds()) + if err != nil { + s.getErrors.Add(1) + } +} + +func (s *Stats) Report() { + setOps := s.setOps.Load() + getOps := s.getOps.Load() + setErrors := s.setErrors.Load() + getErrors := s.getErrors.Load() + setLatencyTotal := s.setLatency.Load() + getLatencyTotal := s.getLatency.Load() + + fmt.Printf("\n=== Performance Report ===\n") + fmt.Printf("Operations:\n") + fmt.Printf(" SET: %d ops (%d errors, %.2f%% success)\n", + setOps, setErrors, + 100.0*float64(setOps-setErrors)/float64(max(setOps, 1))) + fmt.Printf(" GET: %d ops (%d errors, %.2f%% success)\n", + getOps, getErrors, + 100.0*float64(getOps-getErrors)/float64(max(getOps, 1))) + + fmt.Printf("\nLatency:\n") + if setOps > 0 { + successfulSets := setOps - setErrors + if successfulSets > 0 { + // Note: This includes both successful and failed operations + avgSetLatency := float64(setLatencyTotal) / float64(setOps) + fmt.Printf(" SET avg: %.2f ms (includes timeouts)\n", avgSetLatency/1000) + } + } + if getOps > 0 { + successfulGets := getOps - getErrors + if successfulGets > 0 { + // Note: This includes both successful and failed operations + avgGetLatency := float64(getLatencyTotal) / float64(getOps) + fmt.Printf(" GET avg: %.2f ms (includes timeouts)\n", avgGetLatency/1000) + + // Estimate: If failures are timeouts, calculate success-only average + if getErrors > 0 { + timeoutMs := float64(10000) // 10 second timeout in ms + totalTimeoutMs := float64(getErrors) * timeoutMs + successLatencyTotal := float64(getLatencyTotal) - (totalTimeoutMs * 1000) // Convert to microseconds + if successLatencyTotal > 0 && successfulGets > 0 { + avgSuccessLatency := successLatencyTotal / float64(successfulGets) + fmt.Printf(" GET successful only: ~%.2f ms\n", avgSuccessLatency/1000) + } + } + } + } +} + +func (s *Stats) Reset() { + s.setOps.Store(0) + s.getOps.Store(0) + s.setErrors.Store(0) + s.getErrors.Store(0) + s.setLatency.Store(0) + s.getLatency.Store(0) +} + +func max(a, b int64) int64 { + if a > b { + return a + } + return b +} + +// generateNeedles creates a corpus of random needles +func generateNeedles(sizeMB int) ([]*needle.Needle, error) { + needleSize := 192 // bytes per needle + needlesPerMB := (1024 * 1024) / needleSize + totalNeedles := sizeMB * needlesPerMB + + fmt.Printf("Generating %d needles (~%d MB)... ", totalNeedles, sizeMB) + + needles := make([]*needle.Needle, totalNeedles) + for i := 0; i < totalNeedles; i++ { + data := make([]byte, 160) + if _, err := rand.Read(data); err != nil { + return nil, fmt.Errorf("failed to generate random data: %w", err) + } + + n, err := needle.New(data) + if err != nil { + return nil, fmt.Errorf("failed to create needle: %w", err) + } + needles[i] = n + + // Progress indicator + if i > 0 && i%(needlesPerMB*10) == 0 { + fmt.Print(".") + } + } + + fmt.Println(" āœ…") + return needles, nil +} + +// setWorker performs SET operations without blocking +func setWorker(ctx context.Context, c *client.Client, needles []*needle.Needle, stats *Stats, wg *sync.WaitGroup, workerId int, numWorkers int, maxConcurrent int) { + defer wg.Done() + + // Create a semaphore to limit concurrent requests per worker + sem := make(chan struct{}, maxConcurrent) // Limit in-flight requests per worker + + // Each worker handles needles with index % numWorkers == workerId + for i := workerId; i < len(needles); i += numWorkers { + select { + case <-ctx.Done(): + // Drain remaining requests + for j := 0; j < len(sem); j++ { + select { + case <-sem: + default: + return + } + } + return + default: + // Acquire semaphore slot + sem <- struct{}{} + + // Fire off the SET request in a goroutine (non-blocking) + go func(needle *needle.Needle) { + defer func() { <-sem }() // Release semaphore slot when done + + start := time.Now() + err := c.Set(ctx, needle) + stats.RecordSet(time.Since(start), err) + }(needles[i]) + + // Small delay every N requests to prevent CPU spinning + if i%100 == 0 { + time.Sleep(1 * time.Microsecond) + } + } + } + + // Wait for all in-flight requests to complete + for i := 0; i < cap(sem); i++ { + sem <- struct{}{} + } +} + +// getWorker performs GET operations without blocking +func getWorker(ctx context.Context, c *client.Client, needles []*needle.Needle, stats *Stats, wg *sync.WaitGroup, workerId int, maxConcurrent int) { + defer wg.Done() + + // Create a semaphore to limit concurrent requests per worker + // This prevents overwhelming the system + sem := make(chan struct{}, maxConcurrent) // Limit in-flight requests per worker + + // Launch requests continuously + for i := 0; ; i++ { + select { + case <-ctx.Done(): + // Drain remaining requests + for j := 0; j < len(sem); j++ { + select { + case <-sem: + default: + return + } + } + return + default: + // Acquire semaphore slot + sem <- struct{}{} + + // Pick a needle to GET (round-robin through corpus) + idx := (workerId*1000 + i) % len(needles) + hash := needles[idx].Hash() + + // Fire off the request in a goroutine (non-blocking) + go func(h needle.Hash) { + defer func() { <-sem }() // Release semaphore slot when done + + start := time.Now() + _, err := c.Get(ctx, h) + stats.RecordGet(time.Since(start), err) + + // Log first few errors for debugging + if err != nil && stats.getErrors.Load() < 5 { + fmt.Printf("[GET Error] Hash %x: %v\n", h, err) + } + }(hash) + + // Small delay to prevent CPU spinning + if i%100 == 0 { + time.Sleep(1 * time.Microsecond) + } + } + } +} + +func main() { + var ( + endpoint = flag.String("endpoint", "localhost:1337", "Haystack server endpoint") + sizeMB = flag.Int("size", 100, "Size of test corpus in MB") + setWorkers = flag.Int("set-workers", 5, "Number of SET workers") + getWorkers = flag.Int("get-workers", 10, "Number of GET workers") + getDuration = flag.Duration("get-duration", 30*time.Second, "Duration for GET test") + poolSize = flag.Int("pool", 0, "Connection pool size (0 = auto, based on workers)") + reportFreq = flag.Duration("report", 5*time.Second, "Reporting frequency") + ) + flag.Parse() + + // Auto-size the pool based on workers if not specified + actualPoolSize := *poolSize + if actualPoolSize == 0 { + // Use 2x the max number of workers as pool size + if *setWorkers > *getWorkers { + actualPoolSize = 2 * *setWorkers + } else { + actualPoolSize = 2 * *getWorkers + } + if actualPoolSize < 10 { + actualPoolSize = 10 + } + if actualPoolSize > 100 { + actualPoolSize = 100 // Cap at 100 to be reasonable + } + } + + // Calculate per-worker concurrency based on total workers + maxConcurrentPerWorker := 100 + totalWorkers := *setWorkers + if *getWorkers > totalWorkers { + totalWorkers = *getWorkers + } + if totalWorkers > 10 { + // Scale down per-worker concurrency for many workers + maxConcurrentPerWorker = 1000 / totalWorkers + if maxConcurrentPerWorker < 10 { + maxConcurrentPerWorker = 10 + } + } + + fmt.Printf("šŸ”„ Haystack Stress Test\n") + fmt.Printf("========================\n") + fmt.Printf("Endpoint: %s\n", *endpoint) + fmt.Printf("Corpus Size: %d MB\n", *sizeMB) + fmt.Printf("SET Workers: %d\n", *setWorkers) + fmt.Printf("GET Workers: %d\n", *getWorkers) + fmt.Printf("GET Duration: %s\n", *getDuration) + fmt.Printf("Pool Size: %d\n", actualPoolSize) + fmt.Printf("Concurrency: %d per worker\n", maxConcurrentPerWorker) + fmt.Printf("\n") + + // Create client with custom pool size + cfg := &client.Config{ + Address: *endpoint, + MaxConnections: actualPoolSize, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + } + + c, err := client.New(cfg) + if err != nil { + log.Fatalf("Failed to create client: %v", err) + } + defer c.Close() + + // Test connectivity + fmt.Print("Testing connectivity... ") + ctx := context.Background() + testData := make([]byte, 160) + rand.Read(testData) + testNeedle, _ := needle.New(testData) + + if err := c.Set(ctx, testNeedle); err != nil { + fmt.Printf("āŒ\n") + log.Fatalf("Failed to connect: %v", err) + } + fmt.Printf("āœ…\n\n") + + // Generate test corpus + needles, err := generateNeedles(*sizeMB) + if err != nil { + log.Fatalf("Failed to generate needles: %v", err) + } + + // Setup signal handling + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + + // Statistics + stats := &Stats{} + + // Phase 1: SET all needles + fmt.Printf("\nšŸ“¤ Phase 1: Storing %d needles with %d workers\n", len(needles), *setWorkers) + fmt.Println("========================================") + + setCtx, setCancel := context.WithCancel(ctx) + var setWg sync.WaitGroup + + // Progress tracking for SET phase + setStartTime := time.Now() + setReportTicker := time.NewTicker(*reportFreq) + defer setReportTicker.Stop() + + // Start SET workers + for i := 0; i < *setWorkers; i++ { + setWg.Add(1) + go setWorker(setCtx, c, needles, stats, &setWg, i, *setWorkers, maxConcurrentPerWorker) + } + + // Monitor SET phase + go func() { + for { + select { + case <-setCtx.Done(): + return + case <-setReportTicker.C: + setOps := stats.setOps.Load() + elapsed := time.Since(setStartTime) + remaining := len(needles) - int(setOps) + fmt.Printf("[%s] Stored: %d/%d (%.1f/s), Remaining: %d\n", + elapsed.Round(time.Second), + setOps, len(needles), + float64(setOps)/elapsed.Seconds(), + remaining) + + // Check if all needles are stored + if int(setOps) >= len(needles) { + setCancel() + return + } + case <-sigChan: + fmt.Println("\nāš ļø Interrupted!") + setCancel() + return + } + } + }() + + // Wait for SET phase to complete + setWg.Wait() + setCancel() + + setDuration := time.Since(setStartTime) + fmt.Printf("\nāœ… SET Phase Complete!\n") + fmt.Printf(" Stored: %d needles in %s\n", stats.setOps.Load(), setDuration) + fmt.Printf(" Throughput: %.2f ops/sec\n", float64(stats.setOps.Load())/setDuration.Seconds()) + fmt.Printf(" Errors: %d\n", stats.setErrors.Load()) + + // Small delay to ensure data is actually stored on server + fmt.Println("\nWaiting 2 seconds for data to settle...") + time.Sleep(2 * time.Second) + + // Reset stats for GET phase + getStats := &Stats{} + + // Phase 2: GET operations + fmt.Printf("\nšŸ“„ Phase 2: Reading needles with %d workers for %s\n", *getWorkers, *getDuration) + fmt.Println("==============================================") + + getCtx, getCancel := context.WithTimeout(ctx, *getDuration) + defer getCancel() + + var getWg sync.WaitGroup + getStartTime := time.Now() + getReportTicker := time.NewTicker(*reportFreq) + defer getReportTicker.Stop() + + // Start GET workers + for i := 0; i < *getWorkers; i++ { + getWg.Add(1) + go getWorker(getCtx, c, needles, getStats, &getWg, i, maxConcurrentPerWorker) + } + + // Monitor GET phase + for { + select { + case <-sigChan: + fmt.Println("\nāš ļø Interrupted!") + getCancel() + getWg.Wait() + getStats.Report() + return + + case <-getCtx.Done(): + fmt.Println("\nāœ… GET Phase Complete!") + getWg.Wait() + + getDuration := time.Since(getStartTime) + fmt.Printf(" Read: %d operations in %s\n", getStats.getOps.Load(), getDuration) + fmt.Printf(" Throughput: %.2f ops/sec\n", float64(getStats.getOps.Load())/getDuration.Seconds()) + fmt.Printf(" Errors: %d\n", getStats.getErrors.Load()) + + // Final combined report + fmt.Println("\n==================================================") + fmt.Println("šŸ“Š FINAL SUMMARY") + fmt.Println("==================================================") + + fmt.Printf("\nSET Performance:\n") + fmt.Printf(" Operations: %d\n", stats.setOps.Load()) + fmt.Printf(" Errors: %d\n", stats.setErrors.Load()) + fmt.Printf(" Avg Latency: %.2f ms\n", float64(stats.setLatency.Load())/float64(max(stats.setOps.Load(), 1))/1000) + fmt.Printf(" Throughput: %.2f ops/sec\n", float64(stats.setOps.Load())/setDuration.Seconds()) + + fmt.Printf("\nGET Performance:\n") + fmt.Printf(" Operations: %d\n", getStats.getOps.Load()) + fmt.Printf(" Errors: %d\n", getStats.getErrors.Load()) + fmt.Printf(" Avg Latency: %.2f ms\n", float64(getStats.getLatency.Load())/float64(max(getStats.getOps.Load(), 1))/1000) + fmt.Printf(" Throughput: %.2f ops/sec\n", float64(getStats.getOps.Load())/getDuration.Seconds()) + + fmt.Printf("\nTotal Operations: %d\n", stats.setOps.Load()+getStats.getOps.Load()) + return + + case <-getReportTicker.C: + getOps := getStats.getOps.Load() + elapsed := time.Since(getStartTime) + fmt.Printf("[%s] GET: %d ops (%.1f/s), Errors: %d\n", + elapsed.Round(time.Second), + getOps, + float64(getOps)/elapsed.Seconds(), + getStats.getErrors.Load()) + } + } +} diff --git a/examples/deployments/fly.io/fly.toml b/examples/deployments/fly.io/fly.toml index 1a8e287..dd34f18 100644 --- a/examples/deployments/fly.io/fly.toml +++ b/examples/deployments/fly.io/fly.toml @@ -15,9 +15,9 @@ strategy = 'immediate' # Environment variables for haystack configuration [env] HAYSTACK_ADDR = 'fly-global-services:1337' -HAYSTACK_STORAGE = 'memory' +HAYSTACK_STORAGE = 'mmap' HAYSTACK_DATA_DIR = '/data' -HAYSTACK_LOG_LEVEL = 'debug' +HAYSTACK_LOG_LEVEL = 'info' # Persistent volume for mmap storage [mounts] diff --git a/scripts/quick-stress.sh b/scripts/quick-stress.sh new file mode 100644 index 0000000..b0a466b --- /dev/null +++ b/scripts/quick-stress.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +# Quick stress test for Haystack server +# A simpler version that focuses on SET operations first + +set -e + +# Colors +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +ENDPOINT="${1:-haystack-example-trunk.fly.dev:1337}" + +echo -e "${BLUE}⚔ Quick Haystack Stress Test${NC}" +echo "================================" +echo -e "${YELLOW}Target: $ENDPOINT${NC}" +echo "" + +# Build if needed +if [ ! -f /tmp/haystack-stress ]; then + echo "Building stress tool..." + go build -o /tmp/haystack-stress ./cmd/stress +fi + +# 1. Quick SET test +echo -e "${BLUE}1. SET Performance Test (10 workers, 20s)${NC}" +/tmp/haystack-stress \ + -endpoint "$ENDPOINT" \ + -workers 10 \ + -duration 20s \ + -workload set \ + -pool 5 \ + -report 5s + +echo "" + +# 2. Mixed workload +echo -e "${BLUE}2. Mixed Workload Test (20 workers, 30s)${NC}" +/tmp/haystack-stress \ + -endpoint "$ENDPOINT" \ + -workers 20 \ + -duration 30s \ + -workload mixed \ + -pool 10 \ + -report 10s + +echo "" +echo -e "${GREEN}āœ… Quick stress test completed!${NC}" +echo "" +echo "To monitor server logs:" +echo " fly logs -a haystack-example-trunk" \ No newline at end of file diff --git a/scripts/stress-remote.sh b/scripts/stress-remote.sh new file mode 100755 index 0000000..cd9e628 --- /dev/null +++ b/scripts/stress-remote.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +# Stress test for remote Haystack server +# Generates a corpus of data, stores it, then reads it back + +set -e + +# Colors +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +ENDPOINT="${1:-haystack-example-trunk.fly.dev:1337}" + +echo -e "${BLUE}šŸ”„ Haystack Remote Stress Test${NC}" +echo "======================================" +echo -e "${YELLOW}Target: $ENDPOINT${NC}" +echo "" + +echo "" +echo -e "${BLUE}Test Configuration:${NC}" +echo "• 10 MB of test data (~54,613 needles)" +echo "• Phase 1: Store all data with 5 workers" +echo "• Phase 2: Read data for 30 seconds with 20 workers" +echo "" + +read -p "Continue with stress test? (y/N): " -n 1 -r +echo +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Aborted." + exit 0 +fi + +echo "" +echo -e "${GREEN}Starting stress test...${NC}" +echo "" + +go run ./cmd/stress \ + -endpoint "$ENDPOINT" \ + -size 10 \ + -set-workers 5 \ + -get-workers 20 \ + -get-duration 30s \ + -pool 15 \ + -report 5s + +echo "" +echo -e "${GREEN}āœ… Stress test completed!${NC}" +echo "" +echo "To view server logs:" +echo " fly logs -a haystack-example-trunk" +echo "" +echo "For a more intensive test, run:" +echo " go run ./cmd/stress -endpoint $ENDPOINT -size 100 -set-workers 10 -get-workers 50 -get-duration 60s" \ No newline at end of file diff --git a/scripts/stress-test.sh b/scripts/stress-test.sh new file mode 100755 index 0000000..2ce7ad9 --- /dev/null +++ b/scripts/stress-test.sh @@ -0,0 +1,105 @@ +#!/bin/bash + +# Haystack Stress Testing Script +# Tests the deployed Haystack server under various load conditions + +set -e + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +# Default values +DEFAULT_ENDPOINT="haystack-example-trunk.fly.dev:1337" +ENDPOINT="${1:-$DEFAULT_ENDPOINT}" + +echo -e "${BLUE}šŸ”„ Haystack Stress Testing Suite${NC}" +echo "======================================" +echo -e "${YELLOW}Target: $ENDPOINT${NC}" +echo "" + +# Build the stress tool +echo -e "${BLUE}Building stress test tool...${NC}" +go build -o /tmp/haystack-stress ./cmd/stress +echo -e "${GREEN}āœ“ Built${NC}" +echo "" + +# Function to run a stress test +run_stress_test() { + local name="$1" + local workers="$2" + local duration="$3" + local workload="$4" + local pool="${5:-5}" + + echo -e "${YELLOW}Test: $name${NC}" + echo "Workers: $workers, Duration: $duration, Workload: $workload, Pool: $pool" + echo "---" + + /tmp/haystack-stress \ + -endpoint "$ENDPOINT" \ + -workers "$workers" \ + -duration "$duration" \ + -workload "$workload" \ + -pool "$pool" \ + -report 10s + + echo "" + echo "---" + sleep 2 +} + +# 1. Warm-up test +echo -e "${BLUE}1. Warm-up Test${NC}" +echo "Light load to establish baseline" +run_stress_test "Warm-up" 5 15s mixed 3 +echo "" + +# 2. Throughput test +echo -e "${BLUE}2. Throughput Test${NC}" +echo "Testing maximum throughput with many workers" +run_stress_test "High Throughput" 50 30s mixed 10 +echo "" + +# 3. SET-heavy workload +echo -e "${BLUE}3. Write-Heavy Test${NC}" +echo "Testing write performance" +run_stress_test "Write Heavy" 20 30s set 5 +echo "" + +# 4. GET-heavy workload +echo -e "${BLUE}4. Read-Heavy Test${NC}" +echo "Testing read performance" +run_stress_test "Read Heavy" 30 30s get 5 +echo "" + +# 5. Realistic workload +echo -e "${BLUE}5. Realistic Workload Test${NC}" +echo "Simulating real-world usage patterns with bursts" +run_stress_test "Realistic" 25 45s realistic 8 +echo "" + +# 6. Stress test (if confirmed) +echo -e "${RED}6. Maximum Stress Test${NC}" +echo -e "${YELLOW}āš ļø This will put significant load on the server${NC}" +read -p "Run maximum stress test? (y/N): " -n 1 -r +echo +if [[ $REPLY =~ ^[Yy]$ ]]; then + run_stress_test "Maximum Stress" 100 60s mixed 20 +else + echo "Skipped maximum stress test" +fi + +echo "" +echo -e "${GREEN}āœ… Stress testing completed!${NC}" +echo "" +echo -e "${BLUE}Summary:${NC}" +echo "- The server handled various workload patterns" +echo "- Check the individual test results above for performance metrics" +echo "- Monitor server logs for any issues: fly logs -a haystack-example-trunk" + +# Clean up +rm -f /tmp/haystack-stress \ No newline at end of file diff --git a/wc b/wc deleted file mode 100644 index d88ddca..0000000 Binary files a/wc and /dev/null differ