diff --git a/admin.go b/admin.go index 2841ed85..9f92db8c 100644 --- a/admin.go +++ b/admin.go @@ -1,6 +1,7 @@ package caddy2 import ( + "bytes" "context" "encoding/json" "fmt" @@ -8,8 +9,10 @@ import ( "log" "net" "net/http" + "net/http/pprof" "strings" "sync" + "time" ) var ( @@ -30,6 +33,14 @@ func StartAdmin(addr string) error { mux := http.NewServeMux() mux.HandleFunc("/load", handleLoadConfig) + ///// BEGIN PPROF STUFF ////// + mux.HandleFunc("/debug/pprof/", pprof.Index) + mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) + mux.HandleFunc("/debug/pprof/profile", pprof.Profile) + mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + mux.HandleFunc("/debug/pprof/trace", pprof.Trace) + ///// END PPROF STUFF ////// + for _, m := range GetModules("admin") { moduleValue, err := m.New() if err != nil { @@ -40,7 +51,11 @@ func StartAdmin(addr string) error { } cfgEndptSrv = &http.Server{ - Handler: mux, + Handler: mux, + ReadTimeout: 5 * time.Second, + ReadHeaderTimeout: 5 * time.Second, + IdleTimeout: 5 * time.Second, + MaxHeaderBytes: 1024 * 256, } go cfgEndptSrv.Serve(ln) @@ -74,6 +89,7 @@ type AdminRoute struct { } func handleLoadConfig(w http.ResponseWriter, r *http.Request) { + r.Close = true if r.Method != "POST" { http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return @@ -94,14 +110,31 @@ func handleLoadConfig(w http.ResponseWriter, r *http.Request) { // Load loads and starts a configuration. func Load(r io.Reader) error { + buf := bufPool.Get().(*bytes.Buffer) + buf.Reset() + defer bufPool.Put(buf) + + _, err := io.Copy(buf, io.LimitReader(r, 1024*1024)) + if err != nil { + return err + } + var cfg Config - err := json.NewDecoder(r).Decode(&cfg) + err = json.Unmarshal(buf.Bytes(), &cfg) if err != nil { return fmt.Errorf("decoding config: %v", err) } + err = Start(cfg) if err != nil { return fmt.Errorf("starting: %v", err) } + return nil } + +var bufPool = sync.Pool{ + New: func() interface{} { + return new(bytes.Buffer) + }, +} diff --git a/admin_test.go b/admin_test.go new file mode 100644 index 00000000..fe2932cb --- /dev/null +++ b/admin_test.go @@ -0,0 +1,30 @@ +package caddy2 + +import ( + "strings" + "testing" +) + +func BenchmarkLoad(b *testing.B) { + for i := 0; i < b.N; i++ { + r := strings.NewReader(`{ + "testval": "Yippee!", + "modules": { + "http": { + "servers": { + "myserver": { + "listen": ["tcp/localhost:8080-8084"], + "read_timeout": "30s" + }, + "yourserver": { + "listen": ["127.0.0.1:5000"], + "read_header_timeout": "15s" + } + } + } + } + } + `) + Load(r) + } +} diff --git a/caddy.go b/caddy.go index 2f12776b..40b73c5a 100644 --- a/caddy.go +++ b/caddy.go @@ -3,10 +3,16 @@ package caddy2 import ( "encoding/json" "fmt" + "log" + "runtime/debug" "strings" + "sync" "time" ) +var currentCfg *Config +var currentCfgMu sync.Mutex + // Start runs Caddy with the given config. func Start(cfg Config) error { cfg.runners = make(map[string]Runner) @@ -26,16 +32,33 @@ func Start(cfg Config) error { for name, r := range cfg.runners { err := r.Run() if err != nil { + // TODO: If any one has an error, stop the others return fmt.Errorf("%s module: %v", name, err) } } + currentCfgMu.Lock() + if currentCfg != nil { + for _, r := range cfg.runners { + err := r.Cancel() + if err != nil { + log.Println(err) + } + } + } + currentCfg = &cfg + currentCfgMu.Unlock() + + // TODO: debugging memory leak... + debug.FreeOSMemory() + return nil } // Runner is a thing that Caddy runs. type Runner interface { Run() error + Cancel() error } // Config represents a Caddy configuration. diff --git a/cmd/caddy2/main.go b/cmd/caddy2/main.go index 9e482c14..234a88dd 100644 --- a/cmd/caddy2/main.go +++ b/cmd/caddy2/main.go @@ -5,17 +5,19 @@ import ( "bitbucket.org/lightcodelabs/caddy2" + _ "net/http/pprof" + // this is where modules get plugged in _ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp" _ "bitbucket.org/lightcodelabs/dynamicconfig" ) func main() { - err := caddy2.Start("127.0.0.1:1234") + err := caddy2.StartAdmin("127.0.0.1:1234") if err != nil { log.Fatal(err) } - defer caddy2.Stop() + defer caddy2.StopAdmin() select {} } diff --git a/listeners.go b/listeners.go index 962cb1d2..0a2fe1c2 100644 --- a/listeners.go +++ b/listeners.go @@ -3,15 +3,29 @@ package caddy2 import ( "fmt" "net" + "sync" "sync/atomic" ) // Listen returns a listener suitable for use in a Caddy module. func Listen(proto, addr string) (net.Listener, error) { + lnKey := proto + "/" + addr + + listenersMu.Lock() + defer listenersMu.Unlock() + + // if listener already exists, return it + if ln, ok := listeners[lnKey]; ok { + return &fakeCloseListener{Listener: ln}, nil + } + + // or, create new one and save it ln, err := net.Listen(proto, addr) if err != nil { return nil, err } + listeners[lnKey] = ln + return &fakeCloseListener{Listener: ln}, nil } @@ -49,3 +63,8 @@ func (fcl *fakeCloseListener) CloseUnderlying() error { // Close() is called, indicating that it is pretending to // be closed so that the server using it can terminate. var ErrSwappingServers = fmt.Errorf("listener 'closed' 😉") + +var ( + listeners = make(map[string]net.Listener) + listenersMu sync.Mutex +) diff --git a/modules/caddyhttp/caddyhttp.go b/modules/caddyhttp/caddyhttp.go index 99efef04..ec2637a7 100644 --- a/modules/caddyhttp/caddyhttp.go +++ b/modules/caddyhttp/caddyhttp.go @@ -1,6 +1,7 @@ package caddyhttp import ( + "context" "fmt" "log" "net" @@ -24,13 +25,15 @@ func init() { type httpModuleConfig struct { Servers map[string]httpServerConfig `json:"servers"` + + servers []*http.Server } func (hc *httpModuleConfig) Run() error { - fmt.Printf("RUNNING: %#v\n", hc) + // fmt.Printf("RUNNING: %#v\n", hc) for _, srv := range hc.Servers { - s := http.Server{ + s := &http.Server{ ReadTimeout: time.Duration(srv.ReadTimeout), ReadHeaderTimeout: time.Duration(srv.ReadHeaderTimeout), } @@ -53,11 +56,21 @@ func (hc *httpModuleConfig) Run() error { return nil } +func (hc *httpModuleConfig) Cancel() error { + for _, s := range hc.servers { + err := s.Shutdown(context.Background()) // TODO + if err != nil { + return err + } + } + return nil +} + func parseListenAddr(a string) (proto string, addrs []string, err error) { proto = "tcp" - if idx := strings.Index(a, ":::"); idx >= 0 { + if idx := strings.Index(a, "/"); idx >= 0 { proto = strings.ToLower(strings.TrimSpace(a[:idx])) - a = a[idx+3:] + a = a[idx+1:] } var host, port string host, port, err = net.SplitHostPort(a) diff --git a/modules/caddyhttp/caddyhttp_test.go b/modules/caddyhttp/caddyhttp_test.go index c65a9a27..610a5f04 100644 --- a/modules/caddyhttp/caddyhttp_test.go +++ b/modules/caddyhttp/caddyhttp_test.go @@ -28,22 +28,22 @@ func TestParseListenerAddr(t *testing.T) { expectAddrs: []string{":1234"}, }, { - input: "tcp::::1234", + input: "tcp/:1234", expectProto: "tcp", expectAddrs: []string{":1234"}, }, { - input: "tcp6::::1234", + input: "tcp6/:1234", expectProto: "tcp6", expectAddrs: []string{":1234"}, }, { - input: "tcp4:::localhost:1234", + input: "tcp4/localhost:1234", expectProto: "tcp4", expectAddrs: []string{"localhost:1234"}, }, { - input: "unix:::localhost:1234-1236", + input: "unix/localhost:1234-1236", expectProto: "unix", expectAddrs: []string{"localhost:1234", "localhost:1235", "localhost:1236"}, },