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

contrib/internal/httputil: generate all possible http interfaces for the response writer wrapper #278

Merged
merged 5 commits into from
Aug 1, 2018

Conversation

dd-caleb
Copy link
Contributor

This fixes: #256

So here's the idea. We need to wrap the WriteHeader call because that's really the only place to get the http response code. But we also want to make sure we don't accidentally remove interface implementations on the response writer (Flusher, Hijacker, etc).

One solution would be to implement a super struct:

w = struct {
	http.ResponseWriter
	http.Flusher
	http.Pusher
	http.CloseNotifier
	http.Hijacker
}{w, hFlusher, hPusher, hCloseNotifier, hHijacker}

Because of field embedding we can call the methods of each interface on the outer struct (w.Flush() -> w.Flusher.Flush()), which in turn makes it so that we now actually implement each of the interfaces.

Unfortunately this won't work because if we didn't already implement one of the http interfaces, we would falsely indicate to the calling code that we did. For example client code might have:

if flusher, ok := w.(http.Flusher); ok {

Which would now return true even though we didn't actually implement the method originally.

So to overcome this problem I wrote a script which generates all the possible combinations and creates an anonymous struct for each case implementing only the interfaces that the original response writer did.

Does that make sense?

// This code is generated because we have to account for all the permutations
// of the interfaces.
func wrapResponseWriter(w http.ResponseWriter, span ddtrace.Span) http.ResponseWriter {
`)
Copy link
Contributor

Choose a reason for hiding this comment

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

Would you mind using a template here? This is too cryptic and hard to follow.

@gbbr gbbr added this to the 1.1.0 milestone Jul 25, 2018
@gbbr gbbr added the bug unintended behavior that has to be fixed label Jul 25, 2018
@gbbr
Copy link
Contributor

gbbr commented Jul 25, 2018

This is a great solution. I love it. But please use a template so it's easy to see and work with the generated output.

@@ -0,0 +1,119 @@
// Code generated by ./gen do not modify
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's respect the standard comment heading for generated files outlined here: golang/go#13560 (comment)

Copy link
Contributor

@gbbr gbbr left a comment

Choose a reason for hiding this comment

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

This is a fantastic solution

})
}

func combinations(strs []string, pick int) (all [][]string) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I've spent quite a lot of time here trying to make sense of this algorithm. Even though it looks short and witty, and possibly a well known algorithm for computer science aficiandos, I'd expect a large percentage of people visiting this code to be like me and not understand what's going on without spending time.

Would it be possible to augment it with documentation describing in detail how it works? Or, if it's a common algorithm that has a Wiki page, simply to link to it?

I was also wondering if we could rename strs to list. "strs" makes me think "strings" which is unclear when reading. But maybe it's just me.


func combinations(strs []string, pick int) (all [][]string) {
switch pick {
case 0:
Copy link
Contributor

Choose a reason for hiding this comment

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

I usually like to put a comment in switch cases like this. For example:

    case 0:
        // nothing to do

combos = append(combos, combinations(interfaces, pick))
}

template.Must(template.ParseFiles("./gen.gohtml")).Execute(os.Stdout, map[string]interface{}{
Copy link
Contributor

@gbbr gbbr Jul 31, 2018

Choose a reason for hiding this comment

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

Thanks for being patient again. And I'm sorry for being unclear. I think it would be nicer, easier to handle and make sense of this code, if both the template text and the Go code would be in the same file. Basically, at the end of the Go code we'd put a const template=... with a backtick, like here or here.

Just like in those two examples, we could also just keep this in the httputil folder using the // +build !ignore flag. All in one file: make_responsewriter.go (or similar). I found this worked well in the past, avoiding the need for a new package.

w = newResponseWriter(w, span)
switch {
{{- range .Combinations }}
{{- range . }}
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we possibly indent all these {{ - range . }} and {{- end }} commands? It's slightly difficult to see where each starts and ends.

Also, what is this - prefix? I've not used it before.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The - prefix removes whitespace before the block.

@@ -0,0 +1,36 @@
package main
Copy link
Contributor

@gbbr gbbr Jul 31, 2018

Choose a reason for hiding this comment

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

Let's document this package and explain what it does. It shouldn't be a package level documentation, it should be the documentation of this utility, something like "This program generates the implementation of all possible combinations of the http.ResponseWriter's interface implementations [...]" and so on. To make it clear that it's not package level documentation we should add a space between the documentation and package declaration.

@dd-caleb
Copy link
Contributor Author

@gbbr thanks for the review. I renamed the code generated and added some additional documentation.

Sorry that combinations was unclear. It's an algorithm I have a hard time reasoning about, so I tried to add an explanation of what it's doing and how it works.

Copy link
Contributor

@gbbr gbbr left a comment

Choose a reason for hiding this comment

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

Thanks for adding the explanation. It makes a huge difference. I am wondering if it would be fun to generate the tests for this code too, using the same combinations.

It would also be nice to have a basic unit test for the "combinations" function.

}

// combinations returns all possible unique selections of size `pick` of a list
// of strings for which order does not matter: wikipedia.org/wiki/Combination
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure if the link adds much. It doesn't link to this specific algorithm implemented here. If we use a link it should serve as a shortcut to understanding this algorithm. The wiki page is too vast.

switch {
{{- range .Combinations }}
{{- range . }}
case {{ range $i, $v := . }}{{ if gt $i 0 }} && {{ end }}ok{{.}}{{ end }}:
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it possible to remove $v here just like in Go code? Not sure. It's unused.

@dd-caleb
Copy link
Contributor Author

dd-caleb commented Aug 1, 2018

@gbbr I moved the combinations function to a separate, internal lists package so that I could add a test for it

Copy link
Contributor

@gbbr gbbr left a comment

Choose a reason for hiding this comment

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

Thanks!

@dd-caleb dd-caleb merged commit 588a6ff into v1 Aug 1, 2018
@dd-caleb dd-caleb deleted the caleb/response-writer branch August 1, 2018 15:00
mingrammer pushed a commit to mingrammer/dd-trace-go that referenced this pull request Dec 22, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug unintended behavior that has to be fixed
Projects
None yet
Development

Successfully merging this pull request may close these issues.

contrib/internal/httputil: do not replace http.ResponseWriter
2 participants