- Graceful shutdown: SIGINT/SIGTERM handling with 5s timeout - Readiness probe (/readyz): pings database before reporting ready - Connection pool: documented MaxOpenConns(1) for SQLite single-writer constraint - Load test tool: cmd/loadtest with configurable concurrency/total requests - Shell script: scripts/load-test.sh for quick load testing - pprof endpoints: /debug/pprof/* when HATCH_DEBUG env var is set Co-Authored-By: Paperclip <noreply@paperclip.ing>
99 lines
2.4 KiB
Go
99 lines
2.4 KiB
Go
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
func main() {
|
|
url := flag.String("url", "http://localhost:8080/healthz", "URL to test")
|
|
concurrency := flag.Int("c", 10, "concurrent workers")
|
|
total := flag.Int("n", 1000, "total requests")
|
|
flag.Parse()
|
|
|
|
var (
|
|
successCount int64
|
|
failCount int64
|
|
totalLatency int64 // microseconds
|
|
minLatency int64 = 1<<63 - 1
|
|
maxLatency int64
|
|
)
|
|
|
|
client := &http.Client{
|
|
Timeout: 10 * time.Second,
|
|
Transport: &http.Transport{
|
|
MaxIdleConns: *concurrency,
|
|
MaxIdleConnsPerHost: *concurrency,
|
|
IdleConnTimeout: 90 * time.Second,
|
|
},
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
requests := make(chan int, *total)
|
|
for i := 0; i < *total; i++ {
|
|
requests <- i
|
|
}
|
|
close(requests)
|
|
|
|
start := time.Now()
|
|
|
|
for i := 0; i < *concurrency; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for range requests {
|
|
reqStart := time.Now()
|
|
resp, err := client.Get(*url)
|
|
latency := time.Since(reqStart).Microseconds()
|
|
if err != nil {
|
|
atomic.AddInt64(&failCount, 1)
|
|
continue
|
|
}
|
|
io.Copy(io.Discard, resp.Body)
|
|
resp.Body.Close()
|
|
if resp.StatusCode == http.StatusOK {
|
|
atomic.AddInt64(&successCount, 1)
|
|
} else {
|
|
atomic.AddInt64(&failCount, 1)
|
|
}
|
|
atomic.AddInt64(&totalLatency, latency)
|
|
// Update min/max using atomic operations for simplicity
|
|
for {
|
|
old := atomic.LoadInt64(&minLatency)
|
|
if latency >= old || atomic.CompareAndSwapInt64(&minLatency, old, latency) {
|
|
break
|
|
}
|
|
}
|
|
for {
|
|
old := atomic.LoadInt64(&maxLatency)
|
|
if latency <= old || atomic.CompareAndSwapInt64(&maxLatency, old, latency) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
duration := time.Since(start)
|
|
|
|
fmt.Printf("Load test completed\n")
|
|
fmt.Printf("URL: %s\n", *url)
|
|
fmt.Printf("Total requests: %d\n", *total)
|
|
fmt.Printf("Concurrency: %d\n", *concurrency)
|
|
fmt.Printf("Duration: %v\n", duration)
|
|
fmt.Printf("Success: %d\n", atomic.LoadInt64(&successCount))
|
|
fmt.Printf("Failed: %d\n", atomic.LoadInt64(&failCount))
|
|
if *total > 0 {
|
|
avgLatency := float64(atomic.LoadInt64(&totalLatency)) / float64(*total)
|
|
fmt.Printf("Average latency: %.2f ms\n", avgLatency/1000)
|
|
fmt.Printf("Min latency: %.2f ms\n", float64(atomic.LoadInt64(&minLatency))/1000)
|
|
fmt.Printf("Max latency: %.2f ms\n", float64(atomic.LoadInt64(&maxLatency))/1000)
|
|
fmt.Printf("Requests/sec: %.2f\n", float64(*total)/duration.Seconds())
|
|
}
|
|
} |