rdev/internal/handlers/projects_bench_test.go
jordan 72d16929ca feat: Implement hexagonal architecture with services, webhooks, queue, and telemetry
Major refactoring to hexagonal (ports & adapters) architecture:

- Add service layer (apikey_service, project_service) for business logic
- Add webhook system with dispatcher and delivery tracking
- Add command queue with priority-based processing
- Add rate limiting with sliding window algorithm
- Add audit logging for command execution
- Add OpenTelemetry integration (traces, metrics, spans)
- Add circuit breaker for fault tolerance
- Add cached repository wrapper for performance
- Add comprehensive validation package
- Add Kubernetes client integration for pod management
- Add database migrations (allowed_ips, audit_log, rate_limiting, queue, webhooks)
- Add network policy and PodDisruptionBudget for k8s
- Remove legacy executor and projects/registry packages
- Untrack secrets.yaml (now managed via envault)
- Add coverage.out to .gitignore
- Add e2e test infrastructure with docker-compose
- Add comprehensive documentation (API, architecture, operations, plans)
- Add golangci-lint config and pre-commit hook

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 19:57:46 -07:00

282 lines
6.2 KiB
Go

package handlers
import (
"bytes"
"encoding/json"
"net/http/httptest"
"sync"
"testing"
"github.com/go-chi/chi/v5"
"github.com/orchard9/rdev/internal/adapter/kubernetes"
)
// setupBenchHandler creates a handler for benchmarking.
func setupBenchHandler() (*ProjectsHandler, chi.Router) {
repo := kubernetes.NewProjectRepository("test-namespace")
exec := kubernetes.NewExecutor("test-namespace")
h := NewProjectsHandler(repo, exec)
router := chi.NewRouter()
h.Mount(router)
return h, router
}
// BenchmarkRunClaude benchmarks the RunClaude endpoint.
// This measures the handler overhead excluding actual command execution.
func BenchmarkRunClaude(b *testing.B) {
_, router := setupBenchHandler()
body := ClaudeRequest{Prompt: "test prompt"}
bodyBytes, _ := json.Marshal(body)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
req := httptest.NewRequest("POST", "/projects/pantheon/claude",
bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
}
}
// BenchmarkRunShell benchmarks the RunShell endpoint.
func BenchmarkRunShell(b *testing.B) {
_, router := setupBenchHandler()
body := ShellRequest{Command: "ls -la"}
bodyBytes, _ := json.Marshal(body)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
req := httptest.NewRequest("POST", "/projects/pantheon/shell",
bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
}
}
// BenchmarkRunGit benchmarks the RunGit endpoint.
func BenchmarkRunGit(b *testing.B) {
_, router := setupBenchHandler()
body := GitRequest{Args: []string{"status"}}
bodyBytes, _ := json.Marshal(body)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
req := httptest.NewRequest("POST", "/projects/pantheon/git",
bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
}
}
// BenchmarkList benchmarks the List endpoint.
func BenchmarkList(b *testing.B) {
_, router := setupBenchHandler()
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
req := httptest.NewRequest("GET", "/projects", nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
}
}
// BenchmarkGet benchmarks the Get endpoint.
func BenchmarkGet(b *testing.B) {
_, router := setupBenchHandler()
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
req := httptest.NewRequest("GET", "/projects/pantheon", nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
}
}
// BenchmarkSSEStreaming benchmarks the SSE event throughput.
// This measures how fast events can be written through the stream manager.
func BenchmarkSSEStreaming(b *testing.B) {
h, _ := setupBenchHandler()
// Subscribe to a stream
streamID := "bench-stream"
events := h.streams.Subscribe(streamID)
defer h.streams.Unsubscribe(streamID, events)
// Drain events in background
done := make(chan struct{})
go func() {
for range events {
}
close(done)
}()
eventData := map[string]any{
"line": "benchmark output line",
"stream": "stdout",
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
h.streams.Send(streamID, "output", eventData)
}
// Cleanup
h.streams.Close(streamID)
<-done
}
// BenchmarkSSEParallelStreaming benchmarks concurrent SSE event throughput.
func BenchmarkSSEParallelStreaming(b *testing.B) {
h, _ := setupBenchHandler()
streamID := "bench-parallel-stream"
events := h.streams.Subscribe(streamID)
defer h.streams.Unsubscribe(streamID, events)
// Drain events in background
done := make(chan struct{})
go func() {
for range events {
}
close(done)
}()
eventData := map[string]any{
"line": "benchmark output line",
"stream": "stdout",
}
b.ResetTimer()
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
h.streams.Send(streamID, "output", eventData)
}
})
// Cleanup
h.streams.Close(streamID)
<-done
}
// BenchmarkJSONSerialization benchmarks response JSON serialization.
func BenchmarkJSONSerialization(b *testing.B) {
response := map[string]any{
"id": "cmd-test-001",
"project": "pantheon",
"type": "claude",
"status": "running",
"stream_url": "/projects/pantheon/events?stream_id=cmd-test-001",
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = json.Marshal(response)
}
}
// BenchmarkConcurrentRequests benchmarks concurrent request handling.
func BenchmarkConcurrentRequests(b *testing.B) {
_, router := setupBenchHandler()
body := ClaudeRequest{Prompt: "test"}
bodyBytes, _ := json.Marshal(body)
b.ResetTimer()
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
req := httptest.NewRequest("POST", "/projects/pantheon/claude",
bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
}
})
}
// BenchmarkRouteMatching benchmarks chi router pattern matching.
func BenchmarkRouteMatching(b *testing.B) {
_, router := setupBenchHandler()
paths := []string{
"/projects",
"/projects/pantheon",
"/projects/pantheon/claude",
"/projects/pantheon/shell",
"/projects/pantheon/git",
"/projects/pantheon/events",
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
path := paths[i%len(paths)]
req := httptest.NewRequest("GET", path, nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
}
}
// BenchmarkMultipleSubscribers benchmarks event fanout to multiple subscribers.
func BenchmarkMultipleSubscribers(b *testing.B) {
h, _ := setupBenchHandler()
streamID := "bench-multi-stream"
const numSubscribers = 10
// Create multiple subscribers
subscribers := make([]chan streamEvent, numSubscribers)
var wg sync.WaitGroup
for i := 0; i < numSubscribers; i++ {
subscribers[i] = h.streams.Subscribe(streamID)
wg.Add(1)
go func(ch chan streamEvent) {
defer wg.Done()
for range ch {
}
}(subscribers[i])
}
eventData := map[string]any{
"line": "benchmark output line",
"stream": "stdout",
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
h.streams.Send(streamID, "output", eventData)
}
// Cleanup
h.streams.Close(streamID)
wg.Wait()
}