Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mars Simulator #601

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 182 additions & 0 deletions toxics/mars.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package toxics

import (
"fmt"
"math"
"time"

"github.com/rs/zerolog/log"
"github.com/Shopify/toxiproxy/v2/stream"
)

// The MarsToxic simulates the communication delay to Mars based on current orbital positions.
// There are more accurate orbital models, but this is a simple approximation.
//
// Further possibilities here:
// * drop packets entirely during solar conjunction
// * corrupt frames in the liminal period before/after conjunction
// * buffering through the disk (maybe a FIFO, idk) would model data in flight better
//
// We could to the hard block but we're kind of at the wrong layer to do corruption.
type MarsToxic struct {
// Optional additional latency in milliseconds
ExtraLatency int64 `json:"extra_latency"`
// Rate in KB/s (0 means unlimited)
Rate int64 `json:"rate"`
// Reference time for testing, if zero current time is used
ReferenceTime time.Time `json:"-"`
// Speed of light in km/s (defaults to 299792.458 if 0) It's (probably?)
// obvious you won't want to change this. It's useful for testing.
SpeedOfLight float64 `json:"speed_of_light"`
}

// Since we're buffering for several minutes, we need a large buffer.
// Maybe this should really be unbounded... this is actually a kind of awkward thing to model without
// a, you know, hundred million kilometre long buffer of functionally infinite
// capacity connecting the two points.
func (t *MarsToxic) GetBufferSize() int {
return 1024 * 1024
}

// This is accurate to within a something like 1-2%, and probably not
// hundreds/thousands of years away from 2000.
// This is a simple sinusoidal approximation; a real calculation would require
// quite a lot more doing (which could be fun, but...)
func (t *MarsToxic) Delay() time.Duration {
// Constants for Mars distance calculation
minDistance := 54.6e6 // km at opposition
maxDistance := 401.0e6 // km at conjunction
meanDistance := (maxDistance + minDistance) / 2
amplitude := (maxDistance - minDistance) / 2
synodicPeriod := 779.96 // More precise synodic period in days

// Calculate days since Jan 1, 2000
// July 27, 2018 was a recent opposition, which was day 6763 since Jan 1, 2000
baseDate := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
var daysSince2000 float64
if !t.ReferenceTime.IsZero() {
daysSince2000 = t.ReferenceTime.Sub(baseDate).Hours() / 24
} else {
daysSince2000 = time.Since(baseDate).Hours() / 24
}

// Calculate phase based on synodic period
phase := 2 * math.Pi * math.Mod(daysSince2000-6763, synodicPeriod) / synodicPeriod

// Calculate current distance in kilometers
distanceKm := meanDistance - amplitude*math.Cos(phase)

// Speed of light is exactly 299,792.458 km/s by default
speedOfLight := t.SpeedOfLight
if speedOfLight <= 0 {
speedOfLight = 299792.458 // km/s
}

// One-way time = distance / speed of light
// Convert to milliseconds
delayMs := int64((distanceKm / speedOfLight) * 1000)

// Add any extra latency specified
delayMs += t.ExtraLatency

return time.Duration(delayMs) * time.Millisecond
}

func (t *MarsToxic) Pipe(stub *ToxicStub) {
logger := log.With().
Str("component", "MarsToxic").
Str("method", "Pipe").
Str("toxic_type", "mars").
Str("addr", fmt.Sprintf("%p", t)).
Logger()

var sleep time.Duration = 0
for {
select {
case <-stub.Interrupt:
logger.Trace().Msg("MarsToxic was interrupted")
return
case c := <-stub.Input:
if c == nil {
stub.Close()
return
}

// Set timestamp when we receive the chunk
if c.Timestamp.IsZero() {
c.Timestamp = time.Now()
}

// Calculate Mars delay once for this chunk
marsDelay := t.Delay()

// Calculate bandwidth delay if rate is set
if t.Rate > 0 {
bytesPerSecond := t.Rate * 1024

// If chunk is too large, split it
if int64(len(c.Data)) > bytesPerSecond/10 { // 100ms worth of data
bytesPerInterval := bytesPerSecond/10 // bytes per 100ms
remainingData := c.Data
chunkStart := c.Timestamp

// First, wait for Mars delay
select {
case <-time.After(marsDelay):
case <-stub.Interrupt:
return
}

for len(remainingData) > 0 {
chunkSize := int(bytesPerInterval)
if chunkSize > len(remainingData) {
chunkSize = len(remainingData)
}

chunk := &stream.StreamChunk{
Data: remainingData[:chunkSize],
Timestamp: chunkStart,
}

select {
case <-time.After(100 * time.Millisecond):
chunkStart = chunkStart.Add(100 * time.Millisecond)
stub.Output <- chunk
remainingData = remainingData[chunkSize:]
case <-stub.Interrupt:
logger.Trace().Msg("MarsToxic was interrupted during writing data")
return
}
}
continue
}

// For small chunks, calculate bandwidth delay
sleep = time.Duration(float64(len(c.Data)) / float64(bytesPerSecond) * float64(time.Second))
}

// Apply both Mars delay and bandwidth delay
totalDelay := marsDelay
if sleep > 0 {
totalDelay += sleep
}

select {
case <-time.After(totalDelay):
c.Timestamp = c.Timestamp.Add(totalDelay)
stub.Output <- c
case <-stub.Interrupt:
logger.Trace().Msg("MarsToxic was interrupted during writing data")
err := stub.WriteOutput(c, 5*time.Second)
if err != nil {
logger.Warn().Err(err).Msg("Could not write last packets after interrupt")
}
return
}
}
}
}

