Skip to content

Commit

Permalink
First pass at MarsToxic: estimated latency but unbounded bandwidth
Browse files Browse the repository at this point in the history
  • Loading branch information
burke committed Nov 11, 2024
1 parent 1c8b9b1 commit 2721212
Show file tree
Hide file tree
Showing 2 changed files with 164 additions and 0 deletions.
98 changes: 98 additions & 0 deletions toxics/mars.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package toxics

import (
"math"
"time"
)

// 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
//
// 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"`
// Reference time for testing, if zero current time is used
ReferenceTime time.Time `json:"-"`
}

// 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
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) {
for {
select {
case <-stub.Interrupt:
return
case c := <-stub.Input:
if c == nil {
stub.Close()
return
}
sleep := t.Delay() - time.Since(c.Timestamp)
select {
case <-time.After(sleep):
c.Timestamp = c.Timestamp.Add(sleep)
stub.Output <- c
case <-stub.Interrupt:
// Exit fast without applying latency.
stub.Output <- c // Don't drop any data on the floor
return
}
}
}
}

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

import (
"testing"
"time"

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

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.03) // 3% 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,
)
}
}

0 comments on commit 2721212

Please sign in to comment.