diff --git a/internals/design/design.go b/internals/design/design.go new file mode 100644 index 0000000..46baa77 --- /dev/null +++ b/internals/design/design.go @@ -0,0 +1,202 @@ +package simulation + +import "encoding/json" + +type Node struct { + ID string `json:"id"` + Type string `json:"type"` + Position Position `josn:"position"` + Props map[string]interface{} `json:"props"` +} + +type Position struct { + X int `json:"x"` + Y int `json:"y"` +} + +type Connection struct { + Source string `json:"source"` + Target string `json:"target"` + Label string `json:"label,omitempty"` + Direction string `json:"direction,omitempty"` + Protocol string `json:"protocol,omitempty"` + TLS bool `json:"tls,omitemity"` + Capacity int `json:"capacity,omitempty"` +} + +type Design struct { + Nodes []Node `json:"nodes"` + Connections []Connection +} + +type Cache struct { + Label string `json:"label"` + CacheTTL int `json:"cacheTTL"` + MaxEntries int `json:"maxEntries"` + EvictionPolicy string `json:"evictionPolicy"` +} + +type CDN struct { + Label string `json:"label"` + TTL int `json:"ttl"` + GeoReplication string `json:"geoReplication"` + CachingStrategy string `json:"cachingStrategy"` + Compression string `json:"compression"` + HTTP2 string `json:"http2"` +} + +type Database struct { + Label string `json:"label"` + Replication int `json:"replication"` +} + +type DataPipeline struct { + Label string `json:"label"` + BatchSize int `json:"batchSize"` + Transformation string `json:"transformation"` +} + +type LoadBalancer struct { + Label string `json:"label"` + Algorithm string `json:"algorithm"` +} + +type MessageQueue struct { + Label string `json:"label"` + QueueCapacity int `json:"queueCapacity"` + RetentionSeconds int `json:"retentionSeconds"` +} + +type Microservice struct { + Label string `json:"label"` + InstanceCount int `json:"instanceCount"` + CPU int `json:"cpu"` + RAMGb int `json:"ramGb"` + RPSCapacity int `json:"rpsCapacity"` + MonthlyUSD int `json:"monthlyUsd"` + ScalingStrategy string `json:"scalingStrategy"` + APIVersion string `json:"apiVersion"` +} + +type Monitoring struct { + Label string `json:"label"` + Tool string `json:"tool"` + AlertMetric string `json:"alertMetric"` // e.g., "cpu", "latency" + ThresholdValue int `json:"thresholdValue"` // e.g., 80 + ThresholdUnit string `json:"thresholdUnit"` // e.g., "percent", "ms" +} + +type ThirdPartyService struct { + Label string `json:"label"` + Provider string `json:"provider"` + Latency int `json:"latency"` +} + +type WebServer struct { + CPU int `json:"cpu"` + RamGb int `json:"ramGb"` + RPSCapacity int `json:"rpsCapacity"` + MonthlyCostUsd int `json:"monthlyCostUsd"` +} + +func (n *Node) UnmarshalJSON(data []byte) error { + type Alias Node // avoid infinite recursion + aux := &struct { + Props json.RawMessage `json:"props"` + *Alias + }{ + Alias: (*Alias)(n), + } + + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + switch n.Type { + case "cache": + var p Cache + if err := json.Unmarshal(aux.Props, &p); err != nil { + return err + } + n.Props = structToMap(p) + + case "cdn": + var p CDN + if err := json.Unmarshal(aux.Props, &p); err != nil { + return err + } + n.Props = structToMap(p) + + case "database": + var p Database + if err := json.Unmarshal(aux.Props, &p); err != nil { + return err + } + n.Props = structToMap(p) + + case "data pipeline": + var p DataPipeline + if err := json.Unmarshal(aux.Props, &p); err != nil { + return err + } + n.Props = structToMap(p) + + case "loadBalancer": + var p LoadBalancer + if err := json.Unmarshal(aux.Props, &p); err != nil { + return err + } + n.Props = structToMap(p) + + case "messageQueue": + var p MessageQueue + if err := json.Unmarshal(aux.Props, &p); err != nil { + return err + } + n.Props = structToMap(p) + + case "microservice": + var p Microservice + if err := json.Unmarshal(aux.Props, &p); err != nil { + return err + } + n.Props = structToMap(p) + + case "monitoring/alerting": + var p Monitoring + if err := json.Unmarshal(aux.Props, &p); err != nil { + return err + } + n.Props = structToMap(p) + + case "third party service": + var p ThirdPartyService + if err := json.Unmarshal(aux.Props, &p); err != nil { + return err + } + n.Props = structToMap(p) + + case "webserver": + var p WebServer + if err := json.Unmarshal(aux.Props, &p); err != nil { + return err + } + n.Props = structToMap(p) + + default: + var generic map[string]interface{} + if err := json.Unmarshal(aux.Props, &generic); err != nil { + return err + } + n.Props = generic + } + + return nil +} + +func structToMap(v interface{}) map[string]interface{} { + data, _ := json.Marshal(v) + var result map[string]interface{} + json.Unmarshal(data, &result) + return result +} diff --git a/internals/simulation/simulation.go b/internals/design/simulation.go similarity index 96% rename from internals/simulation/simulation.go rename to internals/design/simulation.go index 290e31f..8a51e48 100644 --- a/internals/simulation/simulation.go +++ b/internals/design/simulation.go @@ -56,15 +56,6 @@ type ComponentSpec struct { DbReadReplica DbReadReplica } -type Design struct { - NumWebServerSmall int - NumWebServerMedium int - CacheType CacheType // Enum (see below) - CacheTTL string - NumDbReplicas int - PromotionDelaySeconds int -} - type FailureEvent struct { Type string Time int diff --git a/internals/level/level.go b/internals/level/level.go new file mode 100644 index 0000000..e301129 --- /dev/null +++ b/internals/level/level.go @@ -0,0 +1,105 @@ +package level + +import ( + "encoding/json" + "fmt" + "os" +) + +type Level struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Difficulty Difficulty `json:"difficulty"` + + TargetRPS int `json:"targetRps"` + DurationSec int `json:"durationSec"` + MaxMonthlyUSD int `json:"maxMonthlyUsd"` + MaxP95LatencyMs int `json:"maxP95LatencyMs"` + RequiredAvailabilityPct float64 `json:"requiredAvailabilityPct"` + + MustInclude []string `json:"mustInclude,omitempty"` + MustNotInclude []string `json:"mustNotInclude,omitempty"` + EncouragedComponents []string `json:"encouragedComponents,omitempty"` + DiscouragedComponents []string `json:"discouragedComponents,omitempty"` + MinReplicas map[string]int `json:"minReplicas,omitempty"` + MaxLatencyPerNodeType map[string]int `json:"maxLatencyPerNodeType,omitempty"` + CustomValidators []string `json:"customValidators,omitempty"` + + FailureEvents []FailureEvent `json:"failureEvents,omitempty"` + ScoringWeights map[string]float64 `json:"scoringWeights,omitempty"` + + Hints []string `json:"hints,omitempty"` +} + +type Difficulty string + +const ( + DifficultyEasy Difficulty = "easy" + DifficultyMedium Difficulty = "medium" + DifficultyHard Difficulty = "hard" +) + +var Registry map[string]map[string]Level + +type FailureEvent struct { + Type string `json:"type"` + TimeSec int `json:"timeSec"` + TargetID string `json:"targetId,omitempty"` +} + +func LoadLevels(path string) ([]Level, error) { + file, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("Error opening levels.json: %w", err) + } + defer file.Close() + + var levels []Level + err = json.NewDecoder(file).Decode(&levels) + if err != nil { + return nil, fmt.Errorf("Error decoding levels.json: %w", err) + } + + return levels, nil +} + +func InitRegistry(levels []Level) { + Registry = make(map[string]map[string]Level) + for _, lvl := range levels { + // check if level already exists here + if _, ok := Registry[lvl.Name]; !ok { + Registry[lvl.Name] = make(map[string]Level) + } + // populate it + Registry[lvl.Name][string(lvl.Difficulty)] = lvl + } +} + +func GetLevel(name string, difficulty Difficulty) (*Level, error) { + diffMap, ok := Registry[name] + if !ok { + return nil, fmt.Errorf("level name %s not found", name) + } + + lvl, ok := diffMap[string(difficulty)] + if !ok { + return nil, fmt.Errorf("difficulty %s not available for level '%s'", difficulty, name) + } + return &lvl, nil +} + +func (d *Difficulty) UnmarshalJSON(b []byte) error { + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + + switch s { + case string(DifficultyEasy), string(DifficultyMedium), string(DifficultyHard): + *d = Difficulty(s) + return nil + default: + return fmt.Errorf("invalid difficulty: %q", s) + } +}