diff --git a/toxics/mars.go b/toxics/mars.go new file mode 100644 index 00000000..daba7e2a --- /dev/null +++ b/toxics/mars.go @@ -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)) +} \ No newline at end of file diff --git a/toxics/mars_test.go b/toxics/mars_test.go new file mode 100644 index 00000000..605c5286 --- /dev/null +++ b/toxics/mars_test.go @@ -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, + ) + } +} \ No newline at end of file