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

Add toxic slicer #66

Merged
merged 6 commits into from
Jul 23, 2015
Merged
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
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this toxic is interrupted at this point in the loop, the remainder of c will be dropped. In this case we would want to just send off the remainder like in the latency toxic.
Toxics can be interrupted without the stream closing, so we need to make sure it's in a usable state.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

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