From cfcb154a7d3288f7fd10b7d3988dec6c13ad91d5 Mon Sep 17 00:00:00 2001 From: Stephanie Gredell Date: Tue, 24 Jun 2025 00:08:08 -0700 Subject: [PATCH] added a bunch of comments for understanding --- internal/simulation/engine.go | 71 +++++++++++++++++++++-------- internal/simulation/loadbalancer.go | 36 +++++++++++---- 2 files changed, 79 insertions(+), 28 deletions(-) diff --git a/internal/simulation/engine.go b/internal/simulation/engine.go index 884190f..cff7621 100644 --- a/internal/simulation/engine.go +++ b/internal/simulation/engine.go @@ -6,46 +6,65 @@ import ( "systemdesigngame/internal/design" ) +// a unit that flows through the system type Request struct { - ID string + ID string + // when a request was created Timestamp int + // total time spent on system LatencyMS int - Origin string - Type string - Path []string + // where the request originated from (node ID) + Origin string + // could be GET or POST + Type string + // records where it's been (used to prevent loops) + Path []string } +// Every node implements this interface and is used by the engine to operate all nodes in a uniform way. type SimulationNode interface { GetID() string Type() string - Tick(tick int, currentTimeMs int) - Receive(req *Request) + Tick(tick int, currentTimeMs int) // Advance the node's state + Receive(req *Request) // Accept new requests Emit() []*Request IsAlive() bool - GetTargets() []string - GetQueue() []*Request + GetTargets() []string // Connection to other nodes + GetQueue() []*Request // Requests currently pending } type Engine struct { - Nodes map[string]SimulationNode + // Map of Node ID -> actual node, Represents all nodes in the graph + Nodes map[string]SimulationNode + // all tick snapshots Timeline []*TickSnapshot + // how many ticks to run Duration int - TickMs int + // no used here but we could use it if we want it configurable + TickMs int } +// what hte system looks like given a tick type TickSnapshot struct { - TickMs int + TickMs int + // Queue size at each node QueueSizes map[string]int NodeHealth map[string]NodeState - Emitted map[string][]*Request + // what each node output that tick before routing + Emitted map[string][]*Request } +// used for tracking health/debugging each node at tick type NodeState struct { QueueSize int Alive bool } +// Takes a level design and produces a runnable engine from it. func NewEngineFromDesign(design design.Design, duration int, tickMs int) *Engine { + + // Iterate over each nodes and then construct the simulation nodes + // Each constructed simulation node is then stored in the nodeMap nodeMap := make(map[string]SimulationNode) for _, n := range design.Nodes { @@ -179,17 +198,21 @@ func NewEngineFromDesign(design design.Design, duration int, tickMs int) *Engine } func (e *Engine) Run() { + // clear and set defaults const tickMS = 100 currentTimeMs := 0 e.Timeline = e.Timeline[:0] + // start ticking. This is really where the simulation begins for tick := 0; tick < e.Duration; tick++ { + + // find the entry points (where traffic enters) or else print a warning entries := e.findEntryPoints() if len(entries) == 0 { fmt.Println("[ERROR] No entry points found! Simulation will not inject requests.") } - // inject new requests + // inject new requests of each entry node every tick for _, node := range entries { if shouldInject(tick) { req := &Request{ @@ -204,22 +227,26 @@ func (e *Engine) Run() { } } - // snapshot for this tick + // snapshot to record what happened this tick snapshot := &TickSnapshot{ TickMs: tick, NodeHealth: make(map[string]NodeState), } for id, node := range e.Nodes { + // capture health data before processing snapshot.NodeHealth[id] = NodeState{ QueueSize: len(node.GetQueue()), Alive: node.IsAlive(), } + // tick all nodes node.Tick(tick, currentTimeMs) - // emit and forward requests to connected nodes + // get all processed requets and fan it out to all connected targets for _, req := range node.Emit() { + snapshot.Emitted[node.GetID()] = append(snapshot.Emitted[node.GetID()], req) + for _, targetID := range node.GetTargets() { if target, ok := e.Nodes[targetID]; ok && target.IsAlive() && !hasVisited(req, targetID) { // Deep copy request and update path @@ -233,6 +260,7 @@ func (e *Engine) Run() { } + // store the snapshot and advance time e.Timeline = append(e.Timeline, snapshot) currentTimeMs += tickMS } @@ -264,11 +292,18 @@ func generateRequestID(tick int) string { } func asFloat64(v interface{}) float64 { - if v == nil { + switch val := v.(type) { + case float64: + return val + case int: + return float64(val) + case int64: + return float64(val) + case float32: + return float64(val) + default: return 0 } - - return v.(float64) } func asString(v interface{}) string { diff --git a/internal/simulation/loadbalancer.go b/internal/simulation/loadbalancer.go index 6b7bf9d..72b00be 100644 --- a/internal/simulation/loadbalancer.go +++ b/internal/simulation/loadbalancer.go @@ -5,13 +5,21 @@ import ( ) type LoadBalancerNode struct { - ID string - Label string + // unique identifier for the node + ID string + // human readable name + Label string + // load balancing strategy Algorithm string - Queue []*Request - Targets []string - Counter int - Alive bool + // list of incoming requests to be processed + Queue []*Request + // IDs of downstream nodes (e.g. webservers) + Targets []string + // use to track round-robin state (i.e. which target is next) + Counter int + // bool for health check + Alive bool + // requests that this node has handled (ready to be emitted) Processed []*Request } @@ -27,39 +35,47 @@ func (lb *LoadBalancerNode) IsAlive() bool { return lb.Alive } +// Acceps an incoming request by adding it to the Queue which will be processed on the next tick func (lb *LoadBalancerNode) Receive(req *Request) { lb.Queue = append(lb.Queue, req) } func (lb *LoadBalancerNode) Tick(tick int, currentTimeMs int) { + // clear out the process so it starts fresh lb.Processed = nil + // for each pending request... for _, req := range lb.Queue { + // if there are no targets to forward to, skip processing if len(lb.Targets) == 0 { continue } - var target string + // placeholder for algorithm-specific logic. TODO. switch lb.Algorithm { case "random": - target = lb.Targets[rand.Intn(len(lb.Targets))] + fallthrough case "round-robin": fallthrough default: - target = lb.Targets[lb.Counter%len(lb.Targets)] lb.Counter++ } - req.Path = append([]string{target}, req.Path...) + // Append the load balancer's ID to the request's path to record it's journey through the system + req.Path = append(req.Path, lb.ID) + // Simulate networking delay req.LatencyMS += 10 + // Mark the request as processed so it can be emitted to targets lb.Processed = append(lb.Processed, req) } + // clear the queue after processing. Ready for next tick. lb.Queue = lb.Queue[:0] } +// return the list of process requests and then clear the processed requests func (lb *LoadBalancerNode) Emit() []*Request { out := lb.Processed lb.Processed = nil