9 changed files with 484 additions and 43 deletions
@ -0,0 +1,93 @@
@@ -0,0 +1,93 @@
|
||||
package simulation |
||||
|
||||
type MessageQueueNode struct { |
||||
ID string |
||||
Label string |
||||
QueueSize int |
||||
MessageTTL int // TTL in milliseconds
|
||||
DeadLetter bool |
||||
CurrentLoad int |
||||
Queue []*Request |
||||
Alive bool |
||||
Targets []string |
||||
output []*Request |
||||
deadLetterOutput []*Request |
||||
} |
||||
|
||||
func (n *MessageQueueNode) GetID() string { return n.ID } |
||||
func (n *MessageQueueNode) Type() string { return "messagequeue" } |
||||
func (n *MessageQueueNode) IsAlive() bool { return n.Alive } |
||||
|
||||
func (n *MessageQueueNode) Tick(tick int, currentTimeMs int) { |
||||
if len(n.Queue) == 0 { |
||||
return |
||||
} |
||||
|
||||
// Message queues have very high throughput
|
||||
maxProcessPerTick := 20 // Higher than database (3) or CDN (10)
|
||||
processCount := min(len(n.Queue), maxProcessPerTick) |
||||
|
||||
// Check for queue overflow (simulate back pressure)
|
||||
if len(n.Queue) > n.QueueSize { |
||||
// Move oldest messages to dead letter queue if enabled
|
||||
if n.DeadLetter { |
||||
overflow := len(n.Queue) - n.QueueSize |
||||
for i := 0; i < overflow; i++ { |
||||
deadReq := n.Queue[i] |
||||
deadReq.Type = "DEAD_LETTER" |
||||
deadReq.Path = append(deadReq.Path, n.ID+"(dead)") |
||||
n.deadLetterOutput = append(n.deadLetterOutput, deadReq) |
||||
} |
||||
n.Queue = n.Queue[overflow:] |
||||
} else { |
||||
// Drop messages if no dead letter queue
|
||||
n.Queue = n.Queue[:n.QueueSize] |
||||
} |
||||
} |
||||
|
||||
// Process messages with TTL check
|
||||
for i := 0; i < processCount; i++ { |
||||
req := n.Queue[0] |
||||
n.Queue = n.Queue[1:] |
||||
|
||||
// Check TTL (time to live) - use current time in milliseconds
|
||||
messageAgeMs := currentTimeMs - req.Timestamp |
||||
if messageAgeMs > n.MessageTTL { |
||||
// Message expired
|
||||
if n.DeadLetter { |
||||
req.Type = "EXPIRED" |
||||
req.Path = append(req.Path, n.ID+"(expired)") |
||||
n.deadLetterOutput = append(n.deadLetterOutput, req) |
||||
} |
||||
// Otherwise drop expired message
|
||||
continue |
||||
} |
||||
|
||||
// Message queue adds minimal latency (very fast)
|
||||
req.LatencyMS += 2 |
||||
req.Path = append(req.Path, n.ID) |
||||
n.output = append(n.output, req) |
||||
} |
||||
} |
||||
|
||||
func (n *MessageQueueNode) Receive(req *Request) { |
||||
if req == nil { |
||||
return |
||||
} |
||||
|
||||
// Message queues have very low receive overhead
|
||||
req.LatencyMS += 1 |
||||
|
||||
n.Queue = append(n.Queue, req) |
||||
} |
||||
|
||||
func (n *MessageQueueNode) Emit() []*Request { |
||||
// Return both normal messages and dead letter messages
|
||||
allRequests := append(n.output, n.deadLetterOutput...) |
||||
|
||||
// Clear queues
|
||||
n.output = n.output[:0] |
||||
n.deadLetterOutput = n.deadLetterOutput[:0] |
||||
|
||||
return allRequests |
||||
} |
||||
@ -0,0 +1,103 @@
@@ -0,0 +1,103 @@
|
||||
package simulation |
||||
|
||||
import "math/rand" |
||||
|
||||
type MicroserviceNode struct { |
||||
ID string |
||||
Label string |
||||
APIEndpoint string |
||||
RateLimit int // max requests per tick
|
||||
CircuitBreaker bool |
||||
CircuitState string // "closed", "open", "half-open"
|
||||
ErrorCount int |
||||
CurrentLoad int |
||||
Queue []*Request |
||||
Output []*Request |
||||
Alive bool |
||||
Targets []string |
||||
} |
||||
|
||||
func (n *MicroserviceNode) GetID() string { return n.ID } |
||||
|
||||
func (n *MicroserviceNode) Type() string { return "microservice" } |
||||
|
||||
func (n *MicroserviceNode) IsAlive() bool { return n.Alive } |
||||
|
||||
func (n *MicroserviceNode) Receive(req *Request) { |
||||
n.Queue = append(n.Queue, req) |
||||
} |
||||
|
||||
func (n *MicroserviceNode) Emit() []*Request { |
||||
out := append([]*Request(nil), n.Output...) |
||||
n.Output = n.Output[:0] |
||||
return out |
||||
} |
||||
|
||||
func (n *MicroserviceNode) Tick(tick int, currentTimeMs int) { |
||||
if !n.Alive { |
||||
return |
||||
} |
||||
|
||||
// Simulate circuit breaker state transitions
|
||||
switch n.CircuitState { |
||||
case "open": |
||||
// Skip processing
|
||||
return |
||||
case "half-open": |
||||
// Allow limited testing traffic
|
||||
if len(n.Queue) == 0 { |
||||
return |
||||
} |
||||
req := n.Queue[0] |
||||
n.Queue = n.Queue[1:] |
||||
req.LatencyMS += 15 |
||||
req.Path = append(req.Path, n.ID+"(half-open)") |
||||
success := simulateSuccess() |
||||
if success { |
||||
n.CircuitState = "closed" |
||||
n.ErrorCount = 0 |
||||
n.Output = append(n.Output, req) |
||||
} else { |
||||
n.ErrorCount++ |
||||
n.CircuitState = "open" |
||||
} |
||||
return |
||||
} |
||||
|
||||
// "closed" circuit - normal processing
|
||||
toProcess := min(len(n.Queue), n.RateLimit) |
||||
for i := 0; i < toProcess; i++ { |
||||
req := n.Queue[i] |
||||
req.LatencyMS += 10 |
||||
req.Path = append(req.Path, n.ID) |
||||
|
||||
if simulateFailure() { |
||||
n.ErrorCount++ |
||||
if n.CircuitBreaker && n.ErrorCount > 5 { |
||||
n.CircuitState = "open" |
||||
break |
||||
} |
||||
continue |
||||
} |
||||
|
||||
n.Output = append(n.Output, req) |
||||
} |
||||
n.Queue = n.Queue[toProcess:] |
||||
|
||||
// Health check: simple example
|
||||
n.Alive = n.CircuitState != "open" |
||||
} |
||||
|
||||
func simulateFailure() bool { |
||||
// Simulate 10% failure rate
|
||||
return randInt(1, 100) <= 10 |
||||
} |
||||
|
||||
func simulateSuccess() bool { |
||||
// Simulate 90% success rate
|
||||
return randInt(1, 100) <= 90 |
||||
} |
||||
|
||||
func randInt(min, max int) int { |
||||
return min + rand.Intn(max-min+1) |
||||
} |
||||
@ -0,0 +1,66 @@
@@ -0,0 +1,66 @@
|
||||
package simulation |
||||
|
||||
type MonitoringNode struct { |
||||
ID string |
||||
Label string |
||||
Tool string |
||||
AlertMetric string |
||||
ThresholdValue int |
||||
ThresholdUnit string |
||||
Queue []*Request |
||||
Alive bool |
||||
Targets []string |
||||
Metrics map[string]int |
||||
Alerts []*Request |
||||
} |
||||
|
||||
func (n *MonitoringNode) GetID() string { return n.ID } |
||||
|
||||
func (n *MonitoringNode) Type() string { return "monitoring" } |
||||
|
||||
func (n *MonitoringNode) IsAlive() bool { return n.Alive } |
||||
|
||||
func (n *MonitoringNode) Receive(req *Request) { |
||||
n.Queue = append(n.Queue, req) |
||||
} |
||||
|
||||
func (n *MonitoringNode) Emit() []*Request { |
||||
out := append([]*Request(nil), n.Alerts...) |
||||
n.Alerts = n.Alerts[:0] |
||||
return out |
||||
} |
||||
|
||||
func (n *MonitoringNode) Tick(tick int, currentTimeMs int) { |
||||
if !n.Alive { |
||||
return |
||||
} |
||||
|
||||
if n.Metrics == nil { |
||||
n.Metrics = make(map[string]int) |
||||
} |
||||
|
||||
// Simulate processing requests as metrics
|
||||
for _, req := range n.Queue { |
||||
// For now, pretend all requests are relevant to the AlertMetric
|
||||
n.Metrics[n.AlertMetric] += 1 |
||||
req.LatencyMS += 1 |
||||
req.Path = append(req.Path, n.ID) |
||||
|
||||
if n.Metrics[n.AlertMetric] > n.ThresholdValue { |
||||
alert := &Request{ |
||||
ID: "alert-" + req.ID, |
||||
Timestamp: currentTimeMs, |
||||
Origin: n.ID, |
||||
Type: "alert", |
||||
LatencyMS: 0, |
||||
Path: []string{n.ID, "alert"}, |
||||
} |
||||
n.Alerts = append(n.Alerts, alert) |
||||
|
||||
// Reset after alert (or you could continue accumulating)
|
||||
n.Metrics[n.AlertMetric] = 0 |
||||
} |
||||
} |
||||
|
||||
n.Queue = nil |
||||
} |
||||
@ -0,0 +1,77 @@
@@ -0,0 +1,77 @@
|
||||
package simulation |
||||
|
||||
type ThirdPartyServiceNode struct { |
||||
ID string |
||||
Label string |
||||
APIEndpoint string |
||||
RateLimit int // Max requests per tick
|
||||
RetryPolicy string // "exponential", "fixed", etc.
|
||||
CurrentLoad int |
||||
Queue []*Request |
||||
ErrorCount int |
||||
RetryCount int |
||||
Alive bool |
||||
Targets []string |
||||
Output []*Request |
||||
} |
||||
|
||||
// --- Interface Methods ---
|
||||
|
||||
func (n *ThirdPartyServiceNode) GetID() string { return n.ID } |
||||
func (n *ThirdPartyServiceNode) Type() string { return "thirdpartyservice" } |
||||
func (n *ThirdPartyServiceNode) IsAlive() bool { return n.Alive } |
||||
func (n *ThirdPartyServiceNode) Receive(req *Request) { |
||||
n.Queue = append(n.Queue, req) |
||||
} |
||||
func (n *ThirdPartyServiceNode) Emit() []*Request { |
||||
out := append([]*Request(nil), n.Output...) |
||||
n.Output = n.Output[:0] |
||||
return out |
||||
} |
||||
|
||||
// Add missing Queue method for interface compliance
|
||||
func (n *ThirdPartyServiceNode) GetQueue() []*Request { |
||||
return n.Queue |
||||
} |
||||
|
||||
// --- Simulation Logic ---
|
||||
|
||||
func (n *ThirdPartyServiceNode) Tick(tick int, currentTimeMs int) { |
||||
if !n.Alive { |
||||
return |
||||
} |
||||
|
||||
// Simulate third-party call behavior with success/failure
|
||||
maxProcess := min(n.RateLimit, len(n.Queue)) |
||||
newQueue := n.Queue[maxProcess:] |
||||
n.Queue = nil |
||||
|
||||
for i := 0; i < maxProcess; i++ { |
||||
req := newQueue[i] |
||||
success := simulateThirdPartySuccess(req) |
||||
|
||||
if success { |
||||
req.LatencyMS += 100 + randInt(0, 50) // simulate response time
|
||||
req.Path = append(req.Path, n.ID) |
||||
n.Output = append(n.Output, req) |
||||
} else { |
||||
n.ErrorCount++ |
||||
n.RetryCount++ |
||||
if n.RetryPolicy == "exponential" && n.RetryCount < 3 { |
||||
n.Queue = append(n.Queue, req) // retry again next tick
|
||||
} |
||||
} |
||||
} |
||||
|
||||
// Simulate degradation if too many errors
|
||||
if n.ErrorCount > 10 { |
||||
n.Alive = false |
||||
} |
||||
} |
||||
|
||||
// --- Helpers ---
|
||||
|
||||
func simulateThirdPartySuccess(req *Request) bool { |
||||
// 90% success rate
|
||||
return randInt(0, 100) < 90 |
||||
} |
||||
Loading…
Reference in new issue