diff --git a/README.md b/README.md index 8449b45..3586f20 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ -[![Build Status](https://travis-ci.org/dotcloud/go-redis-server.png)](https://travis-ci.org/dotcloud/go-redis-server) - Redis server protocol library ============================= diff --git a/auto.go b/auto.go index f3bece3..369d740 100644 --- a/auto.go +++ b/auto.go @@ -105,7 +105,7 @@ func (srv *Server) handlerFn(autoHandler interface{}, f *reflect.Value, checkers ret = result[0].Interface() return srv.createReply(request, ret) } - return &StatusReply{code: "OK"}, nil + return &StatusReply{Code: "OK"}, nil }, nil } @@ -148,13 +148,12 @@ func (srv *Server) createReply(r *Request, val interface{}) (ReplyWriter, error) case *MonitorReply: c := make(chan string) srv.MonitorChans = append(srv.MonitorChans, c) - println("len monitor: ", len(srv.MonitorChans)) + fmt.Println(Stderr, "len monitor: ", len(srv.MonitorChans)) v.c = c return v, nil case *ChannelWriter: return v, nil case *MultiChannelWriter: - println("New client") for _, mcw := range v.Chans { mcw.clientChan = r.ClientChan } diff --git a/auto_test.go b/auto_test.go index f506af9..d3b7e20 100644 --- a/auto_test.go +++ b/auto_test.go @@ -129,6 +129,7 @@ func TestAutoHandler(t *testing.T) { if err != nil { t.Fatalf("Unexpected error: %s", err) } + defer srv.Close() expected := []struct { request *Request expected []string diff --git a/debug.go b/debug.go index 7fd252f..9320531 100644 --- a/debug.go +++ b/debug.go @@ -2,26 +2,36 @@ package redis import ( "fmt" + "io" "os" "runtime" "strings" ) -// Debug function, if the debug flag is set, then display. Do nothing otherwise -// If Docker is in damon mode, also send the debug info on the socket -// Convenience debug function, courtesy of http://github.com/dotcloud/docker -func Debugf(format string, a ...interface{}) { +var Stderr = io.Writer(os.Stderr) + +// This closure is a no-op unless the DEBUG env is non empty. +var Debugf = func(format string, a ...interface{}) {} + +func init() { if os.Getenv("DEBUG") != "" { + Debugf = ActualDebugf + } - // Retrieve the stack infos - _, file, line, ok := runtime.Caller(1) - if !ok { - file = "" - line = -1 - } else { - file = file[strings.LastIndex(file, "/")+1:] - } +} - fmt.Fprintf(os.Stderr, fmt.Sprintf("[%d] [debug] %s:%d %s\n", os.Getpid(), file, line, format), a...) +// If Docker is in damon mode, also send the debug info on the socket +// Convenience debug function, courtesy of http://github.com/dotcloud/docker +func ActualDebugf(format string, a ...interface{}) { + // Retrieve the stack infos + _, file, line, ok := runtime.Caller(1) + if !ok { + file = "" + line = -1 + } else { + file = file[strings.LastIndex(file, "/")+1:] } + fmt.Fprintf(Stderr, "[%d] [debug] %s:%d ", os.Getpid(), file, line) + fmt.Fprintf(Stderr, format, a...) + fmt.Fprintln(Stderr) } diff --git a/defaultHandler.go b/defaultHandler.go index ac0fbbd..e64fca3 100644 --- a/defaultHandler.go +++ b/defaultHandler.go @@ -8,10 +8,11 @@ import ( ) type ( - HashValue map[string][]byte - HashHash map[string]HashValue - HashSub map[string][]*ChannelWriter - HashBrStack map[string]*Stack + HashValue map[string][]byte + HashHash map[string]HashValue + HashSub map[string][]*ChannelWriter + HashBrStack map[string]*Stack + HashOrderedSet map[string]*OrderedSet ) type Database struct { @@ -23,15 +24,18 @@ type Database struct { brstack HashBrStack sub HashSub + + orderedSet HashOrderedSet } func NewDatabase(parent *Database) *Database { db := &Database{ - values: make(HashValue), - sub: make(HashSub), - brstack: make(HashBrStack), - children: map[int]*Database{}, - parent: parent, + values: make(HashValue), + sub: make(HashSub), + brstack: make(HashBrStack), + children: map[int]*Database{}, + parent: parent, + orderedSet: make(HashOrderedSet), } db.children[0] = db return db @@ -126,6 +130,12 @@ func (h *DefaultHandler) Lrange(key string, start, stop int) ([][]byte, error) { } } + if stop < 0 { + if stop = h.brstack[key].Len() + stop; stop < 0 { + stop = 0 + } + } + var ret [][]byte for i := start; i <= stop; i++ { if val := h.brstack[key].GetIndex(i); val != nil { @@ -234,6 +244,9 @@ func (h *DefaultHandler) Hset(key, subkey string, value []byte) (int, error) { if h.Database == nil { h.Database = NewDatabase(nil) } + if h.hvalues == nil { + h.hvalues = make(HashHash) + } if _, exists := h.hvalues[key]; !exists { h.hvalues[key] = make(HashValue) ret = 1 @@ -281,16 +294,21 @@ func (h *DefaultHandler) Del(key string, keys ...string) (int, error) { delete(h.values, k) count++ } - if _, exists := h.hvalues[key]; exists { + if _, exists := h.hvalues[k]; exists { delete(h.hvalues, k) count++ } + + if _, exists := h.brstack[k]; exists { + delete(h.brstack, k) + count++ + } } return count, nil } func (h *DefaultHandler) Ping() (*StatusReply, error) { - return &StatusReply{code: "PONG"}, nil + return &StatusReply{Code: "PONG"}, nil } func (h *DefaultHandler) Subscribe(channels ...[]byte) (*MultiChannelWriter, error) { @@ -353,7 +371,7 @@ func (h *DefaultHandler) Select(key string) error { h.dbs[h.currentDb] = h.Database h.currentDb = index if _, exists := h.dbs[index]; !exists { - println("DB not exits, create ", index) + fmt.Println(Stderr, "DB not exits, create ", index) h.dbs[index] = NewDatabase(nil) } h.Database = h.dbs[index] @@ -364,6 +382,145 @@ func (h *DefaultHandler) Monitor() (*MonitorReply, error) { return &MonitorReply{}, nil } +var lock = make(chan bool, 1) + +func (h *DefaultHandler) Incr(key string) (int, error) { + if h.Database == nil { + h.Database = NewDatabase(nil) + } + + lock <- true + + temp, _ := strconv.Atoi(string(h.values[key])) + temp = temp + 1 + h.values[key] = []byte(strconv.Itoa(temp)) + + <-lock + + return temp, nil +} + +func (h *DefaultHandler) Decr(key string) (int, error) { + if h.Database == nil { + h.Database = NewDatabase(nil) + } + + lock <- true + + temp, _ := strconv.Atoi(string(h.values[key])) + temp = temp - 1 + h.values[key] = []byte(strconv.Itoa(temp)) + + <-lock + + return temp, nil +} + +func (h *DefaultHandler) Expire(key, after string) (int, error) { + if h.Database == nil { + h.Database = NewDatabase(nil) + } + + d, _ := strconv.Atoi(after) + + time.AfterFunc(time.Duration(d)*time.Second, func() { + h.Del(key) + }) + + return 1, nil +} + +func (h *DefaultHandler) Exists(key string) (int, error) { + if h.Database == nil { + h.Database = NewDatabase(nil) + } + + _, exists := h.values[key] + if exists { + return 1, nil + } else { + return 0, nil + } +} + +func (h *DefaultHandler) Zadd(key string, score int, value []byte, values ...[]byte) (int, error) { + values = append([][]byte{value}, values...) + + if h.Database == nil { + h.Database = NewDatabase(nil) + } + + if _, exists := h.orderedSet[key]; !exists { + h.orderedSet[key] = NewOrderedSet() + } + + ctr := 0 + for _, v := range values { + ctr = ctr + h.orderedSet[key].Add(score, v) + } + + return ctr, nil +} + +func (h *DefaultHandler) Zrange(key string, min int, max int) ([][]byte, error) { + if h.Database == nil { + h.Database = NewDatabase(nil) + } + + if _, exists := h.orderedSet[key]; !exists { + return [][]byte{}, nil + } + + r := h.orderedSet[key].Range(min, max) + + return r, nil +} + +func (h *DefaultHandler) Zrangebyscore(key string, min int, max int) ([][]byte, error) { + if h.Database == nil { + h.Database = NewDatabase(nil) + } + + if _, exists := h.orderedSet[key]; !exists { + return [][]byte{}, nil + } + + r := h.orderedSet[key].RangeByScore(min, max) + + return r, nil +} + +func (h *DefaultHandler) Zrem(key string, value []byte, values ...[]byte) (int, error) { + values = append([][]byte{value}, values...) + + if h.Database == nil { + h.Database = NewDatabase(nil) + } + + if _, exists := h.orderedSet[key]; !exists { + return 0, nil + } + + ctr := 0 + for _, v := range values { + ctr += h.orderedSet[key].Rem(v) + } + + return ctr, nil +} + +func (h *DefaultHandler) Zremrangebyscore(key string, min int, max int) (int, error) { + if h.Database == nil { + h.Database = NewDatabase(nil) + } + + if _, exists := h.orderedSet[key]; !exists { + return 0, nil + } + + return h.orderedSet[key].RemRangeByScore(min, max), nil +} + func NewDefaultHandler() *DefaultHandler { db := NewDatabase(nil) ret := &DefaultHandler{ diff --git a/example/main.go b/example/main.go index 97cd01d..197c1f7 100644 --- a/example/main.go +++ b/example/main.go @@ -2,7 +2,8 @@ package main import ( "fmt" - redis "github.com/dotcloud/go-redis-server" + + redis "github.com/platinasystems/go-redis-server" ) type MyHandler struct { @@ -45,7 +46,7 @@ func main() { if err := srv.RegisterFct("test2", Test2); err != nil { panic(err) } - if err := srv.ListenAndServe(); err != nil { + if err := srv.Start(); err != nil { panic(err) } } diff --git a/example/simple/main.go b/example/simple/main.go index f0ade9c..0721cb5 100644 --- a/example/simple/main.go +++ b/example/simple/main.go @@ -1,7 +1,7 @@ package main import ( - redis "github.com/dotcloud/go-redis-server" + redis "github.com/platinasystems/go-redis-server" ) func main() { @@ -9,7 +9,7 @@ func main() { if err != nil { panic(err) } - if err := server.ListenAndServe(); err != nil { + if err := server.Start(); err != nil { panic(err) } } diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8a9d0f7 --- /dev/null +++ b/go.mod @@ -0,0 +1 @@ +module github.com/platinasystems/go-redis-server diff --git a/handler.go b/handler.go index 959e520..37856f6 100644 --- a/handler.go +++ b/handler.go @@ -28,6 +28,9 @@ func (srv *Server) Register(name string, fn HandlerFn) { } func (srv *Server) Apply(r *Request) (ReplyWriter, error) { + srv.Lock() + defer srv.Unlock() + if srv == nil || srv.methods == nil { Debugf("The method map is uninitialized") return ErrMethodNotSupported, nil diff --git a/handler_test.go b/handler_test.go index 0730bca..4c7699c 100644 --- a/handler_test.go +++ b/handler_test.go @@ -9,6 +9,7 @@ func TestEmptyHandler(t *testing.T) { c := make(chan struct{}) defer close(c) srv := &Server{} + defer srv.Close() reply, err := srv.ApplyString(&Request{}) if err != nil { t.Fatalf("Unexpected error: %s", err) @@ -23,6 +24,7 @@ func TestCustomHandler(t *testing.T) { if err != nil { t.Fatal(err) } + defer srv.Close() srv.Register("GET", func(r *Request) (ReplyWriter, error) { return &BulkReply{value: []byte("42")}, nil }) diff --git a/ordered_set.go b/ordered_set.go new file mode 100644 index 0000000..f8f76fa --- /dev/null +++ b/ordered_set.go @@ -0,0 +1,160 @@ +/* + Warning: a very unsophisticated, i.e., unefficent ordered set implementation. + Should make use of binary search in the future, e.g., sort.Search. +*/ + +package redis + +type OrderedSet struct { + index map[string]bool + elements []orderedSetElement +} + +type orderedSetElement struct { + score int + value []byte +} + +func NewOrderedSet() *OrderedSet { + newSet := OrderedSet{ + index: map[string]bool{}, + elements: []orderedSetElement{}, + } + return &newSet +} + +func (self *OrderedSet) Add(score int, value []byte) int { + if _, ok := self.index[string(value)]; ok { + return 0 + } + self.index[string(value)] = true + + addIndex := 0 + for i, e := range self.elements { + if score < e.score { + addIndex = i + break + } else if i == len(self.elements)-1 { + addIndex = i + 1 + } + } + + newElement := orderedSetElement{ + score: score, + value: value, + } + + self.elements = append( + self.elements[:addIndex], append( + []orderedSetElement{newElement}, self.elements[addIndex:]...)...) + + return 1 +} + +func (self *OrderedSet) Range(lowerIndex, upperIndex int) [][]byte { + result := [][]byte{} + + lower, upper, ok := self.lowerAndUpperFromIndexes(lowerIndex, upperIndex) + if !ok { + return result + } + + for _, e := range self.elements[lower:upper] { + result = append(result, e.value) + } + + return result +} + +func (self *OrderedSet) RangeByScore(lowerScore, upperScore int) [][]byte { + result := [][]byte{} + + lower, upper, ok := self.lowerAndUpperFromScores(lowerScore, upperScore) + if !ok { + return result + } + + for _, e := range self.elements[lower:upper] { + result = append(result, e.value) + } + + return result +} + +func (self *OrderedSet) Rem(value []byte) int { + if _, ok := self.index[string(value)]; !ok { + return 0 + } + + delete(self.index, string(value)) + + remIndex := 0 + for i, e := range self.elements { + if string(e.value) == string(value) { + remIndex = i + break + } + } + + self.elements = append(self.elements[:remIndex], self.elements[remIndex+1:]...) + + return 1 +} + +func (self *OrderedSet) RemRangeByScore(lowerScore, upperScore int) int { + lower, upper, ok := self.lowerAndUpperFromScores(lowerScore, upperScore) + if !ok { + return 0 + } + + for _, e := range self.elements[lower:upper] { + delete(self.index, string(e.value)) + } + + length := len(self.elements) + + self.elements = append(self.elements[:lower], self.elements[upper:]...) + + return length - len(self.elements) +} + +func (self *OrderedSet) lowerAndUpperFromIndexes(lowerIndex, upperIndex int) (int, int, bool) { + lower := 0 + upper := 0 + + if lowerIndex < 0 { + lower = len(self.elements) + 1 + lowerIndex + } + + if upperIndex < 0 { + upper = len(self.elements) + 1 + upperIndex + } else if len(self.elements) <= upperIndex { + upper = len(self.elements) + } + + if len(self.elements) <= lower || upper < lower { + return 0, 0, false + } + + return lower, upper, true +} + +func (self *OrderedSet) lowerAndUpperFromScores(lowerScore, upperScore int) (int, int, bool) { + lower := 0 + upper := 0 + + for i, e := range self.elements { + if lowerScore >= e.score { + lower = i + 1 + } + if upperScore >= e.score { + upper = i + 1 + } + } + + if len(self.elements) <= lower || upper < lower { + return 0, 0, false + } + + return lower, upper, true +} diff --git a/parser.go b/parser.go index caaf49c..1c161e6 100644 --- a/parser.go +++ b/parser.go @@ -8,8 +8,7 @@ import ( "strings" ) -func parseRequest(conn io.ReadCloser) (*Request, error) { - r := bufio.NewReader(conn) +func parseRequest(r *bufio.Reader) (*Request, error) { // first line of redis request should be: // *CRLF line, err := r.ReadString('\n') @@ -21,7 +20,7 @@ func parseRequest(conn io.ReadCloser) (*Request, error) { // Multiline request: if line[0] == '*' { - if _, err := fmt.Sscanf(line, "*%d\r", &argsCount); err != nil { + if _, err := fmt.Sscanf(line, "*%d\r\n", &argsCount); err != nil { return nil, malformed("*", line) } // All next lines are pairs of: @@ -43,7 +42,6 @@ func parseRequest(conn io.ReadCloser) (*Request, error) { return &Request{ Name: strings.ToLower(string(firstArg)), Args: args, - Body: conn, }, nil } @@ -59,7 +57,6 @@ func parseRequest(conn io.ReadCloser) (*Request, error) { return &Request{ Name: strings.ToLower(string(fields[0])), Args: args, - Body: conn, }, nil } @@ -71,7 +68,7 @@ func readArgument(r *bufio.Reader) ([]byte, error) { return nil, malformed("$", line) } var argSize int - if _, err := fmt.Sscanf(line, "$%d\r", &argSize); err != nil { + if _, err := fmt.Sscanf(line, "$%d\r\n", &argSize); err != nil { return nil, malformed("$", line) } @@ -100,16 +97,18 @@ func readArgument(r *bufio.Reader) ([]byte, error) { } func malformed(expected string, got string) error { - Debugf("Mailformed request:'%s does not match %s\\r\\n'", got, expected) - return fmt.Errorf("Mailformed request:'%s does not match %s\\r\\n'", got, expected) + Debugf("Malformed request: %q does not match %q\n", got, expected) + return fmt.Errorf("Malformed request: %q does not match %q\r\n", + got, expected) } func malformedLength(expected int, got int) error { return fmt.Errorf( - "Mailformed request: argument length '%d does not match %d\\r\\n'", + "Malformed request: argument length %d does not match %d\r\n", got, expected) } func malformedMissingCRLF() error { - return fmt.Errorf("Mailformed request: line should end with \\r\\n") + return fmt.Errorf("Malformed request: line should end with %q\r\n", + "\r\n") } diff --git a/parser_test.go b/parser_test.go index 32a30ef..a47d04a 100644 --- a/parser_test.go +++ b/parser_test.go @@ -3,6 +3,7 @@ package redis import ( "bufio" "bytes" + "io" "io/ioutil" "strings" "testing" @@ -36,7 +37,7 @@ func TestParseBadRequests(t *testing.T) { "*2\r\n$3\r\ngEt\r\n$100\r\nx\r\n", } for _, v := range requests { - _, err := parseRequest(ioutil.NopCloser(strings.NewReader(v))) + _, err := parseRequest(bufio.NewReader(strings.NewReader(v))) if err == nil { t.Fatalf("Expected error for request [%s]", v) } @@ -55,7 +56,7 @@ func TestSucess(t *testing.T) { } for _, p := range expected { - request, err := parseRequest(ioutil.NopCloser(strings.NewReader(p.s))) + request, err := parseRequest(bufio.NewReader(strings.NewReader(p.s))) if err != nil { t.Fatalf("Un xxpected eror %s when parsting", err, p.s) } @@ -73,6 +74,22 @@ func TestSucess(t *testing.T) { } } +func TestPipielines(t *testing.T) { + chain := strings.Repeat("*2\r\n$3\r\ngEt\r\n$1\r\nx\r\n", 10) + reader := bufio.NewReader(strings.NewReader(chain)) + for i := 0; i < 10; i++ { + _, err := parseRequest(reader) + if err != nil { + t.Fatalf("Unexpected error %s when parsing", err) + } + } + _, err := parseRequest(reader) + if err != io.EOF { + t.Fatalf("Expected EOF but received %+v", err) + } + +} + func b(args ...string) [][]byte { arr := make([][]byte, len(args)) for i := 0; i < len(args); i += 1 { diff --git a/reply.go b/reply.go index fa9485a..1b1d042 100644 --- a/reply.go +++ b/reply.go @@ -11,11 +11,15 @@ import ( type ReplyWriter io.WriterTo type StatusReply struct { - code string + Code string +} + +func NewStatusReply(code string) *StatusReply { + return &StatusReply{code} } func (r *StatusReply) WriteTo(w io.Writer) (int64, error) { - n, err := w.Write([]byte("+" + r.code + "\r\n")) + n, err := w.Write([]byte("+" + r.Code + "\r\n")) return int64(n), err } @@ -93,7 +97,7 @@ func (r *MonitorReply) WriteTo(w io.Writer) (int64, error) { statusReply := &StatusReply{} totalBytes := int64(0) for line := range r.c { - statusReply.code = line + statusReply.Code = line if n, err := statusReply.WriteTo(w); err != nil { totalBytes += n return int64(totalBytes), err diff --git a/reply_test.go b/reply_test.go index e9dfff3..fc43fdf 100644 --- a/reply_test.go +++ b/reply_test.go @@ -12,7 +12,7 @@ func TestWriteStatus(t *testing.T) { reply ReplyWriter expected string }{ - {&StatusReply{code: "OK"}, "+OK\r\n"}, + {&StatusReply{Code: "OK"}, "+OK\r\n"}, {&IntegerReply{number: 42}, ":42\r\n"}, {&ErrorReply{code: "ERROR", message: "Something went wrong"}, "-ERROR Something went wrong\r\n"}, {&BulkReply{}, "$-1\r\n"}, diff --git a/request.go b/request.go index 4888e69..7a295da 100644 --- a/request.go +++ b/request.go @@ -1,7 +1,6 @@ package redis import ( - "io" "strconv" ) @@ -10,7 +9,6 @@ type Request struct { Args [][]byte Host string ClientChan chan struct{} - Body io.ReadCloser } func (r *Request) HasArgument(index int) bool { diff --git a/server.go b/server.go index c4a4131..5f6774a 100644 --- a/server.go +++ b/server.go @@ -5,35 +5,67 @@ package redis import ( + "bufio" "fmt" - "io" - "io/ioutil" + "time" + // "io" + // "io/ioutil" "net" "reflect" + "sync" ) type Server struct { - Proto string - Addr string // TCP address to listen on, ":6389" if empty + sync.Mutex + Proto string // default, "tcp" + Addr string // default, + // if Proto == unix then "/tmp/redis.sock" else ":6389" MonitorChans []chan string methods map[string]HandlerFn + listener net.Listener } -func (srv *Server) ListenAndServe() error { +func (srv *Server) listen() error { addr := srv.Addr if srv.Proto == "" { srv.Proto = "tcp" } - if srv.Proto == "unix" && addr == "" { - addr = "/tmp/redis.sock" - } else if addr == "" { - addr = ":6389" + if addr == "" { + if srv.Proto == "unix" { + addr = "/tmp/redis.sock" + } else { + addr = ":6389" + } + } + for i := 0; ; i++ { + l, e := net.Listen(srv.Proto, addr) + if e == nil { + srv.listener = l + break + } else if i < 30 { + // retry for devices that are still in ipv6 + // duplicate address detection + time.Sleep(100 * time.Millisecond) + } else { + return e + } } - l, e := net.Listen(srv.Proto, addr) - if e != nil { - return e + + // if port was 0 and proto is tcp, the listener would use a random port + srv.Addr = srv.listener.Addr().String() + return nil +} + +func (srv *Server) Start() error { + return srv.Serve(srv.listener) +} + +// Close shuts down the network port/socket +func (srv *Server) Close() error { + if srv.listener == nil { + return nil } - return srv.Serve(l) + return srv.listener.Close() } // Serve accepts incoming connections on the Listener l, creating a @@ -65,15 +97,17 @@ func (srv *Server) ServeClient(conn net.Conn) (err error) { clientChan := make(chan struct{}) // Read on `conn` in order to detect client disconnect - go func() { - // Close chan in order to trigger eventual selects - defer close(clientChan) - defer Debugf("Client disconnected") - // FIXME: move conn within the request. - if false { - io.Copy(ioutil.Discard, conn) - } - }() + /* + go func() { + // Close chan in order to trigger eventual selects + defer close(clientChan) + defer Debugf("Client disconnected") + // FIXME: move conn within the request. + if false { + io.Copy(ioutil.Discard, conn) + } + }() + */ var clientAddr string @@ -88,11 +122,16 @@ func (srv *Server) ServeClient(conn net.Conn) (err error) { clientAddr = co.RemoteAddr().String() } + reader := bufio.NewReader(conn) for { - request, err := parseRequest(conn) + request, err := parseRequest(reader) if err != nil { return err } + if request.Name == "quit" { + fmt.Fprintln(conn, "+OK") + break + } request.Host = clientAddr request.ClientChan = clientChan reply, err := srv.Apply(request) @@ -126,15 +165,19 @@ func NewServer(c *Config) (*Server, error) { rh := reflect.TypeOf(c.handler) for i := 0; i < rh.NumMethod(); i++ { method := rh.Method(i) - if method.Name[0] > 'a' && method.Name[0] < 'z' { + if method.Name[0] >= 'a' && method.Name[0] <= 'z' { continue } - println(method.Name) handlerFn, err := srv.createHandlerFn(c.handler, &method.Func) if err != nil { return nil, err } srv.Register(method.Name, handlerFn) } + + err := srv.listen() + if err != nil { + return nil, err + } return srv, nil }