From b37f7973bdc28f8c878959398f115c5a52f3d508 Mon Sep 17 00:00:00 2001 From: Iwasaki Yudai Date: Mon, 24 Aug 2015 19:37:36 +0900 Subject: [PATCH] Credit braintree/manners --- Godeps/Godeps.json | 13 +- .../src/github.com/braintree/manners/LICENSE | 19 ++ .../github.com/braintree/manners/README.md | 36 +++ .../braintree/manners/helpers_test.go | 118 +++++++++ .../braintree/manners/interfaces.go | 7 + .../github.com/braintree/manners/server.go | 228 ++++++++++++++++ .../braintree/manners/server_test.go | 243 ++++++++++++++++++ .../github.com/braintree/manners/static.go | 35 +++ .../braintree/manners/test_helpers/certs.go | 29 +++ .../braintree/manners/test_helpers/conn.go | 13 + .../manners/test_helpers/listener.go | 34 +++ .../manners/test_helpers/temp_file.go | 27 ++ .../manners/test_helpers/wait_group.go | 33 +++ .../braintree/manners/transition_test.go | 54 ++++ LICENSE | 23 ++ 15 files changed, 904 insertions(+), 8 deletions(-) create mode 100644 Godeps/_workspace/src/github.com/braintree/manners/LICENSE create mode 100644 Godeps/_workspace/src/github.com/braintree/manners/README.md create mode 100644 Godeps/_workspace/src/github.com/braintree/manners/helpers_test.go create mode 100644 Godeps/_workspace/src/github.com/braintree/manners/interfaces.go create mode 100644 Godeps/_workspace/src/github.com/braintree/manners/server.go create mode 100644 Godeps/_workspace/src/github.com/braintree/manners/server_test.go create mode 100644 Godeps/_workspace/src/github.com/braintree/manners/static.go create mode 100644 Godeps/_workspace/src/github.com/braintree/manners/test_helpers/certs.go create mode 100644 Godeps/_workspace/src/github.com/braintree/manners/test_helpers/conn.go create mode 100644 Godeps/_workspace/src/github.com/braintree/manners/test_helpers/listener.go create mode 100644 Godeps/_workspace/src/github.com/braintree/manners/test_helpers/temp_file.go create mode 100644 Godeps/_workspace/src/github.com/braintree/manners/test_helpers/wait_group.go create mode 100644 Godeps/_workspace/src/github.com/braintree/manners/transition_test.go diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index d2e7832..beede7b 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -2,6 +2,11 @@ "ImportPath": "github.com/yudai/gotty", "GoVersion": "go1.5", "Deps": [ + { + "ImportPath": "github.com/braintree/manners", + "Comment": "0.4.0-6-g9e2a271", + "Rev": "9e2a2714de21eb092ead2ef56d8c7a60d7928819" + }, { "ImportPath": "github.com/codegangsta/cli", "Comment": "1.2.0-139-g142e6cd", @@ -11,14 +16,6 @@ "ImportPath": "github.com/elazarl/go-bindata-assetfs", "Rev": "d5cac425555ca5cf00694df246e04f05e6a55150" }, - { - "ImportPath": "github.com/gorilla/context", - "Rev": "1c83b3eabd45b6d76072b66b746c20815fb2872d" - }, - { - "ImportPath": "github.com/gorilla/mux", - "Rev": "ee1815431e497d3850809578c93ab6705f1a19f7" - }, { "ImportPath": "github.com/gorilla/websocket", "Rev": "b6ab76f1fe9803ee1d59e7e5b2a797c1fe897ce5" diff --git a/Godeps/_workspace/src/github.com/braintree/manners/LICENSE b/Godeps/_workspace/src/github.com/braintree/manners/LICENSE new file mode 100644 index 0000000..91ef5be --- /dev/null +++ b/Godeps/_workspace/src/github.com/braintree/manners/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2014 Braintree, a division of PayPal, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Godeps/_workspace/src/github.com/braintree/manners/README.md b/Godeps/_workspace/src/github.com/braintree/manners/README.md new file mode 100644 index 0000000..09f6f96 --- /dev/null +++ b/Godeps/_workspace/src/github.com/braintree/manners/README.md @@ -0,0 +1,36 @@ +# Manners + +A *polite* webserver for Go. + +Manners allows you to shut your Go webserver down gracefully, without dropping any requests. It can act as a drop-in replacement for the standard library's http.ListenAndServe function: + +```go +func main() { + handler := MyHTTPHandler() + manners.ListenAndServe(":7000", handler) +} +``` + +Then, when you want to shut the server down: + +```go +manners.Close() +``` + +(Note that this does not block until all the requests are finished. Rather, the call to manners.ListenAndServe will stop blocking when all the requests are finished.) + +Manners ensures that all requests are served by incrementing a WaitGroup when a request comes in and decrementing it when the request finishes. + +If your request handler spawns Goroutines that are not guaranteed to finish with the request, you can ensure they are also completed with the `StartRoutine` and `FinishRoutine` functions on the server. + +### Known Issues + +Manners does not correctly shut down long-lived keepalive connections when issued a shutdown command. Clients on an idle keepalive connection may see a connection reset error rather than a close. See https://github.com/braintree/manners/issues/13 for details. + +### Compatability + +Manners 0.3.0 and above uses standard library functionality introduced in Go 1.3. + +### Installation + +`go get github.com/braintree/manners` diff --git a/Godeps/_workspace/src/github.com/braintree/manners/helpers_test.go b/Godeps/_workspace/src/github.com/braintree/manners/helpers_test.go new file mode 100644 index 0000000..bde3703 --- /dev/null +++ b/Godeps/_workspace/src/github.com/braintree/manners/helpers_test.go @@ -0,0 +1,118 @@ +package manners + +import ( + "bufio" + "crypto/tls" + "io/ioutil" + "net" + "net/http" + "testing" +) + +func newServer() *GracefulServer { + return NewWithServer(new(http.Server)) +} + +// a simple step-controllable http client +type client struct { + tls bool + addr net.Addr + connected chan error + sendrequest chan bool + idle chan error + idlerelease chan bool + closed chan bool +} + +func (c *client) Run() { + go func() { + var err error + conn, err := net.Dial(c.addr.Network(), c.addr.String()) + if err != nil { + c.connected <- err + return + } + if c.tls { + conn = tls.Client(conn, &tls.Config{InsecureSkipVerify: true}) + } + c.connected <- nil + for <-c.sendrequest { + _, err = conn.Write([]byte("GET / HTTP/1.1\nHost: localhost:8000\n\n")) + if err != nil { + c.idle <- err + } + // Read response; no content + scanner := bufio.NewScanner(conn) + for scanner.Scan() { + // our null handler doesn't send a body, so we know the request is + // done when we reach the blank line after the headers + if scanner.Text() == "" { + break + } + } + c.idle <- scanner.Err() + <-c.idlerelease + } + conn.Close() + ioutil.ReadAll(conn) + c.closed <- true + }() +} + +func newClient(addr net.Addr, tls bool) *client { + return &client{ + addr: addr, + tls: tls, + connected: make(chan error), + sendrequest: make(chan bool), + idle: make(chan error), + idlerelease: make(chan bool), + closed: make(chan bool), + } +} + +// a handler that returns 200 ok with no body +var nullHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + +func startGenericServer(t *testing.T, server *GracefulServer, statechanged chan http.ConnState, runner func() error) (l net.Listener, errc chan error) { + server.Addr = "localhost:0" + server.Handler = nullHandler + if statechanged != nil { + // Wrap the ConnState handler with something that will notify + // the statechanged channel when a state change happens + server.ConnState = func(conn net.Conn, newState http.ConnState) { + statechanged <- newState + } + } + + server.up = make(chan net.Listener) + exitchan := make(chan error) + + go func() { + exitchan <- runner() + }() + + // wait for server socket to be bound + select { + case l = <-server.up: + // all good + + case err := <-exitchan: + // all bad + t.Fatal("Server failed to start", err) + } + return l, exitchan +} + +func startServer(t *testing.T, server *GracefulServer, statechanged chan http.ConnState) ( + l net.Listener, errc chan error) { + return startGenericServer(t, server, statechanged, server.ListenAndServe) +} + +func startTLSServer(t *testing.T, server *GracefulServer, certFile, keyFile string, statechanged chan http.ConnState) (l net.Listener, errc chan error) { + runner := func() error { + return server.ListenAndServeTLS(certFile, keyFile) + } + + return startGenericServer(t, server, statechanged, runner) +} diff --git a/Godeps/_workspace/src/github.com/braintree/manners/interfaces.go b/Godeps/_workspace/src/github.com/braintree/manners/interfaces.go new file mode 100644 index 0000000..fd07328 --- /dev/null +++ b/Godeps/_workspace/src/github.com/braintree/manners/interfaces.go @@ -0,0 +1,7 @@ +package manners + +type waitGroup interface { + Add(int) + Done() + Wait() +} diff --git a/Godeps/_workspace/src/github.com/braintree/manners/server.go b/Godeps/_workspace/src/github.com/braintree/manners/server.go new file mode 100644 index 0000000..869fe05 --- /dev/null +++ b/Godeps/_workspace/src/github.com/braintree/manners/server.go @@ -0,0 +1,228 @@ +/* +Package manners provides a wrapper for a standard net/http server that +ensures all active HTTP client have completed their current request +before the server shuts down. + +It can be used a drop-in replacement for the standard http package, +or can wrap a pre-configured Server. + +eg. + + http.Handle("/hello", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Hello\n")) + }) + + log.Fatal(manners.ListenAndServe(":8080", nil)) + +or for a customized server: + + s := manners.NewWithServer(&http.Server{ + Addr: ":8080", + Handler: myHandler, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + MaxHeaderBytes: 1 << 20, + }) + log.Fatal(s.ListenAndServe()) + +The server will shut down cleanly when the Close() method is called: + + go func() { + sigchan := make(chan os.Signal, 1) + signal.Notify(sigchan, os.Interrupt, os.Kill) + <-sigchan + log.Info("Shutting down...") + manners.Close() + }() + + http.Handle("/hello", myHandler) + log.Fatal(manners.ListenAndServe(":8080", nil)) +*/ +package manners + +import ( + "crypto/tls" + "net" + "net/http" + "sync" + "sync/atomic" +) + +// A GracefulServer maintains a WaitGroup that counts how many in-flight +// requests the server is handling. When it receives a shutdown signal, +// it stops accepting new requests but does not actually shut down until +// all in-flight requests terminate. +// +// GracefulServer embeds the underlying net/http.Server making its non-override +// methods and properties avaiable. +// +// It must be initialized by calling NewWithServer. +type GracefulServer struct { + *http.Server + + shutdown chan bool + wg waitGroup + + lcsmu sync.RWMutex + lastConnState map[net.Conn]http.ConnState + + up chan net.Listener // Only used by test code. +} + +// NewWithServer wraps an existing http.Server object and returns a +// GracefulServer that supports all of the original Server operations. +func NewWithServer(s *http.Server) *GracefulServer { + return &GracefulServer{ + Server: s, + shutdown: make(chan bool), + wg: new(sync.WaitGroup), + lastConnState: make(map[net.Conn]http.ConnState), + } +} + +// Close stops the server from accepting new requets and begins shutting down. +// It returns true if it's the first time Close is called. +func (s *GracefulServer) Close() bool { + return <-s.shutdown +} + +// ListenAndServe provides a graceful equivalent of net/http.Serve.ListenAndServe. +func (s *GracefulServer) ListenAndServe() error { + addr := s.Addr + if addr == "" { + addr = ":http" + } + listener, err := net.Listen("tcp", addr) + if err != nil { + return err + } + + return s.Serve(listener) +} + +// ListenAndServeTLS provides a graceful equivalent of net/http.Serve.ListenAndServeTLS. +func (s *GracefulServer) ListenAndServeTLS(certFile, keyFile string) error { + // direct lift from net/http/server.go + addr := s.Addr + if addr == "" { + addr = ":https" + } + config := &tls.Config{} + if s.TLSConfig != nil { + *config = *s.TLSConfig + } + if config.NextProtos == nil { + config.NextProtos = []string{"http/1.1"} + } + + var err error + config.Certificates = make([]tls.Certificate, 1) + config.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return err + } + + ln, err := net.Listen("tcp", addr) + if err != nil { + return err + } + + return s.Serve(tls.NewListener(ln, config)) +} + +// Serve provides a graceful equivalent net/http.Server.Serve. +func (s *GracefulServer) Serve(listener net.Listener) error { + var closing int32 + + go func() { + s.shutdown <- true + close(s.shutdown) + atomic.StoreInt32(&closing, 1) + s.Server.SetKeepAlivesEnabled(false) + listener.Close() + }() + + originalConnState := s.Server.ConnState + + // s.ConnState is invoked by the net/http.Server every time a connectiion + // changes state. It keeps track of each connection's state over time, + // enabling manners to handle persisted connections correctly. + s.ConnState = func(conn net.Conn, newState http.ConnState) { + s.lcsmu.RLock() + lastConnState := s.lastConnState[conn] + s.lcsmu.RUnlock() + + switch newState { + + // New connection -> StateNew + case http.StateNew: + s.StartRoutine() + + // (StateNew, StateIdle) -> StateActive + case http.StateActive: + // The connection transitioned from idle back to active + if lastConnState == http.StateIdle { + s.StartRoutine() + } + + // StateActive -> StateIdle + // Immediately close newly idle connections; if not they may make + // one more request before SetKeepAliveEnabled(false) takes effect. + case http.StateIdle: + if atomic.LoadInt32(&closing) == 1 { + conn.Close() + } + s.FinishRoutine() + + // (StateNew, StateActive, StateIdle) -> (StateClosed, StateHiJacked) + // If the connection was idle we do not need to decrement the counter. + case http.StateClosed, http.StateHijacked: + if lastConnState != http.StateIdle { + s.FinishRoutine() + } + + } + + s.lcsmu.Lock() + if newState == http.StateClosed || newState == http.StateHijacked { + delete(s.lastConnState, conn) + } else { + s.lastConnState[conn] = newState + } + s.lcsmu.Unlock() + + if originalConnState != nil { + originalConnState(conn, newState) + } + } + + // A hook to allow the server to notify others when it is ready to receive + // requests; only used by tests. + if s.up != nil { + s.up <- listener + } + + err := s.Server.Serve(listener) + + // This block is reached when the server has received a shut down command + // or a real error happened. + if err == nil || atomic.LoadInt32(&closing) == 1 { + s.wg.Wait() + return nil + } + + return err +} + +// StartRoutine increments the server's WaitGroup. Use this if a web request +// starts more goroutines and these goroutines are not guaranteed to finish +// before the request. +func (s *GracefulServer) StartRoutine() { + s.wg.Add(1) +} + +// FinishRoutine decrements the server's WaitGroup. Use this to complement +// StartRoutine(). +func (s *GracefulServer) FinishRoutine() { + s.wg.Done() +} diff --git a/Godeps/_workspace/src/github.com/braintree/manners/server_test.go b/Godeps/_workspace/src/github.com/braintree/manners/server_test.go new file mode 100644 index 0000000..2f54eaf --- /dev/null +++ b/Godeps/_workspace/src/github.com/braintree/manners/server_test.go @@ -0,0 +1,243 @@ +package manners + +import ( + helpers "github.com/braintree/manners/test_helpers" + "net" + "net/http" + "testing" + "time" +) + +// Tests that the server allows in-flight requests to complete +// before shutting down. +func TestGracefulness(t *testing.T) { + server := newServer() + wg := helpers.NewWaitGroup() + server.wg = wg + statechanged := make(chan http.ConnState) + listener, exitchan := startServer(t, server, statechanged) + + client := newClient(listener.Addr(), false) + client.Run() + + // wait for client to connect, but don't let it send the request yet + if err := <-client.connected; err != nil { + t.Fatal("Client failed to connect to server", err) + } + // avoid a race between the client connection and the server accept + if state := <-statechanged; state != http.StateNew { + t.Fatal("Unexpected state", state) + } + + server.Close() + + waiting := <-wg.WaitCalled + if waiting < 1 { + t.Errorf("Expected the waitgroup to equal 1 at shutdown; actually %d", waiting) + } + + // allow the client to finish sending the request and make sure the server exits after + // (client will be in connected but idle state at that point) + client.sendrequest <- true + close(client.sendrequest) + if err := <-exitchan; err != nil { + t.Error("Unexpected error during shutdown", err) + } +} + +// Tests that the server begins to shut down when told to and does not accept +// new requests once shutdown has begun +func TestShutdown(t *testing.T) { + server := newServer() + wg := helpers.NewWaitGroup() + server.wg = wg + statechanged := make(chan http.ConnState) + listener, exitchan := startServer(t, server, statechanged) + + client1 := newClient(listener.Addr(), false) + client1.Run() + + // wait for client1 to connect + if err := <-client1.connected; err != nil { + t.Fatal("Client failed to connect to server", err) + } + // avoid a race between the client connection and the server accept + if state := <-statechanged; state != http.StateNew { + t.Fatal("Unexpected state", state) + } + + // start the shutdown; once it hits waitgroup.Wait() + // the listener should of been closed, though client1 is still connected + if server.Close() != true { + t.Fatal("first call to Close returned false") + } + if server.Close() != false { + t.Fatal("second call to Close returned true") + } + + waiting := <-wg.WaitCalled + if waiting != 1 { + t.Errorf("Waitcount should be one, got %d", waiting) + } + + // should get connection refused at this point + client2 := newClient(listener.Addr(), false) + client2.Run() + + if err := <-client2.connected; err == nil { + t.Fatal("client2 connected when it should of received connection refused") + } + + // let client1 finish so the server can exit + close(client1.sendrequest) // don't bother sending an actual request + + <-exitchan +} + +// Test that a connection is closed upon reaching an idle state if and only if the server +// is shutting down. +func TestCloseOnIdle(t *testing.T) { + server := newServer() + wg := helpers.NewWaitGroup() + server.wg = wg + fl := helpers.NewListener() + runner := func() error { + return server.Serve(fl) + } + + startGenericServer(t, server, nil, runner) + + // Change to idle state while server is not closing; Close should not be called + conn := &helpers.Conn{} + server.ConnState(conn, http.StateIdle) + if conn.CloseCalled { + t.Error("Close was called unexpected") + } + + server.Close() + + // wait until the server calls Close() on the listener + // by that point the atomic closing variable will have been updated, avoiding a race. + <-fl.CloseCalled + + conn = &helpers.Conn{} + server.ConnState(conn, http.StateIdle) + if !conn.CloseCalled { + t.Error("Close was not called") + } +} + +func waitForState(t *testing.T, waiter chan http.ConnState, state http.ConnState, errmsg string) { + for { + select { + case ns := <-waiter: + if ns == state { + return + } + case <-time.After(time.Second): + t.Fatal(errmsg) + } + } +} + +// Test that a request moving from active->idle->active using an actual +// network connection still results in a corect shutdown +func TestStateTransitionActiveIdleActive(t *testing.T) { + server := newServer() + wg := helpers.NewWaitGroup() + statechanged := make(chan http.ConnState) + server.wg = wg + listener, exitchan := startServer(t, server, statechanged) + + client := newClient(listener.Addr(), false) + client.Run() + + // wait for client to connect, but don't let it send the request + if err := <-client.connected; err != nil { + t.Fatal("Client failed to connect to server", err) + } + + for i := 0; i < 2; i++ { + client.sendrequest <- true + waitForState(t, statechanged, http.StateActive, "Client failed to reach active state") + <-client.idle + client.idlerelease <- true + waitForState(t, statechanged, http.StateIdle, "Client failed to reach idle state") + } + + // client is now in an idle state + + server.Close() + waiting := <-wg.WaitCalled + if waiting != 0 { + t.Errorf("Waitcount should be zero, got %d", waiting) + } + + if err := <-exitchan; err != nil { + t.Error("Unexpected error during shutdown", err) + } +} + +// Test state transitions from new->active->-idle->closed using an actual +// network connection and make sure the waitgroup count is correct at the end. +func TestStateTransitionActiveIdleClosed(t *testing.T) { + var ( + listener net.Listener + exitchan chan error + ) + + keyFile, err1 := helpers.NewTempFile(helpers.Key) + certFile, err2 := helpers.NewTempFile(helpers.Cert) + defer keyFile.Unlink() + defer certFile.Unlink() + + if err1 != nil || err2 != nil { + t.Fatal("Failed to create temporary files", err1, err2) + } + + for _, withTLS := range []bool{false, true} { + server := newServer() + wg := helpers.NewWaitGroup() + statechanged := make(chan http.ConnState) + server.wg = wg + if withTLS { + listener, exitchan = startTLSServer(t, server, certFile.Name(), keyFile.Name(), statechanged) + } else { + listener, exitchan = startServer(t, server, statechanged) + } + + client := newClient(listener.Addr(), withTLS) + client.Run() + + // wait for client to connect, but don't let it send the request + if err := <-client.connected; err != nil { + t.Fatal("Client failed to connect to server", err) + } + + client.sendrequest <- true + waitForState(t, statechanged, http.StateActive, "Client failed to reach active state") + + err := <-client.idle + if err != nil { + t.Fatalf("tls=%t unexpected error from client %s", withTLS, err) + } + + client.idlerelease <- true + waitForState(t, statechanged, http.StateIdle, "Client failed to reach idle state") + + // client is now in an idle state + close(client.sendrequest) + <-client.closed + waitForState(t, statechanged, http.StateClosed, "Client failed to reach closed state") + + server.Close() + waiting := <-wg.WaitCalled + if waiting != 0 { + t.Errorf("Waitcount should be zero, got %d", waiting) + } + + if err := <-exitchan; err != nil { + t.Error("Unexpected error during shutdown", err) + } + } +} diff --git a/Godeps/_workspace/src/github.com/braintree/manners/static.go b/Godeps/_workspace/src/github.com/braintree/manners/static.go new file mode 100644 index 0000000..2a74b09 --- /dev/null +++ b/Godeps/_workspace/src/github.com/braintree/manners/static.go @@ -0,0 +1,35 @@ +package manners + +import ( + "net" + "net/http" +) + +var defaultServer *GracefulServer + +// ListenAndServe provides a graceful version of the function provided by the +// net/http package. Call Close() to stop the server. +func ListenAndServe(addr string, handler http.Handler) error { + defaultServer = NewWithServer(&http.Server{Addr: addr, Handler: handler}) + return defaultServer.ListenAndServe() +} + +// ListenAndServeTLS provides a graceful version of the function provided by the +// net/http package. Call Close() to stop the server. +func ListenAndServeTLS(addr string, certFile string, keyFile string, handler http.Handler) error { + defaultServer = NewWithServer(&http.Server{Addr: addr, Handler: handler}) + return defaultServer.ListenAndServeTLS(certFile, keyFile) +} + +// Serve provides a graceful version of the function provided by the net/http +// package. Call Close() to stop the server. +func Serve(l net.Listener, handler http.Handler) error { + defaultServer = NewWithServer(&http.Server{Handler: handler}) + return defaultServer.Serve(l) +} + +// Shuts down the default server used by ListenAndServe, ListenAndServeTLS and +// Serve. It returns true if it's the first time Close is called. +func Close() bool { + return defaultServer.Close() +} diff --git a/Godeps/_workspace/src/github.com/braintree/manners/test_helpers/certs.go b/Godeps/_workspace/src/github.com/braintree/manners/test_helpers/certs.go new file mode 100644 index 0000000..ede248b --- /dev/null +++ b/Godeps/_workspace/src/github.com/braintree/manners/test_helpers/certs.go @@ -0,0 +1,29 @@ +package test_helpers + +// A PEM-encoded TLS cert with SAN IPs "127.0.0.1" and "[::1]", expiring at the +// last second of 2049 (the end of ASN.1 time). + +// generated from src/pkg/crypto/tls: +// go run generate_cert.go --rsa-bits 512 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h +var ( + Cert = []byte(`-----BEGIN CERTIFICATE----- +MIIBdzCCASOgAwIBAgIBADALBgkqhkiG9w0BAQUwEjEQMA4GA1UEChMHQWNtZSBD +bzAeFw03MDAxMDEwMDAwMDBaFw00OTEyMzEyMzU5NTlaMBIxEDAOBgNVBAoTB0Fj +bWUgQ28wWjALBgkqhkiG9w0BAQEDSwAwSAJBAN55NcYKZeInyTuhcCwFMhDHCmwa +IUSdtXdcbItRB/yfXGBhiex00IaLXQnSU+QZPRZWYqeTEbFSgihqi1PUDy8CAwEA +AaNoMGYwDgYDVR0PAQH/BAQDAgCkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1Ud +EwEB/wQFMAMBAf8wLgYDVR0RBCcwJYILZXhhbXBsZS5jb22HBH8AAAGHEAAAAAAA +AAAAAAAAAAAAAAEwCwYJKoZIhvcNAQEFA0EAAoQn/ytgqpiLcZu9XKbCJsJcvkgk +Se6AbGXgSlq+ZCEVo0qIwSgeBqmsJxUu7NCSOwVJLYNEBO2DtIxoYVk+MA== +-----END CERTIFICATE-----`) + + Key = []byte(`-----BEGIN RSA PRIVATE KEY----- +MIIBPAIBAAJBAN55NcYKZeInyTuhcCwFMhDHCmwaIUSdtXdcbItRB/yfXGBhiex0 +0IaLXQnSU+QZPRZWYqeTEbFSgihqi1PUDy8CAwEAAQJBAQdUx66rfh8sYsgfdcvV +NoafYpnEcB5s4m/vSVe6SU7dCK6eYec9f9wpT353ljhDUHq3EbmE4foNzJngh35d +AekCIQDhRQG5Li0Wj8TM4obOnnXUXf1jRv0UkzE9AHWLG5q3AwIhAPzSjpYUDjVW +MCUXgckTpKCuGwbJk7424Nb8bLzf3kllAiA5mUBgjfr/WtFSJdWcPQ4Zt9KTMNKD +EUO0ukpTwEIl6wIhAMbGqZK3zAAFdq8DD2jPx+UJXnh0rnOkZBzDtJ6/iN69AiEA +1Aq8MJgTaYsDQWyU/hDq5YkDJc9e9DSCvUIzqxQWMQE= +-----END RSA PRIVATE KEY-----`) +) diff --git a/Godeps/_workspace/src/github.com/braintree/manners/test_helpers/conn.go b/Godeps/_workspace/src/github.com/braintree/manners/test_helpers/conn.go new file mode 100644 index 0000000..8c610f5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/braintree/manners/test_helpers/conn.go @@ -0,0 +1,13 @@ +package test_helpers + +import "net" + +type Conn struct { + net.Conn + CloseCalled bool +} + +func (c *Conn) Close() error { + c.CloseCalled = true + return nil +} diff --git a/Godeps/_workspace/src/github.com/braintree/manners/test_helpers/listener.go b/Godeps/_workspace/src/github.com/braintree/manners/test_helpers/listener.go new file mode 100644 index 0000000..a74ac11 --- /dev/null +++ b/Godeps/_workspace/src/github.com/braintree/manners/test_helpers/listener.go @@ -0,0 +1,34 @@ +package test_helpers + +import ( + "net" + "errors" +) + +type Listener struct { + AcceptRelease chan bool + CloseCalled chan bool +} + +func NewListener() *Listener { + return &Listener{ + make(chan bool, 1), + make(chan bool, 1), + } +} + +func (l *Listener) Addr() net.Addr { + addr, _ := net.ResolveTCPAddr("tcp", "127.0.0.1:8080") + return addr +} + +func (l *Listener) Close() error { + l.CloseCalled <- true + l.AcceptRelease <- true + return nil +} + +func (l *Listener) Accept() (net.Conn, error) { + <-l.AcceptRelease + return nil, errors.New("connection closed") +} diff --git a/Godeps/_workspace/src/github.com/braintree/manners/test_helpers/temp_file.go b/Godeps/_workspace/src/github.com/braintree/manners/test_helpers/temp_file.go new file mode 100644 index 0000000..c4aa263 --- /dev/null +++ b/Godeps/_workspace/src/github.com/braintree/manners/test_helpers/temp_file.go @@ -0,0 +1,27 @@ +package test_helpers + +import ( + "io/ioutil" + "os" +) + +type TempFile struct { + *os.File +} + +func NewTempFile(content []byte) (*TempFile, error) { + f, err := ioutil.TempFile("", "graceful-test") + if err != nil { + return nil, err + } + + f.Write(content) + return &TempFile{f}, nil +} + +func (tf *TempFile) Unlink() { + if tf.File != nil { + os.Remove(tf.Name()) + tf.File = nil + } +} diff --git a/Godeps/_workspace/src/github.com/braintree/manners/test_helpers/wait_group.go b/Godeps/_workspace/src/github.com/braintree/manners/test_helpers/wait_group.go new file mode 100644 index 0000000..1df590d --- /dev/null +++ b/Godeps/_workspace/src/github.com/braintree/manners/test_helpers/wait_group.go @@ -0,0 +1,33 @@ +package test_helpers + +import "sync" + +type WaitGroup struct { + sync.Mutex + Count int + WaitCalled chan int +} + +func NewWaitGroup() *WaitGroup { + return &WaitGroup{ + WaitCalled: make(chan int, 1), + } +} + +func (wg *WaitGroup) Add(delta int) { + wg.Lock() + wg.Count++ + wg.Unlock() +} + +func (wg *WaitGroup) Done() { + wg.Lock() + wg.Count-- + wg.Unlock() +} + +func (wg *WaitGroup) Wait() { + wg.Lock() + wg.WaitCalled <- wg.Count + wg.Unlock() +} diff --git a/Godeps/_workspace/src/github.com/braintree/manners/transition_test.go b/Godeps/_workspace/src/github.com/braintree/manners/transition_test.go new file mode 100644 index 0000000..34fe5c6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/braintree/manners/transition_test.go @@ -0,0 +1,54 @@ +package manners + +import ( + helpers "github.com/braintree/manners/test_helpers" + "net/http" + "strings" + "testing" +) + +func TestStateTransitions(t *testing.T) { + tests := []transitionTest{ + transitionTest{[]http.ConnState{http.StateNew, http.StateActive}, 1}, + transitionTest{[]http.ConnState{http.StateNew, http.StateClosed}, 0}, + transitionTest{[]http.ConnState{http.StateNew, http.StateActive, http.StateClosed}, 0}, + transitionTest{[]http.ConnState{http.StateNew, http.StateActive, http.StateHijacked}, 0}, + transitionTest{[]http.ConnState{http.StateNew, http.StateActive, http.StateIdle}, 0}, + transitionTest{[]http.ConnState{http.StateNew, http.StateActive, http.StateIdle, http.StateActive}, 1}, + transitionTest{[]http.ConnState{http.StateNew, http.StateActive, http.StateIdle, http.StateActive, http.StateIdle}, 0}, + transitionTest{[]http.ConnState{http.StateNew, http.StateActive, http.StateIdle, http.StateActive, http.StateClosed}, 0}, + transitionTest{[]http.ConnState{http.StateNew, http.StateActive, http.StateIdle, http.StateActive, http.StateIdle, http.StateClosed}, 0}, + } + + for _, test := range tests { + testStateTransition(t, test) + } +} + +type transitionTest struct { + states []http.ConnState + expectedWgCount int +} + +func testStateTransition(t *testing.T, test transitionTest) { + server := newServer() + wg := helpers.NewWaitGroup() + server.wg = wg + startServer(t, server, nil) + + conn := &helpers.Conn{} + for _, newState := range test.states { + server.ConnState(conn, newState) + } + + server.Close() + waiting := <-wg.WaitCalled + if waiting != test.expectedWgCount { + names := make([]string, len(test.states)) + for i, s := range test.states { + names[i] = s.String() + } + transitions := strings.Join(names, " -> ") + t.Errorf("%s - Waitcount should be %d, got %d", transitions, test.expectedWgCount, waiting) + } +} diff --git a/LICENSE b/LICENSE index 4c490c8..7c56738 100644 --- a/LICENSE +++ b/LICENSE @@ -220,3 +220,26 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +# braintree/manners + +Copyright (c) 2014 Braintree, a division of PayPal, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file