## Phase 8: Enterprise Extractor Improvements ✅ - 14 security extractors (TLS, JWT, SQL injection, XSS, etc.) - 10 framework-specific extractors (Spring, Django, Rails, etc.) - Config file security detection (YAML, TOML) ## Phase 9: Autonomous Extractor Generation ✅ - Shadow mode executor with TP/FP tracking - Graduation pipeline with confidence thresholds - Auto-rollback on regression detection - Cross-project pattern syncing ## UAT Suite Complete (14 scripts, 90 tests) - test-core-detection.sh (6 tests) - test-declarative-extractors.sh (5 tests) - test-domain-frameworks.sh (5 tests) - test-domain-unreal.sh (3 tests) - test-llm-extraction.sh (6 tests) - test-eval-harness.sh (5 tests) - test-cross-language.sh (3 tests) - test-precommit-performance.sh (4 tests) - test-output-formats.sh (8 tests) - test-drift-detection.sh (6 tests) - test-exit-codes.sh (12 tests) + 3 more scripts ## Other Changes - Updated roadmap to mark Phase 8-9 complete - Added .gitignore entries for build artifacts - Updated pre-commit: 800 line limit, exclude tests/data/cmd Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
292 lines
8.0 KiB
Go
292 lines
8.0 KiB
Go
// Package main provides load testing for StemeDB.
|
|
//
|
|
// This tool benchmarks StemeDB performance with three scenarios:
|
|
// - Baseline: 10K assertions to establish latency baseline
|
|
// - Sustained: 1K writes/sec for configurable duration
|
|
// - Concurrent: 100 concurrent readers with background writes
|
|
//
|
|
// Usage:
|
|
//
|
|
// go run ./cmd/load-test --api-url http://localhost:18180
|
|
// go run ./cmd/load-test --scenario sustained --duration 1h
|
|
// go run ./cmd/load-test --scenario concurrent --readers 100
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/orchard9/stemedb-go/steme"
|
|
)
|
|
|
|
const (
|
|
defaultAPIURL = "http://127.0.0.1:18180"
|
|
defaultKeysFile = "demo/keys/agents.json"
|
|
defaultOutputDir = "uat/production-readiness/results"
|
|
defaultDuration = 5 * time.Minute
|
|
defaultTargetRPS = 1000
|
|
defaultReaderCount = 100
|
|
)
|
|
|
|
// DemoAgent represents a demo agent from agents.json.
|
|
type DemoAgent struct {
|
|
Seed string `json:"seed"`
|
|
PublicKey string `json:"public_key"`
|
|
Tier int `json:"tier"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
func main() {
|
|
// Parse flags
|
|
apiURL := flag.String("api-url", "", "StemeDB API URL (default: $STEMEDB_API_URL or http://127.0.0.1:18180)")
|
|
keysFile := flag.String("keys-file", "", "Path to agents.json (default: demo/keys/agents.json)")
|
|
scenario := flag.String("scenario", "all", "Test scenario: baseline|sustained|concurrent|all")
|
|
duration := flag.Duration("duration", defaultDuration, "Duration for sustained write test")
|
|
targetRPS := flag.Int("target-rps", defaultTargetRPS, "Target requests per second for sustained test")
|
|
readers := flag.Int("readers", defaultReaderCount, "Number of concurrent readers")
|
|
outputDir := flag.String("output", "", "Output directory for reports (default: uat/production-readiness/results)")
|
|
verbose := flag.Bool("verbose", false, "Enable verbose output")
|
|
flag.Parse()
|
|
|
|
// Resolve API URL
|
|
url := *apiURL
|
|
if url == "" {
|
|
url = os.Getenv("STEMEDB_API_URL")
|
|
}
|
|
if url == "" {
|
|
url = defaultAPIURL
|
|
}
|
|
|
|
// Resolve keys file
|
|
keysPath := *keysFile
|
|
if keysPath == "" {
|
|
keysPath = findKeysFile()
|
|
}
|
|
|
|
// Resolve output directory
|
|
outDir := *outputDir
|
|
if outDir == "" {
|
|
outDir = findOutputDir()
|
|
}
|
|
|
|
// Check if meter is disabled
|
|
meterDisabled := os.Getenv("STEMEDB_METER_ENABLED") == "false"
|
|
|
|
fmt.Println("=== StemeDB Load Tester ===")
|
|
fmt.Println()
|
|
fmt.Printf("API URL: %s\n", url)
|
|
fmt.Printf("Keys File: %s\n", keysPath)
|
|
fmt.Printf("Scenario: %s\n", *scenario)
|
|
fmt.Printf("Output Dir: %s\n", outDir)
|
|
if *scenario == "sustained" || *scenario == "all" {
|
|
fmt.Printf("Duration: %v\n", *duration)
|
|
fmt.Printf("Target RPS: %d\n", *targetRPS)
|
|
if !meterDisabled {
|
|
fmt.Println("WARNING: Meter is enabled. Set STEMEDB_METER_ENABLED=false for accurate sustained test results.")
|
|
}
|
|
}
|
|
if *scenario == "concurrent" || *scenario == "all" {
|
|
fmt.Printf("Readers: %d\n", *readers)
|
|
}
|
|
fmt.Println()
|
|
|
|
// Load agents
|
|
clients, signers, err := loadAgents(keysPath, url)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to load agents: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("Loaded %d demo agents\n\n", len(clients))
|
|
|
|
// Verify server is reachable
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
// Handle interrupt
|
|
sigCh := make(chan os.Signal, 1)
|
|
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
|
go func() {
|
|
<-sigCh
|
|
fmt.Println("\nInterrupted. Stopping tests...")
|
|
cancel()
|
|
}()
|
|
|
|
health, err := clients[0].Health(ctx)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Server not reachable at %s: %v\n", url, err)
|
|
fmt.Fprintln(os.Stderr, "Start the server with: STEMEDB_METER_ENABLED=false cargo run --release --bin stemedb-api")
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("Server Status: %s (v%s, %d assertions)\n\n", health.Status, health.Version, health.AssertionsCount)
|
|
|
|
// Prepare scenario config
|
|
cfg := ScenarioConfig{
|
|
Clients: clients,
|
|
Signers: signers,
|
|
Duration: *duration,
|
|
TargetRPS: *targetRPS,
|
|
Readers: *readers,
|
|
Verbose: *verbose,
|
|
}
|
|
|
|
// Run scenarios
|
|
results := TestResults{
|
|
APIURL: url,
|
|
Duration: *duration,
|
|
TargetRPS: *targetRPS,
|
|
ReaderCount: *readers,
|
|
MeterDisabled: meterDisabled,
|
|
}
|
|
|
|
switch *scenario {
|
|
case "baseline":
|
|
results.BaselineMetrics, err = RunBaselineLatency(ctx, cfg)
|
|
case "sustained":
|
|
results.SustainedMetrics, err = RunSustainedWrites(ctx, cfg)
|
|
case "concurrent":
|
|
results.ConcurrentMetrics, err = RunConcurrentReaders(ctx, cfg)
|
|
case "all":
|
|
results.BaselineMetrics, err = RunBaselineLatency(ctx, cfg)
|
|
if err == nil {
|
|
results.SustainedMetrics, err = RunSustainedWrites(ctx, cfg)
|
|
}
|
|
if err == nil {
|
|
results.ConcurrentMetrics, err = RunConcurrentReaders(ctx, cfg)
|
|
}
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "Unknown scenario: %s\n", *scenario)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if err != nil && err != context.Canceled {
|
|
fmt.Fprintf(os.Stderr, "Test failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Generate report
|
|
fmt.Println("=== Generating Report ===")
|
|
reportPath, err := GenerateReport(results, outDir)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to generate report: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("Report saved to: %s\n", reportPath)
|
|
|
|
// Print summary
|
|
fmt.Println()
|
|
fmt.Println("=== Summary ===")
|
|
printSummary(results)
|
|
}
|
|
|
|
// loadAgents loads demo agents and creates clients.
|
|
func loadAgents(path, baseURL string) ([]*steme.Client, []*steme.Signer, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to read %s: %w", path, err)
|
|
}
|
|
|
|
var agents map[string]DemoAgent
|
|
if err := json.Unmarshal(data, &agents); err != nil {
|
|
return nil, nil, fmt.Errorf("failed to parse %s: %w", path, err)
|
|
}
|
|
|
|
var clients []*steme.Client
|
|
var signers []*steme.Signer
|
|
|
|
for name, agent := range agents {
|
|
signer, err := steme.NewSignerFromHex(agent.Seed)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to create signer for %s: %w", name, err)
|
|
}
|
|
signers = append(signers, signer)
|
|
clients = append(clients, steme.NewClient(baseURL, signer))
|
|
}
|
|
|
|
return clients, signers, nil
|
|
}
|
|
|
|
// findKeysFile locates the agents.json file.
|
|
func findKeysFile() string {
|
|
// Try current directory
|
|
if _, err := os.Stat(defaultKeysFile); err == nil {
|
|
return defaultKeysFile
|
|
}
|
|
|
|
// Try one level up (if running from cmd/load-test)
|
|
upPath := filepath.Join("..", "..", defaultKeysFile)
|
|
if _, err := os.Stat(upPath); err == nil {
|
|
return upPath
|
|
}
|
|
|
|
return defaultKeysFile
|
|
}
|
|
|
|
// findOutputDir locates the output directory.
|
|
func findOutputDir() string {
|
|
// Try current directory
|
|
if _, err := os.Stat("uat/production-readiness"); err == nil {
|
|
return defaultOutputDir
|
|
}
|
|
|
|
// Try one level up
|
|
upPath := filepath.Join("..", "..", defaultOutputDir)
|
|
if _, err := os.Stat(filepath.Dir(upPath)); err == nil {
|
|
return upPath
|
|
}
|
|
|
|
return defaultOutputDir
|
|
}
|
|
|
|
func printSummary(results TestResults) {
|
|
allPass := true
|
|
|
|
if results.BaselineMetrics != nil {
|
|
m := results.BaselineMetrics
|
|
pass := m.WriteP99() <= Targets.BaselineP99 && m.WriteErrorRate() == 0
|
|
status := "PASS"
|
|
if !pass {
|
|
status = "FAIL"
|
|
allPass = false
|
|
}
|
|
fmt.Printf("Baseline: %s (p99: %v, errors: %d)\n", status, m.WriteP99(), m.WriteErrors())
|
|
}
|
|
|
|
if results.SustainedMetrics != nil {
|
|
m := results.SustainedMetrics
|
|
pass := m.WriteP99() <= Targets.SustainedP99 && m.WriteErrorRate() <= Targets.SustainedErrRate
|
|
status := "PASS"
|
|
if !pass {
|
|
status = "FAIL"
|
|
allPass = false
|
|
}
|
|
fmt.Printf("Sustained: %s (p99: %v, %.0f/sec, errors: %d)\n",
|
|
status, m.WriteP99(), m.WriteThroughput(), m.WriteErrors())
|
|
}
|
|
|
|
if results.ConcurrentMetrics != nil {
|
|
m := results.ConcurrentMetrics
|
|
pass := m.DegradationPct() <= Targets.DegradationPct
|
|
status := "PASS"
|
|
if !pass {
|
|
status = "FAIL"
|
|
allPass = false
|
|
}
|
|
fmt.Printf("Concurrent: %s (degradation: %.1f%%, readers: %d)\n",
|
|
status, m.DegradationPct(), m.ReaderCount())
|
|
}
|
|
|
|
fmt.Println()
|
|
if allPass {
|
|
fmt.Println("Overall: PASS - System ready for production load")
|
|
} else {
|
|
fmt.Println("Overall: FAIL - See report for recommendations")
|
|
}
|
|
}
|