-
Notifications
You must be signed in to change notification settings - Fork 451
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 Bidirectional Toxics #132
base: dev
Are you sure you want to change the base?
Conversation
return nil, ErrInvalidStream | ||
} | ||
} else { | ||
wrapper.Stream = "both" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what the behaviour should be here since there's only one option for bidirectional toxics.
Right now stream
can be set to anything and it will just be changed to both
by the server.
Updates to |
ac78131
to
b08f2d9
Compare
I think this should be ready for review now, @sirupsen The WIP http toxic is here: https://github.com/Shopify/toxiproxy/blob/http-toxic/toxics/http.go |
CHANGELOG.md
Outdated
@@ -1,5 +1,6 @@ | |||
# 2.1.0 (Unreleased) | |||
|
|||
* Add bidirectional toxics #132 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we add a little bit more of a comment here on why this is useful? What it allows us to do?
toxics/toxic.go
Outdated
// PipeRequest() will oparate on the upstream, while Pipe() will operate on the downstream. | ||
type BidirectionalToxic interface { | ||
// Defines the packet flow through an upstream ToxicStub. Operates the same as Pipe(). | ||
PipeRequest(*ToxicStub) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we call this PipeUpstream
instead to keep the terminology consistent?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, probably a good idea. I was originally writing this as part of the HTTP toxic, so that's where the naming came from.
CREATING_TOXICS.md
Outdated
Creating a bidirectional toxic is done by implementing a second `Pipe()` function called `PipeRequest()`. | ||
The implementation is same as a regular toxic, and can be paired with other types such as a stateful toxic. | ||
|
||
One use case of a bidirectional toxic is to mock out the backend server entirely, which is shown below: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it'd be more clear in people's mind if we elaborated this example a little bit to a use-case, e.g. what if we did a simple implementation of the Redis protocol (which is super simple) that fails writes of keys directly? Something like..
case c := <-stub.Request:
if c == nil {
stub.Close()
return
}
if strings.HasPrefix(string(c), "SET") {
c <- []byte() # failed to write
} else {
c <- []byte("OK")
}
I'm sure this example doesn't work, but it illustrates it. An alternative would be an HTTP server, but that may be a little bit harder to make as a concise example because the protocol is more complex and AFAIK the Go STDLIB doesn't expose the HTTP parser nicely (or maybe it does?) in which case one that sends 5xx in some cases would be cool.
I'm assuming this will work out of the box with toxicity? In which case perhaps above instead of doing anything fancy, you can just return a 500 HTTP response in the example in <toxicity>
of cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A redis example could be interesting, I'll see if I can throw something together.
Go does expose it's HTTP request/response parsing, but it requires using io.Reader
/ io.Writer
, so it's maybe a little complicated for an example. (You can see it here https://github.com/Shopify/toxiproxy/blob/http-toxic/toxics/http.go)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that might be too complicated for now.
link.go
Outdated
@@ -67,7 +68,12 @@ func (link *ToxicLink) Start(name string, source io.Reader, dest io.WriteCloser) | |||
}() | |||
for i, toxic := range link.toxics.chain[link.direction] { | |||
if stateful, ok := toxic.Toxic.(toxics.StatefulToxic); ok { | |||
link.stubs[i].State = stateful.NewState() | |||
if toxic.PairedToxic == nil || link.pairedLink.stubs[toxic.PairedToxic.Index].State == nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not a huge fan of this added complexity, but since BidirectionalToxics override the entire response I don't see an easy way out. Perhaps we should emphasize more heavily that they're fundamentally different toxics because they don't parse anything to the server...
Another approach we could take is that PipeRequest
can choose whether it passes directly to the downstream toxic or to the backend, in that case the default template for toxics is just to pass the data as is to the upstream. This also allows for some new interesting toxics where you manipulate with the data that's being sent to the backend, e.g. wrong key names.
Could one interface in that case work for all use-cases? Even if it's a bit more verbose. Realistically not that many toxics will be created by third-parties for now, so I think simplifying the core of Toxiproxy for more verbose toxics (that are more powerful?) isn't a bad trade-off to make.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess my documentation could still use some work. All of these things are possible with the current interface.
A bidirectional toxic internally is essentially just an upstream and downstream toxic paired together, with the option of sharing a user-defined state object.
They can be completely independent and work like any other toxic, or they can do fancy things with the state to share data or even completely bypass the backend server.
I originally tried to hide this implementation detail in the docs, but maybe that wasn't such a great idea and makes things more confusing.
a59242a
to
2032741
Compare
+1. This seems very useful. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just gave this a read over. These review comments are mainly for myself. Assuming you're not interested in shipping this yourself @xthexder, I was going to try to get this out.
@@ -445,7 +445,7 @@ func apiError(resp http.ResponseWriter, err error) bool { | |||
|
|||
type proxyToxics struct { | |||
*Proxy | |||
Toxics []toxics.Toxic `json:"toxics"` | |||
Toxics []*toxics.ToxicWrapper `json:"toxics"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did we change from Toxic to ToxicWrapper?
|
||
// If the toxic is paired, synchronize the toxicity so they are always in the same state. | ||
if toxic.PairedToxic != nil { | ||
link.stubs[toxic.Index].Toxicity = link.pairedLink.stubs[toxic.PairedToxic.Index].Toxicity |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If toxicity is always initialized to the paired toxic's toxicity, where is the value actually configured?
// Skip the first noop toxic, it should not be visible | ||
continue | ||
for _, toxic := range c.chain[dir] { | ||
if len(toxic.Name) > 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should have a Hidden
flag rather than relying on no name?
default: | ||
return nil, ErrInvalidStream | ||
if toxics.New(wrapper) == nil { | ||
return nil, ErrInvalidToxicType |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is ErrInvalidToxicType
the only kind of error we could get here?
I should understand how this works.
I was hacking together a MS SQL TDS aware toxic to cut off connections when given batch or RPC calls are sent upstream. If this ever gets anywhere then give me a nudge and i will finish it off 😄 |
2032741
to
0e85ee6
Compare
This is in preparation for adding an HTTP toxic as well as enabling other protocol-aware toxics.
This PR does a few things:
downstream
link can reference anupstream
link.upstream
anddownstream
toxic to share a state object.