mirror of
https://github.com/syntrex-lab/gomcp.git
synced 2026-04-25 04:16:22 +02:00
153 lines
4.7 KiB
Go
153 lines
4.7 KiB
Go
package soc
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"sort"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
domsoc "github.com/syntrex/gomcp/internal/domain/soc"
|
|
"github.com/syntrex/gomcp/internal/infrastructure/audit"
|
|
"github.com/syntrex/gomcp/internal/infrastructure/sqlite"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestLoadTest_SustainedThroughput measures SOC pipeline throughput and latency
|
|
// under sustained concurrent load. Reports p50/p95/p99 latencies and events/sec.
|
|
func TestLoadTest_SustainedThroughput(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping load test in short mode")
|
|
}
|
|
|
|
// Setup service with file-based SQLite for concurrency safety.
|
|
tmpDir := t.TempDir()
|
|
db, err := sqlite.Open(tmpDir + "/loadtest.db")
|
|
require.NoError(t, err)
|
|
|
|
repo, err := sqlite.NewSOCRepo(db)
|
|
require.NoError(t, err)
|
|
|
|
logger, err := audit.NewDecisionLogger(tmpDir)
|
|
require.NoError(t, err)
|
|
|
|
t.Cleanup(func() {
|
|
logger.Close()
|
|
db.Close()
|
|
})
|
|
|
|
svc := NewService(repo, logger)
|
|
svc.DisableRateLimit() // bypass rate limiter for raw throughput
|
|
|
|
// Load test parameters.
|
|
const (
|
|
numWorkers = 16
|
|
eventsPerWkr = 200
|
|
totalEvents = numWorkers * eventsPerWkr
|
|
)
|
|
|
|
categories := []string{"jailbreak", "injection", "exfiltration", "auth_bypass", "tool_abuse"}
|
|
sources := []domsoc.EventSource{domsoc.SourceSentinelCore, domsoc.SourceShield, domsoc.SourceGoMCP}
|
|
|
|
var (
|
|
wg sync.WaitGroup
|
|
latencies = make([]time.Duration, totalEvents)
|
|
errors int64
|
|
incidents int64
|
|
)
|
|
|
|
start := time.Now()
|
|
|
|
for w := 0; w < numWorkers; w++ {
|
|
wg.Add(1)
|
|
go func(workerID int) {
|
|
defer wg.Done()
|
|
for i := 0; i < eventsPerWkr; i++ {
|
|
idx := workerID*eventsPerWkr + i
|
|
evt := domsoc.NewSOCEvent(
|
|
sources[idx%len(sources)],
|
|
domsoc.SeverityHigh,
|
|
categories[idx%len(categories)],
|
|
fmt.Sprintf("load-test w%d-e%d", workerID, i),
|
|
)
|
|
evt.SensorID = fmt.Sprintf("load-sensor-%d", workerID)
|
|
|
|
t0 := time.Now()
|
|
_, inc, err := svc.IngestEvent(evt)
|
|
latencies[idx] = time.Since(t0)
|
|
|
|
if err != nil {
|
|
atomic.AddInt64(&errors, 1)
|
|
}
|
|
if inc != nil {
|
|
atomic.AddInt64(&incidents, 1)
|
|
}
|
|
}
|
|
}(w)
|
|
}
|
|
|
|
wg.Wait()
|
|
totalDuration := time.Since(start)
|
|
|
|
// Compute latency percentiles.
|
|
sort.Slice(latencies, func(i, j int) bool { return latencies[i] < latencies[j] })
|
|
|
|
p50 := percentile(latencies, 50)
|
|
p95 := percentile(latencies, 95)
|
|
p99 := percentile(latencies, 99)
|
|
mean := meanDuration(latencies)
|
|
eventsPerSec := float64(totalEvents) / totalDuration.Seconds()
|
|
|
|
// Report results.
|
|
t.Logf("═══════════════════════════════════════════════")
|
|
t.Logf(" SENTINEL SOC Load Test Results")
|
|
t.Logf("═══════════════════════════════════════════════")
|
|
t.Logf(" Workers: %d", numWorkers)
|
|
t.Logf(" Events/worker: %d", eventsPerWkr)
|
|
t.Logf(" Total events: %d", totalEvents)
|
|
t.Logf(" Duration: %s", totalDuration.Round(time.Millisecond))
|
|
t.Logf(" Throughput: %.0f events/sec", eventsPerSec)
|
|
t.Logf("───────────────────────────────────────────────")
|
|
t.Logf(" Mean: %s", mean.Round(time.Microsecond))
|
|
t.Logf(" P50: %s", p50.Round(time.Microsecond))
|
|
t.Logf(" P95: %s", p95.Round(time.Microsecond))
|
|
t.Logf(" P99: %s", p99.Round(time.Microsecond))
|
|
t.Logf(" Min: %s", latencies[0].Round(time.Microsecond))
|
|
t.Logf(" Max: %s", latencies[len(latencies)-1].Round(time.Microsecond))
|
|
t.Logf("───────────────────────────────────────────────")
|
|
t.Logf(" Errors: %d (%.1f%%)", errors, float64(errors)/float64(totalEvents)*100)
|
|
t.Logf(" Incidents: %d", incidents)
|
|
t.Logf("═══════════════════════════════════════════════")
|
|
|
|
// Assertions: basic sanity checks.
|
|
require.Less(t, float64(errors)/float64(totalEvents), 0.05, "error rate should be < 5%%")
|
|
require.Greater(t, eventsPerSec, float64(100), "should sustain > 100 events/sec")
|
|
}
|
|
|
|
func percentile(sorted []time.Duration, p int) time.Duration {
|
|
if len(sorted) == 0 {
|
|
return 0
|
|
}
|
|
idx := int(math.Ceil(float64(p)/100.0*float64(len(sorted)))) - 1
|
|
if idx < 0 {
|
|
idx = 0
|
|
}
|
|
if idx >= len(sorted) {
|
|
idx = len(sorted) - 1
|
|
}
|
|
return sorted[idx]
|
|
}
|
|
|
|
func meanDuration(ds []time.Duration) time.Duration {
|
|
if len(ds) == 0 {
|
|
return 0
|
|
}
|
|
var total time.Duration
|
|
for _, d := range ds {
|
|
total += d
|
|
}
|
|
return total / time.Duration(len(ds))
|
|
}
|