Skip to content

Commit

Permalink
Merge pull request #66 from connor4312/master
Browse files Browse the repository at this point in the history
Add toxic slicer
  • Loading branch information
sirupsen committed Jul 23, 2015
2 parents 1c19b4d + f3c3d73 commit b84a58e
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* Add a Toxic and Toxics type for the Go client
* Add `Dockerfile`
* Fix latency toxic limiting bandwidth #67
* Add Slicer toxic

# 1.1.0

Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,19 @@ Fields:
- `enabled`: true/false
- `timeout`: time in milliseconds

#### slicer

Slices TCP data up into small bits, optionally adding a delay between each
sliced "packet".

Fields:

- `enabled`: true/false
- `average_size`: size in bytes of an average packet
- `size_variation`: variation in bytes of an average packet (should be smaller than averageSize)
- `delay`: time in microseconds to delay each packet by


### HTTP API

All communication with the Toxiproxy daemon from the client happens through the
Expand Down
1 change: 1 addition & 0 deletions toxic_collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func NewToxicCollection(proxy *Proxy) *ToxicCollection {
new(SlowCloseToxic),
new(LatencyToxic),
new(BandwidthToxic),
new(SlicerToxic),
new(TimeoutToxic),
}

Expand Down
90 changes: 90 additions & 0 deletions toxic_slicer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package main

import (
"math/rand"
"time"
)

// The SlicerToxic slices data into multiple smaller packets
// to simulate real-world TCP behaviour.
type SlicerToxic struct {
Enabled bool `json:"enabled"`
// Average number of bytes to slice at
AverageSize int `json:"average_size"`
// +/- bytes to vary sliced amounts. Must be less than
// the average size
SizeVariation int `json:"size_variation"`
// Microseconds to delay each packet. May be useful since there's
// usually some kind of buffering of network data
Delay int `json:"delay"`
}

func (t *SlicerToxic) Name() string {
return "slicer"
}

func (t *SlicerToxic) IsEnabled() bool {
return t.Enabled
}

func (t *SlicerToxic) SetEnabled(enabled bool) {
t.Enabled = enabled
}

// Returns a list of chunk offsets to slice up a packet of the
// given total size. For example, for a size of 100, output might be:
//
// []int{0, 18, 18, 43, 43, 67, 67, 77, 77, 100}
// ^---^ ^----^ ^----^ ^----^ ^-----^
//
// This tries to get fairly evenly-varying chunks (no tendency
// to have a small/large chunk at the start/end).
func (t *SlicerToxic) chunk(start int, end int) []int {
// Base case:
// If the size is within the random varation, _or already
// less than the average size_, just return it.
// Otherwise split the chunk in about two, and recurse.
if (end-start)-t.AverageSize <= t.SizeVariation {
return []int{start, end}
}

// +1 in the size variation to offset favoring of smaller
// numbers by integer division
mid := start + (end-start)/2 + (rand.Intn(t.SizeVariation*2) - t.SizeVariation) + rand.Intn(1)
left := t.chunk(start, mid)
right := t.chunk(mid, end)

return append(left, right...)
}

func (t *SlicerToxic) Pipe(stub *ToxicStub) {
for {
select {
case <-stub.interrupt:
return
case c := <-stub.input:
if c == nil {
stub.Close()
return
}

chunks := t.chunk(0, len(c.data))
for i := 1; i < len(chunks); i += 2 {
stub.output <- &StreamChunk{
data: c.data[chunks[i-1]:chunks[i]],
timestamp: c.timestamp,
}

select {
case <-stub.interrupt:
stub.output <- &StreamChunk{
data: c.data[chunks[i]:],
timestamp: c.timestamp,
}
return
case <-time.After(time.Duration(t.Delay) * time.Microsecond):
}
}
}
}
}
41 changes: 41 additions & 0 deletions toxic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,47 @@ func TestBandwidthToxic(t *testing.T) {
)
}

func TestSlicerToxic(t *testing.T) {
data := []byte(strings.Repeat("hello world ", 40000)) // 480 kb
slicer := &SlicerToxic{Enabled: true, AverageSize: 1024, SizeVariation: 512, Delay: 10}

input := make(chan *StreamChunk)
output := make(chan *StreamChunk)
stub := NewToxicStub(input, output)

done := make(chan bool)
go func() {
slicer.Pipe(stub)
done <- true
}()
defer func() {
input <- nil
<-done
}()

input <- &StreamChunk{data: data}

buf := make([]byte, 0, len(data))
reads := 0
L:
for {
select {
case c := <-output:
reads++
buf = append(buf, c.data...)
case <-time.After(5 * time.Millisecond):
break L
}
}

if reads < 480/2 || reads > 480/2+480 {
t.Errorf("Expected to read about 480 times, but read %d times.", reads)
}
if bytes.Compare(buf, data) != 0 {
t.Errorf("Server did not read correct buffer from client!")
}
}

func TestToxicUpdate(t *testing.T) {
ln, err := net.Listen("tcp", "localhost:0")
if err != nil {
Expand Down

0 comments on commit b84a58e

Please sign in to comment.