You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

459 lines
13 KiB

package handlers
import (
"encoding/json"
"fmt"
"net/http"
"systemdesigngame/internal/design"
"systemdesigngame/internal/level"
"systemdesigngame/internal/simulation"
)
type SimulationHandler struct{}
type SimulationResponse struct {
Success bool `json:"success"`
Metrics map[string]interface{} `json:"metrics,omitempty"`
Timeline []interface{} `json:"timeline,omitempty"`
Passed bool `json:"passed"`
Score int `json:"score,omitempty"`
Feedback []string `json:"feedback,omitempty"`
LevelName string `json:"levelName,omitempty"`
Error string `json:"error,omitempty"`
}
func (h *SimulationHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var requestBody struct {
Design design.Design `json:"design"`
LevelID string `json:"levelId,omitempty"`
}
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
// Try to decode as just design for backward compatibility
r.Body.Close()
var design design.Design
if err2 := json.NewDecoder(r.Body).Decode(&design); err2 != nil {
http.Error(w, "Invalid request JSON: "+err.Error(), http.StatusBadRequest)
return
}
requestBody.Design = design
}
// Extract the design for processing
design := requestBody.Design
// Run the actual simulation
engine := simulation.NewEngineFromDesign(design, 100)
if engine == nil {
response := SimulationResponse{
Success: false,
Error: "Failed to create simulation engine - no valid components found",
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
return
}
// Set simulation parameters
defaultRPS := 50
targetRPS := defaultRPS
if requestBody.LevelID != "" {
if lvl, err := level.GetLevelByID(requestBody.LevelID); err == nil {
targetRPS = lvl.TargetRPS
}
}
engine.RPS = targetRPS
// Find entry node by analyzing topology
entryNode := findEntryNode(design)
if entryNode == "" {
response := SimulationResponse{
Success: false,
Error: "No entry point found - design must include a component with no incoming connections (webserver, microservice, load balancer, etc.)",
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
return
}
engine.EntryNode = entryNode
// Run simulation for 60 ticks (6 seconds at 100ms per tick)
snapshots := engine.Run(60, 100)
// Calculate metrics from snapshots
metrics := calculateMetrics(snapshots, design)
// Convert snapshots to interface{} for JSON serialization
timeline := make([]interface{}, len(snapshots))
for i, snapshot := range snapshots {
timeline[i] = snapshot
}
// Perform level validation if level info provided
var passed bool
var score int
var feedback []string
var levelName string
if requestBody.LevelID != "" {
if lvl, err := level.GetLevelByID(requestBody.LevelID); err == nil {
levelName = lvl.Name
passed, score, feedback = validateLevel(lvl, design, metrics)
} else {
feedback = []string{"Warning: Level not found, simulation ran without validation"}
}
}
response := SimulationResponse{
Success: true,
Metrics: metrics,
Timeline: timeline,
Passed: passed,
Score: score,
Feedback: feedback,
LevelName: levelName,
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(response); err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
return
}
}
// calculateMetrics computes key performance metrics from simulation snapshots
func calculateMetrics(snapshots []*simulation.TickSnapshot, design design.Design) map[string]interface{} {
if len(snapshots) == 0 {
return map[string]interface{}{
"throughput": 0,
"latency_avg": 0,
"cost_monthly": 0,
"availability": 0,
}
}
totalRequests := 0
totalLatency := 0
totalHealthy := 0
totalNodes := 0
// Calculate aggregate metrics across all snapshots
for _, snapshot := range snapshots {
// Count total requests processed in this tick
for _, requests := range snapshot.Emitted {
totalRequests += len(requests)
for _, req := range requests {
totalLatency += req.LatencyMS
}
}
// Count healthy vs total nodes
for _, healthy := range snapshot.NodeHealth {
totalNodes++
if healthy {
totalHealthy++
}
}
}
// Calculate throughput (requests per second)
// snapshots represent 6 seconds of simulation (60 ticks * 100ms)
simulationSeconds := float64(len(snapshots)) * 0.1 // 100ms per tick
throughput := float64(totalRequests) / simulationSeconds
// Calculate average latency
avgLatency := 0.0
if totalRequests > 0 {
avgLatency = float64(totalLatency) / float64(totalRequests)
}
// Calculate availability percentage
availability := 0.0
if totalNodes > 0 {
availability = (float64(totalHealthy) / float64(totalNodes)) * 100
}
// Calculate monthly cost based on component specifications
monthlyCost := calculateRealMonthlyCost(design.Nodes)
return map[string]interface{}{
"throughput": int(throughput),
"latency_avg": avgLatency,
"cost_monthly": int(monthlyCost),
"availability": availability,
}
}
// findEntryNode analyzes the design topology to find the best entry point
func findEntryNode(design design.Design) string {
// Build map of incoming connections
incomingCount := make(map[string]int)
// Initialize all nodes with 0 incoming connections
for _, node := range design.Nodes {
incomingCount[node.ID] = 0
}
// Count incoming connections for each node
for _, conn := range design.Connections {
incomingCount[conn.Target]++
}
// Find nodes with no incoming connections (potential entry points)
var entryPoints []string
for nodeID, count := range incomingCount {
if count == 0 {
entryPoints = append(entryPoints, nodeID)
}
}
// If multiple entry points exist, prefer certain types
if len(entryPoints) > 1 {
return preferredEntryPoint(design.Nodes, entryPoints)
} else if len(entryPoints) == 1 {
return entryPoints[0]
}
return "" // No entry point found
}
// preferredEntryPoint selects the best entry point from candidates based on component type
func preferredEntryPoint(nodes []design.Node, candidateIDs []string) string {
// Priority order for entry points (most logical first)
priority := []string{
"webserver",
"microservice",
"loadBalancer", // Could be edge load balancer
"cdn", // Edge CDN
"data pipeline", // Data ingestion entry
"messageQueue", // For event-driven architectures
}
// Create lookup for candidate nodes
candidates := make(map[string]design.Node)
for _, node := range nodes {
for _, id := range candidateIDs {
if node.ID == id {
candidates[id] = node
break
}
}
}
// Find highest priority candidate
for _, nodeType := range priority {
for id, node := range candidates {
if node.Type == nodeType {
return id
}
}
}
// If no preferred type, return first candidate
if len(candidateIDs) > 0 {
return candidateIDs[0]
}
return ""
}
// validateLevel checks if the design and simulation results meet level requirements
func validateLevel(lvl *level.Level, design design.Design, metrics map[string]interface{}) (bool, int, []string) {
var feedback []string
var failedRequirements []string
var passedRequirements []string
// Extract metrics
avgLatency := int(metrics["latency_avg"].(float64))
availability := metrics["availability"].(float64)
monthlyCost := metrics["cost_monthly"].(int)
// Check latency requirement (using avg latency as approximation for P95)
if avgLatency <= lvl.MaxP95LatencyMs {
passedRequirements = append(passedRequirements, "Latency requirement met")
} else {
failedRequirements = append(failedRequirements,
fmt.Sprintf("Latency: %dms (max allowed: %dms)", avgLatency, lvl.MaxP95LatencyMs))
}
// Check availability requirement
if availability >= lvl.RequiredAvailabilityPct {
passedRequirements = append(passedRequirements, "Availability requirement met")
} else {
failedRequirements = append(failedRequirements,
fmt.Sprintf("Availability: %.1f%% (required: %.1f%%)", availability, lvl.RequiredAvailabilityPct))
}
// Check cost requirement
if monthlyCost <= lvl.MaxMonthlyUSD {
passedRequirements = append(passedRequirements, "Cost requirement met")
} else {
failedRequirements = append(failedRequirements,
fmt.Sprintf("Cost: $%d/month (max allowed: $%d/month)", monthlyCost, lvl.MaxMonthlyUSD))
}
// Check component requirements
componentFeedback := validateComponentRequirements(lvl, design)
if len(componentFeedback.Failed) > 0 {
failedRequirements = append(failedRequirements, componentFeedback.Failed...)
}
if len(componentFeedback.Passed) > 0 {
passedRequirements = append(passedRequirements, componentFeedback.Passed...)
}
// Determine if passed
passed := len(failedRequirements) == 0
// Calculate score (0-100)
score := calculateScore(len(passedRequirements), len(failedRequirements), metrics)
// Build feedback
if passed {
feedback = append(feedback, "Level completed successfully!")
feedback = append(feedback, "")
feedback = append(feedback, passedRequirements...)
} else {
feedback = append(feedback, "Level failed - requirements not met:")
feedback = append(feedback, "")
feedback = append(feedback, failedRequirements...)
if len(passedRequirements) > 0 {
feedback = append(feedback, "")
feedback = append(feedback, "Requirements passed:")
feedback = append(feedback, passedRequirements...)
}
}
return passed, score, feedback
}
type ComponentValidationResult struct {
Passed []string
Failed []string
}
// validateComponentRequirements checks mustInclude, mustNotInclude, etc.
func validateComponentRequirements(lvl *level.Level, design design.Design) ComponentValidationResult {
result := ComponentValidationResult{}
// Build map of component types in design
componentTypes := make(map[string]int)
for _, node := range design.Nodes {
componentTypes[node.Type]++
}
// Check mustInclude requirements
for _, required := range lvl.MustInclude {
if count, exists := componentTypes[required]; exists && count > 0 {
result.Passed = append(result.Passed, fmt.Sprintf("Required component '%s' included", required))
} else {
result.Failed = append(result.Failed, fmt.Sprintf("Missing required component: '%s'", required))
}
}
// Check mustNotInclude requirements
for _, forbidden := range lvl.MustNotInclude {
if count, exists := componentTypes[forbidden]; exists && count > 0 {
result.Failed = append(result.Failed, fmt.Sprintf("Forbidden component used: '%s'", forbidden))
}
}
// Check minReplicas requirements
for component, minCount := range lvl.MinReplicas {
if count, exists := componentTypes[component]; exists && count >= minCount {
result.Passed = append(result.Passed, fmt.Sprintf("Sufficient '%s' replicas (%d)", component, count))
} else {
actualCount := 0
if exists {
actualCount = count
}
result.Failed = append(result.Failed,
fmt.Sprintf("Insufficient '%s' replicas: %d (minimum: %d)", component, actualCount, minCount))
}
}
return result
}
// calculateScore computes a score from 0-100 based on performance
func calculateScore(passedCount, failedCount int, metrics map[string]interface{}) int {
if failedCount > 0 {
// Failed level - score based on how many requirements passed
return (passedCount * 100) / (passedCount + failedCount)
}
// Passed level - bonus points for performance
baseScore := 70 // Base score for passing
// Performance bonuses (up to 30 points)
performanceBonus := 0
// Throughput bonus (higher throughput = better)
if throughput, ok := metrics["throughput"].(int); ok && throughput > 0 {
performanceBonus += min(10, throughput/100) // 1 point per 100 RPS, max 10
}
// Availability bonus (higher availability = better)
if availability, ok := metrics["availability"].(float64); ok {
if availability >= 99.9 {
performanceBonus += 10
} else if availability >= 99.5 {
performanceBonus += 5
}
}
// Cost efficiency bonus (lower cost = better)
if cost, ok := metrics["cost_monthly"].(int); ok && cost > 0 {
if cost <= 50 {
performanceBonus += 10
} else if cost <= 100 {
performanceBonus += 5
}
}
return min(100, baseScore+performanceBonus)
}
// calculateRealMonthlyCost computes monthly cost based on actual component specifications
func calculateRealMonthlyCost(nodes []design.Node) float64 {
totalCost := 0.0
for _, node := range nodes {
switch node.Type {
case "user":
// User components don't cost anything
continue
case "microservice":
if monthlyUsd, ok := node.Props["monthlyUsd"].(float64); ok {
if instanceCount, ok := node.Props["instanceCount"].(float64); ok {
totalCost += monthlyUsd * instanceCount
}
}
case "webserver":
if monthlyCost, ok := node.Props["monthlyCostUsd"].(float64); ok {
totalCost += monthlyCost
}
default:
// Default cost for other components (cache, database, load balancer, etc.)
totalCost += 20 // $20/month baseline
}
}
return totalCost
}
// Helper function
func min(a, b int) int {
if a < b {
return a
}
return b
}