diff --git a/Gopkg.lock b/Gopkg.lock index 78124580..c93a5a2f 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -133,6 +133,14 @@ pruneopts = "" revision = "0b12d6b5" +[[projects]] + digest = "1:be1d3b7623f11b628068933282015f3dbcd522fa8e6b16d2931edffd42ef2c0b" + name = "github.com/kavu/go_reuseport" + packages = ["."] + pruneopts = "" + revision = "ffa96de2479e10ecd06aca8069bf9c55a86701b5" + version = "v1.4.0" + [[projects]] branch = "master" digest = "1:75dddee0eb82002b5aff6937fdf6d544b85322d2414524a521768fe4b4e5ed3d" @@ -225,6 +233,17 @@ pruneopts = "" revision = "b67b1b8c1658cb01502801c14e33c61e6c4cbb95" +[[projects]] + digest = "1:8aa59623aefb49c419e5b24179583e41df4b8c2f6a567f2cb8156a78a32e554a" + name = "github.com/tidwall/evio" + packages = [ + ".", + "internal", + ] + pruneopts = "" + revision = "3a190d6d209c66b1fee96ee3db9e70c71e3635d5" + version = "v1.0.0" + [[projects]] branch = "master" digest = "1:c5ac96e72d3ff6694602f3273dd71ef04a67c9591465aac92dc1aa8c821b8f91" @@ -449,6 +468,7 @@ "github.com/tidwall/boxtree/d2", "github.com/tidwall/btree", "github.com/tidwall/buntdb", + "github.com/tidwall/evio", "github.com/tidwall/geojson", "github.com/tidwall/geojson/geometry", "github.com/tidwall/gjson", diff --git a/Gopkg.toml b/Gopkg.toml index fd732b74..9f84f88c 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -23,7 +23,8 @@ required = [ "github.com/tidwall/lotsa", "github.com/mmcloughlin/geohash", - "github.com/tidwall/geojson" + "github.com/tidwall/geojson", + "github.com/tidwall/evio" ] [[constraint]] diff --git a/internal/client/README.md b/cmd/tile38-cli/internal/client/README.md similarity index 100% rename from internal/client/README.md rename to cmd/tile38-cli/internal/client/README.md diff --git a/internal/client/conn.go b/cmd/tile38-cli/internal/client/conn.go similarity index 100% rename from internal/client/conn.go rename to cmd/tile38-cli/internal/client/conn.go diff --git a/internal/client/conn_test.go b/cmd/tile38-cli/internal/client/conn_test.go similarity index 100% rename from internal/client/conn_test.go rename to cmd/tile38-cli/internal/client/conn_test.go diff --git a/internal/client/helper.go b/cmd/tile38-cli/internal/client/helper.go similarity index 100% rename from internal/client/helper.go rename to cmd/tile38-cli/internal/client/helper.go diff --git a/internal/client/pool.go b/cmd/tile38-cli/internal/client/pool.go similarity index 100% rename from internal/client/pool.go rename to cmd/tile38-cli/internal/client/pool.go diff --git a/internal/client/pool_test.go b/cmd/tile38-cli/internal/client/pool_test.go similarity index 100% rename from internal/client/pool_test.go rename to cmd/tile38-cli/internal/client/pool_test.go diff --git a/cmd/tile38-cli/main.go b/cmd/tile38-cli/main.go index 4d227c83..d6daf1da 100644 --- a/cmd/tile38-cli/main.go +++ b/cmd/tile38-cli/main.go @@ -16,8 +16,8 @@ import ( "github.com/peterh/liner" "github.com/tidwall/gjson" "github.com/tidwall/resp" + "github.com/tidwall/tile38/cmd/tile38-cli/internal/client" "github.com/tidwall/tile38/core" - "github.com/tidwall/tile38/internal/client" ) func userHomeDir() string { diff --git a/cmd/tile38-server/main.go b/cmd/tile38-server/main.go index 99118d52..784657e6 100644 --- a/cmd/tile38-server/main.go +++ b/cmd/tile38-server/main.go @@ -16,9 +16,9 @@ import ( "syscall" "github.com/tidwall/tile38/core" - "github.com/tidwall/tile38/internal/controller" "github.com/tidwall/tile38/internal/hservice" "github.com/tidwall/tile38/internal/log" + "github.com/tidwall/tile38/internal/server" "golang.org/x/net/context" "google.golang.org/grpc" ) @@ -79,6 +79,7 @@ Advanced Options: --queuefilename path : Event queue path (default:data/queue.db) --http-transport yes/no : HTTP transport (default: yes) --protected-mode yes/no : protected mode (default: yes) + --threads num : number of network threads (default: num cores) Developer Options: --dev : enable developer mode @@ -146,10 +147,11 @@ Developer Options: switch strings.ToLower(os.Args[i]) { case "no": core.ProtectedMode = "no" + continue case "yes": core.ProtectedMode = "yes" + continue } - continue } fmt.Fprintf(os.Stderr, "protected-mode must be 'yes' or 'no'\n") os.Exit(1) @@ -162,10 +164,11 @@ Developer Options: switch strings.ToLower(os.Args[i]) { case "no": core.AppendOnly = "no" + continue case "yes": core.AppendOnly = "yes" + continue } - continue } fmt.Fprintf(os.Stderr, "appendonly must be 'yes' or 'no'\n") os.Exit(1) @@ -189,9 +192,23 @@ Developer Options: switch strings.ToLower(os.Args[i]) { case "1", "true", "yes": httpTransport = true + continue case "0", "false", "no": httpTransport = false + continue } + } + fmt.Fprintf(os.Stderr, "http-transport must be 'yes' or 'no'\n") + os.Exit(1) + case "--threads", "-threads": + i++ + if i < len(os.Args) { + n, err := strconv.ParseUint(os.Args[i], 10, 16) + if err != nil { + fmt.Fprintf(os.Stderr, "threads must be a valid number\n") + os.Exit(1) + } + core.NumThreads = int(n) continue } fmt.Fprintf(os.Stderr, "http-transport must be 'yes' or 'no'\n") @@ -294,7 +311,7 @@ Developer Options: if pidferr != nil { log.Warnf("pidfile: %v", pidferr) } - if err := controller.ListenAndServe(host, port, dir, httpTransport); err != nil { + if err := server.Serve(host, port, dir, httpTransport); err != nil { log.Fatal(err) } } diff --git a/core/options.go b/core/options.go index 3bba1298..76172062 100644 --- a/core/options.go +++ b/core/options.go @@ -17,3 +17,6 @@ var AppendFileName string // QueueFileName allows for custom queue.db file path var QueueFileName string + +// NumThreads is the number of network threads to use. +var NumThreads int diff --git a/internal/controller/client.go b/internal/controller/client.go deleted file mode 100644 index ef7806d1..00000000 --- a/internal/controller/client.go +++ /dev/null @@ -1,250 +0,0 @@ -package controller - -import ( - "errors" - "fmt" - "net" - "sort" - "strings" - "time" - - "github.com/tidwall/resp" - "github.com/tidwall/tile38/internal/server" -) - -// Conn represents a simple resp connection. -type Conn struct { - conn net.Conn - rd *resp.Reader - wr *resp.Writer -} - -type clientConn struct { - id int - name astring - opened atime - last atime - conn *server.Conn -} - -// DialTimeout dials a resp server. -func DialTimeout(address string, timeout time.Duration) (*Conn, error) { - tcpconn, err := net.DialTimeout("tcp", address, timeout) - if err != nil { - return nil, err - } - conn := &Conn{ - conn: tcpconn, - rd: resp.NewReader(tcpconn), - wr: resp.NewWriter(tcpconn), - } - return conn, nil -} - -// Close closes the connection. -func (conn *Conn) Close() error { - conn.wr.WriteMultiBulk("quit") - return conn.conn.Close() -} - -// Do performs a command and returns a resp value. -func (conn *Conn) Do(commandName string, args ...interface{}) (val resp.Value, err error) { - if err := conn.wr.WriteMultiBulk(commandName, args...); err != nil { - return val, err - } - val, _, err = conn.rd.ReadValue() - return val, err -} - -type byID []*clientConn - -func (arr byID) Len() int { - return len(arr) -} -func (arr byID) Less(a, b int) bool { - return arr[a].id < arr[b].id -} -func (arr byID) Swap(a, b int) { - arr[a], arr[b] = arr[b], arr[a] -} -func (c *Controller) cmdClient(msg *server.Message, conn *server.Conn) (resp.Value, error) { - start := time.Now() - - if len(msg.Values) == 1 { - return server.NOMessage, errInvalidNumberOfArguments - } - switch strings.ToLower(msg.Values[1].String()) { - default: - return server.NOMessage, errors.New("Syntax error, try CLIENT " + - "(LIST | KILL | GETNAME | SETNAME)") - case "list": - if len(msg.Values) != 2 { - return server.NOMessage, errInvalidNumberOfArguments - } - var list []*clientConn - c.connsmu.RLock() - for _, cc := range c.conns { - list = append(list, cc) - } - c.connsmu.RUnlock() - sort.Sort(byID(list)) - now := time.Now() - var buf []byte - for _, cc := range list { - buf = append(buf, - fmt.Sprintf("id=%d addr=%s name=%s age=%d idle=%d\n", - cc.id, cc.conn.RemoteAddr().String(), cc.name.get(), - now.Sub(cc.opened.get())/time.Second, - now.Sub(cc.last.get())/time.Second, - )..., - ) - } - switch msg.OutputType { - case server.JSON: - return resp.StringValue(`{"ok":true,"list":` + jsonString(string(buf)) + `,"elapsed":"` + time.Now().Sub(start).String() + "\"}"), nil - case server.RESP: - return resp.BytesValue(buf), nil - } - return server.NOMessage, nil - case "getname": - if len(msg.Values) != 2 { - return server.NOMessage, errInvalidNumberOfArguments - } - name := "" - c.connsmu.RLock() - if cc, ok := c.conns[conn]; ok { - name = cc.name.get() - } - c.connsmu.RUnlock() - switch msg.OutputType { - case server.JSON: - return resp.StringValue(`{"ok":true,"name":` + jsonString(name) + `,"elapsed":"` + time.Now().Sub(start).String() + "\"}"), nil - case server.RESP: - return resp.StringValue(name), nil - } - case "setname": - if len(msg.Values) != 3 { - return server.NOMessage, errInvalidNumberOfArguments - } - name := msg.Values[2].String() - for i := 0; i < len(name); i++ { - if name[i] < '!' || name[i] > '~' { - errstr := "Client names cannot contain spaces, newlines or special characters." - return server.NOMessage, errors.New(errstr) - } - } - c.connsmu.RLock() - if cc, ok := c.conns[conn]; ok { - cc.name.set(name) - } - c.connsmu.RUnlock() - switch msg.OutputType { - case server.JSON: - return resp.StringValue(`{"ok":true,"elapsed":"` + time.Now().Sub(start).String() + "\"}"), nil - case server.RESP: - return resp.SimpleStringValue("OK"), nil - } - case "kill": - if len(msg.Values) < 3 { - return server.NOMessage, errInvalidNumberOfArguments - } - var useAddr bool - var addr string - var useID bool - var id string - for i := 2; i < len(msg.Values); i++ { - arg := msg.Values[i].String() - if strings.Contains(arg, ":") { - addr = arg - useAddr = true - break - } - switch strings.ToLower(arg) { - default: - return server.NOMessage, errors.New("No such client") - case "addr": - i++ - if i == len(msg.Values) { - return server.NOMessage, errors.New("syntax error") - } - addr = msg.Values[i].String() - useAddr = true - case "id": - i++ - if i == len(msg.Values) { - return server.NOMessage, errors.New("syntax error") - } - id = msg.Values[i].String() - useID = true - } - } - var cclose *clientConn - c.connsmu.RLock() - for _, cc := range c.conns { - if useID && fmt.Sprintf("%d", cc.id) == id { - cclose = cc - break - } else if useAddr && cc.conn.RemoteAddr().String() == addr { - cclose = cc - break - } - } - c.connsmu.RUnlock() - if cclose == nil { - return server.NOMessage, errors.New("No such client") - } - - var res resp.Value - switch msg.OutputType { - case server.JSON: - res = resp.StringValue(`{"ok":true,"elapsed":"` + time.Now().Sub(start).String() + "\"}") - case server.RESP: - res = resp.SimpleStringValue("OK") - } - - if cclose.conn == conn { - // closing self, return response now - // NOTE: This is the only exception where we do convert response to a string - var outBytes []byte - switch msg.OutputType { - case server.JSON: - outBytes = res.Bytes() - case server.RESP: - outBytes, _ = res.MarshalRESP() - } - cclose.conn.Write(outBytes) - } - cclose.conn.Close() - return res, nil - } - return server.NOMessage, errors.New("invalid output type") -} - -/* -func (c *Controller) cmdClientList(msg *server.Message) (string, error) { - - var ok bool - var key string - if vs, key, ok = tokenval(vs); !ok || key == "" { - return "", errInvalidNumberOfArguments - } - - col := c.getCol(key) - if col == nil { - if msg.OutputType == server.RESP { - return "+none\r\n", nil - } - return "", errKeyNotFound - } - - typ := "hash" - - switch msg.OutputType { - case server.JSON: - return `{"ok":true,"type":` + string(typ) + `,"elapsed":"` + time.Now().Sub(start).String() + "\"}", nil - case server.RESP: - return "+" + typ + "\r\n", nil - } - return "", nil -} -*/ diff --git a/internal/controller/controller.go b/internal/controller/controller.go deleted file mode 100644 index 5430605d..00000000 --- a/internal/controller/controller.go +++ /dev/null @@ -1,810 +0,0 @@ -package controller - -import ( - "bytes" - "crypto/rand" - "errors" - "fmt" - "io" - "net" - "os" - "path" - "path/filepath" - "runtime" - "runtime/debug" - "strconv" - "strings" - "sync" - "time" - - "github.com/tidwall/buntdb" - "github.com/tidwall/geojson" - "github.com/tidwall/geojson/geometry" - "github.com/tidwall/resp" - "github.com/tidwall/tile38/core" - "github.com/tidwall/tile38/internal/collection" - "github.com/tidwall/tile38/internal/ds" - "github.com/tidwall/tile38/internal/endpoint" - "github.com/tidwall/tile38/internal/expire" - "github.com/tidwall/tile38/internal/log" - "github.com/tidwall/tile38/internal/server" -) - -var errOOM = errors.New("OOM command not allowed when used memory > 'maxmemory'") - -const goingLive = "going live" - -const hookLogPrefix = "hook:log:" - -type commandDetailsT struct { - command string - key, id string - field string - value float64 - obj geojson.Object - fields []float64 - fmap map[string]int - oldObj geojson.Object - oldFields []float64 - updated bool - timestamp time.Time - - parent bool // when true, only children are forwarded - pattern string // PDEL key pattern - children []*commandDetailsT // for multi actions such as "PDEL" -} - -// Controller is a tile38 controller -type Controller struct { - // static values - host string - port int - http bool - dir string - started time.Time - config *Config - epc *endpoint.Manager - - // env opts - geomParseOpts geojson.ParseOptions - - // atomics - followc aint // counter increases when follow property changes - statsTotalConns aint // counter for total connections - statsTotalCommands aint // counter for total commands - statsExpired aint // item expiration counter - lastShrinkDuration aint - currentShrinkStart atime - stopBackgroundExpiring abool - stopWatchingMemory abool - stopWatchingAutoGC abool - outOfMemory abool - - connsmu sync.RWMutex - conns map[*server.Conn]*clientConn - - exlistmu sync.RWMutex - exlist []exitem - - mu sync.RWMutex - aof *os.File // active aof file - aofsz int // active size of the aof file - qdb *buntdb.DB // hook queue log - qidx uint64 // hook queue log last idx - cols ds.BTree // data collections - expires map[string]map[string]time.Time // synced with cols - - follows map[*bytes.Buffer]bool - fcond *sync.Cond - lstack []*commandDetailsT - lives map[*liveBuffer]bool - lcond *sync.Cond - fcup bool // follow caught up - fcuponce bool // follow caught up once - shrinking bool // aof shrinking flag - shrinklog [][]string // aof shrinking log - hooks map[string]*Hook // hook name - hookcols map[string]map[string]*Hook // col key - aofconnM map[net.Conn]bool - luascripts *lScriptMap - luapool *lStatePool - - pubsub *pubsub - hookex expire.List -} - -// ListenAndServe starts a new tile38 server -func ListenAndServe(host string, port int, dir string, http bool) error { - return ListenAndServeEx(host, port, dir, nil, http) -} - -// ListenAndServeEx ... -func ListenAndServeEx(host string, port int, dir string, ln *net.Listener, http bool) error { - if core.AppendFileName == "" { - core.AppendFileName = path.Join(dir, "appendonly.aof") - } - if core.QueueFileName == "" { - core.QueueFileName = path.Join(dir, "queue.db") - } - - log.Infof("Server started, Tile38 version %s, git %s", core.Version, core.GitSHA) - c := &Controller{ - host: host, - port: port, - dir: dir, - follows: make(map[*bytes.Buffer]bool), - fcond: sync.NewCond(&sync.Mutex{}), - lives: make(map[*liveBuffer]bool), - lcond: sync.NewCond(&sync.Mutex{}), - hooks: make(map[string]*Hook), - hookcols: make(map[string]map[string]*Hook), - aofconnM: make(map[net.Conn]bool), - expires: make(map[string]map[string]time.Time), - started: time.Now(), - conns: make(map[*server.Conn]*clientConn), - http: http, - pubsub: newPubsub(), - } - - c.hookex.Expired = func(item expire.Item) { - switch v := item.(type) { - case *Hook: - c.possiblyExpireHook(v.Name) - } - } - c.epc = endpoint.NewManager(c) - c.luascripts = c.newScriptMap() - c.luapool = c.newPool() - defer c.luapool.Shutdown() - - if err := os.MkdirAll(dir, 0700); err != nil { - return err - } - var err error - c.config, err = loadConfig(filepath.Join(dir, "config")) - if err != nil { - return err - } - - c.geomParseOpts = *geojson.DefaultParseOptions - n, err := strconv.ParseUint(os.Getenv("T38IDXGEOM"), 10, 32) - if err == nil { - c.geomParseOpts.IndexGeometry = int(n) - } - n, err = strconv.ParseUint(os.Getenv("T38IDXMULTI"), 10, 32) - if err == nil { - c.geomParseOpts.IndexChildren = int(n) - } - indexKind := os.Getenv("T38IDXGEOMKIND") - switch indexKind { - default: - log.Errorf("Unknown index kind: %s", indexKind) - case "": - case "None": - c.geomParseOpts.IndexGeometryKind = geometry.None - case "RTree": - c.geomParseOpts.IndexGeometryKind = geometry.RTree - case "QuadTree": - c.geomParseOpts.IndexGeometryKind = geometry.QuadTree - } - if c.geomParseOpts.IndexGeometryKind == geometry.None { - log.Debugf("Geom indexing: %s", - c.geomParseOpts.IndexGeometryKind, - ) - } else { - log.Debugf("Geom indexing: %s (%d points)", - c.geomParseOpts.IndexGeometryKind, - c.geomParseOpts.IndexGeometry, - ) - } - log.Debugf("Multi indexing: RTree (%d points)", c.geomParseOpts.IndexChildren) - - // load the queue before the aof - qdb, err := buntdb.Open(core.QueueFileName) - if err != nil { - return err - } - var qidx uint64 - if err := qdb.View(func(tx *buntdb.Tx) error { - val, err := tx.Get("hook:idx") - if err != nil { - if err == buntdb.ErrNotFound { - return nil - } - return err - } - qidx = stringToUint64(val) - return nil - }); err != nil { - return err - } - err = qdb.CreateIndex("hooks", hookLogPrefix+"*", buntdb.IndexJSONCaseSensitive("hook")) - if err != nil { - return err - } - - c.qdb = qdb - c.qidx = qidx - if err := c.migrateAOF(); err != nil { - return err - } - if core.AppendOnly == "yes" { - f, err := os.OpenFile(core.AppendFileName, os.O_CREATE|os.O_RDWR, 0600) - if err != nil { - return err - } - c.aof = f - if err := c.loadAOF(); err != nil { - return err - } - } - c.fillExpiresList() - if c.config.followHost() != "" { - go c.follow(c.config.followHost(), c.config.followPort(), c.followc.get()) - } - defer func() { - c.followc.add(1) // this will force any follow communication to die - }() - go c.processLives() - go c.watchOutOfMemory() - go c.watchLuaStatePool() - go c.watchAutoGC() - go c.backgroundExpiring() - defer func() { - c.stopBackgroundExpiring.set(true) - c.stopWatchingMemory.set(true) - c.stopWatchingAutoGC.set(true) - }() - handler := func(conn *server.Conn, msg *server.Message, rd *server.PipelineReader, w io.Writer, websocket bool) error { - c.connsmu.RLock() - if cc, ok := c.conns[conn]; ok { - cc.last.set(time.Now()) - } - c.connsmu.RUnlock() - c.statsTotalCommands.add(1) - err := c.handleInputCommand(conn, msg, w) - if err != nil { - if err.Error() == goingLive { - return c.goLive(err, conn, rd, msg, websocket) - } - return err - } - return nil - } - protected := func() bool { - if core.ProtectedMode == "no" { - // --protected-mode no - return false - } - if host != "" && host != "127.0.0.1" && host != "::1" && host != "localhost" { - // -h address - return false - } - is := c.config.protectedMode() != "no" && c.config.requirePass() == "" - return is - } - - var clientID aint - opened := func(conn *server.Conn) { - if c.config.keepAlive() > 0 { - err := conn.SetKeepAlive( - time.Duration(c.config.keepAlive()) * time.Second) - if err != nil { - log.Warnf("could not set keepalive for connection: %v", - conn.RemoteAddr().String()) - } - } - - cc := &clientConn{} - cc.id = clientID.add(1) - cc.opened.set(time.Now()) - cc.conn = conn - - c.connsmu.Lock() - c.conns[conn] = cc - c.connsmu.Unlock() - - c.statsTotalConns.add(1) - } - - closed := func(conn *server.Conn) { - c.connsmu.Lock() - delete(c.conns, conn) - c.connsmu.Unlock() - } - - return server.ListenAndServe(host, port, protected, handler, opened, closed, ln, http) -} - -func (c *Controller) watchAutoGC() { - t := time.NewTicker(time.Second) - defer t.Stop() - s := time.Now() - for range t.C { - if c.stopWatchingAutoGC.on() { - return - } - autoGC := c.config.autoGC() - if autoGC == 0 { - continue - } - if time.Now().Sub(s) < time.Second*time.Duration(autoGC) { - continue - } - var mem1, mem2 runtime.MemStats - runtime.ReadMemStats(&mem1) - log.Debugf("autogc(before): "+ - "alloc: %v, heap_alloc: %v, heap_released: %v", - mem1.Alloc, mem1.HeapAlloc, mem1.HeapReleased) - - runtime.GC() - debug.FreeOSMemory() - runtime.ReadMemStats(&mem2) - log.Debugf("autogc(after): "+ - "alloc: %v, heap_alloc: %v, heap_released: %v", - mem2.Alloc, mem2.HeapAlloc, mem2.HeapReleased) - s = time.Now() - } -} - -func (c *Controller) watchOutOfMemory() { - t := time.NewTicker(time.Second * 2) - defer t.Stop() - var mem runtime.MemStats - for range t.C { - func() { - if c.stopWatchingMemory.on() { - return - } - oom := c.outOfMemory.on() - if c.config.maxMemory() == 0 { - if oom { - c.outOfMemory.set(false) - } - return - } - if oom { - runtime.GC() - } - runtime.ReadMemStats(&mem) - c.outOfMemory.set(int(mem.HeapAlloc) > c.config.maxMemory()) - }() - } -} - -func (c *Controller) watchLuaStatePool() { - t := time.NewTicker(time.Second * 10) - defer t.Stop() - for range t.C { - func() { - c.luapool.Prune() - }() - } -} - -func (c *Controller) setCol(key string, col *collection.Collection) { - c.cols.Set(key, col) -} - -func (c *Controller) getCol(key string) *collection.Collection { - if value, ok := c.cols.Get(key); ok { - return value.(*collection.Collection) - } - return nil -} - -func (c *Controller) scanGreaterOrEqual( - key string, iterator func(key string, col *collection.Collection) bool, -) { - c.cols.Ascend(key, func(ikey string, ivalue interface{}) bool { - return iterator(ikey, ivalue.(*collection.Collection)) - }) -} - -func (c *Controller) deleteCol(key string) *collection.Collection { - if prev, ok := c.cols.Delete(key); ok { - return prev.(*collection.Collection) - } - return nil -} - -func isReservedFieldName(field string) bool { - switch field { - case "z", "lat", "lon": - return true - } - return false -} - -func (c *Controller) handleInputCommand(conn *server.Conn, msg *server.Message, w io.Writer) error { - var words []string - for _, v := range msg.Values { - words = append(words, v.String()) - } - start := time.Now() - serializeOutput := func(res resp.Value) (string, error) { - var resStr string - var err error - switch msg.OutputType { - case server.JSON: - resStr = res.String() - case server.RESP: - var resBytes []byte - resBytes, err = res.MarshalRESP() - resStr = string(resBytes) - } - return resStr, err - } - writeOutput := func(res string) error { - switch msg.ConnType { - default: - err := fmt.Errorf("unsupported conn type: %v", msg.ConnType) - log.Error(err) - return err - case server.WebSocket: - return server.WriteWebSocketMessage(w, []byte(res)) - case server.HTTP: - _, err := fmt.Fprintf(w, "HTTP/1.1 200 OK\r\n"+ - "Connection: close\r\n"+ - "Content-Length: %d\r\n"+ - "Content-Type: application/json; charset=utf-8\r\n"+ - "\r\n", len(res)+2) - if err != nil { - return err - } - _, err = io.WriteString(w, res) - if err != nil { - return err - } - _, err = io.WriteString(w, "\r\n") - return err - case server.RESP: - var err error - if msg.OutputType == server.JSON { - _, err = fmt.Fprintf(w, "$%d\r\n%s\r\n", len(res), res) - } else { - _, err = io.WriteString(w, res) - } - return err - case server.Native: - _, err := fmt.Fprintf(w, "$%d %s\r\n", len(res), res) - return err - } - } - // Ping. Just send back the response. No need to put through the pipeline. - if msg.Command == "ping" || msg.Command == "echo" { - switch msg.OutputType { - case server.JSON: - if len(msg.Values) > 1 { - return writeOutput(`{"ok":true,"` + msg.Command + `":` + jsonString(msg.Values[1].String()) + `,"elapsed":"` + time.Now().Sub(start).String() + `"}`) - } - return writeOutput(`{"ok":true,"` + msg.Command + `":"pong","elapsed":"` + time.Now().Sub(start).String() + `"}`) - case server.RESP: - if len(msg.Values) > 1 { - data, _ := msg.Values[1].MarshalRESP() - return writeOutput(string(data)) - } - return writeOutput("+PONG\r\n") - } - return nil - } - writeErr := func(errMsg string) error { - switch msg.OutputType { - case server.JSON: - return writeOutput(`{"ok":false,"err":` + jsonString(errMsg) + `,"elapsed":"` + time.Now().Sub(start).String() + "\"}") - case server.RESP: - if errMsg == errInvalidNumberOfArguments.Error() { - return writeOutput("-ERR wrong number of arguments for '" + msg.Command + "' command\r\n") - } - v, _ := resp.ErrorValue(errors.New("ERR " + errMsg)).MarshalRESP() - return writeOutput(string(v)) - } - return nil - } - - var write bool - - if !conn.Authenticated || msg.Command == "auth" { - if c.config.requirePass() != "" { - password := "" - // This better be an AUTH command or the Message should contain an Auth - if msg.Command != "auth" && msg.Auth == "" { - // Just shut down the pipeline now. The less the client connection knows the better. - return writeErr("authentication required") - } - if msg.Auth != "" { - password = msg.Auth - } else { - if len(msg.Values) > 1 { - password = msg.Values[1].String() - } - } - if c.config.requirePass() != strings.TrimSpace(password) { - return writeErr("invalid password") - } - conn.Authenticated = true - if msg.ConnType != server.HTTP { - resStr, _ := serializeOutput(server.OKMessage(msg, start)) - return writeOutput(resStr) - } - } else if msg.Command == "auth" { - return writeErr("invalid password") - } - } - // choose the locking strategy - switch msg.Command { - default: - c.mu.RLock() - defer c.mu.RUnlock() - case "set", "del", "drop", "fset", "flushdb", - "setchan", "pdelchan", "delchan", - "sethook", "pdelhook", "delhook", - "expire", "persist", "jset", "pdel": - // write operations - write = true - c.mu.Lock() - defer c.mu.Unlock() - if c.config.followHost() != "" { - return writeErr("not the leader") - } - if c.config.readOnly() { - return writeErr("read only") - } - case "eval", "evalsha": - // write operations (potentially) but no AOF for the script command itself - c.mu.Lock() - defer c.mu.Unlock() - if c.config.followHost() != "" { - return writeErr("not the leader") - } - if c.config.readOnly() { - return writeErr("read only") - } - case "get", "keys", "scan", "nearby", "within", "intersects", "hooks", - "chans", "search", "ttl", "bounds", "server", "info", "type", "jget", - "evalro", "evalrosha": - // read operations - c.mu.RLock() - defer c.mu.RUnlock() - if c.config.followHost() != "" && !c.fcuponce { - return writeErr("catching up to leader") - } - case "follow", "readonly", "config": - // system operations - // does not write to aof, but requires a write lock. - c.mu.Lock() - defer c.mu.Unlock() - case "output": - // this is local connection operation. Locks not needed. - case "echo": - case "massinsert": - // dev operation - c.mu.Lock() - defer c.mu.Unlock() - case "sleep": - // dev operation - c.mu.RLock() - defer c.mu.RUnlock() - case "shutdown": - // dev operation - c.mu.Lock() - defer c.mu.Unlock() - case "aofshrink": - c.mu.RLock() - defer c.mu.RUnlock() - case "client": - c.mu.Lock() - defer c.mu.Unlock() - case "evalna", "evalnasha": - // No locking for scripts, otherwise writes cannot happen within scripts - case "subscribe", "psubscribe", "publish": - // No locking for pubsub - } - - res, d, err := c.command(msg, w, conn) - - if res.Type() == resp.Error { - return writeErr(res.String()) - } - if err != nil { - if err.Error() == goingLive { - return err - } - return writeErr(err.Error()) - } - if write { - if err := c.writeAOF(resp.ArrayValue(msg.Values), &d); err != nil { - if _, ok := err.(errAOFHook); ok { - return writeErr(err.Error()) - } - log.Fatal(err) - return err - } - } - - if !isRespValueEmptyString(res) { - var resStr string - resStr, err := serializeOutput(res) - if err != nil { - return err - } - if err := writeOutput(resStr); err != nil { - return err - } - } - - return nil -} - -func isRespValueEmptyString(val resp.Value) bool { - return !val.IsNull() && (val.Type() == resp.SimpleString || val.Type() == resp.BulkString) && len(val.Bytes()) == 0 -} - -func randomKey(n int) string { - b := make([]byte, n) - nn, err := rand.Read(b) - if err != nil { - panic(err) - } - if nn != n { - panic("random failed") - } - return fmt.Sprintf("%x", b) -} - -func (c *Controller) reset() { - c.aofsz = 0 - c.cols = ds.BTree{} - c.exlistmu.Lock() - c.exlist = nil - c.exlistmu.Unlock() - c.expires = make(map[string]map[string]time.Time) -} - -func (c *Controller) command( - msg *server.Message, w io.Writer, conn *server.Conn, -) ( - res resp.Value, d commandDetailsT, err error, -) { - switch msg.Command { - default: - err = fmt.Errorf("unknown command '%s'", msg.Values[0]) - case "set": - res, d, err = c.cmdSet(msg) - case "fset": - res, d, err = c.cmdFset(msg) - case "del": - res, d, err = c.cmdDel(msg) - case "pdel": - res, d, err = c.cmdPdel(msg) - case "drop": - res, d, err = c.cmdDrop(msg) - case "flushdb": - res, d, err = c.cmdFlushDB(msg) - - case "sethook": - res, d, err = c.cmdSetHook(msg, false) - case "delhook": - res, d, err = c.cmdDelHook(msg, false) - case "pdelhook": - res, d, err = c.cmdPDelHook(msg, false) - case "hooks": - res, err = c.cmdHooks(msg, false) - - case "setchan": - res, d, err = c.cmdSetHook(msg, true) - case "delchan": - res, d, err = c.cmdDelHook(msg, true) - case "pdelchan": - res, d, err = c.cmdPDelHook(msg, true) - case "chans": - res, err = c.cmdHooks(msg, true) - - case "expire": - res, d, err = c.cmdExpire(msg) - case "persist": - res, d, err = c.cmdPersist(msg) - case "ttl": - res, err = c.cmdTTL(msg) - case "shutdown": - if !core.DevMode { - err = fmt.Errorf("unknown command '%s'", msg.Values[0]) - return - } - log.Fatal("shutdown requested by developer") - case "massinsert": - if !core.DevMode { - err = fmt.Errorf("unknown command '%s'", msg.Values[0]) - return - } - res, err = c.cmdMassInsert(msg) - case "sleep": - if !core.DevMode { - err = fmt.Errorf("unknown command '%s'", msg.Values[0]) - return - } - res, err = c.cmdSleep(msg) - case "follow": - res, err = c.cmdFollow(msg) - case "readonly": - res, err = c.cmdReadOnly(msg) - case "stats": - res, err = c.cmdStats(msg) - case "server": - res, err = c.cmdServer(msg) - case "info": - res, err = c.cmdInfo(msg) - case "scan": - res, err = c.cmdScan(msg) - case "nearby": - res, err = c.cmdNearby(msg) - case "within": - res, err = c.cmdWithin(msg) - case "intersects": - res, err = c.cmdIntersects(msg) - case "search": - res, err = c.cmdSearch(msg) - case "bounds": - res, err = c.cmdBounds(msg) - case "get": - res, err = c.cmdGet(msg) - case "jget": - res, err = c.cmdJget(msg) - case "jset": - res, d, err = c.cmdJset(msg) - case "jdel": - res, d, err = c.cmdJdel(msg) - case "type": - res, err = c.cmdType(msg) - case "keys": - res, err = c.cmdKeys(msg) - case "output": - res, err = c.cmdOutput(msg) - case "aof": - res, err = c.cmdAOF(msg) - case "aofmd5": - res, err = c.cmdAOFMD5(msg) - case "gc": - runtime.GC() - debug.FreeOSMemory() - res = server.OKMessage(msg, time.Now()) - case "aofshrink": - go c.aofshrink() - res = server.OKMessage(msg, time.Now()) - case "config get": - res, err = c.cmdConfigGet(msg) - case "config set": - res, err = c.cmdConfigSet(msg) - case "config rewrite": - res, err = c.cmdConfigRewrite(msg) - case "config", "script": - // These get rewritten into "config foo" and "script bar" - err = fmt.Errorf("unknown command '%s'", msg.Values[0]) - if len(msg.Values) > 1 { - command := msg.Values[0].String() + " " + msg.Values[1].String() - msg.Values[1] = resp.StringValue(command) - msg.Values = msg.Values[1:] - msg.Command = strings.ToLower(command) - return c.command(msg, w, conn) - } - case "client": - res, err = c.cmdClient(msg, conn) - case "eval", "evalro", "evalna": - res, err = c.cmdEvalUnified(false, msg) - case "evalsha", "evalrosha", "evalnasha": - res, err = c.cmdEvalUnified(true, msg) - case "script load": - res, err = c.cmdScriptLoad(msg) - case "script exists": - res, err = c.cmdScriptExists(msg) - case "script flush": - res, err = c.cmdScriptFlush(msg) - case "subscribe": - res, err = c.cmdSubscribe(msg) - case "psubscribe": - res, err = c.cmdPsubscribe(msg) - case "publish": - res, err = c.cmdPublish(msg) - } - return -} diff --git a/internal/controller/aof.go b/internal/server/aof.go similarity index 64% rename from internal/controller/aof.go rename to internal/server/aof.go index fa248e74..ef127b0c 100644 --- a/internal/controller/aof.go +++ b/internal/server/aof.go @@ -1,4 +1,4 @@ -package controller +package server import ( "errors" @@ -15,11 +15,8 @@ import ( "github.com/tidwall/redcon" "github.com/tidwall/resp" "github.com/tidwall/tile38/internal/log" - "github.com/tidwall/tile38/internal/server" ) -// AsyncHooks indicates that the hooks should happen in the background. - type errAOFHook struct { err error } @@ -30,8 +27,8 @@ func (err errAOFHook) Error() string { var errInvalidAOF = errors.New("invalid aof file") -func (c *Controller) loadAOF() error { - fi, err := c.aof.Stat() +func (server *Server) loadAOF() error { + fi, err := server.aof.Stat() if err != nil { return err } @@ -56,9 +53,9 @@ func (c *Controller) loadAOF() error { var buf []byte var args [][]byte var packet [0xFFFF]byte - var msg server.Message + var msg Message for { - n, err := c.aof.Read(packet[:]) + n, err := server.aof.Read(packet[:]) if err != nil { if err == io.EOF { if len(buf) > 0 { @@ -68,7 +65,7 @@ func (c *Controller) loadAOF() error { } return err } - c.aofsz += n + server.aofsz += n data := packet[:n] if len(buf) > 0 { data = append(buf, data...) @@ -83,12 +80,11 @@ func (c *Controller) loadAOF() error { break } if len(args) > 0 { - msg.Values = msg.Values[:0] + msg.Args = msg.Args[:0] for _, arg := range args { - msg.Values = append(msg.Values, resp.BytesValue(arg)) + msg.Args = append(msg.Args, string(arg)) } - msg.Command = qlower(args[0]) - if _, _, err := c.command(&msg, nil, nil); err != nil { + if _, _, err := server.command(&msg, nil); err != nil { if commandErrIsFatal(err) { return err } @@ -104,22 +100,22 @@ func (c *Controller) loadAOF() error { } } -func qlower(s []byte) string { - if len(s) == 3 { - if s[0] == 'S' && s[1] == 'E' && s[2] == 'T' { - return "set" - } - if s[0] == 'D' && s[1] == 'E' && s[2] == 'L' { - return "del" - } - } - for i := 0; i < len(s); i++ { - if s[i] >= 'A' || s[i] <= 'Z' { - return strings.ToLower(string(s)) - } - } - return string(s) -} +// func qlower(s []byte) string { +// if len(s) == 3 { +// if s[0] == 'S' && s[1] == 'E' && s[2] == 'T' { +// return "set" +// } +// if s[0] == 'D' && s[1] == 'E' && s[2] == 'L' { +// return "del" +// } +// } +// for i := 0; i < len(s); i++ { +// if s[i] >= 'A' || s[i] <= 'Z' { +// return strings.ToLower(string(s)) +// } +// } +// return string(s) +// } func commandErrIsFatal(err error) bool { // FSET (and other writable commands) may return errors that we need @@ -132,83 +128,91 @@ func commandErrIsFatal(err error) bool { return true } -func (c *Controller) writeAOF(value resp.Value, d *commandDetailsT) error { +func (server *Server) flushAOF() { + if len(server.aofbuf) > 0 { + _, err := server.aof.Write(server.aofbuf) + if err != nil { + panic(err) + } + server.aofbuf = server.aofbuf[:0] + } +} + +func (server *Server) writeAOF(args []string, d *commandDetailsT) error { + if d != nil && !d.updated { // just ignore writes if the command did not update return nil } - if c.shrinking { - var values []string - for _, value := range value.Array() { - values = append(values, value.String()) - } - c.shrinklog = append(c.shrinklog, values) + + if server.shrinking { + nargs := make([]string, len(args)) + copy(nargs, args) + server.shrinklog = append(server.shrinklog, nargs) } - data, err := value.MarshalRESP() - if err != nil { - return err - } - if c.aof != nil { - n, err := c.aof.Write(data) - if err != nil { - return err + + if server.aof != nil { + n := len(server.aofbuf) + server.aofbuf = redcon.AppendArray(server.aofbuf, len(args)) + for _, arg := range args { + server.aofbuf = redcon.AppendBulkString(server.aofbuf, arg) } - c.aofsz += n + server.aofsz += len(server.aofbuf) - n } // notify aof live connections that we have new data - c.fcond.L.Lock() - c.fcond.Broadcast() - c.fcond.L.Unlock() + server.fcond.L.Lock() + server.fcond.Broadcast() + server.fcond.L.Unlock() // process geofences if d != nil { // webhook geofences - if c.config.followHost() == "" { + if server.config.followHost() == "" { // for leader only if d.parent { // queue children for _, d := range d.children { - if err := c.queueHooks(d); err != nil { + if err := server.queueHooks(d); err != nil { return err } } } else { // queue parent - if err := c.queueHooks(d); err != nil { + if err := server.queueHooks(d); err != nil { return err } } } // live geofences - c.lcond.L.Lock() + server.lcond.L.Lock() if d.parent { // queue children for _, d := range d.children { - c.lstack = append(c.lstack, d) + server.lstack = append(server.lstack, d) } } else { // queue parent - c.lstack = append(c.lstack, d) + server.lstack = append(server.lstack, d) } - c.lcond.Broadcast() - c.lcond.L.Unlock() + server.lcond.Broadcast() + server.lcond.L.Unlock() } return nil } -func (c *Controller) queueHooks(d *commandDetailsT) error { +func (server *Server) queueHooks(d *commandDetailsT) error { // big list of all of the messages var hmsgs []string var hooks []*Hook // find the hook by the key - if hm, ok := c.hookcols[d.key]; ok { + if hm, ok := server.hookcols[d.key]; ok { for _, hook := range hm { // match the fence msgs := FenceMatch(hook.Name, hook.ScanWriter, hook.Fence, hook.Metas, d) if len(msgs) > 0 { if hook.channel { - c.Publish(hook.Name, msgs...) + server.Publish(hook.Name, msgs...) } else { // append each msg to the big list hmsgs = append(hmsgs, msgs...) @@ -222,17 +226,17 @@ func (c *Controller) queueHooks(d *commandDetailsT) error { } // queue the message in the buntdb database - err := c.qdb.Update(func(tx *buntdb.Tx) error { + err := server.qdb.Update(func(tx *buntdb.Tx) error { for _, msg := range hmsgs { - c.qidx++ // increment the log id - key := hookLogPrefix + uint64ToString(c.qidx) + server.qidx++ // increment the log id + key := hookLogPrefix + uint64ToString(server.qidx) _, _, err := tx.Set(key, string(msg), hookLogSetDefaults) if err != nil { return err } - log.Debugf("queued hook: %d", c.qidx) + log.Debugf("queued hook: %d", server.qidx) } - _, _, err := tx.Set("hook:idx", uint64ToString(c.qidx), nil) + _, _, err := tx.Set("hook:idx", uint64ToString(server.qidx), nil) if err != nil { return err } @@ -269,86 +273,86 @@ func (s liveAOFSwitches) Error() string { return goingLive } -func (c *Controller) cmdAOFMD5(msg *server.Message) (res resp.Value, err error) { +func (server *Server) cmdAOFMD5(msg *Message) (res resp.Value, err error) { start := time.Now() - vs := msg.Values[1:] + vs := msg.Args[1:] var ok bool var spos, ssize string if vs, spos, ok = tokenval(vs); !ok || spos == "" { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } if vs, ssize, ok = tokenval(vs); !ok || ssize == "" { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } if len(vs) != 0 { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } pos, err := strconv.ParseInt(spos, 10, 64) if err != nil || pos < 0 { - return server.NOMessage, errInvalidArgument(spos) + return NOMessage, errInvalidArgument(spos) } size, err := strconv.ParseInt(ssize, 10, 64) if err != nil || size < 0 { - return server.NOMessage, errInvalidArgument(ssize) + return NOMessage, errInvalidArgument(ssize) } - sum, err := c.checksum(pos, size) + sum, err := server.checksum(pos, size) if err != nil { - return server.NOMessage, err + return NOMessage, err } switch msg.OutputType { - case server.JSON: + case JSON: res = resp.StringValue( fmt.Sprintf(`{"ok":true,"md5":"%s","elapsed":"%s"}`, sum, time.Now().Sub(start))) - case server.RESP: + case RESP: res = resp.SimpleStringValue(sum) } return res, nil } -func (c *Controller) cmdAOF(msg *server.Message) (res resp.Value, err error) { - if c.aof == nil { - return server.NOMessage, errors.New("aof disabled") +func (server *Server) cmdAOF(msg *Message) (res resp.Value, err error) { + if server.aof == nil { + return NOMessage, errors.New("aof disabled") } - vs := msg.Values[1:] + vs := msg.Args[1:] var ok bool var spos string if vs, spos, ok = tokenval(vs); !ok || spos == "" { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } if len(vs) != 0 { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } pos, err := strconv.ParseInt(spos, 10, 64) if err != nil || pos < 0 { - return server.NOMessage, errInvalidArgument(spos) + return NOMessage, errInvalidArgument(spos) } - f, err := os.Open(c.aof.Name()) + f, err := os.Open(server.aof.Name()) if err != nil { - return server.NOMessage, err + return NOMessage, err } defer f.Close() n, err := f.Seek(0, 2) if err != nil { - return server.NOMessage, err + return NOMessage, err } if n < pos { - return server.NOMessage, errors.New("pos is too big, must be less that the aof_size of leader") + return NOMessage, errors.New("pos is too big, must be less that the aof_size of leader") } var s liveAOFSwitches s.pos = pos - return server.NOMessage, s + return NOMessage, s } -func (c *Controller) liveAOF(pos int64, conn net.Conn, rd *server.PipelineReader, msg *server.Message) error { - c.mu.Lock() - c.aofconnM[conn] = true - c.mu.Unlock() +func (server *Server) liveAOF(pos int64, conn net.Conn, rd *PipelineReader, msg *Message) error { + server.mu.Lock() + server.aofconnM[conn] = true + server.mu.Unlock() defer func() { - c.mu.Lock() - delete(c.aofconnM, conn) - c.mu.Unlock() + server.mu.Lock() + delete(server.aofconnM, conn) + server.mu.Unlock() conn.Close() }() @@ -356,9 +360,9 @@ func (c *Controller) liveAOF(pos int64, conn net.Conn, rd *server.PipelineReader return err } - c.mu.RLock() - f, err := os.Open(c.aof.Name()) - c.mu.RUnlock() + server.mu.RLock() + f, err := os.Open(server.aof.Name()) + server.mu.RUnlock() if err != nil { return err } @@ -384,7 +388,7 @@ func (c *Controller) liveAOF(pos int64, conn net.Conn, rd *server.PipelineReader return } for _, v := range vs { - switch v.Command { + switch v.Command() { default: log.Error("received a live command that was not QUIT") return @@ -420,9 +424,9 @@ func (c *Controller) liveAOF(pos int64, conn net.Conn, rd *server.PipelineReader } continue } - c.fcond.L.Lock() - c.fcond.Wait() - c.fcond.L.Unlock() + server.fcond.L.Lock() + server.fcond.Wait() + server.fcond.L.Unlock() } }() if err != nil { diff --git a/internal/controller/aofmigrate.go b/internal/server/aofmigrate.go similarity index 98% rename from internal/controller/aofmigrate.go rename to internal/server/aofmigrate.go index 2171df23..d23524ab 100644 --- a/internal/controller/aofmigrate.go +++ b/internal/server/aofmigrate.go @@ -1,4 +1,4 @@ -package controller +package server import ( "bufio" @@ -83,7 +83,7 @@ func NewLegacyAOFReader(r io.Reader) *LegacyAOFReader { return rd } -func (c *Controller) migrateAOF() error { +func (c *Server) migrateAOF() error { _, err := os.Stat(path.Join(c.dir, "appendonly.aof")) if err == nil { return nil diff --git a/internal/controller/aofshrink.go b/internal/server/aofshrink.go similarity index 82% rename from internal/controller/aofshrink.go rename to internal/server/aofshrink.go index e7a3eff6..999b26a4 100644 --- a/internal/controller/aofshrink.go +++ b/internal/server/aofshrink.go @@ -1,4 +1,4 @@ -package controller +package server import ( "math" @@ -8,9 +8,9 @@ import ( "strings" "time" + "github.com/tidwall/geojson" "github.com/tidwall/tile38/core" "github.com/tidwall/tile38/internal/collection" - "github.com/tidwall/geojson" "github.com/tidwall/tile38/internal/log" ) @@ -18,25 +18,25 @@ const maxkeys = 8 const maxids = 32 const maxchunk = 4 * 1024 * 1024 -func (c *Controller) aofshrink() { - if c.aof == nil { +func (server *Server) aofshrink() { + if server.aof == nil { return } start := time.Now() - c.mu.Lock() - if c.shrinking { - c.mu.Unlock() + server.mu.Lock() + if server.shrinking { + server.mu.Unlock() return } - c.shrinking = true - c.shrinklog = nil - c.mu.Unlock() + server.shrinking = true + server.shrinklog = nil + server.mu.Unlock() defer func() { - c.mu.Lock() - c.shrinking = false - c.shrinklog = nil - c.mu.Unlock() + server.mu.Lock() + server.shrinking = false + server.shrinklog = nil + server.mu.Unlock() log.Infof("aof shrink ended %v", time.Now().Sub(start)) return }() @@ -60,9 +60,9 @@ func (c *Controller) aofshrink() { } keysdone = true func() { - c.mu.Lock() - defer c.mu.Unlock() - c.scanGreaterOrEqual(nextkey, func(key string, col *collection.Collection) bool { + server.mu.Lock() + defer server.mu.Unlock() + server.scanGreaterOrEqual(nextkey, func(key string, col *collection.Collection) bool { if len(keys) == maxkeys { keysdone = false nextkey = key @@ -86,16 +86,16 @@ func (c *Controller) aofshrink() { // load more objects func() { idsdone = true - c.mu.Lock() - defer c.mu.Unlock() - col := c.getCol(keys[0]) + server.mu.Lock() + defer server.mu.Unlock() + col := server.getCol(keys[0]) if col == nil { return } - var fnames = col.FieldArr() // reload an array of field names to match each object - var exm = c.expires[keys[0]] // the expiration map - var now = time.Now() // used for expiration - var count = 0 // the object count + var fnames = col.FieldArr() // reload an array of field names to match each object + var exm = server.expires[keys[0]] // the expiration map + var now = time.Now() // used for expiration + var count = 0 // the object count col.ScanGreaterOrEqual(nextid, false, func(id string, obj geojson.Object, fields []float64) bool { if count == maxids { @@ -167,9 +167,9 @@ func (c *Controller) aofshrink() { // first load the names of the hooks var hnames []string func() { - c.mu.Lock() - defer c.mu.Unlock() - for name := range c.hooks { + server.mu.Lock() + defer server.mu.Unlock() + for name := range server.hooks { hnames = append(hnames, name) } }() @@ -177,9 +177,9 @@ func (c *Controller) aofshrink() { sort.Strings(hnames) for _, name := range hnames { func() { - c.mu.Lock() - defer c.mu.Unlock() - hook := c.hooks[name] + server.mu.Lock() + defer server.mu.Unlock() + hook := server.hooks[name] if hook == nil { return } @@ -203,8 +203,8 @@ func (c *Controller) aofshrink() { values = append(values, "ex", strconv.FormatFloat(ex, 'f', 1, 64)) } - for _, value := range hook.Message.Values { - values = append(values, value.String()) + for _, value := range hook.Message.Args { + values = append(values, value) } // append the values to the aof buffer aofbuf = append(aofbuf, '*') @@ -232,10 +232,14 @@ func (c *Controller) aofshrink() { // finally grab any new data that may have been written since // the aofshrink has started and swap out the files. return func() error { - c.mu.Lock() - defer c.mu.Unlock() + server.mu.Lock() + defer server.mu.Unlock() + + // flush the aof buffer + server.flushAOF() + aofbuf = aofbuf[:0] - for _, values := range c.shrinklog { + for _, values := range server.shrinklog { // append the values to the aof buffer aofbuf = append(aofbuf, '*') aofbuf = append(aofbuf, strconv.FormatInt(int64(len(values)), 10)...) @@ -260,7 +264,7 @@ func (c *Controller) aofshrink() { // anything below this point is unrecoverable. just log and exit process // back up the live aof, just in case of fatal error - if err := c.aof.Close(); err != nil { + if err := server.aof.Close(); err != nil { log.Fatalf("shrink live aof close fatal operation: %v", err) } if err := f.Close(); err != nil { @@ -272,21 +276,21 @@ func (c *Controller) aofshrink() { if err := os.Rename(core.AppendFileName+"-shrink", core.AppendFileName); err != nil { log.Fatalf("shrink rename fatal operation: %v", err) } - c.aof, err = os.OpenFile(core.AppendFileName, os.O_CREATE|os.O_RDWR, 0600) + server.aof, err = os.OpenFile(core.AppendFileName, os.O_CREATE|os.O_RDWR, 0600) if err != nil { log.Fatalf("shrink openfile fatal operation: %v", err) } var n int64 - n, err = c.aof.Seek(0, 2) + n, err = server.aof.Seek(0, 2) if err != nil { log.Fatalf("shrink seek end fatal operation: %v", err) } - c.aofsz = int(n) + server.aofsz = int(n) os.Remove(core.AppendFileName + "-bak") // ignore error // kill all followers connections - for conn := range c.aofconnM { + for conn := range server.aofconnM { conn.Close() } return nil diff --git a/internal/controller/atomic.go b/internal/server/atomic.go similarity index 98% rename from internal/controller/atomic.go rename to internal/server/atomic.go index 882bb253..706e2cdf 100644 --- a/internal/controller/atomic.go +++ b/internal/server/atomic.go @@ -1,4 +1,4 @@ -package controller +package server import ( "sync" diff --git a/internal/controller/atomic_test.go b/internal/server/atomic_test.go similarity index 94% rename from internal/controller/atomic_test.go rename to internal/server/atomic_test.go index b26152f6..6ed7cc97 100644 --- a/internal/controller/atomic_test.go +++ b/internal/server/atomic_test.go @@ -1,4 +1,4 @@ -package controller +package server import "testing" diff --git a/internal/controller/bson.go b/internal/server/bson.go similarity index 97% rename from internal/controller/bson.go rename to internal/server/bson.go index d5a89b76..bcc9d2ea 100644 --- a/internal/controller/bson.go +++ b/internal/server/bson.go @@ -1,4 +1,4 @@ -package controller +package server import ( "crypto/md5" diff --git a/internal/controller/checksum.go b/internal/server/checksum.go similarity index 93% rename from internal/controller/checksum.go rename to internal/server/checksum.go index 246c7b3c..dddfa9f9 100644 --- a/internal/controller/checksum.go +++ b/internal/server/checksum.go @@ -1,4 +1,4 @@ -package controller +package server import ( "crypto/md5" @@ -14,7 +14,7 @@ import ( ) // checksum performs a simple md5 checksum on the aof file -func (c *Controller) checksum(pos, size int64) (sum string, err error) { +func (c *Server) checksum(pos, size int64) (sum string, err error) { if pos+size > int64(c.aofsz) { return "", io.EOF } @@ -55,7 +55,7 @@ func (c *Controller) checksum(pos, size int64) (sum string, err error) { return fmt.Sprintf("%x", sumr.Sum(nil)), nil } -func connAOFMD5(conn *Conn, pos, size int64) (sum string, err error) { +func connAOFMD5(conn *RESPConn, pos, size int64) (sum string, err error) { v, err := conn.Do("aofmd5", pos, size) if err != nil { return "", err @@ -74,7 +74,7 @@ func connAOFMD5(conn *Conn, pos, size int64) (sum string, err error) { return sum, nil } -func (c *Controller) matchChecksums(conn *Conn, pos, size int64) (match bool, err error) { +func (c *Server) matchChecksums(conn *RESPConn, pos, size int64) (match bool, err error) { sum, err := c.checksum(pos, size) if err != nil { if err == io.EOF { @@ -138,7 +138,7 @@ func getEndOfLastValuePositionInFile(fname string, startPos int64) (int64, error // followCheckSome is not a full checksum. It just "checks some" data. // We will do some various checksums on the leader until we find the correct position to start at. -func (c *Controller) followCheckSome(addr string, followc int) (pos int64, err error) { +func (c *Server) followCheckSome(addr string, followc int) (pos int64, err error) { if core.ShowDebugMessages { log.Debug("follow:", addr, ":check some") } diff --git a/internal/server/client.go b/internal/server/client.go new file mode 100644 index 00000000..3cb38d7c --- /dev/null +++ b/internal/server/client.go @@ -0,0 +1,236 @@ +package server + +import ( + "errors" + "fmt" + "io" + "sort" + "strings" + "sync" + "time" + + "github.com/tidwall/evio" + "github.com/tidwall/resp" +) + +// Client is an remote connection into to Tile38 +type Client struct { + id int // unique id + authd bool // client has been authenticated + outputType Type // Null, JSON, or RESP + remoteAddr string // original remote address + in evio.InputStream // input stream + pr PipelineReader // command reader + out []byte // output write buffer + + goLiveErr error // error type used for going line + goLiveMsg *Message // last message for go live + + mu sync.Mutex // guard + conn io.ReadWriteCloser // out-of-loop connection. + name string // optional defined name + opened time.Time // when the client was created/opened, unix nano + last time.Time // last client request/response, unix nano + +} + +// Write ... +func (client *Client) Write(b []byte) (n int, err error) { + client.out = append(client.out, b...) + return len(b), nil +} + +type byID []*Client + +func (arr byID) Len() int { + return len(arr) +} +func (arr byID) Less(a, b int) bool { + return arr[a].id < arr[b].id +} +func (arr byID) Swap(a, b int) { + arr[a], arr[b] = arr[b], arr[a] +} + +func (c *Server) cmdClient(msg *Message, client *Client) (resp.Value, error) { + start := time.Now() + + if len(msg.Args) == 1 { + return NOMessage, errInvalidNumberOfArguments + } + switch strings.ToLower(msg.Args[1]) { + default: + return NOMessage, errors.New("Syntax error, try CLIENT " + + "(LIST | KILL | GETNAME | SETNAME)") + case "list": + if len(msg.Args) != 2 { + return NOMessage, errInvalidNumberOfArguments + } + var list []*Client + c.connsmu.RLock() + for _, cc := range c.conns { + list = append(list, cc) + } + c.connsmu.RUnlock() + sort.Sort(byID(list)) + now := time.Now() + var buf []byte + for _, client := range list { + client.mu.Lock() + buf = append(buf, + fmt.Sprintf("id=%d addr=%s name=%s age=%d idle=%d\n", + client.id, + client.remoteAddr, + client.name, + now.Sub(client.opened)/time.Second, + now.Sub(client.last)/time.Second, + )..., + ) + client.mu.Unlock() + } + switch msg.OutputType { + case JSON: + return resp.StringValue(`{"ok":true,"list":` + jsonString(string(buf)) + `,"elapsed":"` + time.Now().Sub(start).String() + "\"}"), nil + case RESP: + return resp.BytesValue(buf), nil + } + return NOMessage, nil + case "getname": + if len(msg.Args) != 2 { + return NOMessage, errInvalidNumberOfArguments + } + name := "" + switch msg.OutputType { + case JSON: + client.mu.Lock() + name := client.name + client.mu.Unlock() + return resp.StringValue(`{"ok":true,"name":` + + jsonString(name) + + `,"elapsed":"` + time.Now().Sub(start).String() + "\"}"), nil + case RESP: + return resp.StringValue(name), nil + } + case "setname": + if len(msg.Args) != 3 { + return NOMessage, errInvalidNumberOfArguments + } + name := msg.Args[2] + for i := 0; i < len(name); i++ { + if name[i] < '!' || name[i] > '~' { + errstr := "Client names cannot contain spaces, newlines or special characters." + return NOMessage, errors.New(errstr) + } + } + client.mu.Lock() + client.name = name + client.mu.Unlock() + switch msg.OutputType { + case JSON: + return resp.StringValue(`{"ok":true,"elapsed":"` + time.Now().Sub(start).String() + "\"}"), nil + case RESP: + return resp.SimpleStringValue("OK"), nil + } + case "kill": + if len(msg.Args) < 3 { + return NOMessage, errInvalidNumberOfArguments + } + var useAddr bool + var addr string + var useID bool + var id string + for i := 2; i < len(msg.Args); i++ { + arg := msg.Args[i] + if strings.Contains(arg, ":") { + addr = arg + useAddr = true + break + } + switch strings.ToLower(arg) { + default: + return NOMessage, errors.New("No such client") + case "addr": + i++ + if i == len(msg.Args) { + return NOMessage, errors.New("syntax error") + } + addr = msg.Args[i] + useAddr = true + case "id": + i++ + if i == len(msg.Args) { + return NOMessage, errors.New("syntax error") + } + id = msg.Args[i] + useID = true + } + } + var cclose *Client + c.connsmu.RLock() + for _, cc := range c.conns { + if useID && fmt.Sprintf("%d", cc.id) == id { + cclose = cc + break + } else if useAddr && client.remoteAddr == addr { + cclose = cc + break + } + } + c.connsmu.RUnlock() + if cclose == nil { + return NOMessage, errors.New("No such client") + } + + var res resp.Value + switch msg.OutputType { + case JSON: + res = resp.StringValue(`{"ok":true,"elapsed":"` + time.Now().Sub(start).String() + "\"}") + case RESP: + res = resp.SimpleStringValue("OK") + } + + client.conn.Close() + // closing self, return response now + // NOTE: This is the only exception where we do convert response to a string + var outBytes []byte + switch msg.OutputType { + case JSON: + outBytes = res.Bytes() + case RESP: + outBytes, _ = res.MarshalRESP() + } + cclose.conn.Write(outBytes) + cclose.conn.Close() + return res, nil + } + return NOMessage, errors.New("invalid output type") +} + +/* +func (c *Controller) cmdClientList(msg *Message) (string, error) { + + var ok bool + var key string + if vs, key, ok = tokenval(vs); !ok || key == "" { + return "", errInvalidNumberOfArguments + } + + col := c.getCol(key) + if col == nil { + if msg.OutputType == RESP { + return "+none\r\n", nil + } + return "", errKeyNotFound + } + + typ := "hash" + + switch msg.OutputType { + case JSON: + return `{"ok":true,"type":` + string(typ) + `,"elapsed":"` + time.Now().Sub(start).String() + "\"}", nil + case RESP: + return "+" + typ + "\r\n", nil + } + return "", nil +} +*/ diff --git a/internal/controller/config.go b/internal/server/config.go similarity index 92% rename from internal/controller/config.go rename to internal/server/config.go index 2ee82cfa..a7220fad 100644 --- a/internal/controller/config.go +++ b/internal/server/config.go @@ -1,4 +1,4 @@ -package controller +package server import ( "encoding/json" @@ -13,7 +13,6 @@ import ( "github.com/tidwall/gjson" "github.com/tidwall/resp" "github.com/tidwall/tile38/internal/glob" - "github.com/tidwall/tile38/internal/server" ) const ( @@ -327,64 +326,64 @@ func (config *Config) getProperty(name string) string { } } -func (c *Controller) cmdConfigGet(msg *server.Message) (res resp.Value, err error) { +func (c *Server) cmdConfigGet(msg *Message) (res resp.Value, err error) { start := time.Now() - vs := msg.Values[1:] + vs := msg.Args[1:] var ok bool var name string if vs, name, ok = tokenval(vs); !ok { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } if len(vs) != 0 { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } m := c.config.getProperties(name) switch msg.OutputType { - case server.JSON: + case JSON: data, err := json.Marshal(m) if err != nil { - return server.NOMessage, err + return NOMessage, err } res = resp.StringValue(`{"ok":true,"properties":` + string(data) + `,"elapsed":"` + time.Now().Sub(start).String() + "\"}") - case server.RESP: + case RESP: vals := respValuesSimpleMap(m) res = resp.ArrayValue(vals) } return } -func (c *Controller) cmdConfigSet(msg *server.Message) (res resp.Value, err error) { +func (c *Server) cmdConfigSet(msg *Message) (res resp.Value, err error) { start := time.Now() - vs := msg.Values[1:] + vs := msg.Args[1:] var ok bool var name string if vs, name, ok = tokenval(vs); !ok { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } var value string if vs, value, ok = tokenval(vs); !ok { if strings.ToLower(name) != RequirePass { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } } if len(vs) != 0 { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } if err := c.config.setProperty(name, value, false); err != nil { - return server.NOMessage, err + return NOMessage, err } - return server.OKMessage(msg, start), nil + return OKMessage(msg, start), nil } -func (c *Controller) cmdConfigRewrite(msg *server.Message) (res resp.Value, err error) { +func (c *Server) cmdConfigRewrite(msg *Message) (res resp.Value, err error) { start := time.Now() - vs := msg.Values[1:] + vs := msg.Args[1:] if len(vs) != 0 { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } c.config.write(true) - return server.OKMessage(msg, start), nil + return OKMessage(msg, start), nil } func (config *Config) followHost() string { diff --git a/internal/controller/crud.go b/internal/server/crud.go similarity index 79% rename from internal/controller/crud.go rename to internal/server/crud.go index bf9285f4..8ab3e237 100644 --- a/internal/controller/crud.go +++ b/internal/server/crud.go @@ -1,4 +1,4 @@ -package controller +package server import ( "bytes" @@ -14,7 +14,6 @@ import ( "github.com/tidwall/tile38/internal/collection" "github.com/tidwall/tile38/internal/ds" "github.com/tidwall/tile38/internal/glob" - "github.com/tidwall/tile38/internal/server" ) type fvt struct { @@ -49,30 +48,30 @@ func orderFields(fmap map[string]int, fields []float64) []fvt { sort.Sort(byField(fvs)) return fvs } -func (c *Controller) cmdBounds(msg *server.Message) (resp.Value, error) { +func (server *Server) cmdBounds(msg *Message) (resp.Value, error) { start := time.Now() - vs := msg.Values[1:] + vs := msg.Args[1:] var ok bool var key string if vs, key, ok = tokenval(vs); !ok || key == "" { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } if len(vs) != 0 { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } - col := c.getCol(key) + col := server.getCol(key) if col == nil { - if msg.OutputType == server.RESP { + if msg.OutputType == RESP { return resp.NullValue(), nil } - return server.NOMessage, errKeyNotFound + return NOMessage, errKeyNotFound } vals := make([]resp.Value, 0, 2) var buf bytes.Buffer - if msg.OutputType == server.JSON { + if msg.OutputType == JSON { buf.WriteString(`{"ok":true`) } minX, minY, maxX, maxY := col.Bounds() @@ -81,7 +80,7 @@ func (c *Controller) cmdBounds(msg *server.Message) (resp.Value, error) { Min: geometry.Point{X: minX, Y: minY}, Max: geometry.Point{X: maxX, Y: maxY}, }) - if msg.OutputType == server.JSON { + if msg.OutputType == JSON { buf.WriteString(`,"bounds":`) buf.WriteString(string(bbox.AppendJSON(nil))) } else { @@ -97,55 +96,55 @@ func (c *Controller) cmdBounds(msg *server.Message) (resp.Value, error) { })) } switch msg.OutputType { - case server.JSON: + case JSON: buf.WriteString(`,"elapsed":"` + time.Now().Sub(start).String() + "\"}") return resp.StringValue(buf.String()), nil - case server.RESP: + case RESP: return vals[0], nil } - return server.NOMessage, nil + return NOMessage, nil } -func (c *Controller) cmdType(msg *server.Message) (resp.Value, error) { +func (server *Server) cmdType(msg *Message) (resp.Value, error) { start := time.Now() - vs := msg.Values[1:] + vs := msg.Args[1:] var ok bool var key string if vs, key, ok = tokenval(vs); !ok || key == "" { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } - col := c.getCol(key) + col := server.getCol(key) if col == nil { - if msg.OutputType == server.RESP { + if msg.OutputType == RESP { return resp.SimpleStringValue("none"), nil } - return server.NOMessage, errKeyNotFound + return NOMessage, errKeyNotFound } typ := "hash" switch msg.OutputType { - case server.JSON: + case JSON: return resp.StringValue(`{"ok":true,"type":` + string(typ) + `,"elapsed":"` + time.Now().Sub(start).String() + "\"}"), nil - case server.RESP: + case RESP: return resp.SimpleStringValue(typ), nil } - return server.NOMessage, nil + return NOMessage, nil } -func (c *Controller) cmdGet(msg *server.Message) (resp.Value, error) { +func (server *Server) cmdGet(msg *Message) (resp.Value, error) { start := time.Now() - vs := msg.Values[1:] + vs := msg.Args[1:] var ok bool var key, id, typ, sprecision string if vs, key, ok = tokenval(vs); !ok || key == "" { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } if vs, id, ok = tokenval(vs); !ok || id == "" { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } withfields := false @@ -154,25 +153,25 @@ func (c *Controller) cmdGet(msg *server.Message) (resp.Value, error) { vs = vs[1:] } - col := c.getCol(key) + col := server.getCol(key) if col == nil { - if msg.OutputType == server.RESP { + if msg.OutputType == RESP { return resp.NullValue(), nil } - return server.NOMessage, errKeyNotFound + return NOMessage, errKeyNotFound } o, fields, ok := col.Get(id) - ok = ok && !c.hasExpired(key, id) + ok = ok && !server.hasExpired(key, id) if !ok { - if msg.OutputType == server.RESP { + if msg.OutputType == RESP { return resp.NullValue(), nil } - return server.NOMessage, errIDNotFound + return NOMessage, errIDNotFound } vals := make([]resp.Value, 0, 2) var buf bytes.Buffer - if msg.OutputType == server.JSON { + if msg.OutputType == JSON { buf.WriteString(`{"ok":true`) } vs, typ, ok = tokenval(vs) @@ -182,16 +181,16 @@ func (c *Controller) cmdGet(msg *server.Message) (resp.Value, error) { } switch typ { default: - return server.NOMessage, errInvalidArgument(typ) + return NOMessage, errInvalidArgument(typ) case "object": - if msg.OutputType == server.JSON { + if msg.OutputType == JSON { buf.WriteString(`,"object":`) buf.WriteString(string(o.AppendJSON(nil))) } else { vals = append(vals, resp.StringValue(o.String())) } case "point": - if msg.OutputType == server.JSON { + if msg.OutputType == JSON { buf.WriteString(`,"point":`) buf.Write(appendJSONSimplePoint(nil, o)) } else { @@ -215,24 +214,24 @@ func (c *Controller) cmdGet(msg *server.Message) (resp.Value, error) { } case "hash": if vs, sprecision, ok = tokenval(vs); !ok || sprecision == "" { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } - if msg.OutputType == server.JSON { + if msg.OutputType == JSON { buf.WriteString(`,"hash":`) } precision, err := strconv.ParseInt(sprecision, 10, 64) if err != nil || precision < 1 || precision > 64 { - return server.NOMessage, errInvalidArgument(sprecision) + return NOMessage, errInvalidArgument(sprecision) } center := o.Center() p := geohash.EncodeWithPrecision(center.Y, center.X, uint(precision)) - if msg.OutputType == server.JSON { + if msg.OutputType == JSON { buf.WriteString(`"` + p + `"`) } else { vals = append(vals, resp.StringValue(p)) } case "bounds": - if msg.OutputType == server.JSON { + if msg.OutputType == JSON { buf.WriteString(`,"bounds":`) buf.Write(appendJSONSimpleBounds(nil, o)) } else { @@ -251,17 +250,17 @@ func (c *Controller) cmdGet(msg *server.Message) (resp.Value, error) { } if len(vs) != 0 { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } if withfields { fvs := orderFields(col.FieldMap(), fields) if len(fvs) > 0 { fvals := make([]resp.Value, 0, len(fvs)*2) - if msg.OutputType == server.JSON { + if msg.OutputType == JSON { buf.WriteString(`,"fields":{`) } for i, fv := range fvs { - if msg.OutputType == server.JSON { + if msg.OutputType == JSON { if i > 0 { buf.WriteString(`,`) } @@ -271,7 +270,7 @@ func (c *Controller) cmdGet(msg *server.Message) (resp.Value, error) { } i++ } - if msg.OutputType == server.JSON { + if msg.OutputType == JSON { buf.WriteString(`}`) } else { vals = append(vals, resp.ArrayValue(fvals)) @@ -279,10 +278,10 @@ func (c *Controller) cmdGet(msg *server.Message) (resp.Value, error) { } } switch msg.OutputType { - case server.JSON: + case JSON: buf.WriteString(`,"elapsed":"` + time.Now().Sub(start).String() + "\"}") return resp.StringValue(buf.String()), nil - case server.RESP: + case RESP: var oval resp.Value if withfields { oval = resp.ArrayValue(vals) @@ -291,12 +290,12 @@ func (c *Controller) cmdGet(msg *server.Message) (resp.Value, error) { } return oval, nil } - return server.NOMessage, nil + return NOMessage, nil } -func (c *Controller) cmdDel(msg *server.Message) (res resp.Value, d commandDetailsT, err error) { +func (server *Server) cmdDel(msg *Message) (res resp.Value, d commandDetailsT, err error) { start := time.Now() - vs := msg.Values[1:] + vs := msg.Args[1:] var ok bool if vs, d.key, ok = tokenval(vs); !ok || d.key == "" { err = errInvalidNumberOfArguments @@ -311,24 +310,24 @@ func (c *Controller) cmdDel(msg *server.Message) (res resp.Value, d commandDetai return } found := false - col := c.getCol(d.key) + col := server.getCol(d.key) if col != nil { d.obj, d.fields, ok = col.Delete(d.id) if ok { if col.Count() == 0 { - c.deleteCol(d.key) + server.deleteCol(d.key) } found = true } } - c.clearIDExpires(d.key, d.id) + server.clearIDExpires(d.key, d.id) d.command = "del" d.updated = found d.timestamp = time.Now() switch msg.OutputType { - case server.JSON: + case JSON: res = resp.StringValue(`{"ok":true,"elapsed":"` + time.Now().Sub(start).String() + "\"}") - case server.RESP: + case RESP: if d.updated { res = resp.IntegerValue(1) } else { @@ -338,9 +337,9 @@ func (c *Controller) cmdDel(msg *server.Message) (res resp.Value, d commandDetai return } -func (c *Controller) cmdPdel(msg *server.Message) (res resp.Value, d commandDetailsT, err error) { +func (server *Server) cmdPdel(msg *Message) (res resp.Value, d commandDetailsT, err error) { start := time.Now() - vs := msg.Values[1:] + vs := msg.Args[1:] var ok bool if vs, d.key, ok = tokenval(vs); !ok || d.key == "" { err = errInvalidNumberOfArguments @@ -369,7 +368,7 @@ func (c *Controller) cmdPdel(msg *server.Message) (res resp.Value, d commandDeta } var expired int - col := c.getCol(d.key) + col := server.getCol(d.key) if col != nil { g := glob.Parse(d.pattern, false) if g.Limits[0] == "" && g.Limits[1] == "" { @@ -386,7 +385,7 @@ func (c *Controller) cmdPdel(msg *server.Message) (res resp.Value, d commandDeta } else { d.children[i] = dc } - c.clearIDExpires(d.key, dc.id) + server.clearIDExpires(d.key, dc.id) } if atLeastOneNotDeleted { var nchildren []*commandDetailsT @@ -398,7 +397,7 @@ func (c *Controller) cmdPdel(msg *server.Message) (res resp.Value, d commandDeta d.children = nchildren } if col.Count() == 0 { - c.deleteCol(d.key) + server.deleteCol(d.key) } } d.command = "pdel" @@ -406,9 +405,9 @@ func (c *Controller) cmdPdel(msg *server.Message) (res resp.Value, d commandDeta d.timestamp = now d.parent = true switch msg.OutputType { - case server.JSON: + case JSON: res = resp.StringValue(`{"ok":true,"elapsed":"` + time.Now().Sub(start).String() + "\"}") - case server.RESP: + case RESP: total := len(d.children) - expired if total < 0 { total = 0 @@ -418,9 +417,9 @@ func (c *Controller) cmdPdel(msg *server.Message) (res resp.Value, d commandDeta return } -func (c *Controller) cmdDrop(msg *server.Message) (res resp.Value, d commandDetailsT, err error) { +func (server *Server) cmdDrop(msg *Message) (res resp.Value, d commandDetailsT, err error) { start := time.Now() - vs := msg.Values[1:] + vs := msg.Args[1:] var ok bool if vs, d.key, ok = tokenval(vs); !ok || d.key == "" { err = errInvalidNumberOfArguments @@ -430,9 +429,9 @@ func (c *Controller) cmdDrop(msg *server.Message) (res resp.Value, d commandDeta err = errInvalidNumberOfArguments return } - col := c.getCol(d.key) + col := server.getCol(d.key) if col != nil { - c.deleteCol(d.key) + server.deleteCol(d.key) d.updated = true } else { d.key = "" // ignore the details @@ -440,11 +439,11 @@ func (c *Controller) cmdDrop(msg *server.Message) (res resp.Value, d commandDeta } d.command = "drop" d.timestamp = time.Now() - c.clearKeyExpires(d.key) + server.clearKeyExpires(d.key) switch msg.OutputType { - case server.JSON: + case JSON: res = resp.StringValue(`{"ok":true,"elapsed":"` + time.Now().Sub(start).String() + "\"}") - case server.RESP: + case RESP: if d.updated { res = resp.IntegerValue(1) } else { @@ -454,36 +453,36 @@ func (c *Controller) cmdDrop(msg *server.Message) (res resp.Value, d commandDeta return } -func (c *Controller) cmdFlushDB(msg *server.Message) (res resp.Value, d commandDetailsT, err error) { +func (server *Server) cmdFlushDB(msg *Message) (res resp.Value, d commandDetailsT, err error) { start := time.Now() - vs := msg.Values[1:] + vs := msg.Args[1:] if len(vs) != 0 { err = errInvalidNumberOfArguments return } - c.cols = ds.BTree{} - c.exlistmu.Lock() - c.exlist = nil - c.exlistmu.Unlock() - c.expires = make(map[string]map[string]time.Time) - c.hooks = make(map[string]*Hook) - c.hookcols = make(map[string]map[string]*Hook) + server.cols = ds.BTree{} + server.exlistmu.Lock() + server.exlist = nil + server.exlistmu.Unlock() + server.expires = make(map[string]map[string]time.Time) + server.hooks = make(map[string]*Hook) + server.hookcols = make(map[string]map[string]*Hook) d.command = "flushdb" d.updated = true d.timestamp = time.Now() switch msg.OutputType { - case server.JSON: + case JSON: res = resp.StringValue(`{"ok":true,"elapsed":"` + time.Now().Sub(start).String() + "\"}") - case server.RESP: + case RESP: res = resp.SimpleStringValue("OK") } return } -func (c *Controller) parseSetArgs(vs []resp.Value) ( +func (server *Server) parseSetArgs(vs []string) ( d commandDetailsT, fields []string, values []float64, xx, nx bool, - expires *float64, etype []byte, evs []resp.Value, err error, + expires *float64, etype []byte, evs []string, err error, ) { var ok bool var typ []byte @@ -496,7 +495,7 @@ func (c *Controller) parseSetArgs(vs []resp.Value) ( return } var arg []byte - var nvs []resp.Value + var nvs []string for { if nvs, arg, ok = tokenvalbytes(vs); !ok || len(arg) == 0 { err = errInvalidNumberOfArguments @@ -689,7 +688,7 @@ func (c *Controller) parseSetArgs(vs []resp.Value) ( err = errInvalidNumberOfArguments return } - d.obj, err = geojson.Parse(object, &c.geomParseOpts) + d.obj, err = geojson.Parse(object, &server.geomParseOpts) if err != nil { return } @@ -700,29 +699,29 @@ func (c *Controller) parseSetArgs(vs []resp.Value) ( return } -func (c *Controller) cmdSet(msg *server.Message) (res resp.Value, d commandDetailsT, err error) { - if c.config.maxMemory() > 0 && c.outOfMemory.on() { +func (server *Server) cmdSet(msg *Message) (res resp.Value, d commandDetailsT, err error) { + if server.config.maxMemory() > 0 && server.outOfMemory.on() { err = errOOM return } start := time.Now() - vs := msg.Values[1:] + vs := msg.Args[1:] var fmap map[string]int var fields []string var values []float64 var xx, nx bool var ex *float64 - d, fields, values, xx, nx, ex, _, _, err = c.parseSetArgs(vs) + d, fields, values, xx, nx, ex, _, _, err = server.parseSetArgs(vs) if err != nil { return } - col := c.getCol(d.key) + col := server.getCol(d.key) if col == nil { if xx { goto notok } col = collection.New() - c.setCol(d.key, col) + server.setCol(d.key, col) } if xx || nx { _, _, ok := col.Get(d.id) @@ -730,12 +729,12 @@ func (c *Controller) cmdSet(msg *server.Message) (res resp.Value, d commandDetai goto notok } } - c.clearIDExpires(d.key, d.id) + server.clearIDExpires(d.key, d.id) d.oldObj, d.oldFields, d.fields = col.Set(d.id, d.obj, fields, values) d.command = "set" d.updated = true // perhaps we should do a diff on the previous object? d.timestamp = time.Now() - if msg.ConnType != server.Null || msg.OutputType != server.Null { + if msg.ConnType != Null || msg.OutputType != Null { // likely loaded from aof at server startup, ignore field remapping. fmap = col.FieldMap() d.fmap = make(map[string]int) @@ -744,33 +743,33 @@ func (c *Controller) cmdSet(msg *server.Message) (res resp.Value, d commandDetai } } if ex != nil { - c.expireAt(d.key, d.id, d.timestamp.Add(time.Duration(float64(time.Second)*(*ex)))) + server.expireAt(d.key, d.id, d.timestamp.Add(time.Duration(float64(time.Second)*(*ex)))) } switch msg.OutputType { default: - case server.JSON: + case JSON: res = resp.StringValue(`{"ok":true,"elapsed":"` + time.Now().Sub(start).String() + "\"}") - case server.RESP: + case RESP: res = resp.SimpleStringValue("OK") } return notok: switch msg.OutputType { default: - case server.JSON: + case JSON: if nx { err = errIDAlreadyExists } else { err = errIDNotFound } return - case server.RESP: + case RESP: res = resp.NullValue() } return } -func (c *Controller) parseFSetArgs(vs []resp.Value) ( +func (server *Server) parseFSetArgs(vs []string) ( d commandDetailsT, fields []string, values []float64, xx bool, err error, ) { var ok bool @@ -813,20 +812,20 @@ func (c *Controller) parseFSetArgs(vs []resp.Value) ( return } -func (c *Controller) cmdFset(msg *server.Message) (res resp.Value, d commandDetailsT, err error) { - if c.config.maxMemory() > 0 && c.outOfMemory.on() { +func (server *Server) cmdFset(msg *Message) (res resp.Value, d commandDetailsT, err error) { + if server.config.maxMemory() > 0 && server.outOfMemory.on() { err = errOOM return } start := time.Now() - vs := msg.Values[1:] + vs := msg.Args[1:] var fields []string var values []float64 var xx bool var updateCount int - d, fields, values, xx, err = c.parseFSetArgs(vs) + d, fields, values, xx, err = server.parseFSetArgs(vs) - col := c.getCol(d.key) + col := server.getCol(d.key) if col == nil { err = errKeyNotFound return @@ -849,17 +848,17 @@ func (c *Controller) cmdFset(msg *server.Message) (res resp.Value, d commandDeta } switch msg.OutputType { - case server.JSON: + case JSON: res = resp.StringValue(`{"ok":true,"elapsed":"` + time.Now().Sub(start).String() + "\"}") - case server.RESP: + case RESP: res = resp.IntegerValue(updateCount) } return } -func (c *Controller) cmdExpire(msg *server.Message) (res resp.Value, d commandDetailsT, err error) { +func (server *Server) cmdExpire(msg *Message) (res resp.Value, d commandDetailsT, err error) { start := time.Now() - vs := msg.Values[1:] + vs := msg.Args[1:] var key, id, svalue string var ok bool if vs, key, ok = tokenval(vs); !ok || key == "" { @@ -885,23 +884,23 @@ func (c *Controller) cmdExpire(msg *server.Message) (res resp.Value, d commandDe return } ok = false - col := c.getCol(key) + col := server.getCol(key) if col != nil { _, _, ok = col.Get(id) - ok = ok && !c.hasExpired(key, id) + ok = ok && !server.hasExpired(key, id) } if ok { - c.expireAt(key, id, time.Now().Add(time.Duration(float64(time.Second)*value))) + server.expireAt(key, id, time.Now().Add(time.Duration(float64(time.Second)*value))) d.updated = true } switch msg.OutputType { - case server.JSON: + case JSON: if ok { res = resp.StringValue(`{"ok":true,"elapsed":"` + time.Now().Sub(start).String() + "\"}") } else { return resp.SimpleStringValue(""), d, errIDNotFound } - case server.RESP: + case RESP: if ok { res = resp.IntegerValue(1) } else { @@ -911,9 +910,9 @@ func (c *Controller) cmdExpire(msg *server.Message) (res resp.Value, d commandDe return } -func (c *Controller) cmdPersist(msg *server.Message) (res resp.Value, d commandDetailsT, err error) { +func (server *Server) cmdPersist(msg *Message) (res resp.Value, d commandDetailsT, err error) { start := time.Now() - vs := msg.Values[1:] + vs := msg.Args[1:] var key, id string var ok bool if vs, key, ok = tokenval(vs); !ok || key == "" { @@ -930,16 +929,16 @@ func (c *Controller) cmdPersist(msg *server.Message) (res resp.Value, d commandD } var cleared bool ok = false - col := c.getCol(key) + col := server.getCol(key) if col != nil { _, _, ok = col.Get(id) - ok = ok && !c.hasExpired(key, id) + ok = ok && !server.hasExpired(key, id) if ok { - cleared = c.clearIDExpires(key, id) + cleared = server.clearIDExpires(key, id) } } if !ok { - if msg.OutputType == server.RESP { + if msg.OutputType == RESP { return resp.IntegerValue(0), d, nil } return resp.SimpleStringValue(""), d, errIDNotFound @@ -948,9 +947,9 @@ func (c *Controller) cmdPersist(msg *server.Message) (res resp.Value, d commandD d.updated = cleared d.timestamp = time.Now() switch msg.OutputType { - case server.JSON: + case JSON: res = resp.SimpleStringValue(`{"ok":true,"elapsed":"` + time.Now().Sub(start).String() + "\"}") - case server.RESP: + case RESP: if cleared { res = resp.IntegerValue(1) } else { @@ -960,9 +959,9 @@ func (c *Controller) cmdPersist(msg *server.Message) (res resp.Value, d commandD return } -func (c *Controller) cmdTTL(msg *server.Message) (res resp.Value, err error) { +func (server *Server) cmdTTL(msg *Message) (res resp.Value, err error) { start := time.Now() - vs := msg.Values[1:] + vs := msg.Args[1:] var key, id string var ok bool if vs, key, ok = tokenval(vs); !ok || key == "" { @@ -980,13 +979,13 @@ func (c *Controller) cmdTTL(msg *server.Message) (res resp.Value, err error) { var v float64 ok = false var ok2 bool - col := c.getCol(key) + col := server.getCol(key) if col != nil { _, _, ok = col.Get(id) - ok = ok && !c.hasExpired(key, id) + ok = ok && !server.hasExpired(key, id) if ok { var at time.Time - at, ok2 = c.getExpires(key, id) + at, ok2 = server.getExpires(key, id) if ok2 { if time.Now().After(at) { ok2 = false @@ -1000,7 +999,7 @@ func (c *Controller) cmdTTL(msg *server.Message) (res resp.Value, err error) { } } switch msg.OutputType { - case server.JSON: + case JSON: if ok { var ttl string if ok2 { @@ -1013,7 +1012,7 @@ func (c *Controller) cmdTTL(msg *server.Message) (res resp.Value, err error) { } else { return resp.SimpleStringValue(""), errIDNotFound } - case server.RESP: + case RESP: if ok { if ok2 { res = resp.IntegerValue(int(v)) diff --git a/internal/controller/dev.go b/internal/server/dev.go similarity index 55% rename from internal/controller/dev.go rename to internal/server/dev.go index 008388bf..7609ea1d 100644 --- a/internal/controller/dev.go +++ b/internal/server/dev.go @@ -1,17 +1,15 @@ -package controller +package server import ( "errors" "fmt" "math/rand" "strconv" - "strings" "sync/atomic" "time" "github.com/tidwall/resp" "github.com/tidwall/tile38/internal/log" - "github.com/tidwall/tile38/internal/server" ) // MASSINSERT num_keys num_points [minx miny maxx maxy] @@ -23,9 +21,9 @@ func randMassInsertPosition(minLat, minLon, maxLat, maxLon float64) (float64, fl return lat, lon } -func (c *Controller) cmdMassInsert(msg *server.Message) (res resp.Value, err error) { +func (c *Server) cmdMassInsert(msg *Message) (res resp.Value, err error) { start := time.Now() - vs := msg.Values[1:] + vs := msg.Args[1:] minLat, minLon, maxLat, maxLon := -90.0, -180.0, 90.0, 180.0 //37.10776, -122.67145, 38.19502, -121.62775 @@ -33,62 +31,63 @@ func (c *Controller) cmdMassInsert(msg *server.Message) (res resp.Value, err err var cols, objs int var ok bool if vs, snumCols, ok = tokenval(vs); !ok || snumCols == "" { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } if vs, snumPoints, ok = tokenval(vs); !ok || snumPoints == "" { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } if len(vs) != 0 { var sminLat, sminLon, smaxLat, smaxLon string if vs, sminLat, ok = tokenval(vs); !ok || sminLat == "" { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } if vs, sminLon, ok = tokenval(vs); !ok || sminLon == "" { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } if vs, smaxLat, ok = tokenval(vs); !ok || smaxLat == "" { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } if vs, smaxLon, ok = tokenval(vs); !ok || smaxLon == "" { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } var err error if minLat, err = strconv.ParseFloat(sminLat, 64); err != nil { - return server.NOMessage, err + return NOMessage, err } if minLon, err = strconv.ParseFloat(sminLon, 64); err != nil { - return server.NOMessage, err + return NOMessage, err } if maxLat, err = strconv.ParseFloat(smaxLat, 64); err != nil { - return server.NOMessage, err + return NOMessage, err } if maxLon, err = strconv.ParseFloat(smaxLon, 64); err != nil { - return server.NOMessage, err + return NOMessage, err } if len(vs) != 0 { - return server.NOMessage, errors.New("invalid number of arguments") + return NOMessage, errors.New("invalid number of arguments") } } n, err := strconv.ParseUint(snumCols, 10, 64) if err != nil { - return server.NOMessage, errInvalidArgument(snumCols) + return NOMessage, errInvalidArgument(snumCols) } cols = int(n) n, err = strconv.ParseUint(snumPoints, 10, 64) if err != nil { - return server.NOMessage, errInvalidArgument(snumPoints) + return NOMessage, errInvalidArgument(snumPoints) } - docmd := func(values []resp.Value) error { - nmsg := &server.Message{} + docmd := func(args []string) error { + nmsg := &Message{} *nmsg = *msg - nmsg.Values = values - nmsg.Command = strings.ToLower(values[0].String()) + nmsg.Args = args var d commandDetailsT - _, d, err = c.command(nmsg, nil, nil) + _, d, err = c.command(nmsg, nil) if err != nil { return err } - return c.writeAOF(resp.ArrayValue(nmsg.Values), &d) + + return c.writeAOF(nmsg.Args, &d) + } rand.Seed(time.Now().UnixNano()) objs = int(n) @@ -99,19 +98,21 @@ func (c *Controller) cmdMassInsert(msg *server.Message) (res resp.Value, err err // lock cycle for j := 0; j < objs; j++ { id := strconv.FormatInt(int64(j), 10) - var values []resp.Value + var values []string if j%8 == 0 { - values = append(values, resp.StringValue("set"), - resp.StringValue(key), resp.StringValue(id), - resp.StringValue("STRING"), resp.StringValue(fmt.Sprintf("str%v", j))) + values = append(values, "set", key, id, "STRING", fmt.Sprintf("str%v", j)) } else { lat, lon := randMassInsertPosition(minLat, minLon, maxLat, maxLon) - values = make([]resp.Value, 0, 16) - values = append(values, resp.StringValue("set"), resp.StringValue(key), resp.StringValue(id)) + values = make([]string, 0, 16) + values = append(values, "set", key, id) if useRandField { - values = append(values, resp.StringValue("FIELD"), resp.StringValue("fname"), resp.FloatValue(rand.Float64()*10)) + values = append(values, "FIELD", "fname", + strconv.FormatFloat(rand.Float64()*10, 'f', -1, 64)) } - values = append(values, resp.StringValue("POINT"), resp.FloatValue(lat), resp.FloatValue(lon)) + values = append(values, "POINT", + strconv.FormatFloat(lat, 'f', -1, 64), + strconv.FormatFloat(lon, 'f', -1, 64), + ) } if err := docmd(values); err != nil { log.Fatal(err) @@ -125,15 +126,15 @@ func (c *Controller) cmdMassInsert(msg *server.Message) (res resp.Value, err err }(key) } log.Infof("massinsert: done %d objects", atomic.LoadUint64(&k)) - return server.OKMessage(msg, start), nil + return OKMessage(msg, start), nil } -func (c *Controller) cmdSleep(msg *server.Message) (res resp.Value, err error) { +func (c *Server) cmdSleep(msg *Message) (res resp.Value, err error) { start := time.Now() - if len(msg.Values) != 2 { - return server.NOMessage, errInvalidNumberOfArguments + if len(msg.Args) != 2 { + return NOMessage, errInvalidNumberOfArguments } - d, _ := strconv.ParseFloat(msg.Values[1].String(), 64) + d, _ := strconv.ParseFloat(msg.Args[1], 64) time.Sleep(time.Duration(float64(time.Second) * d)) - return server.OKMessage(msg, start), nil + return OKMessage(msg, start), nil } diff --git a/internal/controller/expire.go b/internal/server/expire.go similarity index 79% rename from internal/controller/expire.go rename to internal/server/expire.go index 8d27e1a8..39448c26 100644 --- a/internal/controller/expire.go +++ b/internal/server/expire.go @@ -1,4 +1,4 @@ -package controller +package server import ( "log" @@ -6,8 +6,6 @@ import ( "time" "github.com/tidwall/btree" - "github.com/tidwall/resp" - "github.com/tidwall/tile38/internal/server" ) type exitem struct { @@ -33,7 +31,7 @@ func (a *exitem) Less(v btree.Item, ctx interface{}) bool { } // fillExpiresList occurs once at startup -func (c *Controller) fillExpiresList() { +func (c *Server) fillExpiresList() { c.exlistmu.Lock() c.exlist = c.exlist[:0] for key, m := range c.expires { @@ -45,7 +43,7 @@ func (c *Controller) fillExpiresList() { } // clearIDExpires clears a single item from the expires list. -func (c *Controller) clearIDExpires(key, id string) (cleared bool) { +func (c *Server) clearIDExpires(key, id string) (cleared bool) { if len(c.expires) == 0 { return false } @@ -62,12 +60,12 @@ func (c *Controller) clearIDExpires(key, id string) (cleared bool) { } // clearKeyExpires clears all items that are marked as expires from a single key. -func (c *Controller) clearKeyExpires(key string) { +func (c *Server) clearKeyExpires(key string) { delete(c.expires, key) } // expireAt marks an item as expires at a specific time. -func (c *Controller) expireAt(key, id string, at time.Time) { +func (c *Server) expireAt(key, id string, at time.Time) { m := c.expires[key] if m == nil { m = make(map[string]time.Time) @@ -80,7 +78,7 @@ func (c *Controller) expireAt(key, id string, at time.Time) { } // getExpires returns the when an item expires. -func (c *Controller) getExpires(key, id string) (at time.Time, ok bool) { +func (c *Server) getExpires(key, id string) (at time.Time, ok bool) { if len(c.expires) == 0 { return at, false } @@ -93,7 +91,7 @@ func (c *Controller) getExpires(key, id string) (at time.Time, ok bool) { } // hasExpired returns true if an item has expired. -func (c *Controller) hasExpired(key, id string) bool { +func (c *Server) hasExpired(key, id string) bool { at, ok := c.getExpires(key, id) if !ok { return false @@ -103,7 +101,7 @@ func (c *Controller) hasExpired(key, id string) bool { // backgroundExpiring watches for when items that have expired must be purged // from the database. It's executes 10 times a seconds. -func (c *Controller) backgroundExpiring() { +func (c *Server) backgroundExpiring() { rand.Seed(time.Now().UnixNano()) var purgelist []exitem for { @@ -128,16 +126,15 @@ func (c *Controller) backgroundExpiring() { for _, item := range purgelist { if c.hasExpired(item.key, item.id) { // purge from database - msg := &server.Message{} - msg.Values = resp.MultiBulkValue("del", item.key, item.id).Array() - msg.Command = "del" + msg := &Message{} + msg.Args = []string{"del", item.key, item.id} _, d, err := c.cmdDel(msg) if err != nil { c.mu.Unlock() log.Fatal(err) continue } - if err := c.writeAOF(resp.ArrayValue(msg.Values), &d); err != nil { + if err := c.writeAOF(msg.Args, &d); err != nil { c.mu.Unlock() log.Fatal(err) continue diff --git a/internal/controller/fence.go b/internal/server/fence.go similarity index 98% rename from internal/controller/fence.go rename to internal/server/fence.go index ef433f43..0390f93f 100644 --- a/internal/controller/fence.go +++ b/internal/server/fence.go @@ -1,4 +1,4 @@ -package controller +package server import ( "math" @@ -9,7 +9,6 @@ import ( "github.com/tidwall/geojson/geometry" "github.com/tidwall/gjson" "github.com/tidwall/tile38/internal/glob" - "github.com/tidwall/tile38/internal/server" ) // FenceMatch executes a fence match returns back json messages for fence detection. @@ -175,7 +174,7 @@ func fenceMatch( } sw.fmap = details.fmap sw.fullFields = true - sw.msg.OutputType = server.JSON + sw.msg.OutputType = JSON sw.writeObject(ScanWriterParams{ id: details.id, o: details.obj, @@ -354,7 +353,7 @@ func fenceMatchObject(fence *liveFenceSwitches, obj geojson.Object) bool { } func fenceMatchRoam( - c *Controller, fence *liveFenceSwitches, + c *Server, fence *liveFenceSwitches, tkey, tid string, obj geojson.Object, ) (nearbys, faraways []roamMatch) { diff --git a/internal/controller/follow.go b/internal/server/follow.go similarity index 75% rename from internal/controller/follow.go rename to internal/server/follow.go index 873b9bbf..aea9a2d9 100644 --- a/internal/controller/follow.go +++ b/internal/server/follow.go @@ -1,4 +1,4 @@ -package controller +package server import ( "errors" @@ -12,27 +12,26 @@ import ( "github.com/tidwall/resp" "github.com/tidwall/tile38/core" "github.com/tidwall/tile38/internal/log" - "github.com/tidwall/tile38/internal/server" ) var errNoLongerFollowing = errors.New("no longer following") const checksumsz = 512 * 1024 -func (c *Controller) cmdFollow(msg *server.Message) (res resp.Value, err error) { +func (c *Server) cmdFollow(msg *Message) (res resp.Value, err error) { start := time.Now() - vs := msg.Values[1:] + vs := msg.Args[1:] var ok bool var host, sport string if vs, host, ok = tokenval(vs); !ok || host == "" { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } if vs, sport, ok = tokenval(vs); !ok || sport == "" { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } if len(vs) != 0 { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } host = strings.ToLower(host) sport = strings.ToLower(sport) @@ -44,7 +43,7 @@ func (c *Controller) cmdFollow(msg *server.Message) (res resp.Value, err error) } else { n, err := strconv.ParseUint(sport, 10, 64) if err != nil { - return server.NOMessage, errInvalidArgument(sport) + return NOMessage, errInvalidArgument(sport) } port := int(n) update = c.config.followHost() != host || c.config.followPort() != port @@ -54,30 +53,30 @@ func (c *Controller) cmdFollow(msg *server.Message) (res resp.Value, err error) conn, err := DialTimeout(fmt.Sprintf("%s:%d", host, port), time.Second*2) if err != nil { c.mu.Lock() - return server.NOMessage, fmt.Errorf("cannot follow: %v", err) + return NOMessage, fmt.Errorf("cannot follow: %v", err) } defer conn.Close() if auth != "" { if err := c.followDoLeaderAuth(conn, auth); err != nil { - return server.NOMessage, fmt.Errorf("cannot follow: %v", err) + return NOMessage, fmt.Errorf("cannot follow: %v", err) } } m, err := doServer(conn) if err != nil { c.mu.Lock() - return server.NOMessage, fmt.Errorf("cannot follow: %v", err) + return NOMessage, fmt.Errorf("cannot follow: %v", err) } if m["id"] == "" { c.mu.Lock() - return server.NOMessage, fmt.Errorf("cannot follow: invalid id") + return NOMessage, fmt.Errorf("cannot follow: invalid id") } if m["id"] == c.config.serverID() { c.mu.Lock() - return server.NOMessage, fmt.Errorf("cannot follow self") + return NOMessage, fmt.Errorf("cannot follow self") } if m["following"] != "" { c.mu.Lock() - return server.NOMessage, fmt.Errorf("cannot follow a follower") + return NOMessage, fmt.Errorf("cannot follow a follower") } c.mu.Lock() } @@ -94,10 +93,10 @@ func (c *Controller) cmdFollow(msg *server.Message) (res resp.Value, err error) log.Infof("following no one") } } - return server.OKMessage(msg, start), nil + return OKMessage(msg, start), nil } -func doServer(conn *Conn) (map[string]string, error) { +func doServer(conn *RESPConn) (map[string]string, error) { v, err := conn.Do("server") if err != nil { return nil, err @@ -113,29 +112,27 @@ func doServer(conn *Conn) (map[string]string, error) { return m, err } -func (c *Controller) followHandleCommand(values []resp.Value, followc int, w io.Writer) (int, error) { +func (c *Server) followHandleCommand(args []string, followc int, w io.Writer) (int, error) { c.mu.Lock() defer c.mu.Unlock() if c.followc.get() != followc { return c.aofsz, errNoLongerFollowing } - msg := &server.Message{ - Command: strings.ToLower(values[0].String()), - Values: values, - } - _, d, err := c.command(msg, nil, nil) + msg := &Message{Args: args} + + _, d, err := c.command(msg, nil) if err != nil { if commandErrIsFatal(err) { return c.aofsz, err } } - if err := c.writeAOF(resp.ArrayValue(values), &d); err != nil { + if err := c.writeAOF(args, &d); err != nil { return c.aofsz, err } return c.aofsz, nil } -func (c *Controller) followDoLeaderAuth(conn *Conn, auth string) error { +func (c *Server) followDoLeaderAuth(conn *RESPConn, auth string) error { v, err := conn.Do("auth", auth) if err != nil { return err @@ -149,7 +146,7 @@ func (c *Controller) followDoLeaderAuth(conn *Conn, auth string) error { return nil } -func (c *Controller) followStep(host string, port int, followc int) error { +func (c *Server) followStep(host string, port int, followc int) error { if c.followc.get() != followc { return errNoLongerFollowing } @@ -228,8 +225,12 @@ func (c *Controller) followStep(host string, port int, followc int) error { if telnet || v.Type() != resp.Array { return errors.New("invalid multibulk") } + svals := make([]string, len(vals)) + for i := 0; i < len(vals); i++ { + svals[i] = vals[i].String() + } - aofsz, err := c.followHandleCommand(vals, followc, nullw) + aofsz, err := c.followHandleCommand(svals, followc, nullw) if err != nil { return err } @@ -247,7 +248,7 @@ func (c *Controller) followStep(host string, port int, followc int) error { } } -func (c *Controller) follow(host string, port int, followc int) { +func (c *Server) follow(host string, port int, followc int) { for { err := c.followStep(host, port, followc) if err == errNoLongerFollowing { diff --git a/internal/controller/hooks.go b/internal/server/hooks.go similarity index 82% rename from internal/controller/hooks.go rename to internal/server/hooks.go index 5ae165f0..d155532a 100644 --- a/internal/controller/hooks.go +++ b/internal/server/hooks.go @@ -1,4 +1,4 @@ -package controller +package server import ( "bytes" @@ -14,7 +14,6 @@ import ( "github.com/tidwall/tile38/internal/endpoint" "github.com/tidwall/tile38/internal/glob" "github.com/tidwall/tile38/internal/log" - "github.com/tidwall/tile38/internal/server" ) var hookLogSetDefaults = &buntdb.SetOptions{ @@ -36,22 +35,22 @@ func (a hooksByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (c *Controller) cmdSetHook(msg *server.Message, chanCmd bool) ( +func (c *Server) cmdSetHook(msg *Message, chanCmd bool) ( res resp.Value, d commandDetailsT, err error, ) { start := time.Now() - vs := msg.Values[1:] + vs := msg.Args[1:] var name, urls, cmd string var ok bool if vs, name, ok = tokenval(vs); !ok || name == "" { - return server.NOMessage, d, errInvalidNumberOfArguments + return NOMessage, d, errInvalidNumberOfArguments } var endpoints []string if chanCmd { endpoints = []string{"local://" + name} } else { if vs, urls, ok = tokenval(vs); !ok || urls == "" { - return server.NOMessage, d, errInvalidNumberOfArguments + return NOMessage, d, errInvalidNumberOfArguments } for _, url := range strings.Split(urls, ",") { url = strings.TrimSpace(url) @@ -63,7 +62,7 @@ func (c *Controller) cmdSetHook(msg *server.Message, chanCmd bool) ( endpoints = append(endpoints, url) } } - var commandvs []resp.Value + var commandvs []string var cmdlc string var types []string var expires float64 @@ -72,31 +71,31 @@ func (c *Controller) cmdSetHook(msg *server.Message, chanCmd bool) ( for { commandvs = vs if vs, cmd, ok = tokenval(vs); !ok || cmd == "" { - return server.NOMessage, d, errInvalidNumberOfArguments + return NOMessage, d, errInvalidNumberOfArguments } cmdlc = strings.ToLower(cmd) switch cmdlc { default: - return server.NOMessage, d, errInvalidArgument(cmd) + return NOMessage, d, errInvalidArgument(cmd) case "meta": var metakey string var metaval string if vs, metakey, ok = tokenval(vs); !ok || metakey == "" { - return server.NOMessage, d, errInvalidNumberOfArguments + return NOMessage, d, errInvalidNumberOfArguments } if vs, metaval, ok = tokenval(vs); !ok || metaval == "" { - return server.NOMessage, d, errInvalidNumberOfArguments + return NOMessage, d, errInvalidNumberOfArguments } metaMap[metakey] = metaval continue case "ex": var s string if vs, s, ok = tokenval(vs); !ok || s == "" { - return server.NOMessage, d, errInvalidNumberOfArguments + return NOMessage, d, errInvalidNumberOfArguments } v, err := strconv.ParseFloat(s, 64) if err != nil { - return server.NOMessage, d, errInvalidArgument(s) + return NOMessage, d, errInvalidArgument(s) } expires = v expiresSet = true @@ -111,20 +110,18 @@ func (c *Controller) cmdSetHook(msg *server.Message, chanCmd bool) ( s, err := c.cmdSearchArgs(true, cmdlc, vs, types) defer s.Close() if err != nil { - return server.NOMessage, d, err + return NOMessage, d, err } if !s.fence { - return server.NOMessage, d, errors.New("missing FENCE argument") + return NOMessage, d, errors.New("missing FENCE argument") } s.cmd = cmdlc - cmsg := &server.Message{} + cmsg := &Message{} *cmsg = *msg - cmsg.Values = make([]resp.Value, len(commandvs)) + cmsg.Args = make([]string, len(commandvs)) for i := 0; i < len(commandvs); i++ { - cmsg.Values[i] = commandvs[i] + cmsg.Args[i] = commandvs[i] } - cmsg.Command = strings.ToLower(cmsg.Values[0].String()) - metas := make([]FenceMeta, 0, len(metaMap)) for key, val := range metaMap { metas = append(metas, FenceMeta{key, val}) @@ -155,11 +152,11 @@ func (c *Controller) cmdSetHook(msg *server.Message, chanCmd bool) ( s.cursor, s.limit, s.wheres, s.whereins, s.whereevals, s.nofields) if err != nil { - return server.NOMessage, d, err + return NOMessage, d, err } if h, ok := c.hooks[name]; ok { if h.channel != chanCmd { - return server.NOMessage, d, + return NOMessage, d, errors.New("hooks and channels cannot share the same name") } if h.Equals(hook) { @@ -170,9 +167,9 @@ func (c *Controller) cmdSetHook(msg *server.Message, chanCmd bool) ( c.hookex.Push(hook) } switch msg.OutputType { - case server.JSON: - return server.OKMessage(msg, start), d, nil - case server.RESP: + case JSON: + return OKMessage(msg, start), d, nil + case RESP: return resp.IntegerValue(0), d, nil } } @@ -198,27 +195,27 @@ func (c *Controller) cmdSetHook(msg *server.Message, chanCmd bool) ( c.hookex.Push(hook) } switch msg.OutputType { - case server.JSON: - return server.OKMessage(msg, start), d, nil - case server.RESP: + case JSON: + return OKMessage(msg, start), d, nil + case RESP: return resp.IntegerValue(1), d, nil } - return server.NOMessage, d, nil + return NOMessage, d, nil } -func (c *Controller) cmdDelHook(msg *server.Message, chanCmd bool) ( +func (c *Server) cmdDelHook(msg *Message, chanCmd bool) ( res resp.Value, d commandDetailsT, err error, ) { start := time.Now() - vs := msg.Values[1:] + vs := msg.Args[1:] var name string var ok bool if vs, name, ok = tokenval(vs); !ok || name == "" { - return server.NOMessage, d, errInvalidNumberOfArguments + return NOMessage, d, errInvalidNumberOfArguments } if len(vs) != 0 { - return server.NOMessage, d, errInvalidNumberOfArguments + return NOMessage, d, errInvalidNumberOfArguments } if h, ok := c.hooks[name]; ok && h.channel == chanCmd { h.Close() @@ -231,9 +228,9 @@ func (c *Controller) cmdDelHook(msg *server.Message, chanCmd bool) ( d.timestamp = time.Now() switch msg.OutputType { - case server.JSON: - return server.OKMessage(msg, start), d, nil - case server.RESP: + case JSON: + return OKMessage(msg, start), d, nil + case RESP: if d.updated { return resp.IntegerValue(1), d, nil } @@ -242,19 +239,19 @@ func (c *Controller) cmdDelHook(msg *server.Message, chanCmd bool) ( return } -func (c *Controller) cmdPDelHook(msg *server.Message, channel bool) ( +func (c *Server) cmdPDelHook(msg *Message, channel bool) ( res resp.Value, d commandDetailsT, err error, ) { start := time.Now() - vs := msg.Values[1:] + vs := msg.Args[1:] var pattern string var ok bool if vs, pattern, ok = tokenval(vs); !ok || pattern == "" { - return server.NOMessage, d, errInvalidNumberOfArguments + return NOMessage, d, errInvalidNumberOfArguments } if len(vs) != 0 { - return server.NOMessage, d, errInvalidNumberOfArguments + return NOMessage, d, errInvalidNumberOfArguments } count := 0 @@ -277,9 +274,9 @@ func (c *Controller) cmdPDelHook(msg *server.Message, channel bool) ( d.timestamp = time.Now() switch msg.OutputType { - case server.JSON: - return server.OKMessage(msg, start), d, nil - case server.RESP: + case JSON: + return OKMessage(msg, start), d, nil + case RESP: return resp.IntegerValue(count), d, nil } return @@ -288,25 +285,23 @@ func (c *Controller) cmdPDelHook(msg *server.Message, channel bool) ( // possiblyExpireHook will evaluate a hook by it's name for expiration and // purge it from the database if needed. This operation is called from an // independent goroutine -func (c *Controller) possiblyExpireHook(name string) { +func (c *Server) possiblyExpireHook(name string) { c.mu.Lock() if h, ok := c.hooks[name]; ok { if !h.expires.IsZero() && time.Now().After(h.expires) { // purge from database - msg := &server.Message{} + msg := &Message{} if h.channel { - msg.Values = resp.MultiBulkValue("delchan", h.Name).Array() - msg.Command = "delchan" + msg.Args = []string{"delchan", h.Name} } else { - msg.Values = resp.MultiBulkValue("delhook", h.Name).Array() - msg.Command = "delhook" + msg.Args = []string{"delhook", h.Name} } _, d, err := c.cmdDelHook(msg, h.channel) if err != nil { c.mu.Unlock() panic(err) } - if err := c.writeAOF(resp.ArrayValue(msg.Values), &d); err != nil { + if err := c.writeAOF(msg.Args, &d); err != nil { c.mu.Unlock() panic(err) } @@ -316,20 +311,20 @@ func (c *Controller) possiblyExpireHook(name string) { c.mu.Unlock() } -func (c *Controller) cmdHooks(msg *server.Message, channel bool) ( +func (c *Server) cmdHooks(msg *Message, channel bool) ( res resp.Value, err error, ) { start := time.Now() - vs := msg.Values[1:] + vs := msg.Args[1:] var pattern string var ok bool if vs, pattern, ok = tokenval(vs); !ok || pattern == "" { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } if len(vs) != 0 { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } var hooks []*Hook @@ -345,7 +340,7 @@ func (c *Controller) cmdHooks(msg *server.Message, channel bool) ( sort.Sort(hooksByName(hooks)) switch msg.OutputType { - case server.JSON: + case JSON: buf := &bytes.Buffer{} buf.WriteString(`{"ok":true,`) if channel { @@ -370,11 +365,11 @@ func (c *Controller) cmdHooks(msg *server.Message, channel bool) ( } } buf.WriteString(`],"command":[`) - for i, v := range hook.Message.Values { + for i, v := range hook.Message.Args { if i > 0 { buf.WriteString(`,`) } - buf.WriteString(jsonString(v.String())) + buf.WriteString(jsonString(v)) } buf.WriteString(`],"meta":{`) for i, meta := range hook.Metas { @@ -390,7 +385,7 @@ func (c *Controller) cmdHooks(msg *server.Message, channel bool) ( buf.WriteString(`],"elapsed":"` + time.Now().Sub(start).String() + "\"}") return resp.StringValue(buf.String()), nil - case server.RESP: + case RESP: var vals []resp.Value for _, hook := range hooks { var hvals []resp.Value @@ -401,7 +396,11 @@ func (c *Controller) cmdHooks(msg *server.Message, channel bool) ( evals = append(evals, resp.StringValue(endpoint)) } hvals = append(hvals, resp.ArrayValue(evals)) - hvals = append(hvals, resp.ArrayValue(hook.Message.Values)) + avals := make([]resp.Value, len(hook.Message.Args)) + for i := 0; i < len(hook.Message.Args); i++ { + avals[i] = resp.StringValue(hook.Message.Args[i]) + } + hvals = append(hvals, resp.ArrayValue(avals)) var metas []resp.Value for _, meta := range hook.Metas { metas = append(metas, resp.StringValue(meta.Name)) @@ -421,7 +420,7 @@ type Hook struct { Key string Name string Endpoints []string - Message *server.Message + Message *Message Fence *liveFenceSwitches ScanWriter *scanWriter Metas []FenceMeta @@ -461,9 +460,15 @@ func (h *Hook) Equals(hook *Hook) bool { return false } } - - return resp.ArrayValue(h.Message.Values).Equals( - resp.ArrayValue(hook.Message.Values)) + if len(h.Message.Args) != len(hook.Message.Args) { + return false + } + for i := 0; i < len(h.Message.Args); i++ { + if h.Message.Args[i] != hook.Message.Args[i] { + return false + } + } + return true } // FenceMeta is a meta key/value pair for fences diff --git a/internal/controller/json.go b/internal/server/json.go similarity index 69% rename from internal/controller/json.go rename to internal/server/json.go index 93234263..fb8b0cb7 100644 --- a/internal/controller/json.go +++ b/internal/server/json.go @@ -1,4 +1,4 @@ -package controller +package server import ( "bytes" @@ -12,7 +12,6 @@ import ( "github.com/tidwall/resp" "github.com/tidwall/sjson" "github.com/tidwall/tile38/internal/collection" - "github.com/tidwall/tile38/internal/server" ) func appendJSONString(b []byte, s string) []byte { @@ -86,44 +85,44 @@ func jsonTimeFormat(t time.Time) string { return string(b) } -func (c *Controller) cmdJget(msg *server.Message) (resp.Value, error) { +func (c *Server) cmdJget(msg *Message) (resp.Value, error) { start := time.Now() - if len(msg.Values) < 3 { - return server.NOMessage, errInvalidNumberOfArguments + if len(msg.Args) < 3 { + return NOMessage, errInvalidNumberOfArguments } - if len(msg.Values) > 5 { - return server.NOMessage, errInvalidNumberOfArguments + if len(msg.Args) > 5 { + return NOMessage, errInvalidNumberOfArguments } - key := msg.Values[1].String() - id := msg.Values[2].String() + key := msg.Args[1] + id := msg.Args[2] var doget bool var path string var raw bool - if len(msg.Values) > 3 { + if len(msg.Args) > 3 { doget = true - path = msg.Values[3].String() - if len(msg.Values) == 5 { - if strings.ToLower(msg.Values[4].String()) == "raw" { + path = msg.Args[3] + if len(msg.Args) == 5 { + if strings.ToLower(msg.Args[4]) == "raw" { raw = true } else { - return server.NOMessage, errInvalidArgument(msg.Values[4].String()) + return NOMessage, errInvalidArgument(msg.Args[4]) } } } col := c.getCol(key) if col == nil { - if msg.OutputType == server.RESP { + if msg.OutputType == RESP { return resp.NullValue(), nil } - return server.NOMessage, errKeyNotFound + return NOMessage, errKeyNotFound } o, _, ok := col.Get(id) if !ok { - if msg.OutputType == server.RESP { + if msg.OutputType == RESP { return resp.NullValue(), nil } - return server.NOMessage, errIDNotFound + return NOMessage, errIDNotFound } var res gjson.Result if doget { @@ -138,38 +137,38 @@ func (c *Controller) cmdJget(msg *server.Message) (resp.Value, error) { val = res.String() } var buf bytes.Buffer - if msg.OutputType == server.JSON { + if msg.OutputType == JSON { buf.WriteString(`{"ok":true`) } switch msg.OutputType { - case server.JSON: + case JSON: if res.Exists() { buf.WriteString(`,"value":` + jsonString(val)) } buf.WriteString(`,"elapsed":"` + time.Now().Sub(start).String() + "\"}") return resp.StringValue(buf.String()), nil - case server.RESP: + case RESP: if !res.Exists() { return resp.NullValue(), nil } return resp.StringValue(val), nil } - return server.NOMessage, nil + return NOMessage, nil } -func (c *Controller) cmdJset(msg *server.Message) (res resp.Value, d commandDetailsT, err error) { +func (c *Server) cmdJset(msg *Message) (res resp.Value, d commandDetailsT, err error) { // JSET key path value [RAW] start := time.Now() var raw, str bool - switch len(msg.Values) { + switch len(msg.Args) { default: - return server.NOMessage, d, errInvalidNumberOfArguments + return NOMessage, d, errInvalidNumberOfArguments case 5: case 6: - switch strings.ToLower(msg.Values[5].String()) { + switch strings.ToLower(msg.Args[5]) { default: - return server.NOMessage, d, errInvalidArgument(msg.Values[5].String()) + return NOMessage, d, errInvalidArgument(msg.Args[5]) case "raw": raw = true case "str": @@ -177,10 +176,10 @@ func (c *Controller) cmdJset(msg *server.Message) (res resp.Value, d commandDeta } } - key := msg.Values[1].String() - id := msg.Values[2].String() - path := msg.Values[3].String() - val := msg.Values[4].String() + key := msg.Args[1] + id := msg.Args[2] + path := msg.Args[3] + val := msg.Args[4] if !str && !raw { switch val { default: @@ -216,18 +215,12 @@ func (c *Controller) cmdJset(msg *server.Message) (res resp.Value, d commandDeta json, err = sjson.Set(json, path, val) } if err != nil { - return server.NOMessage, d, err + return NOMessage, d, err } if geoobj { nmsg := *msg - nmsg.Values = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(key), - resp.StringValue(id), - resp.StringValue("OBJECT"), - resp.StringValue(json), - } + nmsg.Args = []string{"SET", key, id, "OBJECT", json} // SET key id OBJECT json return c.cmdSet(&nmsg) } @@ -244,33 +237,33 @@ func (c *Controller) cmdJset(msg *server.Message) (res resp.Value, d commandDeta c.clearIDExpires(key, id) col.Set(d.id, d.obj, nil, nil) switch msg.OutputType { - case server.JSON: + case JSON: var buf bytes.Buffer buf.WriteString(`{"ok":true`) buf.WriteString(`,"elapsed":"` + time.Now().Sub(start).String() + "\"}") return resp.StringValue(buf.String()), d, nil - case server.RESP: + case RESP: return resp.SimpleStringValue("OK"), d, nil } - return server.NOMessage, d, nil + return NOMessage, d, nil } -func (c *Controller) cmdJdel(msg *server.Message) (res resp.Value, d commandDetailsT, err error) { +func (c *Server) cmdJdel(msg *Message) (res resp.Value, d commandDetailsT, err error) { start := time.Now() - if len(msg.Values) != 4 { - return server.NOMessage, d, errInvalidNumberOfArguments + if len(msg.Args) != 4 { + return NOMessage, d, errInvalidNumberOfArguments } - key := msg.Values[1].String() - id := msg.Values[2].String() - path := msg.Values[3].String() + key := msg.Args[1] + id := msg.Args[2] + path := msg.Args[3] col := c.getCol(key) if col == nil { - if msg.OutputType == server.RESP { + if msg.OutputType == RESP { return resp.IntegerValue(0), d, nil } - return server.NOMessage, d, errKeyNotFound + return NOMessage, d, errKeyNotFound } var json string @@ -282,27 +275,21 @@ func (c *Controller) cmdJdel(msg *server.Message) (res resp.Value, d commandDeta } njson, err := sjson.Delete(json, path) if err != nil { - return server.NOMessage, d, err + return NOMessage, d, err } if njson == json { switch msg.OutputType { - case server.JSON: - return server.NOMessage, d, errPathNotFound - case server.RESP: + case JSON: + return NOMessage, d, errPathNotFound + case RESP: return resp.IntegerValue(0), d, nil } - return server.NOMessage, d, nil + return NOMessage, d, nil } json = njson if geoobj { nmsg := *msg - nmsg.Values = []resp.Value{ - resp.StringValue("SET"), - resp.StringValue(key), - resp.StringValue(id), - resp.StringValue("OBJECT"), - resp.StringValue(json), - } + nmsg.Args = []string{"SET", key, id, "OBJECT", json} // SET key id OBJECT json return c.cmdSet(&nmsg) } @@ -316,13 +303,13 @@ func (c *Controller) cmdJdel(msg *server.Message) (res resp.Value, d commandDeta c.clearIDExpires(d.key, d.id) col.Set(d.id, d.obj, nil, nil) switch msg.OutputType { - case server.JSON: + case JSON: var buf bytes.Buffer buf.WriteString(`{"ok":true`) buf.WriteString(`,"elapsed":"` + time.Now().Sub(start).String() + "\"}") return resp.StringValue(buf.String()), d, nil - case server.RESP: + case RESP: return resp.IntegerValue(1), d, nil } - return server.NOMessage, d, nil + return NOMessage, d, nil } diff --git a/internal/controller/json_test.go b/internal/server/json_test.go similarity index 93% rename from internal/controller/json_test.go rename to internal/server/json_test.go index cb5efb6f..f9ccd4ee 100644 --- a/internal/controller/json_test.go +++ b/internal/server/json_test.go @@ -1,4 +1,4 @@ -package controller +package server import ( "encoding/json" diff --git a/internal/controller/keys.go b/internal/server/keys.go similarity index 78% rename from internal/controller/keys.go rename to internal/server/keys.go index 0290b397..06a73297 100644 --- a/internal/controller/keys.go +++ b/internal/server/keys.go @@ -1,4 +1,4 @@ -package controller +package server import ( "bytes" @@ -7,25 +7,24 @@ import ( "github.com/tidwall/resp" "github.com/tidwall/tile38/internal/glob" - "github.com/tidwall/tile38/internal/server" ) -func (c *Controller) cmdKeys(msg *server.Message) (res resp.Value, err error) { +func (c *Server) cmdKeys(msg *Message) (res resp.Value, err error) { var start = time.Now() - vs := msg.Values[1:] + vs := msg.Args[1:] var pattern string var ok bool if vs, pattern, ok = tokenval(vs); !ok || pattern == "" { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } if len(vs) != 0 { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } var wr = &bytes.Buffer{} var once bool - if msg.OutputType == server.JSON { + if msg.OutputType == JSON { wr.WriteString(`{"ok":true,"keys":[`) } var everything bool @@ -47,16 +46,16 @@ func (c *Controller) cmdKeys(msg *server.Message) (res resp.Value, err error) { } if match { if once { - if msg.OutputType == server.JSON { + if msg.OutputType == JSON { wr.WriteByte(',') } } else { once = true } switch msg.OutputType { - case server.JSON: + case JSON: wr.WriteString(jsonString(key)) - case server.RESP: + case RESP: vals = append(vals, resp.StringValue(key)) } } @@ -84,7 +83,7 @@ func (c *Controller) cmdKeys(msg *server.Message) (res resp.Value, err error) { c.cols.Ascend(greaterPivot, iterator) } } - if msg.OutputType == server.JSON { + if msg.OutputType == JSON { wr.WriteString(`],"elapsed":"` + time.Now().Sub(start).String() + "\"}") return resp.StringValue(wr.String()), nil } diff --git a/internal/controller/live.go b/internal/server/live.go similarity index 89% rename from internal/controller/live.go rename to internal/server/live.go index 8d678372..b7177011 100644 --- a/internal/controller/live.go +++ b/internal/server/live.go @@ -1,4 +1,4 @@ -package controller +package server import ( "bytes" @@ -9,7 +9,6 @@ import ( "sync" "github.com/tidwall/tile38/internal/log" - "github.com/tidwall/tile38/internal/server" ) type liveBuffer struct { @@ -20,7 +19,7 @@ type liveBuffer struct { cond *sync.Cond } -func (c *Controller) processLives() { +func (c *Server) processLives() { for { c.lcond.L.Lock() for len(c.lstack) > 0 { @@ -47,30 +46,31 @@ func writeLiveMessage( conn net.Conn, message []byte, wrapRESP bool, - connType server.Type, - websocket bool, + connType Type, websocket bool, ) error { if len(message) == 0 { return nil } if websocket { - return server.WriteWebSocketMessage(conn, message) + return WriteWebSocketMessage(conn, message) } var err error switch connType { - case server.RESP: + case RESP: if wrapRESP { _, err = fmt.Fprintf(conn, "$%d\r\n%s\r\n", len(message), string(message)) } else { _, err = conn.Write(message) } - case server.Native: + case Native: _, err = fmt.Fprintf(conn, "$%d %s\r\n", len(message), string(message)) } return err } -func (c *Controller) goLive(inerr error, conn net.Conn, rd *server.PipelineReader, msg *server.Message, websocket bool) error { +func (c *Server) goLive( + inerr error, conn net.Conn, rd *PipelineReader, msg *Message, websocket bool, +) error { addr := conn.RemoteAddr().String() log.Info("live " + addr) defer func() { @@ -139,7 +139,7 @@ func (c *Controller) goLive(inerr error, conn net.Conn, rd *server.PipelineReade if v == nil { continue } - switch v.Command { + switch v.Command() { default: log.Error("received a live command that was not QUIT") return @@ -152,13 +152,13 @@ func (c *Controller) goLive(inerr error, conn net.Conn, rd *server.PipelineReade outputType := msg.OutputType connType := msg.ConnType if websocket { - outputType = server.JSON + outputType = JSON } var livemsg []byte switch outputType { - case server.JSON: + case JSON: livemsg = []byte(`{"ok":true,"live":true}`) - case server.RESP: + case RESP: livemsg = []byte("+OK\r\n") } if err := writeLiveMessage(conn, livemsg, false, connType, websocket); err != nil { diff --git a/internal/controller/output.go b/internal/server/output.go similarity index 57% rename from internal/controller/output.go rename to internal/server/output.go index a1b73bd4..f24f05c3 100644 --- a/internal/controller/output.go +++ b/internal/server/output.go @@ -1,42 +1,41 @@ -package controller +package server import ( "strings" "time" "github.com/tidwall/resp" - "github.com/tidwall/tile38/internal/server" ) -func (c *Controller) cmdOutput(msg *server.Message) (res resp.Value, err error) { +func (c *Server) cmdOutput(msg *Message) (res resp.Value, err error) { start := time.Now() - vs := msg.Values[1:] + vs := msg.Args[1:] var arg string var ok bool if len(vs) != 0 { if _, arg, ok = tokenval(vs); !ok || arg == "" { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } // Setting the original message output type will be picked up by the // server prior to the next command being executed. switch strings.ToLower(arg) { default: - return server.NOMessage, errInvalidArgument(arg) + return NOMessage, errInvalidArgument(arg) case "json": - msg.OutputType = server.JSON + msg.OutputType = JSON case "resp": - msg.OutputType = server.RESP + msg.OutputType = RESP } - return server.OKMessage(msg, start), nil + return OKMessage(msg, start), nil } // return the output switch msg.OutputType { default: - return server.NOMessage, nil - case server.JSON: + return NOMessage, nil + case JSON: return resp.StringValue(`{"ok":true,"output":"json","elapsed":` + time.Now().Sub(start).String() + `}`), nil - case server.RESP: + case RESP: return resp.StringValue("resp"), nil } } diff --git a/internal/controller/pubsub.go b/internal/server/pubsub.go similarity index 83% rename from internal/controller/pubsub.go rename to internal/server/pubsub.go index 6badd3fd..a7c2dca2 100644 --- a/internal/controller/pubsub.go +++ b/internal/server/pubsub.go @@ -1,4 +1,4 @@ -package controller +package server import ( "io" @@ -12,7 +12,6 @@ import ( "github.com/tidwall/redcon" "github.com/tidwall/resp" "github.com/tidwall/tile38/internal/log" - "github.com/tidwall/tile38/internal/server" ) const ( @@ -35,7 +34,7 @@ func newPubsub() *pubsub { } // Publish a message to subscribers -func (c *Controller) Publish(channel string, message ...string) int { +func (c *Server) Publish(channel string, message ...string) int { var msgs []submsg c.pubsub.mu.RLock() if hub := c.pubsub.hubs[pubsubChannel][channel]; hub != nil { @@ -131,53 +130,53 @@ func newSubhub() *subhub { } type liveSubscriptionSwitches struct { - // no fields. everything is managed through the server.Message + // no fields. everything is managed through the Message } func (sub liveSubscriptionSwitches) Error() string { return goingLive } -func (c *Controller) cmdSubscribe(msg *server.Message) (resp.Value, error) { - if len(msg.Values) < 2 { +func (c *Server) cmdSubscribe(msg *Message) (resp.Value, error) { + if len(msg.Args) < 2 { return resp.Value{}, errInvalidNumberOfArguments } - return server.NOMessage, liveSubscriptionSwitches{} + return NOMessage, liveSubscriptionSwitches{} } -func (c *Controller) cmdPsubscribe(msg *server.Message) (resp.Value, error) { - if len(msg.Values) < 2 { +func (c *Server) cmdPsubscribe(msg *Message) (resp.Value, error) { + if len(msg.Args) < 2 { return resp.Value{}, errInvalidNumberOfArguments } - return server.NOMessage, liveSubscriptionSwitches{} + return NOMessage, liveSubscriptionSwitches{} } -func (c *Controller) cmdPublish(msg *server.Message) (resp.Value, error) { +func (c *Server) cmdPublish(msg *Message) (resp.Value, error) { start := time.Now() - if len(msg.Values) != 3 { + if len(msg.Args) != 3 { return resp.Value{}, errInvalidNumberOfArguments } - channel := msg.Values[1].String() - message := msg.Values[2].String() + channel := msg.Args[1] + message := msg.Args[2] //geofence := gjson.Valid(message) && gjson.Get(message, "fence").Bool() n := c.Publish(channel, message) //, geofence) var res resp.Value switch msg.OutputType { - case server.JSON: + case JSON: res = resp.StringValue(`{"ok":true` + `,"published":` + strconv.FormatInt(int64(n), 10) + `,"elapsed":"` + time.Now().Sub(start).String() + `"}`) - case server.RESP: + case RESP: res = resp.IntegerValue(n) } return res, nil } -func (c *Controller) liveSubscription( +func (c *Server) liveSubscription( conn net.Conn, - rd *server.PipelineReader, - msg *server.Message, + rd *PipelineReader, + msg *Message, websocket bool, ) error { defer conn.Close() // close connection when we are done @@ -185,7 +184,7 @@ func (c *Controller) liveSubscription( outputType := msg.OutputType connType := msg.ConnType if websocket { - outputType = server.JSON + outputType = JSON } var start time.Time @@ -199,44 +198,44 @@ func (c *Controller) liveSubscription( } writeOK := func() { switch outputType { - case server.JSON: + case JSON: write([]byte(`{"ok":true` + `,"elapsed":"` + time.Now().Sub(start).String() + `"}`)) - case server.RESP: + case RESP: write([]byte(`+OK\r\n`)) } } writeWrongNumberOfArgsErr := func(command string) { switch outputType { - case server.JSON: + case JSON: write([]byte(`{"ok":false,"err":"invalid number of arguments"` + `,"elapsed":"` + time.Now().Sub(start).String() + `"}`)) - case server.RESP: + case RESP: write([]byte(`-ERR wrong number of arguments ` + `for '` + command + `' command\r\n`)) } } writeOnlyPubsubErr := func() { switch outputType { - case server.JSON: + case JSON: write([]byte(`{"ok":false` + `,"err":"only (P)SUBSCRIBE / (P)UNSUBSCRIBE / ` + `PING / QUIT allowed in this context"` + `,"elapsed":"` + time.Now().Sub(start).String() + `"}`)) - case server.RESP: + case RESP: write([]byte("-ERR only (P)SUBSCRIBE / (P)UNSUBSCRIBE / " + "PING / QUIT allowed in this context\r\n")) } } writeSubscribe := func(command, channel string, num int) { switch outputType { - case server.JSON: + case JSON: write([]byte(`{"ok":true` + `,"command":` + jsonString(command) + `,"channel":` + jsonString(channel) + `,"num":` + strconv.FormatInt(int64(num), 10) + `,"elapsed":"` + time.Now().Sub(start).String() + `"}`)) - case server.RESP: + case RESP: b := redcon.AppendArray(nil, 3) b = redcon.AppendBulkString(b, command) b = redcon.AppendBulkString(b, channel) @@ -247,7 +246,7 @@ func (c *Controller) liveSubscription( writeMessage := func(msg submsg) { if msg.kind == pubsubChannel { switch outputType { - case server.JSON: + case JSON: var data []byte if !gjson.Valid(msg.message) { data = appendJSONString(nil, msg.message) @@ -255,7 +254,7 @@ func (c *Controller) liveSubscription( data = []byte(msg.message) } write(data) - case server.RESP: + case RESP: b := redcon.AppendArray(nil, 3) b = redcon.AppendBulkString(b, "message") b = redcon.AppendBulkString(b, msg.channel) @@ -264,7 +263,7 @@ func (c *Controller) liveSubscription( } } else { switch outputType { - case server.JSON: + case JSON: var data []byte if !gjson.Valid(msg.message) { data = appendJSONString(nil, msg.message) @@ -272,7 +271,7 @@ func (c *Controller) liveSubscription( data = []byte(msg.message) } write(data) - case server.RESP: + case RESP: b := redcon.AppendArray(nil, 4) b = redcon.AppendBulkString(b, "pmessage") b = redcon.AppendBulkString(b, msg.pattern) @@ -325,12 +324,12 @@ func (c *Controller) liveSubscription( } }() - msgs := []*server.Message{msg} + msgs := []*Message{msg} for { for _, msg := range msgs { start = time.Now() var kind int - switch msg.Command { + switch msg.Command() { case "quit": writeOK() return nil @@ -341,14 +340,14 @@ func (c *Controller) liveSubscription( default: writeOnlyPubsubErr() } - if len(msg.Values) < 2 { - writeWrongNumberOfArgsErr(msg.Command) + if len(msg.Args) < 2 { + writeWrongNumberOfArgsErr(msg.Command()) } - for i := 1; i < len(msg.Values); i++ { - channel := msg.Values[i].String() + for i := 1; i < len(msg.Args); i++ { + channel := msg.Args[i] m[kind][channel] = true c.pubsub.register(kind, channel, target) - writeSubscribe(msg.Command, channel, len(m[0])+len(m[1])) + writeSubscribe(msg.Command(), channel, len(m[0])+len(m[1])) } } var err error diff --git a/internal/server/reader.go b/internal/server/reader.go deleted file mode 100644 index b0e4a656..00000000 --- a/internal/server/reader.go +++ /dev/null @@ -1,305 +0,0 @@ -package server - -import ( - "crypto/sha1" - "encoding/base64" - "errors" - "io" - "net/url" - "strconv" - "strings" - - "github.com/tidwall/redcon" - "github.com/tidwall/resp" -) - -var errInvalidHTTP = errors.New("invalid HTTP request") - -// Type is resp type -type Type int - -// Message types -const ( - Null Type = iota - RESP - Telnet - Native - HTTP - WebSocket - JSON -) - -// Message is a resp message -type Message struct { - Command string - Values []resp.Value - ConnType Type - OutputType Type - Auth string -} - -// PipelineReader ... -type PipelineReader struct { - rd io.Reader - wr io.Writer - packet [0xFFFF]byte - buf []byte -} - -const kindHTTP redcon.Kind = 9999 - -// NewPipelineReader ... -func NewPipelineReader(rd io.ReadWriter) *PipelineReader { - return &PipelineReader{rd: rd, wr: rd} -} - -func readcrlfline(packet []byte) (line string, leftover []byte, ok bool) { - for i := 1; i < len(packet); i++ { - if packet[i] == '\n' && packet[i-1] == '\r' { - return string(packet[:i-1]), packet[i+1:], true - } - } - return "", packet, false -} - -func readNextHTTPCommand(packet []byte, argsIn [][]byte, msg *Message, wr io.Writer) ( - complete bool, args [][]byte, kind redcon.Kind, leftover []byte, err error, -) { - args = argsIn[:0] - msg.ConnType = HTTP - msg.OutputType = JSON - opacket := packet - - ready, err := func() (bool, error) { - var line string - var ok bool - - // read header - var headers []string - for { - line, packet, ok = readcrlfline(packet) - if !ok { - return false, nil - } - if line == "" { - break - } - headers = append(headers, line) - } - parts := strings.Split(headers[0], " ") - if len(parts) != 3 { - return false, errInvalidHTTP - } - method := parts[0] - path := parts[1] - if len(path) == 0 || path[0] != '/' { - return false, errInvalidHTTP - } - path, err = url.QueryUnescape(path[1:]) - if err != nil { - return false, errInvalidHTTP - } - if method != "GET" && method != "POST" { - return false, errInvalidHTTP - } - contentLength := 0 - websocket := false - websocketVersion := 0 - websocketKey := "" - for _, header := range headers[1:] { - if header[0] == 'a' || header[0] == 'A' { - if strings.HasPrefix(strings.ToLower(header), "authorization:") { - msg.Auth = strings.TrimSpace(header[len("authorization:"):]) - } - } else if header[0] == 'u' || header[0] == 'U' { - if strings.HasPrefix(strings.ToLower(header), "upgrade:") && strings.ToLower(strings.TrimSpace(header[len("upgrade:"):])) == "websocket" { - websocket = true - } - } else if header[0] == 's' || header[0] == 'S' { - if strings.HasPrefix(strings.ToLower(header), "sec-websocket-version:") { - var n uint64 - n, err = strconv.ParseUint(strings.TrimSpace(header[len("sec-websocket-version:"):]), 10, 64) - if err != nil { - return false, err - } - websocketVersion = int(n) - } else if strings.HasPrefix(strings.ToLower(header), "sec-websocket-key:") { - websocketKey = strings.TrimSpace(header[len("sec-websocket-key:"):]) - } - } else if header[0] == 'c' || header[0] == 'C' { - if strings.HasPrefix(strings.ToLower(header), "content-length:") { - var n uint64 - n, err = strconv.ParseUint(strings.TrimSpace(header[len("content-length:"):]), 10, 64) - if err != nil { - return false, err - } - contentLength = int(n) - } - } - } - if websocket && websocketVersion >= 13 && websocketKey != "" { - msg.ConnType = WebSocket - if wr == nil { - return false, errors.New("connection is nil") - } - sum := sha1.Sum([]byte(websocketKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")) - accept := base64.StdEncoding.EncodeToString(sum[:]) - wshead := "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: " + accept + "\r\n\r\n" - if _, err = wr.Write([]byte(wshead)); err != nil { - return false, err - } - } else if contentLength > 0 { - msg.ConnType = HTTP - if len(packet) < contentLength { - return false, nil - } - path += string(packet[:contentLength]) - packet = packet[contentLength:] - } - if path == "" { - return true, nil - } - nmsg, err := readNativeMessageLine([]byte(path)) - if err != nil { - return false, err - } - - msg.OutputType = JSON - msg.Values = nmsg.Values - msg.Command = commandValues(nmsg.Values) - return true, nil - }() - if err != nil || !ready { - return false, args[:0], kindHTTP, opacket, err - } - return true, args[:0], kindHTTP, packet, nil -} -func readNextCommand(packet []byte, argsIn [][]byte, msg *Message, wr io.Writer) ( - complete bool, args [][]byte, kind redcon.Kind, leftover []byte, err error, -) { - if packet[0] == 'G' || packet[0] == 'P' { - // could be an HTTP request - var line []byte - for i := 1; i < len(packet); i++ { - if packet[i] == '\n' { - if packet[i-1] == '\r' { - line = packet[:i+1] - break - } - } - } - if len(line) == 0 { - return false, argsIn[:0], redcon.Redis, packet, nil - } - if len(line) > 11 && string(line[len(line)-11:len(line)-5]) == " HTTP/" { - return readNextHTTPCommand(packet, argsIn, msg, wr) - } - } - return redcon.ReadNextCommand(packet, args) -} - -// ReadMessages ... -func (rd *PipelineReader) ReadMessages() ([]*Message, error) { - var msgs []*Message -moreData: - n, err := rd.rd.Read(rd.packet[:]) - if err != nil { - return nil, err - } - if n == 0 { - // need more data - goto moreData - } - data := rd.packet[:n] - if len(rd.buf) > 0 { - data = append(rd.buf, data...) - } - for len(data) > 0 { - msg := &Message{} - complete, args, kind, leftover, err := readNextCommand(data, nil, msg, rd.wr) - if err != nil { - break - } - if !complete { - break - } - if kind == kindHTTP { - if len(msg.Values) == 0 { - return nil, errInvalidHTTP - } - msgs = append(msgs, msg) - } else if len(args) > 0 { - msg.Command = strings.ToLower(string(args[0])) - for i := 0; i < len(args); i++ { - args[i] = append([]byte{}, args[i]...) - msg.Values = append(msg.Values, resp.BytesValue(args[i])) - } - switch kind { - case redcon.Redis: - msg.ConnType = RESP - msg.OutputType = RESP - case redcon.Tile38: - msg.ConnType = Native - msg.OutputType = JSON - case redcon.Telnet: - msg.ConnType = RESP - msg.OutputType = RESP - } - msgs = append(msgs, msg) - } - data = leftover - } - if len(data) > 0 { - rd.buf = append(rd.buf[:0], data...) - } else if len(rd.buf) > 0 { - rd.buf = rd.buf[:0] - } - if err != nil && len(msgs) == 0 { - return nil, err - } - return msgs, nil -} - -func readNativeMessageLine(line []byte) (*Message, error) { - values := make([]resp.Value, 0, 16) -reading: - for len(line) != 0 { - if line[0] == '{' { - // The native protocol cannot understand json boundaries so it assumes that - // a json element must be at the end of the line. - values = append(values, resp.StringValue(string(line))) - break - } - if line[0] == '"' && line[len(line)-1] == '"' { - if len(values) > 0 && - strings.ToLower(values[0].String()) == "set" && - strings.ToLower(values[len(values)-1].String()) == "string" { - // Setting a string value that is contained inside double quotes. - // This is only because of the boundary issues of the native protocol. - values = append(values, resp.StringValue(string(line[1:len(line)-1]))) - break - } - } - i := 0 - for ; i < len(line); i++ { - if line[i] == ' ' { - value := string(line[:i]) - if value != "" { - values = append(values, resp.StringValue(value)) - } - line = line[i+1:] - continue reading - } - } - values = append(values, resp.StringValue(string(line))) - break - } - return &Message{Command: commandValues(values), Values: values, ConnType: Native, OutputType: JSON}, nil -} - -func commandValues(values []resp.Value) string { - if len(values) == 0 { - return "" - } - return strings.ToLower(values[0].String()) -} diff --git a/internal/controller/readonly.go b/internal/server/readonly.go similarity index 60% rename from internal/controller/readonly.go rename to internal/server/readonly.go index 6a9a758f..9f8b0911 100644 --- a/internal/controller/readonly.go +++ b/internal/server/readonly.go @@ -1,4 +1,4 @@ -package controller +package server import ( "strings" @@ -6,25 +6,24 @@ import ( "github.com/tidwall/resp" "github.com/tidwall/tile38/internal/log" - "github.com/tidwall/tile38/internal/server" ) -func (c *Controller) cmdReadOnly(msg *server.Message) (res resp.Value, err error) { +func (c *Server) cmdReadOnly(msg *Message) (res resp.Value, err error) { start := time.Now() - vs := msg.Values[1:] + vs := msg.Args[1:] var arg string var ok bool if vs, arg, ok = tokenval(vs); !ok || arg == "" { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } if len(vs) != 0 { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } update := false switch strings.ToLower(arg) { default: - return server.NOMessage, errInvalidArgument(arg) + return NOMessage, errInvalidArgument(arg) case "yes": if !c.config.readOnly() { update = true @@ -41,5 +40,5 @@ func (c *Controller) cmdReadOnly(msg *server.Message) (res resp.Value, err error if update { c.config.write(false) } - return server.OKMessage(msg, start), nil + return OKMessage(msg, start), nil } diff --git a/internal/server/respconn.go b/internal/server/respconn.go new file mode 100644 index 00000000..0908f428 --- /dev/null +++ b/internal/server/respconn.go @@ -0,0 +1,46 @@ +package server + +import ( + "net" + "time" + + "github.com/tidwall/resp" +) + +// RESPConn represents a simple resp connection. +type RESPConn struct { + conn net.Conn + rd *resp.Reader + wr *resp.Writer +} + +// DialTimeout dials a resp +func DialTimeout(address string, timeout time.Duration) (*RESPConn, error) { + tcpconn, err := net.DialTimeout("tcp", address, timeout) + if err != nil { + return nil, err + } + conn := &RESPConn{ + conn: tcpconn, + rd: resp.NewReader(tcpconn), + wr: resp.NewWriter(tcpconn), + } + return conn, nil +} + +// Close closes the connection. +func (conn *RESPConn) Close() error { + conn.wr.WriteMultiBulk("quit") + return conn.conn.Close() +} + +// Do performs a command and returns a resp value. +func (conn *RESPConn) Do(commandName string, args ...interface{}) ( + val resp.Value, err error, +) { + if err := conn.wr.WriteMultiBulk(commandName, args...); err != nil { + return val, err + } + val, _, err = conn.rd.ReadValue() + return val, err +} diff --git a/internal/controller/scan.go b/internal/server/scan.go similarity index 83% rename from internal/controller/scan.go rename to internal/server/scan.go index df59f432..1d685b81 100644 --- a/internal/controller/scan.go +++ b/internal/server/scan.go @@ -1,17 +1,16 @@ -package controller +package server import ( "bytes" "errors" "time" - "github.com/tidwall/resp" "github.com/tidwall/geojson" + "github.com/tidwall/resp" "github.com/tidwall/tile38/internal/glob" - "github.com/tidwall/tile38/internal/server" ) -func (c *Controller) cmdScanArgs(vs []resp.Value) ( +func (c *Server) cmdScanArgs(vs []string) ( s liveFenceSwitches, err error, ) { var t searchScanBaseTokens @@ -27,32 +26,32 @@ func (c *Controller) cmdScanArgs(vs []resp.Value) ( return } -func (c *Controller) cmdScan(msg *server.Message) (res resp.Value, err error) { +func (c *Server) cmdScan(msg *Message) (res resp.Value, err error) { start := time.Now() - vs := msg.Values[1:] + vs := msg.Args[1:] s, err := c.cmdScanArgs(vs) if s.usingLua() { defer s.Close() defer func() { if r := recover(); r != nil { - res = server.NOMessage + res = NOMessage err = errors.New(r.(string)) return } }() } if err != nil { - return server.NOMessage, err + return NOMessage, err } wr := &bytes.Buffer{} sw, err := c.newScanWriter( wr, msg, s.key, s.output, s.precision, s.glob, false, s.cursor, s.limit, s.wheres, s.whereins, s.whereevals, s.nofields) if err != nil { - return server.NOMessage, err + return NOMessage, err } - if msg.OutputType == server.JSON { + if msg.OutputType == JSON { wr.WriteString(`{"ok":true`) } sw.writeHead() @@ -90,7 +89,7 @@ func (c *Controller) cmdScan(msg *server.Message) (res resp.Value, err error) { } } sw.writeFoot() - if msg.OutputType == server.JSON { + if msg.OutputType == JSON { wr.WriteString(`,"elapsed":"` + time.Now().Sub(start).String() + "\"}") return resp.BytesValue(wr.Bytes()), nil } diff --git a/internal/controller/scanner.go b/internal/server/scanner.go similarity index 96% rename from internal/controller/scanner.go rename to internal/server/scanner.go index d4e0f99c..4c8c2b67 100644 --- a/internal/controller/scanner.go +++ b/internal/server/scanner.go @@ -1,4 +1,4 @@ -package controller +package server import ( "bytes" @@ -13,7 +13,6 @@ import ( "github.com/tidwall/tile38/internal/clip" "github.com/tidwall/tile38/internal/collection" "github.com/tidwall/tile38/internal/glob" - "github.com/tidwall/tile38/internal/server" ) const limitItems = 100 @@ -32,9 +31,9 @@ const ( type scanWriter struct { mu sync.Mutex - c *Controller + c *Server wr *bytes.Buffer - msg *server.Message + msg *Message col *collection.Collection fmap map[string]int farr []string @@ -71,8 +70,8 @@ type ScanWriterParams struct { clip geojson.Object } -func (c *Controller) newScanWriter( - wr *bytes.Buffer, msg *server.Message, key string, output outputT, +func (c *Server) newScanWriter( + wr *bytes.Buffer, msg *Message, key string, output outputT, precision uint64, globPattern string, matchValues bool, cursor, limit uint64, wheres []whereT, whereins []whereinT, whereevals []whereevalT, nofields bool, ) ( @@ -134,7 +133,7 @@ func (sw *scanWriter) writeHead() { sw.mu.Lock() defer sw.mu.Unlock() switch sw.msg.OutputType { - case server.JSON: + case JSON: if len(sw.farr) > 0 && sw.hasFieldsOutput() { sw.wr.WriteString(`,"fields":[`) for i, field := range sw.farr { @@ -159,7 +158,7 @@ func (sw *scanWriter) writeHead() { case outputCount: } - case server.RESP: + case RESP: } } @@ -171,7 +170,7 @@ func (sw *scanWriter) writeFoot() { cursor = 0 } switch sw.msg.OutputType { - case server.JSON: + case JSON: switch sw.output { default: sw.wr.WriteByte(']') @@ -180,7 +179,7 @@ func (sw *scanWriter) writeFoot() { } sw.wr.WriteString(`,"count":` + strconv.FormatUint(sw.count, 10)) sw.wr.WriteString(`,"cursor":` + strconv.FormatUint(cursor, 10)) - case server.RESP: + case RESP: if sw.output == outputCount { sw.respOut = resp.IntegerValue(int(sw.count)) } else { @@ -354,7 +353,7 @@ func (sw *scanWriter) writeObject(opts ScanWriterParams) bool { opts.o = clip.Clip(opts.o, opts.clip) } switch sw.msg.OutputType { - case server.JSON: + case JSON: var wr bytes.Buffer var jsfields string if sw.once { @@ -418,7 +417,7 @@ func (sw *scanWriter) writeObject(opts ScanWriterParams) bool { wr.WriteString(`}`) } sw.wr.Write(wr.Bytes()) - case server.RESP: + case RESP: vals := make([]resp.Value, 1, 3) vals[0] = resp.StringValue(opts.id) if sw.output == outputIDs { diff --git a/internal/controller/scripts.go b/internal/server/scripts.go similarity index 88% rename from internal/controller/scripts.go rename to internal/server/scripts.go index e975ca77..f5473a93 100644 --- a/internal/controller/scripts.go +++ b/internal/server/scripts.go @@ -1,4 +1,4 @@ -package controller +package server import ( "bytes" @@ -14,7 +14,7 @@ import ( "time" "github.com/tidwall/resp" - "github.com/tidwall/tile38/internal/server" + "github.com/tidwall/tile38/internal/log" "github.com/yuin/gopher-lua" luajson "layeh.com/gopher-json" ) @@ -34,13 +34,13 @@ var errNoLuasAvailable = errors.New("no interpreters available") // Go-routine-safe pool of read-to-go lua states type lStatePool struct { m sync.Mutex - c *Controller + c *Server saved []*lua.LState total int } // newPool returns a new pool of lua states -func (c *Controller) newPool() *lStatePool { +func (c *Server) newPool() *lStatePool { pl := &lStatePool{ saved: make([]*lua.LState, iniLuaPoolSize), c: c, @@ -206,7 +206,7 @@ func (sm *lScriptMap) Flush() { } // NewScriptMap returns a new map with lua scripts -func (c *Controller) newScriptMap() *lScriptMap { +func (c *Server) newScriptMap() *lScriptMap { return &lScriptMap{ scripts: make(map[string]*lua.FunctionProto), } @@ -356,18 +356,18 @@ func makeSafeErr(err error) error { } // Run eval/evalro/evalna command or it's -sha variant -func (c *Controller) cmdEvalUnified(scriptIsSha bool, msg *server.Message) (res resp.Value, err error) { +func (c *Server) cmdEvalUnified(scriptIsSha bool, msg *Message) (res resp.Value, err error) { start := time.Now() - vs := msg.Values[1:] + vs := msg.Args[1:] var ok bool var script, numkeysStr, key, arg string if vs, script, ok = tokenval(vs); !ok || script == "" { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } if vs, numkeysStr, ok = tokenval(vs); !ok || numkeysStr == "" { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } var i, numkeys uint64 @@ -411,7 +411,7 @@ func (c *Controller) cmdEvalUnified(scriptIsSha bool, msg *server.Message) (res luaState, map[string]lua.LValue{ "KEYS": keysTbl, "ARGV": argsTbl, - "EVAL_CMD": lua.LString(msg.Command), + "EVAL_CMD": lua.LString(msg.Command()), }) compiled, ok := c.luascripts.Get(shaSum) @@ -431,7 +431,7 @@ func (c *Controller) cmdEvalUnified(scriptIsSha bool, msg *server.Message) (res } else { fn, err = luaState.Load(strings.NewReader(script), "f_"+shaSum) if err != nil { - return server.NOMessage, makeSafeErr(err) + return NOMessage, makeSafeErr(err) } c.luascripts.Put(shaSum, fn.Proto) } @@ -442,66 +442,66 @@ func (c *Controller) cmdEvalUnified(scriptIsSha bool, msg *server.Message) (res "ARGV": lua.LNil, "EVAL_CMD": lua.LNil, }) - if err := luaState.PCall(0, 1, nil); err != nil { - return server.NOMessage, makeSafeErr(err) + log.Debugf("%v", err.Error()) + return NOMessage, makeSafeErr(err) } ret := luaState.Get(-1) // returned value luaState.Pop(1) switch msg.OutputType { - case server.JSON: + case JSON: var buf bytes.Buffer buf.WriteString(`{"ok":true`) buf.WriteString(`,"result":` + ConvertToJSON(ret)) buf.WriteString(`,"elapsed":"` + time.Now().Sub(start).String() + "\"}") return resp.StringValue(buf.String()), nil - case server.RESP: + case RESP: return ConvertToRESP(ret), nil } - return server.NOMessage, nil + return NOMessage, nil } -func (c *Controller) cmdScriptLoad(msg *server.Message) (resp.Value, error) { +func (c *Server) cmdScriptLoad(msg *Message) (resp.Value, error) { start := time.Now() - vs := msg.Values[1:] + vs := msg.Args[1:] var ok bool var script string if vs, script, ok = tokenval(vs); !ok || script == "" { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } shaSum := Sha1Sum(script) luaState, err := c.luapool.Get() if err != nil { - return server.NOMessage, err + return NOMessage, err } defer c.luapool.Put(luaState) fn, err := luaState.Load(strings.NewReader(script), "f_"+shaSum) if err != nil { - return server.NOMessage, makeSafeErr(err) + return NOMessage, makeSafeErr(err) } c.luascripts.Put(shaSum, fn.Proto) switch msg.OutputType { - case server.JSON: + case JSON: var buf bytes.Buffer buf.WriteString(`{"ok":true`) buf.WriteString(`,"result":"` + shaSum + `"`) buf.WriteString(`,"elapsed":"` + time.Now().Sub(start).String() + "\"}") return resp.StringValue(buf.String()), nil - case server.RESP: + case RESP: return resp.StringValue(shaSum), nil } - return server.NOMessage, nil + return NOMessage, nil } -func (c *Controller) cmdScriptExists(msg *server.Message) (resp.Value, error) { +func (c *Server) cmdScriptExists(msg *Message) (resp.Value, error) { start := time.Now() - vs := msg.Values[1:] + vs := msg.Args[1:] var ok bool var shaSum string @@ -509,7 +509,7 @@ func (c *Controller) cmdScriptExists(msg *server.Message) (resp.Value, error) { var ires int for len(vs) > 0 { if vs, shaSum, ok = tokenval(vs); !ok || shaSum == "" { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } _, ok = c.luascripts.Get(shaSum) if ok { @@ -521,7 +521,7 @@ func (c *Controller) cmdScriptExists(msg *server.Message) (resp.Value, error) { } switch msg.OutputType { - case server.JSON: + case JSON: var buf bytes.Buffer buf.WriteString(`{"ok":true`) var resArray []string @@ -531,7 +531,7 @@ func (c *Controller) cmdScriptExists(msg *server.Message) (resp.Value, error) { buf.WriteString(`,"result":[` + strings.Join(resArray, ",") + `]`) buf.WriteString(`,"elapsed":"` + time.Now().Sub(start).String() + "\"}") return resp.StringValue(buf.String()), nil - case server.RESP: + case RESP: var resArray []resp.Value for _, ires := range results { resArray = append(resArray, resp.IntegerValue(ires)) @@ -541,28 +541,28 @@ func (c *Controller) cmdScriptExists(msg *server.Message) (resp.Value, error) { return resp.SimpleStringValue(""), nil } -func (c *Controller) cmdScriptFlush(msg *server.Message) (resp.Value, error) { +func (c *Server) cmdScriptFlush(msg *Message) (resp.Value, error) { start := time.Now() c.luascripts.Flush() switch msg.OutputType { - case server.JSON: + case JSON: var buf bytes.Buffer buf.WriteString(`{"ok":true`) buf.WriteString(`,"elapsed":"` + time.Now().Sub(start).String() + "\"}") return resp.StringValue(buf.String()), nil - case server.RESP: + case RESP: return resp.StringValue("OK"), nil } return resp.SimpleStringValue(""), nil } -func (c *Controller) commandInScript(msg *server.Message) ( +func (c *Server) commandInScript(msg *Message) ( res resp.Value, d commandDetailsT, err error, ) { - switch msg.Command { + switch msg.Command() { default: - err = fmt.Errorf("unknown command '%s'", msg.Values[0]) + err = fmt.Errorf("unknown command '%s'", msg.Args[0]) case "set": res, d, err = c.cmdSet(msg) case "fset": @@ -609,16 +609,11 @@ func (c *Controller) commandInScript(msg *server.Message) ( return } -func (c *Controller) luaTile38Call(evalcmd string, cmd string, args ...string) (resp.Value, error) { - msg := &server.Message{} - msg.OutputType = server.RESP - msg.Command = strings.ToLower(cmd) - msg.Values = append(msg.Values, resp.StringValue(msg.Command)) - for _, arg := range args { - msg.Values = append(msg.Values, resp.StringValue(arg)) - } - - switch msg.Command { +func (c *Server) luaTile38Call(evalcmd string, cmd string, args ...string) (resp.Value, error) { + msg := &Message{} + msg.OutputType = RESP + msg.Args = append([]string{cmd}, args...) + switch msg.Command() { case "ping", "echo", "auth", "massinsert", "shutdown", "gc", "sethook", "pdelhook", "delhook", "follow", "readonly", "config", "output", "client", @@ -641,10 +636,10 @@ func (c *Controller) luaTile38Call(evalcmd string, cmd string, args ...string) ( } // The eval command has already got the lock. No locking on the call from within the script. -func (c *Controller) luaTile38AtomicRW(msg *server.Message) (resp.Value, error) { +func (c *Server) luaTile38AtomicRW(msg *Message) (resp.Value, error) { var write bool - switch msg.Command { + switch msg.Command() { default: return resp.NullValue(), errCmdNotSupported case "set", "del", "drop", "fset", "flushdb", "expire", "persist", "jset", "pdel": @@ -670,7 +665,7 @@ func (c *Controller) luaTile38AtomicRW(msg *server.Message) (resp.Value, error) } if write { - if err := c.writeAOF(resp.ArrayValue(msg.Values), &d); err != nil { + if err := c.writeAOF(msg.Args, &d); err != nil { return resp.NullValue(), err } } @@ -678,8 +673,8 @@ func (c *Controller) luaTile38AtomicRW(msg *server.Message) (resp.Value, error) return res, nil } -func (c *Controller) luaTile38AtomicRO(msg *server.Message) (resp.Value, error) { - switch msg.Command { +func (c *Server) luaTile38AtomicRO(msg *Message) (resp.Value, error) { + switch msg.Command() { default: return resp.NullValue(), errCmdNotSupported @@ -702,11 +697,11 @@ func (c *Controller) luaTile38AtomicRO(msg *server.Message) (resp.Value, error) return res, nil } -func (c *Controller) luaTile38NonAtomic(msg *server.Message) (resp.Value, error) { +func (c *Server) luaTile38NonAtomic(msg *Message) (resp.Value, error) { var write bool // choose the locking strategy - switch msg.Command { + switch msg.Command() { default: return resp.NullValue(), errCmdNotSupported case "set", "del", "drop", "fset", "flushdb", "expire", "persist", "jset", "pdel": @@ -736,7 +731,7 @@ func (c *Controller) luaTile38NonAtomic(msg *server.Message) (resp.Value, error) } if write { - if err := c.writeAOF(resp.ArrayValue(msg.Values), &d); err != nil { + if err := c.writeAOF(msg.Args, &d); err != nil { return resp.NullValue(), err } } diff --git a/internal/controller/search.go b/internal/server/search.go similarity index 86% rename from internal/controller/search.go rename to internal/server/search.go index 9e5d468c..bfc90ae9 100644 --- a/internal/controller/search.go +++ b/internal/server/search.go @@ -1,4 +1,4 @@ -package controller +package server import ( "bytes" @@ -14,7 +14,6 @@ import ( "github.com/tidwall/resp" "github.com/tidwall/tile38/internal/bing" "github.com/tidwall/tile38/internal/glob" - "github.com/tidwall/tile38/internal/server" ) const defaultCircleSteps = 64 @@ -57,14 +56,14 @@ func (s liveFenceSwitches) usingLua() bool { return len(s.whereevals) > 0 } -func (c *Controller) cmdSearchArgs( - fromFenceCmd bool, cmd string, vs []resp.Value, types []string, +func (server *Server) cmdSearchArgs( + fromFenceCmd bool, cmd string, vs []string, types []string, ) (s liveFenceSwitches, err error) { var t searchScanBaseTokens if fromFenceCmd { t.fence = true } - vs, t, err = c.parseSearchScanBaseTokens(cmd, t, vs) + vs, t, err = server.parseSearchScanBaseTokens(cmd, t, vs) if err != nil { return } @@ -80,7 +79,7 @@ func (c *Controller) cmdSearchArgs( if _, err := strconv.ParseFloat(typ, 64); err == nil { // It's likely that the output was not specified, but rather the search bounds. s.searchScanBaseTokens.output = defaultSearchOutput - vs = append([]resp.Value{resp.StringValue(typ)}, vs...) + vs = append([]string{typ}, vs...) typ = "BOUNDS" } } @@ -283,7 +282,7 @@ func (c *Controller) cmdSearchArgs( err = errInvalidNumberOfArguments return } - col := c.getCol(key) + col := server.getCol(key) if col == nil { err = errKeyNotFound return @@ -337,35 +336,35 @@ var nearbyTypes = []string{"point"} var withinOrIntersectsTypes = []string{ "geo", "bounds", "hash", "tile", "quadkey", "get", "object", "circle"} -func (c *Controller) cmdNearby(msg *server.Message) (res resp.Value, err error) { +func (server *Server) cmdNearby(msg *Message) (res resp.Value, err error) { start := time.Now() - vs := msg.Values[1:] + vs := msg.Args[1:] wr := &bytes.Buffer{} - s, err := c.cmdSearchArgs(false, "nearby", vs, nearbyTypes) + s, err := server.cmdSearchArgs(false, "nearby", vs, nearbyTypes) if s.usingLua() { defer s.Close() defer func() { if r := recover(); r != nil { - res = server.NOMessage + res = NOMessage err = errors.New(r.(string)) return } }() } if err != nil { - return server.NOMessage, err + return NOMessage, err } s.cmd = "nearby" if s.fence { - return server.NOMessage, s + return NOMessage, s } - sw, err := c.newScanWriter( + sw, err := server.newScanWriter( wr, msg, s.key, s.output, s.precision, s.glob, false, s.cursor, s.limit, s.wheres, s.whereins, s.whereevals, s.nofields) if err != nil { - return server.NOMessage, err + return NOMessage, err } - if msg.OutputType == server.JSON { + if msg.OutputType == JSON { wr.WriteString(`{"ok":true`) } sw.writeHead() @@ -390,10 +389,10 @@ func (c *Controller) cmdNearby(msg *server.Message) (res resp.Value, err error) ignoreGlobMatch: true, }) } - c.nearestNeighbors(&s, sw, s.obj.(*geojson.Circle), &matched, iter) + server.nearestNeighbors(&s, sw, s.obj.(*geojson.Circle), &matched, iter) } sw.writeFoot() - if msg.OutputType == server.JSON { + if msg.OutputType == JSON { wr.WriteString(`,"elapsed":"` + time.Now().Sub(start).String() + "\"}") return resp.BytesValue(wr.Bytes()), nil } @@ -407,14 +406,14 @@ type iterItem struct { dist float64 } -func (c *Controller) nearestNeighbors( +func (server *Server) nearestNeighbors( s *liveFenceSwitches, sw *scanWriter, target *geojson.Circle, matched *uint32, iter func(id string, o geojson.Object, fields []float64, dist *float64, ) bool) { limit := int(sw.cursor + sw.limit) var items []iterItem sw.col.Nearby(target, func(id string, o geojson.Object, fields []float64) bool { - if c.hasExpired(s.key, id) { + if server.hasExpired(s.key, id) { return true } if _, ok := sw.fieldMatch(fields, o); !ok { @@ -444,44 +443,44 @@ func (c *Controller) nearestNeighbors( } } -func (c *Controller) cmdWithin(msg *server.Message) (res resp.Value, err error) { - return c.cmdWithinOrIntersects("within", msg) +func (server *Server) cmdWithin(msg *Message) (res resp.Value, err error) { + return server.cmdWithinOrIntersects("within", msg) } -func (c *Controller) cmdIntersects(msg *server.Message) (res resp.Value, err error) { - return c.cmdWithinOrIntersects("intersects", msg) +func (server *Server) cmdIntersects(msg *Message) (res resp.Value, err error) { + return server.cmdWithinOrIntersects("intersects", msg) } -func (c *Controller) cmdWithinOrIntersects(cmd string, msg *server.Message) (res resp.Value, err error) { +func (server *Server) cmdWithinOrIntersects(cmd string, msg *Message) (res resp.Value, err error) { start := time.Now() - vs := msg.Values[1:] + vs := msg.Args[1:] wr := &bytes.Buffer{} - s, err := c.cmdSearchArgs(false, cmd, vs, withinOrIntersectsTypes) + s, err := server.cmdSearchArgs(false, cmd, vs, withinOrIntersectsTypes) if s.usingLua() { defer s.Close() defer func() { if r := recover(); r != nil { - res = server.NOMessage + res = NOMessage err = errors.New(r.(string)) return } }() } if err != nil { - return server.NOMessage, err + return NOMessage, err } s.cmd = cmd if s.fence { - return server.NOMessage, s + return NOMessage, s } - sw, err := c.newScanWriter( + sw, err := server.newScanWriter( wr, msg, s.key, s.output, s.precision, s.glob, false, s.cursor, s.limit, s.wheres, s.whereins, s.whereevals, s.nofields) if err != nil { - return server.NOMessage, err + return NOMessage, err } - if msg.OutputType == server.JSON { + if msg.OutputType == JSON { wr.WriteString(`{"ok":true`) } sw.writeHead() @@ -490,7 +489,7 @@ func (c *Controller) cmdWithinOrIntersects(cmd string, msg *server.Message) (res sw.col.Within(s.obj, s.sparse, func( id string, o geojson.Object, fields []float64, ) bool { - if c.hasExpired(s.key, id) { + if server.hasExpired(s.key, id) { return true } return sw.writeObject(ScanWriterParams{ @@ -506,7 +505,7 @@ func (c *Controller) cmdWithinOrIntersects(cmd string, msg *server.Message) (res o geojson.Object, fields []float64, ) bool { - if c.hasExpired(s.key, id) { + if server.hasExpired(s.key, id) { return true } params := ScanWriterParams{ @@ -523,18 +522,18 @@ func (c *Controller) cmdWithinOrIntersects(cmd string, msg *server.Message) (res } } sw.writeFoot() - if msg.OutputType == server.JSON { + if msg.OutputType == JSON { wr.WriteString(`,"elapsed":"` + time.Now().Sub(start).String() + "\"}") return resp.BytesValue(wr.Bytes()), nil } return sw.respOut, nil } -func (c *Controller) cmdSeachValuesArgs(vs []resp.Value) ( +func (server *Server) cmdSeachValuesArgs(vs []string) ( s liveFenceSwitches, err error, ) { var t searchScanBaseTokens - vs, t, err = c.parseSearchScanBaseTokens("search", t, vs) + vs, t, err = server.parseSearchScanBaseTokens("search", t, vs) if err != nil { return } @@ -546,32 +545,32 @@ func (c *Controller) cmdSeachValuesArgs(vs []resp.Value) ( return } -func (c *Controller) cmdSearch(msg *server.Message) (res resp.Value, err error) { +func (server *Server) cmdSearch(msg *Message) (res resp.Value, err error) { start := time.Now() - vs := msg.Values[1:] + vs := msg.Args[1:] wr := &bytes.Buffer{} - s, err := c.cmdSeachValuesArgs(vs) + s, err := server.cmdSeachValuesArgs(vs) if s.usingLua() { defer s.Close() defer func() { if r := recover(); r != nil { - res = server.NOMessage + res = NOMessage err = errors.New(r.(string)) return } }() } if err != nil { - return server.NOMessage, err + return NOMessage, err } - sw, err := c.newScanWriter( + sw, err := server.newScanWriter( wr, msg, s.key, s.output, s.precision, s.glob, true, s.cursor, s.limit, s.wheres, s.whereins, s.whereevals, s.nofields) if err != nil { - return server.NOMessage, err + return NOMessage, err } - if msg.OutputType == server.JSON { + if msg.OutputType == JSON { wr.WriteString(`{"ok":true`) } sw.writeHead() @@ -613,7 +612,7 @@ func (c *Controller) cmdSearch(msg *server.Message) (res resp.Value, err error) } } sw.writeFoot() - if msg.OutputType == server.JSON { + if msg.OutputType == JSON { wr.WriteString(`,"elapsed":"` + time.Now().Sub(start).String() + "\"}") return resp.BytesValue(wr.Bytes()), nil } diff --git a/internal/server/server.go b/internal/server/server.go index 1c8819d0..fdd0cb09 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -2,19 +2,1006 @@ package server import ( "bytes" + "crypto/rand" + "crypto/sha1" + "encoding/base64" "encoding/binary" "errors" "fmt" "io" "net" + "net/url" + "os" + "path" + "path/filepath" + "runtime" + "runtime/debug" + "strconv" "strings" + "sync" + "sync/atomic" "time" + "github.com/tidwall/buntdb" + "github.com/tidwall/evio" + "github.com/tidwall/geojson" + "github.com/tidwall/geojson/geometry" + "github.com/tidwall/redcon" "github.com/tidwall/resp" "github.com/tidwall/tile38/core" + "github.com/tidwall/tile38/internal/collection" + "github.com/tidwall/tile38/internal/ds" + "github.com/tidwall/tile38/internal/endpoint" + "github.com/tidwall/tile38/internal/expire" "github.com/tidwall/tile38/internal/log" ) +var errOOM = errors.New("OOM command not allowed when used memory > 'maxmemory'") + +const goingLive = "going live" + +const hookLogPrefix = "hook:log:" + +type commandDetailsT struct { + command string + key, id string + field string + value float64 + obj geojson.Object + fields []float64 + fmap map[string]int + oldObj geojson.Object + oldFields []float64 + updated bool + timestamp time.Time + + parent bool // when true, only children are forwarded + pattern string // PDEL key pattern + children []*commandDetailsT // for multi actions such as "PDEL" +} + +// Server is a tile38 controller +type Server struct { + // static values + host string + port int + http bool + dir string + started time.Time + config *Config + epc *endpoint.Manager + + // env opts + geomParseOpts geojson.ParseOptions + + // atomics + followc aint // counter increases when follow property changes + statsTotalConns aint // counter for total connections + statsTotalCommands aint // counter for total commands + statsExpired aint // item expiration counter + lastShrinkDuration aint + currentShrinkStart atime + stopBackgroundExpiring abool + stopWatchingMemory abool + stopWatchingAutoGC abool + outOfMemory abool + + connsmu sync.RWMutex + conns map[int]*Client + + exlistmu sync.RWMutex + exlist []exitem + + mu sync.RWMutex + aof *os.File // active aof file + aofbuf []byte // prewrite buffer + aofsz int // active size of the aof file + qdb *buntdb.DB // hook queue log + qidx uint64 // hook queue log last idx + cols ds.BTree // data collections + expires map[string]map[string]time.Time // synced with cols + + follows map[*bytes.Buffer]bool + fcond *sync.Cond + lstack []*commandDetailsT + lives map[*liveBuffer]bool + lcond *sync.Cond + fcup bool // follow caught up + fcuponce bool // follow caught up once + shrinking bool // aof shrinking flag + shrinklog [][]string // aof shrinking log + hooks map[string]*Hook // hook name + hookcols map[string]map[string]*Hook // col key + aofconnM map[net.Conn]bool + luascripts *lScriptMap + luapool *lStatePool + + pubsub *pubsub + hookex expire.List +} + +// Serve starts a new tile38 server +func Serve(host string, port int, dir string, http bool) error { + if core.AppendFileName == "" { + core.AppendFileName = path.Join(dir, "appendonly.aof") + } + if core.QueueFileName == "" { + core.QueueFileName = path.Join(dir, "queue.db") + } + log.Infof("Server started, Tile38 version %s, git %s", core.Version, core.GitSHA) + + // Initialize the server + server := &Server{ + host: host, + port: port, + dir: dir, + follows: make(map[*bytes.Buffer]bool), + fcond: sync.NewCond(&sync.Mutex{}), + lives: make(map[*liveBuffer]bool), + lcond: sync.NewCond(&sync.Mutex{}), + hooks: make(map[string]*Hook), + hookcols: make(map[string]map[string]*Hook), + aofconnM: make(map[net.Conn]bool), + expires: make(map[string]map[string]time.Time), + started: time.Now(), + conns: make(map[int]*Client), + http: http, + pubsub: newPubsub(), + } + + server.hookex.Expired = func(item expire.Item) { + switch v := item.(type) { + case *Hook: + server.possiblyExpireHook(v.Name) + } + } + server.epc = endpoint.NewManager(server) + server.luascripts = server.newScriptMap() + server.luapool = server.newPool() + defer server.luapool.Shutdown() + + if err := os.MkdirAll(dir, 0700); err != nil { + return err + } + var err error + server.config, err = loadConfig(filepath.Join(dir, "config")) + if err != nil { + return err + } + + // Allow for geometry indexing options through environment variables: + // T38IDXGEOMKIND -- None, RTree, QuadTree + // T38IDXGEOM -- Min number of points in a geometry for indexing. + // T38IDXMULTI -- Min number of object in a Multi/Collection for indexing. + server.geomParseOpts = *geojson.DefaultParseOptions + n, err := strconv.ParseUint(os.Getenv("T38IDXGEOM"), 10, 32) + if err == nil { + server.geomParseOpts.IndexGeometry = int(n) + } + n, err = strconv.ParseUint(os.Getenv("T38IDXMULTI"), 10, 32) + if err == nil { + server.geomParseOpts.IndexChildren = int(n) + } + indexKind := os.Getenv("T38IDXGEOMKIND") + switch indexKind { + default: + log.Errorf("Unknown index kind: %s", indexKind) + case "": + case "None": + server.geomParseOpts.IndexGeometryKind = geometry.None + case "RTree": + server.geomParseOpts.IndexGeometryKind = geometry.RTree + case "QuadTree": + server.geomParseOpts.IndexGeometryKind = geometry.QuadTree + } + if server.geomParseOpts.IndexGeometryKind == geometry.None { + log.Debugf("Geom indexing: %s", + server.geomParseOpts.IndexGeometryKind, + ) + } else { + log.Debugf("Geom indexing: %s (%d points)", + server.geomParseOpts.IndexGeometryKind, + server.geomParseOpts.IndexGeometry, + ) + } + log.Debugf("Multi indexing: RTree (%d points)", server.geomParseOpts.IndexChildren) + + // Load the queue before the aof + qdb, err := buntdb.Open(core.QueueFileName) + if err != nil { + return err + } + var qidx uint64 + if err := qdb.View(func(tx *buntdb.Tx) error { + val, err := tx.Get("hook:idx") + if err != nil { + if err == buntdb.ErrNotFound { + return nil + } + return err + } + qidx = stringToUint64(val) + return nil + }); err != nil { + return err + } + err = qdb.CreateIndex("hooks", hookLogPrefix+"*", buntdb.IndexJSONCaseSensitive("hook")) + if err != nil { + return err + } + + server.qdb = qdb + server.qidx = qidx + if err := server.migrateAOF(); err != nil { + return err + } + if core.AppendOnly == "yes" { + f, err := os.OpenFile(core.AppendFileName, os.O_CREATE|os.O_RDWR, 0600) + if err != nil { + return err + } + server.aof = f + if err := server.loadAOF(); err != nil { + return err + } + defer func() { + server.flushAOF() + server.aof.Sync() + }() + } + server.fillExpiresList() + + // Start background routines + if server.config.followHost() != "" { + go server.follow(server.config.followHost(), server.config.followPort(), + server.followc.get()) + } + go server.processLives() + go server.watchOutOfMemory() + go server.watchLuaStatePool() + go server.watchAutoGC() + go server.backgroundExpiring() + defer func() { + // Stop background routines + server.stopBackgroundExpiring.set(true) + server.stopWatchingMemory.set(true) + server.stopWatchingAutoGC.set(true) + server.followc.add(1) // this will force any follow communication to die + }() + + // Start the network server + return server.evioServe() +} + +func (server *Server) isProtected() bool { + if core.ProtectedMode == "no" { + // --protected-mode no + return false + } + if server.host != "" && server.host != "127.0.0.1" && + server.host != "::1" && server.host != "localhost" { + // -h address + return false + } + is := server.config.protectedMode() != "no" && server.config.requirePass() == "" + return is +} + +func (server *Server) evioServe() error { + var events evio.Events + if core.NumThreads == 0 { + events.NumLoops = -1 + } else { + events.NumLoops = core.NumThreads + } + events.LoadBalance = evio.LeastConnections + events.Serving = func(eserver evio.Server) (action evio.Action) { + if eserver.NumLoops == 1 { + log.Infof("Running single-threaded") + } else { + log.Infof("Running on %d threads", eserver.NumLoops) + } + for _, addr := range eserver.Addrs { + log.Infof("Ready to accept connections at %s", + addr) + } + return + } + var clientID int64 + events.Opened = func(econn evio.Conn) (out []byte, opts evio.Options, action evio.Action) { + // create the client + client := new(Client) + client.id = int(atomic.AddInt64(&clientID, 1)) + client.opened = time.Now() + client.remoteAddr = econn.RemoteAddr().String() + + // keep track of the client + econn.SetContext(client) + + // add client to server map + server.connsmu.Lock() + server.conns[client.id] = client + server.connsmu.Unlock() + server.statsTotalConns.add(1) + + // set the client keep-alive, if needed + if server.config.keepAlive() > 0 { + opts.TCPKeepAlive = time.Duration(server.config.keepAlive()) * time.Second + } + log.Debugf("Opened connection: %s", client.remoteAddr) + + // check if the connection is protected + if !strings.HasPrefix(client.remoteAddr, "127.0.0.1:") && + !strings.HasPrefix(client.remoteAddr, "[::1]:") { + if server.isProtected() { + // This is a protected server. Only loopback is allowed. + out = append(out, deniedMessage...) + action = evio.Close + return + } + } + return + } + + events.Closed = func(econn evio.Conn, err error) (action evio.Action) { + // load the client + client := econn.Context().(*Client) + + // delete from server map + server.connsmu.Lock() + delete(server.conns, client.id) + server.connsmu.Unlock() + + log.Debugf("Closed connection: %s", client.remoteAddr) + return + } + + events.Data = func(econn evio.Conn, in []byte) (out []byte, action evio.Action) { + // load the client + client := econn.Context().(*Client) + + // read the payload packet from the client input stream. + packet := client.in.Begin(in) + + // load the pipeline reader + pr := &client.pr + rdbuf := bytes.NewBuffer(packet) + pr.rd = rdbuf + pr.wr = client + + msgs, err := pr.ReadMessages() + if err != nil { + log.Error(err) + action = evio.Close + return + } + for _, msg := range msgs { + // Just closing connection if we have deprecated HTTP or WS connection, + // And --http-transport = false + if !server.http && (msg.ConnType == WebSocket || + msg.ConnType == HTTP) { + action = evio.Close + break + } + if msg != nil && msg.Command() != "" { + if client.outputType != Null { + msg.OutputType = client.outputType + } + if msg.Command() == "quit" { + if msg.OutputType == RESP { + io.WriteString(client, "+OK\r\n") + } + action = evio.Close + break + } + + // increment last used + client.mu.Lock() + client.last = time.Now() + client.mu.Unlock() + + // update total command count + server.statsTotalCommands.add(1) + + // handle the command + err := server.handleInputCommand(client, msg) + if err != nil { + if err.Error() == goingLive { + client.goLiveErr = err + client.goLiveMsg = msg + action = evio.Detach + return + } + log.Error(err) + action = evio.Close + return + } + + client.outputType = msg.OutputType + } else { + client.Write([]byte("HTTP/1.1 500 Bad Request\r\nConnection: close\r\n\r\n")) + action = evio.Close + break + } + if msg.ConnType == HTTP || msg.ConnType == WebSocket { + action = evio.Close + break + } + } + + packet = packet[len(packet)-rdbuf.Len():] + client.in.End(packet) + + out = client.out + client.out = nil + return + } + + events.Detached = func(econn evio.Conn, rwc io.ReadWriteCloser) (action evio.Action) { + client := econn.Context().(*Client) + client.conn = rwc + if len(client.out) > 0 { + rwc.Write(client.out) + client.out = nil + } + client.in = evio.InputStream{} + client.pr.rd = rwc + client.pr.wr = rwc + + log.Debugf("Detached connection: %s", client.remoteAddr) + go func() { + defer func() { + // close connection + rwc.Close() + server.connsmu.Lock() + delete(server.conns, client.id) + server.connsmu.Unlock() + log.Debugf("Closed connection: %s", client.remoteAddr) + }() + err := server.goLive( + client.goLiveErr, + &liveConn{econn.RemoteAddr(), rwc}, + &client.pr, + client.goLiveMsg, + client.goLiveMsg.ConnType == WebSocket, + ) + if err != nil { + log.Error(err) + } + }() + return + } + + events.PreWrite = func() { + server.mu.Lock() + defer server.mu.Unlock() + server.flushAOF() + } + + return evio.Serve(events, fmt.Sprintf("%s:%d", server.host, server.port)) +} + +type liveConn struct { + remoteAddr net.Addr + rwc io.ReadWriteCloser +} + +func (conn *liveConn) Close() error { + return conn.rwc.Close() +} + +func (conn *liveConn) LocalAddr() net.Addr { + panic("not supported") +} + +func (conn *liveConn) RemoteAddr() net.Addr { + return conn.remoteAddr +} +func (conn *liveConn) Read(b []byte) (n int, err error) { + return conn.rwc.Read(b) +} + +func (conn *liveConn) Write(b []byte) (n int, err error) { + return conn.rwc.Write(b) +} + +func (conn *liveConn) SetDeadline(deadline time.Time) error { + panic("not supported") +} + +func (conn *liveConn) SetReadDeadline(deadline time.Time) error { + panic("not supported") +} + +func (conn *liveConn) SetWriteDeadline(deadline time.Time) error { + panic("not supported") +} + +func (server *Server) watchAutoGC() { + t := time.NewTicker(time.Second) + defer t.Stop() + s := time.Now() + for range t.C { + if server.stopWatchingAutoGC.on() { + return + } + autoGC := server.config.autoGC() + if autoGC == 0 { + continue + } + if time.Now().Sub(s) < time.Second*time.Duration(autoGC) { + continue + } + var mem1, mem2 runtime.MemStats + runtime.ReadMemStats(&mem1) + log.Debugf("autogc(before): "+ + "alloc: %v, heap_alloc: %v, heap_released: %v", + mem1.Alloc, mem1.HeapAlloc, mem1.HeapReleased) + + runtime.GC() + debug.FreeOSMemory() + runtime.ReadMemStats(&mem2) + log.Debugf("autogc(after): "+ + "alloc: %v, heap_alloc: %v, heap_released: %v", + mem2.Alloc, mem2.HeapAlloc, mem2.HeapReleased) + s = time.Now() + } +} + +func (server *Server) watchOutOfMemory() { + t := time.NewTicker(time.Second * 2) + defer t.Stop() + var mem runtime.MemStats + for range t.C { + func() { + if server.stopWatchingMemory.on() { + return + } + oom := server.outOfMemory.on() + if server.config.maxMemory() == 0 { + if oom { + server.outOfMemory.set(false) + } + return + } + if oom { + runtime.GC() + } + runtime.ReadMemStats(&mem) + server.outOfMemory.set(int(mem.HeapAlloc) > server.config.maxMemory()) + }() + } +} + +func (server *Server) watchLuaStatePool() { + t := time.NewTicker(time.Second * 10) + defer t.Stop() + for range t.C { + func() { + server.luapool.Prune() + }() + } +} + +func (server *Server) setCol(key string, col *collection.Collection) { + server.cols.Set(key, col) +} + +func (server *Server) getCol(key string) *collection.Collection { + if value, ok := server.cols.Get(key); ok { + return value.(*collection.Collection) + } + return nil +} + +func (server *Server) scanGreaterOrEqual( + key string, iterator func(key string, col *collection.Collection) bool, +) { + server.cols.Ascend(key, func(ikey string, ivalue interface{}) bool { + return iterator(ikey, ivalue.(*collection.Collection)) + }) +} + +func (server *Server) deleteCol(key string) *collection.Collection { + if prev, ok := server.cols.Delete(key); ok { + return prev.(*collection.Collection) + } + return nil +} + +func isReservedFieldName(field string) bool { + switch field { + case "z", "lat", "lon": + return true + } + return false +} + +func (server *Server) handleInputCommand(client *Client, msg *Message) error { + start := time.Now() + serializeOutput := func(res resp.Value) (string, error) { + var resStr string + var err error + switch msg.OutputType { + case JSON: + resStr = res.String() + case RESP: + var resBytes []byte + resBytes, err = res.MarshalRESP() + resStr = string(resBytes) + } + return resStr, err + } + writeOutput := func(res string) error { + switch msg.ConnType { + default: + err := fmt.Errorf("unsupported conn type: %v", msg.ConnType) + log.Error(err) + return err + case WebSocket: + return WriteWebSocketMessage(client, []byte(res)) + case HTTP: + _, err := fmt.Fprintf(client, "HTTP/1.1 200 OK\r\n"+ + "Connection: close\r\n"+ + "Content-Length: %d\r\n"+ + "Content-Type: application/json; charset=utf-8\r\n"+ + "\r\n", len(res)+2) + if err != nil { + return err + } + _, err = io.WriteString(client, res) + if err != nil { + return err + } + _, err = io.WriteString(client, "\r\n") + return err + case RESP: + var err error + if msg.OutputType == JSON { + _, err = fmt.Fprintf(client, "$%d\r\n%s\r\n", len(res), res) + } else { + _, err = io.WriteString(client, res) + } + return err + case Native: + _, err := fmt.Fprintf(client, "$%d %s\r\n", len(res), res) + return err + } + } + // Ping. Just send back the response. No need to put through the pipeline. + if msg.Command() == "ping" || msg.Command() == "echo" { + switch msg.OutputType { + case JSON: + if len(msg.Args) > 1 { + return writeOutput(`{"ok":true,"` + msg.Command() + `":` + jsonString(msg.Args[1]) + `,"elapsed":"` + time.Now().Sub(start).String() + `"}`) + } + return writeOutput(`{"ok":true,"` + msg.Command() + `":"pong","elapsed":"` + time.Now().Sub(start).String() + `"}`) + case RESP: + if len(msg.Args) > 1 { + data := redcon.AppendBulkString(nil, msg.Args[1]) + return writeOutput(string(data)) + } + return writeOutput("+PONG\r\n") + } + return nil + } + writeErr := func(errMsg string) error { + switch msg.OutputType { + case JSON: + return writeOutput(`{"ok":false,"err":` + jsonString(errMsg) + `,"elapsed":"` + time.Now().Sub(start).String() + "\"}") + case RESP: + if errMsg == errInvalidNumberOfArguments.Error() { + return writeOutput("-ERR wrong number of arguments for '" + msg.Command() + "' command\r\n") + } + v, _ := resp.ErrorValue(errors.New("ERR " + errMsg)).MarshalRESP() + return writeOutput(string(v)) + } + return nil + } + + var write bool + + if !client.authd || msg.Command() == "auth" { + if server.config.requirePass() != "" { + password := "" + // This better be an AUTH command or the Message should contain an Auth + if msg.Command() != "auth" && msg.Auth == "" { + // Just shut down the pipeline now. The less the client connection knows the better. + return writeErr("authentication required") + } + if msg.Auth != "" { + password = msg.Auth + } else { + if len(msg.Args) > 1 { + password = msg.Args[1] + } + } + if server.config.requirePass() != strings.TrimSpace(password) { + return writeErr("invalid password") + } + client.authd = true + if msg.ConnType != HTTP { + resStr, _ := serializeOutput(OKMessage(msg, start)) + return writeOutput(resStr) + } + } else if msg.Command() == "auth" { + return writeErr("invalid password") + } + } + // choose the locking strategy + switch msg.Command() { + default: + server.mu.RLock() + defer server.mu.RUnlock() + case "set", "del", "drop", "fset", "flushdb", + "setchan", "pdelchan", "delchan", + "sethook", "pdelhook", "delhook", + "expire", "persist", "jset", "pdel": + // write operations + write = true + server.mu.Lock() + defer server.mu.Unlock() + if server.config.followHost() != "" { + return writeErr("not the leader") + } + if server.config.readOnly() { + return writeErr("read only") + } + case "eval", "evalsha": + // write operations (potentially) but no AOF for the script command itself + server.mu.Lock() + defer server.mu.Unlock() + if server.config.followHost() != "" { + return writeErr("not the leader") + } + if server.config.readOnly() { + return writeErr("read only") + } + case "get", "keys", "scan", "nearby", "within", "intersects", "hooks", + "chans", "search", "ttl", "bounds", "server", "info", "type", "jget", + "evalro", "evalrosha": + // read operations + server.mu.RLock() + defer server.mu.RUnlock() + if server.config.followHost() != "" && !server.fcuponce { + return writeErr("catching up to leader") + } + case "follow", "readonly", "config": + // system operations + // does not write to aof, but requires a write lock. + server.mu.Lock() + defer server.mu.Unlock() + case "output": + // this is local connection operation. Locks not needed. + case "echo": + case "massinsert": + // dev operation + server.mu.Lock() + defer server.mu.Unlock() + case "sleep": + // dev operation + server.mu.RLock() + defer server.mu.RUnlock() + case "shutdown": + // dev operation + server.mu.Lock() + defer server.mu.Unlock() + case "aofshrink": + server.mu.RLock() + defer server.mu.RUnlock() + case "client": + server.mu.Lock() + defer server.mu.Unlock() + case "evalna", "evalnasha": + // No locking for scripts, otherwise writes cannot happen within scripts + case "subscribe", "psubscribe", "publish": + // No locking for pubsub + } + + res, d, err := server.command(msg, client) + + if res.Type() == resp.Error { + return writeErr(res.String()) + } + if err != nil { + if err.Error() == goingLive { + return err + } + return writeErr(err.Error()) + } + if write { + if err := server.writeAOF(msg.Args, &d); err != nil { + if _, ok := err.(errAOFHook); ok { + return writeErr(err.Error()) + } + log.Fatal(err) + return err + } + } + + if !isRespValueEmptyString(res) { + var resStr string + resStr, err := serializeOutput(res) + if err != nil { + return err + } + if err := writeOutput(resStr); err != nil { + return err + } + } + return nil +} + +func isRespValueEmptyString(val resp.Value) bool { + return !val.IsNull() && (val.Type() == resp.SimpleString || val.Type() == resp.BulkString) && len(val.Bytes()) == 0 +} + +func randomKey(n int) string { + b := make([]byte, n) + nn, err := rand.Read(b) + if err != nil { + panic(err) + } + if nn != n { + panic("random failed") + } + return fmt.Sprintf("%x", b) +} + +func (server *Server) reset() { + server.aofsz = 0 + server.cols = ds.BTree{} + server.exlistmu.Lock() + server.exlist = nil + server.exlistmu.Unlock() + server.expires = make(map[string]map[string]time.Time) +} + +func (server *Server) command(msg *Message, client *Client) ( + res resp.Value, d commandDetailsT, err error, +) { + switch msg.Command() { + default: + err = fmt.Errorf("unknown command '%s'", msg.Args[0]) + case "set": + res, d, err = server.cmdSet(msg) + case "fset": + res, d, err = server.cmdFset(msg) + case "del": + res, d, err = server.cmdDel(msg) + case "pdel": + res, d, err = server.cmdPdel(msg) + case "drop": + res, d, err = server.cmdDrop(msg) + case "flushdb": + res, d, err = server.cmdFlushDB(msg) + + case "sethook": + res, d, err = server.cmdSetHook(msg, false) + case "delhook": + res, d, err = server.cmdDelHook(msg, false) + case "pdelhook": + res, d, err = server.cmdPDelHook(msg, false) + case "hooks": + res, err = server.cmdHooks(msg, false) + + case "setchan": + res, d, err = server.cmdSetHook(msg, true) + case "delchan": + res, d, err = server.cmdDelHook(msg, true) + case "pdelchan": + res, d, err = server.cmdPDelHook(msg, true) + case "chans": + res, err = server.cmdHooks(msg, true) + + case "expire": + res, d, err = server.cmdExpire(msg) + case "persist": + res, d, err = server.cmdPersist(msg) + case "ttl": + res, err = server.cmdTTL(msg) + case "shutdown": + if !core.DevMode { + err = fmt.Errorf("unknown command '%s'", msg.Args[0]) + return + } + log.Fatal("shutdown requested by developer") + case "massinsert": + if !core.DevMode { + err = fmt.Errorf("unknown command '%s'", msg.Args[0]) + return + } + res, err = server.cmdMassInsert(msg) + case "sleep": + if !core.DevMode { + err = fmt.Errorf("unknown command '%s'", msg.Args[0]) + return + } + res, err = server.cmdSleep(msg) + case "follow": + res, err = server.cmdFollow(msg) + case "readonly": + res, err = server.cmdReadOnly(msg) + case "stats": + res, err = server.cmdStats(msg) + case "server": + res, err = server.cmdServer(msg) + case "info": + res, err = server.cmdInfo(msg) + case "scan": + res, err = server.cmdScan(msg) + case "nearby": + res, err = server.cmdNearby(msg) + case "within": + res, err = server.cmdWithin(msg) + case "intersects": + res, err = server.cmdIntersects(msg) + case "search": + res, err = server.cmdSearch(msg) + case "bounds": + res, err = server.cmdBounds(msg) + case "get": + res, err = server.cmdGet(msg) + case "jget": + res, err = server.cmdJget(msg) + case "jset": + res, d, err = server.cmdJset(msg) + case "jdel": + res, d, err = server.cmdJdel(msg) + case "type": + res, err = server.cmdType(msg) + case "keys": + res, err = server.cmdKeys(msg) + case "output": + res, err = server.cmdOutput(msg) + case "aof": + res, err = server.cmdAOF(msg) + case "aofmd5": + res, err = server.cmdAOFMD5(msg) + case "gc": + runtime.GC() + debug.FreeOSMemory() + res = OKMessage(msg, time.Now()) + case "aofshrink": + go server.aofshrink() + res = OKMessage(msg, time.Now()) + case "config get": + res, err = server.cmdConfigGet(msg) + case "config set": + res, err = server.cmdConfigSet(msg) + case "config rewrite": + res, err = server.cmdConfigRewrite(msg) + case "config", "script": + // These get rewritten into "config foo" and "script bar" + err = fmt.Errorf("unknown command '%s'", msg.Args[0]) + if len(msg.Args) > 1 { + command := msg.Args[0] + " " + msg.Args[1] + msg.Args[1] = command + msg.Args = msg.Args[1:] + return server.command(msg, client) + } + case "client": + res, err = server.cmdClient(msg, client) + case "eval", "evalro", "evalna": + res, err = server.cmdEvalUnified(false, msg) + case "evalsha", "evalrosha", "evalnasha": + res, err = server.cmdEvalUnified(true, msg) + case "script load": + res, err = server.cmdScriptLoad(msg) + case "script exists": + res, err = server.cmdScriptExists(msg) + case "script flush": + res, err = server.cmdScriptFlush(msg) + case "subscribe": + res, err = server.cmdSubscribe(msg) + case "psubscribe": + res, err = server.cmdPsubscribe(msg) + case "publish": + res, err = server.cmdPublish(msg) + } + return +} + // This phrase is copied nearly verbatim from Redis. var deniedMessage = []byte(strings.Replace(strings.TrimSpace(` -DENIED Tile38 is running in protected mode because protected mode is enabled, @@ -34,15 +1021,9 @@ password. NOTE: You only need to do one of the above things in order for the server to start accepting connections from the outside. `), "\n", " ", -1) + "\r\n") -// Conn represents a server connection. -type Conn struct { - net.Conn - Authenticated bool -} - // SetKeepAlive sets the connection keepalive -func (conn Conn) SetKeepAlive(period time.Duration) error { - if tcp, ok := conn.Conn.(*net.TCPConn); ok { +func setKeepAlive(conn net.Conn, period time.Duration) error { + if tcp, ok := conn.(*net.TCPConn); ok { if err := tcp.SetKeepAlive(true); err != nil { return err } @@ -53,120 +1034,6 @@ func (conn Conn) SetKeepAlive(period time.Duration) error { var errCloseHTTP = errors.New("close http") -// ListenAndServe starts a tile38 server at the specified address. -func ListenAndServe( - host string, port int, - protected func() bool, - handler func(conn *Conn, msg *Message, rd *PipelineReader, w io.Writer, websocket bool) error, - opened func(conn *Conn), - closed func(conn *Conn), - lnp *net.Listener, - http bool, -) error { - ln, err := net.Listen("tcp", fmt.Sprintf("%s:%d", host, port)) - if err != nil { - return err - } - if lnp != nil { - *lnp = ln - } - log.Infof("The server is now ready to accept connections on port %d", port) - for { - conn, err := ln.Accept() - if err != nil { - log.Error(err) - return err - } - go handleConn(&Conn{Conn: conn}, protected, handler, opened, closed, http) - } -} - -func handleConn( - conn *Conn, - protected func() bool, - handler func(conn *Conn, msg *Message, rd *PipelineReader, w io.Writer, websocket bool) error, - opened func(conn *Conn), - closed func(conn *Conn), - http bool, -) { - addr := conn.RemoteAddr().String() - opened(conn) - if core.ShowDebugMessages { - log.Debugf("opened connection: %s", addr) - } - defer func() { - conn.Close() - closed(conn) - if core.ShowDebugMessages { - log.Debugf("closed connection: %s", addr) - } - }() - if !strings.HasPrefix(addr, "127.0.0.1:") && !strings.HasPrefix(addr, "[::1]:") { - if protected() { - // This is a protected server. Only loopback is allowed. - conn.Write(deniedMessage) - return - } - } - - wr := &bytes.Buffer{} - outputType := Null - rd := NewPipelineReader(conn) - for { - wr.Reset() - ok := func() bool { - msgs, err := rd.ReadMessages() - if err != nil { - errstr := err.Error() - if err == errCloseHTTP || - err == io.EOF || - strings.Contains(errstr, "use of closed network connection") || - strings.Contains(errstr, "connection reset by peer") { - return false - } - log.Error(err) - return false - } - for _, msg := range msgs { - // Just closing connection if we have deprecated HTTP or WS connection, - // And --http-transport = false - if !http && (msg.ConnType == WebSocket || msg.ConnType == HTTP) { - return false - } - if msg != nil && msg.Command != "" { - if outputType != Null { - msg.OutputType = outputType - } - if msg.Command == "quit" { - if msg.OutputType == RESP { - io.WriteString(wr, "+OK\r\n") - } - return false - } - err := handler(conn, msg, rd, wr, msg.ConnType == WebSocket) - if err != nil { - log.Error(err) - return false - } - outputType = msg.OutputType - } else { - wr.Write([]byte("HTTP/1.1 500 Bad Request\r\nConnection: close\r\n\r\n")) - return false - } - if msg.ConnType == HTTP || msg.ConnType == WebSocket { - return false - } - } - return true - }() - conn.Write(wr.Bytes()) - if !ok { - break - } - } - // all done -} - // WriteWebSocketMessage write a websocket message to an io.Writer. func WriteWebSocketMessage(w io.Writer, data []byte) error { var msg []byte @@ -204,3 +1071,288 @@ func OKMessage(msg *Message, start time.Time) resp.Value { // NOMessage is no message var NOMessage = resp.SimpleStringValue("") + +var errInvalidHTTP = errors.New("invalid HTTP request") + +// Type is resp type +type Type byte + +// Protocol Types +const ( + Null Type = iota + RESP + Telnet + Native + HTTP + WebSocket + JSON +) + +// Message is a resp message +type Message struct { + Args []string + ConnType Type + OutputType Type + Auth string +} + +// Command returns the first argument as a lowercase string +func (msg *Message) Command() string { + return strings.ToLower(msg.Args[0]) +} + +// PipelineReader ... +type PipelineReader struct { + rd io.Reader + wr io.Writer + packet [0xFFFF]byte + buf []byte +} + +const kindHTTP redcon.Kind = 9999 + +// NewPipelineReader ... +func NewPipelineReader(rd io.ReadWriter) *PipelineReader { + return &PipelineReader{rd: rd, wr: rd} +} + +func readcrlfline(packet []byte) (line string, leftover []byte, ok bool) { + for i := 1; i < len(packet); i++ { + if packet[i] == '\n' && packet[i-1] == '\r' { + return string(packet[:i-1]), packet[i+1:], true + } + } + return "", packet, false +} + +func readNextHTTPCommand(packet []byte, argsIn [][]byte, msg *Message, wr io.Writer) ( + complete bool, args [][]byte, kind redcon.Kind, leftover []byte, err error, +) { + args = argsIn[:0] + msg.ConnType = HTTP + msg.OutputType = JSON + opacket := packet + + ready, err := func() (bool, error) { + var line string + var ok bool + + // read header + var headers []string + for { + line, packet, ok = readcrlfline(packet) + if !ok { + return false, nil + } + if line == "" { + break + } + headers = append(headers, line) + } + parts := strings.Split(headers[0], " ") + if len(parts) != 3 { + return false, errInvalidHTTP + } + method := parts[0] + path := parts[1] + if len(path) == 0 || path[0] != '/' { + return false, errInvalidHTTP + } + path, err = url.QueryUnescape(path[1:]) + if err != nil { + return false, errInvalidHTTP + } + if method != "GET" && method != "POST" { + return false, errInvalidHTTP + } + contentLength := 0 + websocket := false + websocketVersion := 0 + websocketKey := "" + for _, header := range headers[1:] { + if header[0] == 'a' || header[0] == 'A' { + if strings.HasPrefix(strings.ToLower(header), "authorization:") { + msg.Auth = strings.TrimSpace(header[len("authorization:"):]) + } + } else if header[0] == 'u' || header[0] == 'U' { + if strings.HasPrefix(strings.ToLower(header), "upgrade:") && strings.ToLower(strings.TrimSpace(header[len("upgrade:"):])) == "websocket" { + websocket = true + } + } else if header[0] == 's' || header[0] == 'S' { + if strings.HasPrefix(strings.ToLower(header), "sec-websocket-version:") { + var n uint64 + n, err = strconv.ParseUint(strings.TrimSpace(header[len("sec-websocket-version:"):]), 10, 64) + if err != nil { + return false, err + } + websocketVersion = int(n) + } else if strings.HasPrefix(strings.ToLower(header), "sec-websocket-key:") { + websocketKey = strings.TrimSpace(header[len("sec-websocket-key:"):]) + } + } else if header[0] == 'c' || header[0] == 'C' { + if strings.HasPrefix(strings.ToLower(header), "content-length:") { + var n uint64 + n, err = strconv.ParseUint(strings.TrimSpace(header[len("content-length:"):]), 10, 64) + if err != nil { + return false, err + } + contentLength = int(n) + } + } + } + if websocket && websocketVersion >= 13 && websocketKey != "" { + msg.ConnType = WebSocket + if wr == nil { + return false, errors.New("connection is nil") + } + sum := sha1.Sum([]byte(websocketKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")) + accept := base64.StdEncoding.EncodeToString(sum[:]) + wshead := "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: " + accept + "\r\n\r\n" + if _, err = wr.Write([]byte(wshead)); err != nil { + return false, err + } + } else if contentLength > 0 { + msg.ConnType = HTTP + if len(packet) < contentLength { + return false, nil + } + path += string(packet[:contentLength]) + packet = packet[contentLength:] + } + if path == "" { + return true, nil + } + nmsg, err := readNativeMessageLine([]byte(path)) + if err != nil { + return false, err + } + + msg.OutputType = JSON + msg.Args = nmsg.Args + return true, nil + }() + if err != nil || !ready { + return false, args[:0], kindHTTP, opacket, err + } + return true, args[:0], kindHTTP, packet, nil +} +func readNextCommand(packet []byte, argsIn [][]byte, msg *Message, wr io.Writer) ( + complete bool, args [][]byte, kind redcon.Kind, leftover []byte, err error, +) { + if packet[0] == 'G' || packet[0] == 'P' { + // could be an HTTP request + var line []byte + for i := 1; i < len(packet); i++ { + if packet[i] == '\n' { + if packet[i-1] == '\r' { + line = packet[:i+1] + break + } + } + } + if len(line) == 0 { + return false, argsIn[:0], redcon.Redis, packet, nil + } + if len(line) > 11 && string(line[len(line)-11:len(line)-5]) == " HTTP/" { + return readNextHTTPCommand(packet, argsIn, msg, wr) + } + } + return redcon.ReadNextCommand(packet, args) +} + +// ReadMessages ... +func (rd *PipelineReader) ReadMessages() ([]*Message, error) { + var msgs []*Message +moreData: + n, err := rd.rd.Read(rd.packet[:]) + if err != nil { + return nil, err + } + if n == 0 { + // need more data + goto moreData + } + data := rd.packet[:n] + if len(rd.buf) > 0 { + data = append(rd.buf, data...) + } + for len(data) > 0 { + msg := &Message{} + complete, args, kind, leftover, err := readNextCommand(data, nil, msg, rd.wr) + if err != nil { + break + } + if !complete { + break + } + if kind == kindHTTP { + if len(msg.Args) == 0 { + return nil, errInvalidHTTP + } + msgs = append(msgs, msg) + } else if len(args) > 0 { + for i := 0; i < len(args); i++ { + msg.Args = append(msg.Args, string(args[i])) + } + switch kind { + case redcon.Redis: + msg.ConnType = RESP + msg.OutputType = RESP + case redcon.Tile38: + msg.ConnType = Native + msg.OutputType = JSON + case redcon.Telnet: + msg.ConnType = RESP + msg.OutputType = RESP + } + msgs = append(msgs, msg) + } + data = leftover + } + if len(data) > 0 { + rd.buf = append(rd.buf[:0], data...) + } else if len(rd.buf) > 0 { + rd.buf = rd.buf[:0] + } + if err != nil && len(msgs) == 0 { + return nil, err + } + return msgs, nil +} + +func readNativeMessageLine(line []byte) (*Message, error) { + var args []string +reading: + for len(line) != 0 { + if line[0] == '{' { + // The native protocol cannot understand json boundaries so it assumes that + // a json element must be at the end of the line. + args = append(args, string(line)) + break + } + if line[0] == '"' && line[len(line)-1] == '"' { + if len(args) > 0 && + strings.ToLower(args[0]) == "set" && + strings.ToLower(args[len(args)-1]) == "string" { + // Setting a string value that is contained inside double quotes. + // This is only because of the boundary issues of the native protocol. + args = append(args, string(line[1:len(line)-1])) + break + } + } + i := 0 + for ; i < len(line); i++ { + if line[i] == ' ' { + arg := string(line[:i]) + if arg != "" { + args = append(args, arg) + } + line = line[i+1:] + continue reading + } + } + args = append(args, string(line)) + break + } + return &Message{Args: args, ConnType: Native, OutputType: JSON}, nil +} diff --git a/internal/controller/stats.go b/internal/server/stats.go similarity index 84% rename from internal/controller/stats.go rename to internal/server/stats.go index 51f0e059..1ee30d07 100644 --- a/internal/controller/stats.go +++ b/internal/server/stats.go @@ -1,4 +1,4 @@ -package controller +package server import ( "bytes" @@ -13,16 +13,15 @@ import ( "github.com/tidwall/resp" "github.com/tidwall/tile38/core" "github.com/tidwall/tile38/internal/collection" - "github.com/tidwall/tile38/internal/server" ) -func (c *Controller) cmdStats(msg *server.Message) (res resp.Value, err error) { +func (c *Server) cmdStats(msg *Message) (res resp.Value, err error) { start := time.Now() - vs := msg.Values[1:] + vs := msg.Args[1:] var ms = []map[string]interface{}{} if len(vs) == 0 { - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments } var vals []resp.Value var key string @@ -40,38 +39,38 @@ func (c *Controller) cmdStats(msg *server.Message) (res resp.Value, err error) { m["num_objects"] = col.Count() m["num_strings"] = col.StringCount() switch msg.OutputType { - case server.JSON: + case JSON: ms = append(ms, m) - case server.RESP: + case RESP: vals = append(vals, resp.ArrayValue(respValuesSimpleMap(m))) } } else { switch msg.OutputType { - case server.JSON: + case JSON: ms = append(ms, nil) - case server.RESP: + case RESP: vals = append(vals, resp.NullValue()) } } } switch msg.OutputType { - case server.JSON: + case JSON: data, err := json.Marshal(ms) if err != nil { - return server.NOMessage, err + return NOMessage, err } res = resp.StringValue(`{"ok":true,"stats":` + string(data) + `,"elapsed":"` + time.Now().Sub(start).String() + "\"}") - case server.RESP: + case RESP: res = resp.ArrayValue(vals) } return res, nil } -func (c *Controller) cmdServer(msg *server.Message) (res resp.Value, err error) { +func (c *Server) cmdServer(msg *Message) (res resp.Value, err error) { start := time.Now() - if len(msg.Values) != 1 { - return server.NOMessage, errInvalidNumberOfArguments + if len(msg.Args) != 1 { + return NOMessage, errInvalidNumberOfArguments } m := make(map[string]interface{}) m["id"] = c.config.serverID() @@ -122,30 +121,30 @@ func (c *Controller) cmdServer(msg *server.Message) (res resp.Value, err error) m["threads"] = runtime.GOMAXPROCS(0) switch msg.OutputType { - case server.JSON: + case JSON: data, err := json.Marshal(m) if err != nil { - return server.NOMessage, err + return NOMessage, err } res = resp.StringValue(`{"ok":true,"stats":` + string(data) + `,"elapsed":"` + time.Now().Sub(start).String() + "\"}") - case server.RESP: + case RESP: vals := respValuesSimpleMap(m) res = resp.ArrayValue(vals) } return res, nil } -func (c *Controller) writeInfoServer(w *bytes.Buffer) { +func (c *Server) writeInfoServer(w *bytes.Buffer) { fmt.Fprintf(w, "tile38_version:%s\r\n", core.Version) fmt.Fprintf(w, "redis_version:%s\r\n", core.Version) //Version of the Redis server fmt.Fprintf(w, "uptime_in_seconds:%d\r\n", time.Now().Sub(c.started)/time.Second) //Number of seconds since Redis server start } -func (c *Controller) writeInfoClients(w *bytes.Buffer) { +func (c *Server) writeInfoClients(w *bytes.Buffer) { c.connsmu.RLock() fmt.Fprintf(w, "connected_clients:%d\r\n", len(c.conns)) // Number of client connections (excluding connections from slaves) c.connsmu.RUnlock() } -func (c *Controller) writeInfoMemory(w *bytes.Buffer) { +func (c *Server) writeInfoMemory(w *bytes.Buffer) { var mem runtime.MemStats runtime.ReadMemStats(&mem) fmt.Fprintf(w, "used_memory:%d\r\n", mem.Alloc) // total number of bytes allocated by Redis using its allocator (either standard libc, jemalloc, or an alternative allocator such as tcmalloc @@ -156,7 +155,7 @@ func boolInt(t bool) int { } return 0 } -func (c *Controller) writeInfoPersistence(w *bytes.Buffer) { +func (c *Server) writeInfoPersistence(w *bytes.Buffer) { fmt.Fprintf(w, "aof_enabled:1\r\n") fmt.Fprintf(w, "aof_rewrite_in_progress:%d\r\n", boolInt(c.shrinking)) // Flag indicating a AOF rewrite operation is on-going fmt.Fprintf(w, "aof_last_rewrite_time_sec:%d\r\n", c.lastShrinkDuration.get()/int(time.Second)) // Duration of the last AOF rewrite operation in seconds @@ -168,28 +167,28 @@ func (c *Controller) writeInfoPersistence(w *bytes.Buffer) { } } -func (c *Controller) writeInfoStats(w *bytes.Buffer) { +func (c *Server) writeInfoStats(w *bytes.Buffer) { fmt.Fprintf(w, "total_connections_received:%d\r\n", c.statsTotalConns.get()) // Total number of connections accepted by the server fmt.Fprintf(w, "total_commands_processed:%d\r\n", c.statsTotalCommands.get()) // Total number of commands processed by the server fmt.Fprintf(w, "expired_keys:%d\r\n", c.statsExpired.get()) // Total number of key expiration events } -func (c *Controller) writeInfoReplication(w *bytes.Buffer) { +func (c *Server) writeInfoReplication(w *bytes.Buffer) { fmt.Fprintf(w, "connected_slaves:%d\r\n", len(c.aofconnM)) // Number of connected slaves } -func (c *Controller) writeInfoCluster(w *bytes.Buffer) { +func (c *Server) writeInfoCluster(w *bytes.Buffer) { fmt.Fprintf(w, "cluster_enabled:0\r\n") } -func (c *Controller) cmdInfo(msg *server.Message) (res resp.Value, err error) { +func (c *Server) cmdInfo(msg *Message) (res resp.Value, err error) { start := time.Now() sections := []string{"server", "clients", "memory", "persistence", "stats", "replication", "cpu", "cluster", "keyspace"} - switch len(msg.Values) { + switch len(msg.Args) { default: - return server.NOMessage, errInvalidNumberOfArguments + return NOMessage, errInvalidNumberOfArguments case 1: case 2: - section := strings.ToLower(msg.Values[1].String()) + section := strings.ToLower(msg.Args[1]) switch section { default: sections = []string{section} @@ -235,13 +234,13 @@ func (c *Controller) cmdInfo(msg *server.Message) (res resp.Value, err error) { } switch msg.OutputType { - case server.JSON: + case JSON: data, err := json.Marshal(w.String()) if err != nil { - return server.NOMessage, err + return NOMessage, err } res = resp.StringValue(`{"ok":true,"info":` + string(data) + `,"elapsed":"` + time.Now().Sub(start).String() + "\"}") - case server.RESP: + case RESP: res = resp.BytesValue(w.Bytes()) } @@ -262,7 +261,7 @@ func respValuesSimpleMap(m map[string]interface{}) []resp.Value { return vals } -func (c *Controller) statsCollections(line string) (string, error) { +func (c *Server) statsCollections(line string) (string, error) { start := time.Now() var key string var ms = []map[string]interface{}{} diff --git a/internal/controller/stats_cpu.go b/internal/server/stats_cpu.go similarity index 93% rename from internal/controller/stats_cpu.go rename to internal/server/stats_cpu.go index a839aea8..83f8733e 100644 --- a/internal/controller/stats_cpu.go +++ b/internal/server/stats_cpu.go @@ -1,6 +1,6 @@ // +build !linux,!darwin -package controller +package server import ( "bytes" diff --git a/internal/controller/stats_cpu_darlin.go b/internal/server/stats_cpu_darlin.go similarity index 89% rename from internal/controller/stats_cpu_darlin.go rename to internal/server/stats_cpu_darlin.go index 00e7cb4c..ef5d7682 100644 --- a/internal/controller/stats_cpu_darlin.go +++ b/internal/server/stats_cpu_darlin.go @@ -1,6 +1,6 @@ // +build linux darwin -package controller +package server import ( "bytes" @@ -8,7 +8,7 @@ import ( "syscall" ) -func (c *Controller) writeInfoCPU(w *bytes.Buffer) { +func (c *Server) writeInfoCPU(w *bytes.Buffer) { var selfRu syscall.Rusage var cRu syscall.Rusage syscall.Getrusage(syscall.RUSAGE_SELF, &selfRu) diff --git a/internal/controller/token.go b/internal/server/token.go similarity index 97% rename from internal/controller/token.go rename to internal/server/token.go index 6b691616..05721ec2 100644 --- a/internal/controller/token.go +++ b/internal/server/token.go @@ -1,4 +1,4 @@ -package controller +package server import ( "errors" @@ -7,7 +7,6 @@ import ( "strconv" "strings" - "github.com/tidwall/resp" "github.com/yuin/gopher-lua" ) @@ -34,18 +33,18 @@ func token(line string) (newLine, token string) { return "", line } -func tokenval(vs []resp.Value) (nvs []resp.Value, token string, ok bool) { +func tokenval(vs []string) (nvs []string, token string, ok bool) { if len(vs) > 0 { - token = vs[0].String() + token = vs[0] nvs = vs[1:] ok = true } return } -func tokenvalbytes(vs []resp.Value) (nvs []resp.Value, token []byte, ok bool) { +func tokenvalbytes(vs []string) (nvs []string, token []byte, ok bool) { if len(vs) > 0 { - token = vs[0].Bytes() + token = []byte(vs[0]) nvs = vs[1:] ok = true } @@ -168,7 +167,7 @@ func (wherein whereinT) match(value float64) bool { } type whereevalT struct { - c *Controller + c *Server luaState *lua.LState fn *lua.LFunction } @@ -249,10 +248,10 @@ type searchScanBaseTokens struct { clip bool } -func (c *Controller) parseSearchScanBaseTokens( - cmd string, t searchScanBaseTokens, vs []resp.Value, +func (c *Server) parseSearchScanBaseTokens( + cmd string, t searchScanBaseTokens, vs []string, ) ( - vsout []resp.Value, tout searchScanBaseTokens, err error, + vsout []string, tout searchScanBaseTokens, err error, ) { var ok bool if vs, t.key, ok = tokenval(vs); !ok || t.key == "" { @@ -622,7 +621,7 @@ func (c *Controller) parseSearchScanBaseTokens( } t.output = defaultSearchOutput - var nvs []resp.Value + var nvs []string var sprecision string var which string if nvs, which, ok = tokenval(vs); ok && which != "" { diff --git a/internal/controller/token_test.go b/internal/server/token_test.go similarity index 99% rename from internal/controller/token_test.go rename to internal/server/token_test.go index 425ad99d..759b1abf 100644 --- a/internal/controller/token_test.go +++ b/internal/server/token_test.go @@ -1,4 +1,4 @@ -package controller +package server import ( "strings" diff --git a/tests/mock_test.go b/tests/mock_test.go index 52d686bd..2deb6c8b 100644 --- a/tests/mock_test.go +++ b/tests/mock_test.go @@ -13,8 +13,8 @@ import ( "github.com/garyburd/redigo/redis" "github.com/tidwall/tile38/core" - "github.com/tidwall/tile38/internal/controller" tlog "github.com/tidwall/tile38/internal/log" + "github.com/tidwall/tile38/internal/server" ) var errTimeout = errors.New("timeout") @@ -51,7 +51,7 @@ func mockOpenServer() (*mockServer, error) { s := &mockServer{port: port} tlog.SetOutput(logOutput) go func() { - if err := controller.ListenAndServe("localhost", port, dir, true); err != nil { + if err := server.Serve("localhost", port, dir, true); err != nil { log.Fatal(err) } }() diff --git a/vendor/github.com/kavu/go_reuseport/.circleci/config.yml b/vendor/github.com/kavu/go_reuseport/.circleci/config.yml new file mode 100644 index 00000000..ca3688dd --- /dev/null +++ b/vendor/github.com/kavu/go_reuseport/.circleci/config.yml @@ -0,0 +1,14 @@ +version: 2 +jobs: + build: + docker: + - image: circleci/golang:1.8 + + working_directory: /go/src/github.com/kavu/go_reuseport + + steps: + - checkout + - run: go get -v -t -d ./... + - run: go test -v -cover ./... + - run: go test -v -cover -race ./... -coverprofile=coverage.txt -covermode=atomic + - run: go test -v -cover -race -benchmem -benchtime=5s -bench=. diff --git a/vendor/github.com/kavu/go_reuseport/.gitignore b/vendor/github.com/kavu/go_reuseport/.gitignore new file mode 100644 index 00000000..8661ba57 --- /dev/null +++ b/vendor/github.com/kavu/go_reuseport/.gitignore @@ -0,0 +1,23 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +.DS_Store diff --git a/vendor/github.com/kavu/go_reuseport/.travis.yml b/vendor/github.com/kavu/go_reuseport/.travis.yml new file mode 100644 index 00000000..e858e343 --- /dev/null +++ b/vendor/github.com/kavu/go_reuseport/.travis.yml @@ -0,0 +1,30 @@ +dist: trusty +sudo: true + +language: go + +go: + - "1.6" + - "1.7" + - "1.8" + - "1.9" + - "1.10" + - "1.11" + - tip + +os: + - linux + - osx + +before_install: + - uname -a + +script: ./test.bash + +matrix: + allow_failures: + - os: osx + - go: tip + +after_success: + - bash <(curl -s https://codecov.io/bash) diff --git a/vendor/github.com/kavu/go_reuseport/LICENSE b/vendor/github.com/kavu/go_reuseport/LICENSE new file mode 100644 index 00000000..5f25159a --- /dev/null +++ b/vendor/github.com/kavu/go_reuseport/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Max Riveiro + +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 diff --git a/vendor/github.com/kavu/go_reuseport/Makefile b/vendor/github.com/kavu/go_reuseport/Makefile new file mode 100644 index 00000000..4aa3d2b6 --- /dev/null +++ b/vendor/github.com/kavu/go_reuseport/Makefile @@ -0,0 +1,9 @@ +lint: + @gometalinter \ + --disable=errcheck \ + --disable=dupl \ + --min-const-length=5 \ + --min-confidence=0.25 \ + --cyclo-over=20 \ + --enable=unused \ + --deadline=100s diff --git a/vendor/github.com/kavu/go_reuseport/README.md b/vendor/github.com/kavu/go_reuseport/README.md new file mode 100644 index 00000000..9e9726b6 --- /dev/null +++ b/vendor/github.com/kavu/go_reuseport/README.md @@ -0,0 +1,48 @@ +# GO_REUSEPORT + +[](https://travis-ci.org/kavu/go_reuseport) +[](https://codecov.io/gh/kavu/go_reuseport) +[](https://godoc.org/github.com/kavu/go_reuseport) + +**GO_REUSEPORT** is a little expirement to create a `net.Listener` that supports [SO_REUSEPORT](http://lwn.net/Articles/542629/) socket option. + +For now, Darwin and Linux (from 3.9) systems are supported. I'll be pleased if you'll test other systems and tell me the results. + documentation on [godoc.org](http://godoc.org/github.com/kavu/go_reuseport "go_reuseport documentation"). + +## Example ## + +```go +package main + +import ( + "fmt" + "html" + "net/http" + "os" + "runtime" + "github.com/kavu/go_reuseport" +) + +func main() { + listener, err := reuseport.Listen("tcp", "localhost:8881") + if err != nil { + panic(err) + } + defer listener.Close() + + server := &http.Server{} + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Println(os.Getgid()) + fmt.Fprintf(w, "Hello, %q\n", html.EscapeString(r.URL.Path)) + }) + + panic(server.Serve(listener)) +} +``` + +Now you can run several instances of this tiny server without `Address already in use` errors. + +## Thanks + +Inspired by [Artur Siekielski](https://github.com/aartur) [post](http://freeprogrammersblog.vhex.net/post/linux-39-introdued-new-way-of-writing-socket-servers/2) about `SO_REUSEPORT`. + diff --git a/vendor/github.com/kavu/go_reuseport/go.mod b/vendor/github.com/kavu/go_reuseport/go.mod new file mode 100644 index 00000000..904b42b0 --- /dev/null +++ b/vendor/github.com/kavu/go_reuseport/go.mod @@ -0,0 +1 @@ +module github.com/kavu/go_reuseport diff --git a/vendor/github.com/kavu/go_reuseport/reuseport.go b/vendor/github.com/kavu/go_reuseport/reuseport.go new file mode 100644 index 00000000..ea4c7c44 --- /dev/null +++ b/vendor/github.com/kavu/go_reuseport/reuseport.go @@ -0,0 +1,50 @@ +// +build linux darwin dragonfly freebsd netbsd openbsd + +// Copyright (C) 2017 Max Riveiro +// +// 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. + +// Package reuseport provides a function that returns a net.Listener powered +// by a net.FileListener with a SO_REUSEPORT option set to the socket. +package reuseport + +import ( + "errors" + "fmt" + "net" + "os" + "syscall" +) + +const fileNameTemplate = "reuseport.%d.%s.%s" + +var errUnsupportedProtocol = errors.New("only tcp, tcp4, tcp6, udp, udp4, udp6 are supported") + +// getSockaddr parses protocol and address and returns implementor +// of syscall.Sockaddr: syscall.SockaddrInet4 or syscall.SockaddrInet6. +func getSockaddr(proto, addr string) (sa syscall.Sockaddr, soType int, err error) { + switch proto { + case "tcp", "tcp4", "tcp6": + return getTCPSockaddr(proto, addr) + case "udp", "udp4", "udp6": + return getUDPSockaddr(proto, addr) + default: + return nil, -1, errUnsupportedProtocol + } +} + +func getSocketFileName(proto, addr string) string { + return fmt.Sprintf(fileNameTemplate, os.Getpid(), proto, addr) +} + +// Listen function is an alias for NewReusablePortListener. +func Listen(proto, addr string) (l net.Listener, err error) { + return NewReusablePortListener(proto, addr) +} + +// ListenPacket is an alias for NewReusablePortPacketConn. +func ListenPacket(proto, addr string) (l net.PacketConn, err error) { + return NewReusablePortPacketConn(proto, addr) +} diff --git a/vendor/github.com/kavu/go_reuseport/reuseport_bsd.go b/vendor/github.com/kavu/go_reuseport/reuseport_bsd.go new file mode 100644 index 00000000..19000e8d --- /dev/null +++ b/vendor/github.com/kavu/go_reuseport/reuseport_bsd.go @@ -0,0 +1,44 @@ +// +build darwin dragonfly freebsd netbsd openbsd + +// Copyright (C) 2017 Ma Weiwei, Max Riveiro +// +// 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. + +package reuseport + +import ( + "runtime" + "syscall" +) + +var reusePort = syscall.SO_REUSEPORT + +func maxListenerBacklog() int { + var ( + n uint32 + err error + ) + + switch runtime.GOOS { + case "darwin", "freebsd": + n, err = syscall.SysctlUint32("kern.ipc.somaxconn") + case "netbsd": + // NOTE: NetBSD has no somaxconn-like kernel state so far + case "openbsd": + n, err = syscall.SysctlUint32("kern.somaxconn") + } + + if n == 0 || err != nil { + return syscall.SOMAXCONN + } + + // FreeBSD stores the backlog in a uint16, as does Linux. + // Assume the other BSDs do too. Truncate number to avoid wrapping. + // See issue 5030. + if n > 1<<16-1 { + n = 1<<16 - 1 + } + return int(n) +} diff --git a/vendor/github.com/kavu/go_reuseport/reuseport_linux.go b/vendor/github.com/kavu/go_reuseport/reuseport_linux.go new file mode 100644 index 00000000..f6f85a49 --- /dev/null +++ b/vendor/github.com/kavu/go_reuseport/reuseport_linux.go @@ -0,0 +1,52 @@ +// +build linux + +// Copyright (C) 2017 Ma Weiwei, Max Riveiro +// +// 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. + +package reuseport + +import ( + "bufio" + "os" + "strconv" + "strings" + "syscall" +) + +var reusePort = 0x0F + +func maxListenerBacklog() int { + fd, err := os.Open("/proc/sys/net/core/somaxconn") + if err != nil { + return syscall.SOMAXCONN + } + defer fd.Close() + + rd := bufio.NewReader(fd) + line, err := rd.ReadString('\n') + if err != nil { + return syscall.SOMAXCONN + } + + f := strings.Fields(line) + if len(f) < 1 { + return syscall.SOMAXCONN + } + + n, err := strconv.Atoi(f[0]) + if err != nil || n == 0 { + return syscall.SOMAXCONN + } + + // Linux stores the backlog in a uint16. + // Truncate number to avoid wrapping. + // See issue 5030. + if n > 1<<16-1 { + n = 1<<16 - 1 + } + + return n +} diff --git a/vendor/github.com/kavu/go_reuseport/reuseport_windows.go b/vendor/github.com/kavu/go_reuseport/reuseport_windows.go new file mode 100644 index 00000000..e1e90df6 --- /dev/null +++ b/vendor/github.com/kavu/go_reuseport/reuseport_windows.go @@ -0,0 +1,19 @@ +// +build windows + +// Copyright (C) 2017 Ma Weiwei, Max Riveiro +// +// 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. + +package reuseport + +import "net" + +func NewReusablePortListener(proto, addr string) (net.Listener, error) { + return net.Listen(proto, addr) +} + +func NewReusablePortPacketConn(proto, addr string) (net.PacketConn, error) { + return net.ListenPacket(proto, addr) +} diff --git a/vendor/github.com/kavu/go_reuseport/tcp.go b/vendor/github.com/kavu/go_reuseport/tcp.go new file mode 100644 index 00000000..76540a15 --- /dev/null +++ b/vendor/github.com/kavu/go_reuseport/tcp.go @@ -0,0 +1,143 @@ +// +build linux darwin dragonfly freebsd netbsd openbsd + +// Copyright (C) 2017 Max Riveiro +// +// 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. + +package reuseport + +import ( + "errors" + "net" + "os" + "syscall" +) + +var ( + listenerBacklogMaxSize = maxListenerBacklog() + errUnsupportedTCPProtocol = errors.New("only tcp, tcp4, tcp6 are supported") +) + +func getTCPSockaddr(proto, addr string) (sa syscall.Sockaddr, soType int, err error) { + var tcp *net.TCPAddr + + tcp, err = net.ResolveTCPAddr(proto, addr) + if err != nil && tcp.IP != nil { + return nil, -1, err + } + + tcpVersion, err := determineTCPProto(proto, tcp) + if err != nil { + return nil, -1, err + } + + switch tcpVersion { + case "tcp": + return &syscall.SockaddrInet4{Port: tcp.Port}, syscall.AF_INET, nil + case "tcp4": + sa := &syscall.SockaddrInet4{Port: tcp.Port} + + if tcp.IP != nil { + copy(sa.Addr[:], tcp.IP[12:16]) // copy last 4 bytes of slice to array + } + + return sa, syscall.AF_INET, nil + case "tcp6": + sa := &syscall.SockaddrInet6{Port: tcp.Port} + + if tcp.IP != nil { + copy(sa.Addr[:], tcp.IP) // copy all bytes of slice to array + } + + if tcp.Zone != "" { + iface, err := net.InterfaceByName(tcp.Zone) + if err != nil { + return nil, -1, err + } + + sa.ZoneId = uint32(iface.Index) + } + + return sa, syscall.AF_INET6, nil + } + + return nil, -1, errUnsupportedProtocol +} + +func determineTCPProto(proto string, ip *net.TCPAddr) (string, error) { + // If the protocol is set to "tcp", we try to determine the actual protocol + // version from the size of the resolved IP address. Otherwise, we simple use + // the protcol given to us by the caller. + + if ip.IP.To4() != nil { + return "tcp4", nil + } + + if ip.IP.To16() != nil { + return "tcp6", nil + } + + switch proto { + case "tcp", "tcp4", "tcp6": + return proto, nil + } + + return "", errUnsupportedTCPProtocol +} + +// NewReusablePortListener returns net.FileListener that created from +// a file discriptor for a socket with SO_REUSEPORT option. +func NewReusablePortListener(proto, addr string) (l net.Listener, err error) { + var ( + soType, fd int + file *os.File + sockaddr syscall.Sockaddr + ) + + if sockaddr, soType, err = getSockaddr(proto, addr); err != nil { + return nil, err + } + + syscall.ForkLock.RLock() + if fd, err = syscall.Socket(soType, syscall.SOCK_STREAM, syscall.IPPROTO_TCP); err != nil { + syscall.ForkLock.RUnlock() + + return nil, err + } + syscall.ForkLock.RUnlock() + + if err = syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1); err != nil { + syscall.Close(fd) + return nil, err + } + + if err = syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, reusePort, 1); err != nil { + syscall.Close(fd) + return nil, err + } + + if err = syscall.Bind(fd, sockaddr); err != nil { + syscall.Close(fd) + return nil, err + } + + // Set backlog size to the maximum + if err = syscall.Listen(fd, listenerBacklogMaxSize); err != nil { + syscall.Close(fd) + return nil, err + } + + file = os.NewFile(uintptr(fd), getSocketFileName(proto, addr)) + if l, err = net.FileListener(file); err != nil { + file.Close() + return nil, err + } + + if err = file.Close(); err != nil { + return nil, err + } + + return l, err +} diff --git a/vendor/github.com/kavu/go_reuseport/tcp_test.go b/vendor/github.com/kavu/go_reuseport/tcp_test.go new file mode 100644 index 00000000..1620f9d3 --- /dev/null +++ b/vendor/github.com/kavu/go_reuseport/tcp_test.go @@ -0,0 +1,218 @@ +// +build linux darwin dragonfly freebsd netbsd openbsd + +// Copyright (C) 2017 Max Riveiro +// +// 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. + +package reuseport + +import ( + "fmt" + "html" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "testing" +) + +const ( + httpServerOneResponse = "1" + httpServerTwoResponse = "2" +) + +var ( + httpServerOne = NewHTTPServer(httpServerOneResponse) + httpServerTwo = NewHTTPServer(httpServerTwoResponse) +) + +func NewHTTPServer(resp string) *httptest.Server { + return httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, resp) + })) +} +func TestNewReusablePortListener(t *testing.T) { + listenerOne, err := NewReusablePortListener("tcp4", "localhost:10081") + if err != nil { + t.Error(err) + } + defer listenerOne.Close() + + listenerTwo, err := NewReusablePortListener("tcp", "127.0.0.1:10081") + if err != nil { + t.Error(err) + } + defer listenerTwo.Close() + + listenerThree, err := NewReusablePortListener("tcp6", "[::1]:10081") + if err != nil { + t.Error(err) + } + defer listenerThree.Close() + + listenerFour, err := NewReusablePortListener("tcp6", ":10081") + if err != nil { + t.Error(err) + } + defer listenerFour.Close() + + listenerFive, err := NewReusablePortListener("tcp4", ":10081") + if err != nil { + t.Error(err) + } + defer listenerFive.Close() + + listenerSix, err := NewReusablePortListener("tcp", ":10081") + if err != nil { + t.Error(err) + } + defer listenerSix.Close() +} + +func TestListen(t *testing.T) { + listenerOne, err := Listen("tcp4", "localhost:10081") + if err != nil { + t.Error(err) + } + defer listenerOne.Close() + + listenerTwo, err := Listen("tcp", "127.0.0.1:10081") + if err != nil { + t.Error(err) + } + defer listenerTwo.Close() + + listenerThree, err := Listen("tcp6", "[::1]:10081") + if err != nil { + t.Error(err) + } + defer listenerThree.Close() + + listenerFour, err := Listen("tcp6", ":10081") + if err != nil { + t.Error(err) + } + defer listenerFour.Close() + + listenerFive, err := Listen("tcp4", ":10081") + if err != nil { + t.Error(err) + } + defer listenerFive.Close() + + listenerSix, err := Listen("tcp", ":10081") + if err != nil { + t.Error(err) + } + defer listenerSix.Close() +} + +func TestNewReusablePortServers(t *testing.T) { + listenerOne, err := NewReusablePortListener("tcp4", "localhost:10081") + if err != nil { + t.Error(err) + } + defer listenerOne.Close() + + listenerTwo, err := NewReusablePortListener("tcp6", ":10081") + if err != nil { + t.Error(err) + } + defer listenerTwo.Close() + + httpServerOne.Listener = listenerOne + httpServerTwo.Listener = listenerTwo + + httpServerOne.Start() + httpServerTwo.Start() + + // Server One — First Response + resp1, err := http.Get(httpServerOne.URL) + if err != nil { + t.Error(err) + } + body1, err := ioutil.ReadAll(resp1.Body) + resp1.Body.Close() + if err != nil { + t.Error(err) + } + if string(body1) != httpServerOneResponse && string(body1) != httpServerTwoResponse { + t.Errorf("Expected %#v or %#v, got %#v.", httpServerOneResponse, httpServerTwoResponse, string(body1)) + } + + // Server Two — First Response + resp2, err := http.Get(httpServerTwo.URL) + if err != nil { + t.Error(err) + } + body2, err := ioutil.ReadAll(resp2.Body) + resp1.Body.Close() + if err != nil { + t.Error(err) + } + if string(body2) != httpServerOneResponse && string(body2) != httpServerTwoResponse { + t.Errorf("Expected %#v or %#v, got %#v.", httpServerOneResponse, httpServerTwoResponse, string(body2)) + } + + httpServerTwo.Close() + + // Server One — Second Response + resp3, err := http.Get(httpServerOne.URL) + if err != nil { + t.Error(err) + } + body3, err := ioutil.ReadAll(resp3.Body) + resp1.Body.Close() + if err != nil { + t.Error(err) + } + if string(body3) != httpServerOneResponse { + t.Errorf("Expected %#v, got %#v.", httpServerOneResponse, string(body3)) + } + + // Server One — Third Response + resp5, err := http.Get(httpServerOne.URL) + if err != nil { + t.Error(err) + } + body5, err := ioutil.ReadAll(resp5.Body) + resp1.Body.Close() + if err != nil { + t.Error(err) + } + if string(body5) != httpServerOneResponse { + t.Errorf("Expected %#v, got %#v.", httpServerOneResponse, string(body5)) + } + + httpServerOne.Close() +} + +func BenchmarkNewReusablePortListener(b *testing.B) { + for i := 0; i < b.N; i++ { + listener, err := NewReusablePortListener("tcp", ":10081") + + if err != nil { + b.Error(err) + } else { + listener.Close() + } + } +} + +func ExampleNewReusablePortListener() { + listener, err := NewReusablePortListener("tcp", ":8881") + if err != nil { + panic(err) + } + defer listener.Close() + + server := &http.Server{} + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Println(os.Getgid()) + fmt.Fprintf(w, "Hello, %q\n", html.EscapeString(r.URL.Path)) + }) + + panic(server.Serve(listener)) +} diff --git a/vendor/github.com/kavu/go_reuseport/test.bash b/vendor/github.com/kavu/go_reuseport/test.bash new file mode 100755 index 00000000..a57c012a --- /dev/null +++ b/vendor/github.com/kavu/go_reuseport/test.bash @@ -0,0 +1,22 @@ +#!/bin/bash + +set -e + +# Thanks to IPFS team +if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then + if [[ "$TRAVIS_SUDO" == true ]]; then + # Ensure that IPv6 is enabled. + # While this is unsupported by TravisCI, it still works for localhost. + sudo sysctl -w net.ipv6.conf.lo.disable_ipv6=0 + sudo sysctl -w net.ipv6.conf.default.disable_ipv6=0 + sudo sysctl -w net.ipv6.conf.all.disable_ipv6=0 + fi +else + # OSX has a default file limit of 256, for some tests we need a + # maximum of 8192. + ulimit -Sn 8192 +fi + +go test -v -cover ./... +go test -v -cover -race ./... -coverprofile=coverage.txt -covermode=atomic +go test -v -cover -race -benchmem -benchtime=5s -bench=. \ No newline at end of file diff --git a/vendor/github.com/kavu/go_reuseport/udp.go b/vendor/github.com/kavu/go_reuseport/udp.go new file mode 100644 index 00000000..2a9201ce --- /dev/null +++ b/vendor/github.com/kavu/go_reuseport/udp.go @@ -0,0 +1,143 @@ +// +build linux darwin dragonfly freebsd netbsd openbsd + +// Copyright (C) 2017 Max Riveiro +// +// 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. + +package reuseport + +import ( + "errors" + "net" + "os" + "syscall" +) + +var errUnsupportedUDPProtocol = errors.New("only udp, udp4, udp6 are supported") + +func getUDPSockaddr(proto, addr string) (sa syscall.Sockaddr, soType int, err error) { + var udp *net.UDPAddr + + udp, err = net.ResolveUDPAddr(proto, addr) + if err != nil && udp.IP != nil { + return nil, -1, err + } + + udpVersion, err := determineUDPProto(proto, udp) + if err != nil { + return nil, -1, err + } + + switch udpVersion { + case "udp": + return &syscall.SockaddrInet4{Port: udp.Port}, syscall.AF_INET, nil + case "udp4": + sa := &syscall.SockaddrInet4{Port: udp.Port} + + if udp.IP != nil { + copy(sa.Addr[:], udp.IP[12:16]) // copy last 4 bytes of slice to array + } + + return sa, syscall.AF_INET, nil + case "udp6": + sa := &syscall.SockaddrInet6{Port: udp.Port} + + if udp.IP != nil { + copy(sa.Addr[:], udp.IP) // copy all bytes of slice to array + } + + if udp.Zone != "" { + iface, err := net.InterfaceByName(udp.Zone) + if err != nil { + return nil, -1, err + } + + sa.ZoneId = uint32(iface.Index) + } + + return sa, syscall.AF_INET6, nil + } + + return nil, -1, errUnsupportedProtocol +} + +func determineUDPProto(proto string, ip *net.UDPAddr) (string, error) { + // If the protocol is set to "udp", we try to determine the actual protocol + // version from the size of the resolved IP address. Otherwise, we simple use + // the protcol given to us by the caller. + + if ip.IP.To4() != nil { + return "udp4", nil + } + + if ip.IP.To16() != nil { + return "udp6", nil + } + + switch proto { + case "udp", "udp4", "udp6": + return proto, nil + } + + return "", errUnsupportedUDPProtocol +} + +// NewReusablePortPacketConn returns net.FilePacketConn that created from +// a file discriptor for a socket with SO_REUSEPORT option. +func NewReusablePortPacketConn(proto, addr string) (l net.PacketConn, err error) { + var ( + soType, fd int + file *os.File + sockaddr syscall.Sockaddr + ) + + if sockaddr, soType, err = getSockaddr(proto, addr); err != nil { + return nil, err + } + + syscall.ForkLock.RLock() + fd, err = syscall.Socket(soType, syscall.SOCK_DGRAM, syscall.IPPROTO_UDP) + if err == nil { + syscall.CloseOnExec(fd) + } + syscall.ForkLock.RUnlock() + if err != nil { + syscall.Close(fd) + return nil, err + } + + defer func() { + if err != nil { + syscall.Close(fd) + } + }() + + if err = syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1); err != nil { + return nil, err + } + + if err = syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, reusePort, 1); err != nil { + return nil, err + } + + if err = syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_BROADCAST, 1); err != nil { + return nil, err + } + + if err = syscall.Bind(fd, sockaddr); err != nil { + return nil, err + } + + file = os.NewFile(uintptr(fd), getSocketFileName(proto, addr)) + if l, err = net.FilePacketConn(file); err != nil { + return nil, err + } + + if err = file.Close(); err != nil { + return nil, err + } + + return l, err +} diff --git a/vendor/github.com/kavu/go_reuseport/udp_test.go b/vendor/github.com/kavu/go_reuseport/udp_test.go new file mode 100644 index 00000000..d6550e36 --- /dev/null +++ b/vendor/github.com/kavu/go_reuseport/udp_test.go @@ -0,0 +1,99 @@ +// +build linux darwin dragonfly freebsd netbsd openbsd + +// Copyright (C) 2017 Max Riveiro +// +// 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. + +package reuseport + +import "testing" + +func TestNewReusablePortPacketConn(t *testing.T) { + listenerOne, err := NewReusablePortPacketConn("udp4", "localhost:10082") + if err != nil { + t.Error(err) + } + defer listenerOne.Close() + + listenerTwo, err := NewReusablePortPacketConn("udp", "127.0.0.1:10082") + if err != nil { + t.Error(err) + } + defer listenerTwo.Close() + + listenerThree, err := NewReusablePortPacketConn("udp6", "[::1]:10082") + if err != nil { + t.Error(err) + } + defer listenerThree.Close() + + listenerFour, err := NewReusablePortListener("udp6", ":10081") + if err != nil { + t.Error(err) + } + defer listenerFour.Close() + + listenerFive, err := NewReusablePortListener("udp4", ":10081") + if err != nil { + t.Error(err) + } + defer listenerFive.Close() + + listenerSix, err := NewReusablePortListener("udp", ":10081") + if err != nil { + t.Error(err) + } + defer listenerSix.Close() +} + +func TestListenPacket(t *testing.T) { + listenerOne, err := ListenPacket("udp4", "localhost:10082") + if err != nil { + t.Error(err) + } + defer listenerOne.Close() + + listenerTwo, err := ListenPacket("udp", "127.0.0.1:10082") + if err != nil { + t.Error(err) + } + defer listenerTwo.Close() + + listenerThree, err := ListenPacket("udp6", "[::1]:10082") + if err != nil { + t.Error(err) + } + defer listenerThree.Close() + + listenerFour, err := ListenPacket("udp6", ":10081") + if err != nil { + t.Error(err) + } + defer listenerFour.Close() + + listenerFive, err := ListenPacket("udp4", ":10081") + if err != nil { + t.Error(err) + } + defer listenerFive.Close() + + listenerSix, err := ListenPacket("udp", ":10081") + if err != nil { + t.Error(err) + } + defer listenerSix.Close() +} + +func BenchmarkNewReusableUDPPortListener(b *testing.B) { + for i := 0; i < b.N; i++ { + listener, err := NewReusablePortPacketConn("udp4", "localhost:10082") + + if err != nil { + b.Error(err) + } else { + listener.Close() + } + } +} diff --git a/vendor/github.com/tidwall/evio/.travis.yml b/vendor/github.com/tidwall/evio/.travis.yml new file mode 100644 index 00000000..c74ca9b9 --- /dev/null +++ b/vendor/github.com/tidwall/evio/.travis.yml @@ -0,0 +1,2 @@ +language: go +script: go test -run none diff --git a/vendor/github.com/tidwall/evio/LICENSE b/vendor/github.com/tidwall/evio/LICENSE new file mode 100644 index 00000000..92a9728f --- /dev/null +++ b/vendor/github.com/tidwall/evio/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2017 Joshua J Baker + +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/vendor/github.com/tidwall/evio/README.md b/vendor/github.com/tidwall/evio/README.md new file mode 100644 index 00000000..84d569b1 --- /dev/null +++ b/vendor/github.com/tidwall/evio/README.md @@ -0,0 +1,176 @@ +
+ +`evio` is an event loop networking framework that is fast and small. It makes direct [epoll](https://en.wikipedia.org/wiki/Epoll) and [kqueue](https://en.wikipedia.org/wiki/Kqueue) syscalls rather than using the standard Go [net](https://golang.org/pkg/net/) package, and works in a similar manner as [libuv](https://github.com/libuv/libuv) and [libevent](https://github.com/libevent/libevent). + +The goal of this project is to create a server framework for Go that performs on par with [Redis](http://redis.io) and [Haproxy](http://www.haproxy.org) for packet handling. My hope is to use this as a foundation for [Tile38](https://github.com/tidwall/tile38) and a future L7 proxy for Go... and a bunch of other stuff. + +**Just to be perfectly clear** + +This project is not intended to be a general purpose replacement for the standard Go net package or goroutines. It's for building specialized services such as key value stores, L7 proxies, static websites, etc. + +You would not want to use this framework if you need to handle long-running requests (milliseconds or more). For example, a web api that needs to connect to a mongo database, authenticate, and respond; just use the Go net/http package instead. + +There are many popular event loop based applications in the wild such as Nginx, Haproxy, Redis, and Memcached. All of these are very fast and written in C. + +The reason I wrote this framework is so that I can build certain networking services that perform like the C apps above, but I also want to continue to work in Go. + +## Features + +- [Fast](#performance) single-threaded or [multithreaded](#multithreaded) event loop +- Built-in [load balancing](#load-balancing) options +- Simple API +- Low memory usage +- Supports tcp, [udp](#udp), and unix sockets +- Allows [multiple network binding](#multiple-addresses) on the same event loop +- Flexible [ticker](#ticker) event +- Fallback for non-epoll/kqueue operating systems by simulating events with the [net](https://golang.org/pkg/net/) package +- [SO_REUSEPORT](#so_reuseport) socket option + +## Getting Started + +### Installing + +To start using evio, install Go and run `go get`: + +```sh +$ go get -u github.com/tidwall/evio +``` + +This will retrieve the library. + +### Usage + +Starting a server is easy with `evio`. Just set up your events and pass them to the `Serve` function along with the binding address(es). Each connections is represented as an `evio.Conn` object that is passed to various events to differentiate the clients. At any point you can close a client or shutdown the server by return a `Close` or `Shutdown` action from an event. + +Example echo server that binds to port 5000: + +```go +package main + +import "github.com/tidwall/evio" + +func main() { + var events evio.Events + events.Data = func(c evio.Conn, in []byte) (out []byte, action evio.Action) { + out = in + return + } + if err := evio.Serve(events, "tcp://localhost:5000"); err != nil { + panic(err.Error()) + } +} +``` + +Here the only event being used is `Data`, which fires when the server receives input data from a client. +The exact same input data is then passed through the output return value, which is then sent back to the client. + +Connect to the echo server: + +```sh +$ telnet localhost 5000 +``` + +### Events + +The event type has a bunch of handy events: + +- `Serving` fires when the server is ready to accept new connections. +- `Opened` fires when a connection has opened. +- `Closed` fires when a connection has closed. +- `Detach` fires when a connection has been detached using the `Detach` return action. +- `Data` fires when the server receives new data from a connection. +- `Tick` fires immediately after the server starts and will fire again after a specified interval. + +### Multiple addresses + +A server can bind to multiple addresses and share the same event loop. + +```go +evio.Serve(events, "tcp://192.168.0.10:5000", "unix://socket") +``` + +### Ticker + +The `Tick` event fires ticks at a specified interval. +The first tick fires immediately after the `Serving` events. + +```go +events.Tick = func() (delay time.Duration, action Action){ + log.Printf("tick") + delay = time.Second + return +} +``` + +## UDP + +The `Serve` function can bind to UDP addresses. + +- All incoming and outgoing packets are not buffered and sent individually. +- The `Opened` and `Closed` events are not availble for UDP sockets, only the `Data` event. + +## Multithreaded + +The `events.NumLoops` options sets the number of loops to use for the server. +A value greater than 1 will effectively make the server multithreaded for multi-core machines. +Which means you must take care when synchonizing memory between event callbacks. +Setting to 0 or 1 will run the server as single-threaded. +Setting to -1 will automatically assign this value equal to `runtime.NumProcs()`. + +## Load balancing + +The `events.LoadBalance` options sets the load balancing method. +Load balancing is always a best effort to attempt to distribute the incoming connections between multiple loops. +This option is only available when `events.NumLoops` is set. + +- `Random` requests that connections are randomly distributed. +- `RoundRobin` requests that connections are distributed to a loop in a round-robin fashion. +- `LeastConnections` assigns the next accepted connection to the loop with the least number of active connections. + +## SO_REUSEPORT + +Servers can utilize the [SO_REUSEPORT](https://lwn.net/Articles/542629/) option which allows multiple sockets on the same host to bind to the same port. + +Just provide `reuseport=true` to an address: + +```go +evio.Serve(events, "tcp://0.0.0.0:1234?reuseport=true")) +``` + +## More examples + +Please check out the [examples](examples) subdirectory for a simplified [redis](examples/redis-server/main.go) clone, an [echo](examples/echo-server/main.go) server, and a very basic [http](examples/http-server/main.go) server. + +To run an example: + +```sh +$ go run examples/http-server/main.go +$ go run examples/redis-server/main.go +$ go run examples/echo-server/main.go +``` + +## Performance + +### Benchmarks + +These benchmarks were run on an ec2 c4.xlarge instance in single-threaded mode (GOMAXPROC=1) over Ipv4 localhost. +Check out [benchmarks](benchmarks) for more info. + +