func init() {
Register("mars", new(MarsToxic))
}
171 changes: 171 additions & 0 deletions toxics/mars_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package toxics_test

import (
"testing"
"time"

"github.com/Shopify/toxiproxy/v2/toxics"
"github.com/Shopify/toxiproxy/v2/stream"
)

func TestMarsDelayCalculation(t *testing.T) {
tests := []struct {
name string
date time.Time
expected time.Duration
}{
{
name: "At Opposition (Closest)",
date: time.Date(2018, 7, 27, 0, 0, 0, 0, time.UTC),
expected: 182 * time.Second, // ~3 minutes at closest approach
},
{
name: "At Conjunction (Farthest)",
date: time.Date(2019, 9, 2, 0, 0, 0, 0, time.UTC), // ~400 days after opposition
expected: 1337 * time.Second, // ~22.3 minutes at farthest point
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
marsToxic := &toxics.MarsToxic{
ReferenceTime: tt.date,
}

delay := marsToxic.Delay()
tolerance := time.Duration(float64(tt.expected) * 0.04) // 4% tolerance
if diff := delay - tt.expected; diff < -tolerance || diff > tolerance {
t.Errorf("Expected delay of %v (±%v), got %v (%.1f%% difference)",
tt.expected,
tolerance,
delay,
float64(diff) / float64(tt.expected) * 100,
)
}
})
}
}

func TestMarsExtraLatencyCalculation(t *testing.T) {
marsToxic := &toxics.MarsToxic{
ReferenceTime: time.Date(2018, 7, 27, 0, 0, 0, 0, time.UTC),
ExtraLatency: 60000, // Add 1 minute
}

expected := 242 * time.Second // ~4 minutes (3 min base + 1 min extra)
delay := marsToxic.Delay()

tolerance := time.Duration(float64(expected) * 0.04) // 4% tolerance
if diff := delay - expected; diff < -tolerance || diff > tolerance {
t.Errorf("Expected delay of %v (±%v), got %v (%.1f%% difference)",
expected,
tolerance,
delay,
float64(diff) / float64(expected) * 100,
)
}
}

func TestMarsBandwidth(t *testing.T) {
marsToxic := &toxics.MarsToxic{
ReferenceTime: time.Date(2018, 7, 27, 0, 0, 0, 0, time.UTC), // At opposition
Rate: 100, // 100 KB/s
SpeedOfLight: 299792.458 * 1000, // 1000x normal speed for faster testing
}

input := make(chan *stream.StreamChunk)
output := make(chan *stream.StreamChunk)
stub := toxics.NewToxicStub(input, output)
done := make(chan bool)

go func() {
marsToxic.Pipe(stub)
done <- true
}()

// Send 50KB of data
dataSize := 50 * 1024 // 50KB

// At 100 KB/s, 50KB should take exactly 0.5 seconds
// Expected timing:
// - Bandwidth delay: 500ms (50KB at 100KB/s)
// - Mars delay: ~182ms (at opposition, with 1000x speed of light)
expectedDelay := 500*time.Millisecond + time.Duration(float64(182*time.Second)/1000)

start := time.Now()

testData := make([]byte, dataSize)
for i := range testData {
testData[i] = byte(i % 256) // Fill with recognizable pattern
}

select {
case input <- &stream.StreamChunk{
Data: testData,
}:
case <-time.After(5 * time.Second):
t.Fatal("Timeout while sending data")
}

// Collect all chunks
var receivedData []byte
timeout := time.After(5 * time.Second)

for len(receivedData) < dataSize {
select {
case chunk := <-output:
receivedData = append(receivedData, chunk.Data...)
case <-timeout:
t.Fatalf("Timeout while receiving data. Got %d of %d bytes", len(receivedData), dataSize)
}
}

elapsed := time.Since(start)

// Should take at least 0.5 seconds (50KB at 100KB/s) plus reduced Mars delay
tolerance := time.Duration(float64(expectedDelay) * 0.04) // 4% tolerance for timing

if elapsed < expectedDelay-tolerance || elapsed > expectedDelay+tolerance {
t.Errorf("Expected total delay of %v (±%v), got %v", expectedDelay, tolerance, elapsed)
}

if len(receivedData) != dataSize {
t.Errorf("Expected %d bytes, got %d", dataSize, len(receivedData))
}

// Verify data integrity
for i := range receivedData {
if receivedData[i] != byte(i%256) {
t.Errorf("Data corruption at byte %d: expected %d, got %d", i, byte(i%256), receivedData[i])
break
}
}

close(input)
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("Timeout waiting for toxic to finish")
}
}

func TestMarsSpeedOfLight(t *testing.T) {
// Test with 1000x speed of light to reduce delays
marsToxic := &toxics.MarsToxic{
ReferenceTime: time.Date(2018, 7, 27, 0, 0, 0, 0, time.UTC), // At opposition
SpeedOfLight: 299792.458 * 1000, // 1000x normal speed
}

delay := marsToxic.Delay()
expected := time.Duration(float64(182*time.Second) / 1000) // ~182ms (normal 182s / 1000)

tolerance := time.Duration(float64(expected) * 0.04) // 4% tolerance
if diff := delay - expected; diff < -tolerance || diff > tolerance {
t.Errorf("Expected delay of %v (±%v), got %v (%.1f%% difference)",
expected,
tolerance,
delay,
float64(diff) / float64(expected) * 100,
)
}
}
Loading