This commit is contained in:
tidwall 2018-08-13 17:05:30 -07:00
parent 3aa394219d
commit eef5f3c7b6
15 changed files with 800 additions and 337 deletions

View File

@ -133,26 +133,9 @@ func commandErrIsFatal(err error) bool {
} }
func (c *Controller) writeAOF(value resp.Value, d *commandDetailsT) error { func (c *Controller) writeAOF(value resp.Value, d *commandDetailsT) error {
if d != nil { if d != nil && !d.updated {
if !d.updated { // just ignore writes if the command did not update
return nil // just ignore writes if the command did not update return nil
}
if c.config.followHost() == "" {
// process hooks, for leader only
if d.parent {
// process children only
for _, d := range d.children {
if err := c.queueHooks(d); err != nil {
return err
}
}
} else {
// process parent
if err := c.queueHooks(d); err != nil {
return err
}
}
}
} }
if c.shrinking { if c.shrinking {
var values []string var values []string
@ -178,14 +161,34 @@ func (c *Controller) writeAOF(value resp.Value, d *commandDetailsT) error {
c.fcond.Broadcast() c.fcond.Broadcast()
c.fcond.L.Unlock() c.fcond.L.Unlock()
// process geofences
if d != nil { if d != nil {
// write to live connection streams // webhook geofences
if c.config.followHost() == "" {
// for leader only
if d.parent {
// queue children
for _, d := range d.children {
if err := c.queueHooks(d); err != nil {
return err
}
}
} else {
// queue parent
if err := c.queueHooks(d); err != nil {
return err
}
}
}
// live geofences
c.lcond.L.Lock() c.lcond.L.Lock()
if d.parent { if d.parent {
// queue children
for _, d := range d.children { for _, d := range d.children {
c.lstack = append(c.lstack, d) c.lstack = append(c.lstack, d)
} }
} else { } else {
// queue parent
c.lstack = append(c.lstack, d) c.lstack = append(c.lstack, d)
} }
c.lcond.Broadcast() c.lcond.Broadcast()
@ -196,7 +199,7 @@ func (c *Controller) writeAOF(value resp.Value, d *commandDetailsT) error {
func (c *Controller) queueHooks(d *commandDetailsT) error { func (c *Controller) queueHooks(d *commandDetailsT) error {
// big list of all of the messages // big list of all of the messages
var hmsgs [][]byte var hmsgs []string
var hooks []*Hook var hooks []*Hook
// find the hook by the key // find the hook by the key
if hm, ok := c.hookcols[d.key]; ok { if hm, ok := c.hookcols[d.key]; ok {
@ -204,9 +207,13 @@ func (c *Controller) queueHooks(d *commandDetailsT) error {
// match the fence // match the fence
msgs := FenceMatch(hook.Name, hook.ScanWriter, hook.Fence, hook.Metas, d) msgs := FenceMatch(hook.Name, hook.ScanWriter, hook.Fence, hook.Metas, d)
if len(msgs) > 0 { if len(msgs) > 0 {
// append each msg to the big list if hook.channel {
hmsgs = append(hmsgs, msgs...) c.Publish(hook.Name, msgs...)
hooks = append(hooks, hook) } else {
// append each msg to the big list
hmsgs = append(hmsgs, msgs...)
hooks = append(hooks, hook)
}
} }
} }
} }
@ -259,7 +266,7 @@ type liveAOFSwitches struct {
} }
func (s liveAOFSwitches) Error() string { func (s liveAOFSwitches) Error() string {
return "going live" return goingLive
} }
func (c *Controller) cmdAOFMD5(msg *server.Message) (res resp.Value, err error) { func (c *Controller) cmdAOFMD5(msg *server.Message) (res resp.Value, err error) {

View File

@ -190,17 +190,23 @@ func (c *Controller) aofshrink() {
if hook == nil { if hook == nil {
return return
} }
hook.mu.Lock() hook.cond.L.Lock()
defer hook.mu.Unlock() defer hook.cond.L.Unlock()
var values []string var values []string
values = append(values, "sethook") if hook.channel {
values = append(values, name) values = append(values, "setchan", name)
values = append(values, strings.Join(hook.Endpoints, ",")) } else {
values = append(values, "sethook", name,
strings.Join(hook.Endpoints, ","))
values = append(values)
}
for _, meta := range hook.Metas {
values = append(values, "meta", meta.Name, meta.Value)
}
for _, value := range hook.Message.Values { for _, value := range hook.Message.Values {
values = append(values, value.String()) values = append(values, value.String())
} }
// append the values to the aof buffer // append the values to the aof buffer
aofbuf = append(aofbuf, '*') aofbuf = append(aofbuf, '*')
aofbuf = append(aofbuf, strconv.FormatInt(int64(len(values)), 10)...) aofbuf = append(aofbuf, strconv.FormatInt(int64(len(values)), 10)...)

View File

@ -29,6 +29,8 @@ import (
var errOOM = errors.New("OOM command not allowed when used memory > 'maxmemory'") var errOOM = errors.New("OOM command not allowed when used memory > 'maxmemory'")
const goingLive = "going live"
const hookLogPrefix = "hook:log:" const hookLogPrefix = "hook:log:"
type collectionT struct { type collectionT struct {
@ -109,6 +111,8 @@ type Controller struct {
aofconnM map[net.Conn]bool aofconnM map[net.Conn]bool
luascripts *lScriptMap luascripts *lScriptMap
luapool *lStatePool luapool *lStatePool
pubsub *pubsub
} }
// ListenAndServe starts a new tile38 server // ListenAndServe starts a new tile38 server
@ -136,10 +140,10 @@ func ListenAndServeEx(host string, port int, dir string, ln *net.Listener, http
expires: make(map[string]map[string]time.Time), expires: make(map[string]map[string]time.Time),
started: time.Now(), started: time.Now(),
conns: make(map[*server.Conn]*clientConn), conns: make(map[*server.Conn]*clientConn),
epc: endpoint.NewManager(),
http: http, http: http,
pubsub: newPubsub(),
} }
c.epc = endpoint.NewManager(c)
c.luascripts = c.NewScriptMap() c.luascripts = c.NewScriptMap()
c.luapool = c.NewPool() c.luapool = c.NewPool()
defer c.luapool.Shutdown() defer c.luapool.Shutdown()
@ -217,7 +221,7 @@ func ListenAndServeEx(host string, port int, dir string, ln *net.Listener, http
c.statsTotalCommands.add(1) c.statsTotalCommands.add(1)
err := c.handleInputCommand(conn, msg, w) err := c.handleInputCommand(conn, msg, w)
if err != nil { if err != nil {
if err.Error() == "going live" { if err.Error() == goingLive {
return c.goLive(err, conn, rd, msg, websocket) return c.goLive(err, conn, rd, msg, websocket)
} }
return err return err
@ -490,7 +494,9 @@ func (c *Controller) handleInputCommand(conn *server.Conn, msg *server.Message,
default: default:
c.mu.RLock() c.mu.RLock()
defer c.mu.RUnlock() defer c.mu.RUnlock()
case "set", "del", "drop", "fset", "flushdb", "sethook", "pdelhook", "delhook", case "set", "del", "drop", "fset", "flushdb",
"setchan", "pdelchan", "delchan",
"sethook", "pdelhook", "delhook",
"expire", "persist", "jset", "pdel": "expire", "persist", "jset", "pdel":
// write operations // write operations
write = true write = true
@ -512,8 +518,9 @@ func (c *Controller) handleInputCommand(conn *server.Conn, msg *server.Message,
if c.config.readOnly() { if c.config.readOnly() {
return writeErr("read only") return writeErr("read only")
} }
case "get", "keys", "scan", "nearby", "within", "intersects", "hooks", "search", case "get", "keys", "scan", "nearby", "within", "intersects", "hooks",
"ttl", "bounds", "server", "info", "type", "jget", "evalro", "evalrosha": "chans", "search", "ttl", "bounds", "server", "info", "type", "jget",
"evalro", "evalrosha":
// read operations // read operations
c.mu.RLock() c.mu.RLock()
defer c.mu.RUnlock() defer c.mu.RUnlock()
@ -548,6 +555,8 @@ func (c *Controller) handleInputCommand(conn *server.Conn, msg *server.Message,
defer c.mu.Unlock() defer c.mu.Unlock()
case "evalna", "evalnasha": case "evalna", "evalnasha":
// No locking for scripts, otherwise writes cannot happen within scripts // 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) res, d, err := c.command(msg, w, conn)
@ -556,7 +565,7 @@ func (c *Controller) handleInputCommand(conn *server.Conn, msg *server.Message,
return writeErr(res.String()) return writeErr(res.String())
} }
if err != nil { if err != nil {
if err.Error() == "going live" { if err.Error() == goingLive {
return err return err
} }
return writeErr(err.Error()) return writeErr(err.Error())
@ -630,20 +639,31 @@ func (c *Controller) command(
res, d, err = c.cmdDrop(msg) res, d, err = c.cmdDrop(msg)
case "flushdb": case "flushdb":
res, d, err = c.cmdFlushDB(msg) res, d, err = c.cmdFlushDB(msg)
case "sethook": case "sethook":
res, d, err = c.cmdSetHook(msg) res, d, err = c.cmdSetHook(msg, false)
case "delhook": case "delhook":
res, d, err = c.cmdDelHook(msg) res, d, err = c.cmdDelHook(msg, false)
case "pdelhook": case "pdelhook":
res, d, err = c.cmdPDelHook(msg) 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": case "expire":
res, d, err = c.cmdExpire(msg) res, d, err = c.cmdExpire(msg)
case "persist": case "persist":
res, d, err = c.cmdPersist(msg) res, d, err = c.cmdPersist(msg)
case "ttl": case "ttl":
res, err = c.cmdTTL(msg) res, err = c.cmdTTL(msg)
case "hooks":
res, err = c.cmdHooks(msg)
case "shutdown": case "shutdown":
if !core.DevMode { if !core.DevMode {
err = fmt.Errorf("unknown command '%s'", msg.Values[0]) err = fmt.Errorf("unknown command '%s'", msg.Values[0])
@ -737,6 +757,12 @@ func (c *Controller) command(
res, err = c.cmdScriptExists(msg) res, err = c.cmdScriptExists(msg)
case "script flush": case "script flush":
res, err = c.cmdScriptFlush(msg) 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 return
} }

View File

@ -12,14 +12,14 @@ import (
) )
// FenceMatch executes a fence match returns back json messages for fence detection. // FenceMatch executes a fence match returns back json messages for fence detection.
func FenceMatch(hookName string, sw *scanWriter, fence *liveFenceSwitches, metas []FenceMeta, details *commandDetailsT) [][]byte { func FenceMatch(hookName string, sw *scanWriter, fence *liveFenceSwitches, metas []FenceMeta, details *commandDetailsT) []string {
msgs := fenceMatch(hookName, sw, fence, metas, details) msgs := fenceMatch(hookName, sw, fence, metas, details)
if len(fence.accept) == 0 { if len(fence.accept) == 0 {
return msgs return msgs
} }
nmsgs := make([][]byte, 0, len(msgs)) nmsgs := make([]string, 0, len(msgs))
for _, msg := range msgs { for _, msg := range msgs {
if fence.accept[gjson.GetBytes(msg, "command").String()] { if fence.accept[gjson.Get(msg, "command").String()] {
nmsgs = append(nmsgs, msg) nmsgs = append(nmsgs, msg)
} }
} }
@ -47,9 +47,12 @@ func appendHookDetails(b []byte, hookName string, metas []FenceMeta) []byte {
func hookJSONString(hookName string, metas []FenceMeta) string { func hookJSONString(hookName string, metas []FenceMeta) string {
return string(appendHookDetails(nil, hookName, metas)) return string(appendHookDetails(nil, hookName, metas))
} }
func fenceMatch(hookName string, sw *scanWriter, fence *liveFenceSwitches, metas []FenceMeta, details *commandDetailsT) [][]byte { func fenceMatch(hookName string, sw *scanWriter, fence *liveFenceSwitches, metas []FenceMeta, details *commandDetailsT) []string {
if details.command == "drop" { if details.command == "drop" {
return [][]byte{[]byte(`{"command":"drop"` + hookJSONString(hookName, metas) + `,"time":` + jsonTimeFormat(details.timestamp) + `}`)} return []string{
`{"command":"drop"` + hookJSONString(hookName, metas) +
`,"time":` + jsonTimeFormat(details.timestamp) + `}`,
}
} }
if len(fence.glob) > 0 && !(len(fence.glob) == 1 && fence.glob[0] == '*') { if len(fence.glob) > 0 && !(len(fence.glob) == 1 && fence.glob[0] == '*') {
match, _ := glob.Match(fence.glob, details.id) match, _ := glob.Match(fence.glob, details.id)
@ -69,7 +72,10 @@ func fenceMatch(hookName string, sw *scanWriter, fence *liveFenceSwitches, metas
} }
} }
if details.command == "del" { if details.command == "del" {
return [][]byte{[]byte(`{"command":"del"` + hookJSONString(hookName, metas) + `,"id":` + jsonString(details.id) + `,"time":` + jsonTimeFormat(details.timestamp) + `}`)} return []string{
`{"command":"del"` + hookJSONString(hookName, metas) + `,"id":` + jsonString(details.id) +
`,"time":` + jsonTimeFormat(details.timestamp) + `}`,
}
} }
var roamkeys, roamids []string var roamkeys, roamids []string
var roammeters []float64 var roammeters []float64
@ -164,14 +170,13 @@ func fenceMatch(hookName string, sw *scanWriter, fence *liveFenceSwitches, metas
return nil return nil
} }
res := make([]byte, sw.wr.Len()) res := sw.wr.String()
copy(res, sw.wr.Bytes())
sw.wr.Reset() sw.wr.Reset()
if len(res) > 0 && res[0] == ',' { if len(res) > 0 && res[0] == ',' {
res = res[1:] res = res[1:]
} }
if sw.output == outputIDs { if sw.output == outputIDs {
res = []byte(`{"id":` + string(res) + `}`) res = `{"id":` + string(res) + `}`
} }
sw.mu.Unlock() sw.mu.Unlock()
@ -195,12 +200,13 @@ func fenceMatch(hookName string, sw *scanWriter, fence *liveFenceSwitches, metas
} }
} }
var msgs [][]byte var msgs []string
if fence.detect == nil || fence.detect[detect] { if fence.detect == nil || fence.detect[detect] {
if len(res) > 0 && res[0] == '{' { if len(res) > 0 && res[0] == '{' {
msgs = append(msgs, makemsg(details.command, group, detect, hookName, metas, details.key, details.timestamp, res[1:])) msgs = append(msgs, makemsg(details.command, group, detect,
hookName, metas, details.key, details.timestamp, res[1:]))
} else { } else {
msgs = append(msgs, res) msgs = append(msgs, string(res))
} }
} }
switch detect { switch detect {
@ -214,11 +220,12 @@ func fenceMatch(hookName string, sw *scanWriter, fence *liveFenceSwitches, metas
} }
case "roam": case "roam":
if len(msgs) > 0 { if len(msgs) > 0 {
var nmsgs [][]byte var nmsgs []string
msg := msgs[0][:len(msgs[0])-1] msg := msgs[0][:len(msgs[0])-1]
for i, id := range roamids { for i, id := range roamids {
nmsg := append([]byte(nil), msg...) var nmsg []byte
nmsg = append(nmsg, msg...)
nmsg = append(nmsg, `,"nearby":{"key":`...) nmsg = append(nmsg, `,"nearby":{"key":`...)
nmsg = appendJSONString(nmsg, roamkeys[i]) nmsg = appendJSONString(nmsg, roamkeys[i])
nmsg = append(nmsg, `,"id":`...) nmsg = append(nmsg, `,"id":`...)
@ -261,7 +268,7 @@ func fenceMatch(hookName string, sw *scanWriter, fence *liveFenceSwitches, metas
nmsg = append(nmsg, '}') nmsg = append(nmsg, '}')
nmsg = append(nmsg, '}') nmsg = append(nmsg, '}')
nmsgs = append(nmsgs, nmsg) nmsgs = append(nmsgs, string(nmsg))
} }
msgs = nmsgs msgs = nmsgs
} }
@ -269,7 +276,10 @@ func fenceMatch(hookName string, sw *scanWriter, fence *liveFenceSwitches, metas
return msgs return msgs
} }
func makemsg(command, group, detect, hookName string, metas []FenceMeta, key string, t time.Time, tail []byte) []byte { func makemsg(
command, group, detect, hookName string,
metas []FenceMeta, key string, t time.Time, tail string,
) string {
var buf []byte var buf []byte
buf = append(append(buf, `{"command":"`...), command...) buf = append(append(buf, `{"command":"`...), command...)
buf = append(append(buf, `","group":"`...), group...) buf = append(append(buf, `","group":"`...), group...)
@ -279,7 +289,7 @@ func makemsg(command, group, detect, hookName string, metas []FenceMeta, key str
buf = appendJSONString(append(buf, `,"key":`...), key) buf = appendJSONString(append(buf, `,"key":`...), key)
buf = appendJSONTimeFormat(append(buf, `,"time":`...), t) buf = appendJSONTimeFormat(append(buf, `,"time":`...), t)
buf = append(append(buf, ','), tail...) buf = append(append(buf, ','), tail...)
return buf return string(buf)
} }
func fenceMatchObject(fence *liveFenceSwitches, obj geojson.Object) bool { func fenceMatchObject(fence *liveFenceSwitches, obj geojson.Object) bool {

View File

@ -2,7 +2,6 @@ package controller
import ( import (
"bytes" "bytes"
"encoding/json"
"errors" "errors"
"sort" "sort"
"strings" "strings"
@ -36,31 +35,38 @@ func (a hooksByName) Swap(i, j int) {
a[i], a[j] = a[j], a[i] a[i], a[j] = a[j], a[i]
} }
func (c *Controller) cmdSetHook(msg *server.Message) (res resp.Value, d commandDetailsT, err error) { func (c *Controller) cmdSetHook(msg *server.Message, chanCmd bool) (
res resp.Value, d commandDetailsT, err error,
) {
start := time.Now() start := time.Now()
vs := msg.Values[1:] vs := msg.Values[1:]
var name, urls, cmd string var name, urls, cmd string
var ok bool var ok bool
if vs, name, ok = tokenval(vs); !ok || name == "" { if vs, name, ok = tokenval(vs); !ok || name == "" {
return server.NOMessage, d, errInvalidNumberOfArguments return server.NOMessage, d, errInvalidNumberOfArguments
} }
if vs, urls, ok = tokenval(vs); !ok || urls == "" {
return server.NOMessage, d, errInvalidNumberOfArguments
}
var endpoints []string var endpoints []string
for _, url := range strings.Split(urls, ",") { if chanCmd {
url = strings.TrimSpace(url) endpoints = []string{"local://" + name}
err := c.epc.Validate(url) } else {
if err != nil { if vs, urls, ok = tokenval(vs); !ok || urls == "" {
log.Errorf("sethook: %v", err) return server.NOMessage, d, errInvalidNumberOfArguments
return resp.SimpleStringValue(""), d, errInvalidArgument(url) }
for _, url := range strings.Split(urls, ",") {
url = strings.TrimSpace(url)
err := c.epc.Validate(url)
if err != nil {
log.Errorf("sethook: %v", err)
return resp.SimpleStringValue(""), d, errInvalidArgument(url)
}
endpoints = append(endpoints, url)
} }
endpoints = append(endpoints, url)
} }
var commandvs []resp.Value var commandvs []resp.Value
var cmdlc string var cmdlc string
var types []string var types []string
// var expires float64
// var expiresSet bool
metaMap := make(map[string]string) metaMap := make(map[string]string)
for { for {
commandvs = vs commandvs = vs
@ -82,6 +88,18 @@ func (c *Controller) cmdSetHook(msg *server.Message) (res resp.Value, d commandD
} }
metaMap[metakey] = metaval metaMap[metakey] = metaval
continue continue
// case "ex":
// var s string
// if vs, s, ok = tokenval(vs); !ok || s == "" {
// return server.NOMessage, d, errInvalidNumberOfArguments
// }
// v, err := strconv.ParseFloat(s, 64)
// if err != nil {
// return server.NOMessage, d, errInvalidArgument(s)
// }
// expires = v
// expiresSet = true
case "nearby": case "nearby":
types = nearbyTypes types = nearbyTypes
case "within", "intersects": case "within", "intersects":
@ -89,7 +107,7 @@ func (c *Controller) cmdSetHook(msg *server.Message) (res resp.Value, d commandD
} }
break break
} }
s, err := c.cmdSearchArgs(cmdlc, vs, types) s, err := c.cmdSearchArgs(true, cmdlc, vs, types)
defer s.Close() defer s.Close()
if err != nil { if err != nil {
return server.NOMessage, d, err return server.NOMessage, d, err
@ -119,12 +137,18 @@ func (c *Controller) cmdSetHook(msg *server.Message) (res resp.Value, d commandD
Endpoints: endpoints, Endpoints: endpoints,
Fence: &s, Fence: &s,
Message: cmsg, Message: cmsg,
db: c.qdb,
epm: c.epc, epm: c.epc,
Metas: metas, Metas: metas,
channel: chanCmd,
cond: sync.NewCond(&sync.Mutex{}),
}
// if expiresSet {
// hook.expires =
// time.Now().Add(time.Duration(expires * float64(time.Second)))
// }
if !chanCmd {
hook.db = c.qdb
} }
hook.cond = sync.NewCond(&hook.mu)
var wr bytes.Buffer var wr bytes.Buffer
hook.ScanWriter, err = c.newScanWriter( hook.ScanWriter, err = c.newScanWriter(
&wr, cmsg, s.key, s.output, s.precision, s.glob, false, &wr, cmsg, s.key, s.output, s.precision, s.glob, false,
@ -134,6 +158,10 @@ func (c *Controller) cmdSetHook(msg *server.Message) (res resp.Value, d commandD
} }
if h, ok := c.hooks[name]; ok { if h, ok := c.hooks[name]; ok {
if h.channel != chanCmd {
return server.NOMessage, d,
errors.New("hooks and channels cannot share the same name")
}
if h.Equals(hook) { if h.Equals(hook) {
// it was a match so we do nothing. But let's signal just // it was a match so we do nothing. But let's signal just
// for good measure. // for good measure.
@ -171,7 +199,9 @@ func (c *Controller) cmdSetHook(msg *server.Message) (res resp.Value, d commandD
return server.NOMessage, d, nil return server.NOMessage, d, nil
} }
func (c *Controller) cmdDelHook(msg *server.Message) (res resp.Value, d commandDetailsT, err error) { func (c *Controller) cmdDelHook(msg *server.Message, chanCmd bool) (
res resp.Value, d commandDetailsT, err error,
) {
start := time.Now() start := time.Now()
vs := msg.Values[1:] vs := msg.Values[1:]
@ -183,7 +213,7 @@ func (c *Controller) cmdDelHook(msg *server.Message) (res resp.Value, d commandD
if len(vs) != 0 { if len(vs) != 0 {
return server.NOMessage, d, errInvalidNumberOfArguments return server.NOMessage, d, errInvalidNumberOfArguments
} }
if h, ok := c.hooks[name]; ok { if h, ok := c.hooks[name]; ok && h.channel == chanCmd {
h.Close() h.Close()
if hm, ok := c.hookcols[h.Key]; ok { if hm, ok := c.hookcols[h.Key]; ok {
delete(hm, h.Name) delete(hm, h.Name)
@ -205,7 +235,9 @@ func (c *Controller) cmdDelHook(msg *server.Message) (res resp.Value, d commandD
return return
} }
func (c *Controller) cmdPDelHook(msg *server.Message) (res resp.Value, d commandDetailsT, err error) { func (c *Controller) cmdPDelHook(msg *server.Message, channel bool) (
res resp.Value, d commandDetailsT, err error,
) {
start := time.Now() start := time.Now()
vs := msg.Values[1:] vs := msg.Values[1:]
@ -219,19 +251,21 @@ func (c *Controller) cmdPDelHook(msg *server.Message) (res resp.Value, d command
} }
count := 0 count := 0
for name := range c.hooks { for name, h := range c.hooks {
match, _ := glob.Match(pattern, name) if h.channel != channel {
if match { continue
if h, ok := c.hooks[name]; ok {
h.Close()
if hm, ok := c.hookcols[h.Key]; ok {
delete(hm, h.Name)
}
delete(c.hooks, h.Name)
d.updated = true
count++
}
} }
match, _ := glob.Match(pattern, name)
if !match {
continue
}
h.Close()
if hm, ok := c.hookcols[h.Key]; ok {
delete(hm, h.Name)
}
delete(c.hooks, h.Name)
d.updated = true
count++
} }
d.timestamp = time.Now() d.timestamp = time.Now()
@ -244,7 +278,9 @@ func (c *Controller) cmdPDelHook(msg *server.Message) (res resp.Value, d command
return return
} }
func (c *Controller) cmdHooks(msg *server.Message) (res resp.Value, err error) { func (c *Controller) cmdHooks(msg *server.Message, channel bool) (
res resp.Value, err error,
) {
start := time.Now() start := time.Now()
vs := msg.Values[1:] vs := msg.Values[1:]
@ -260,6 +296,9 @@ func (c *Controller) cmdHooks(msg *server.Message) (res resp.Value, err error) {
var hooks []*Hook var hooks []*Hook
for name, hook := range c.hooks { for name, hook := range c.hooks {
if hook.channel != channel {
continue
}
match, _ := glob.Match(pattern, name) match, _ := glob.Match(pattern, name)
if match { if match {
hooks = append(hooks, hook) hooks = append(hooks, hook)
@ -270,7 +309,12 @@ func (c *Controller) cmdHooks(msg *server.Message) (res resp.Value, err error) {
switch msg.OutputType { switch msg.OutputType {
case server.JSON: case server.JSON:
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
buf.WriteString(`{"ok":true,"hooks":[`) buf.WriteString(`{"ok":true,`)
if channel {
buf.WriteString(`"chans":[`)
} else {
buf.WriteString(`"hooks":[`)
}
for i, hook := range hooks { for i, hook := range hooks {
if i > 0 { if i > 0 {
buf.WriteByte(',') buf.WriteByte(',')
@ -278,12 +322,14 @@ func (c *Controller) cmdHooks(msg *server.Message) (res resp.Value, err error) {
buf.WriteString(`{`) buf.WriteString(`{`)
buf.WriteString(`"name":` + jsonString(hook.Name)) buf.WriteString(`"name":` + jsonString(hook.Name))
buf.WriteString(`,"key":` + jsonString(hook.Key)) buf.WriteString(`,"key":` + jsonString(hook.Key))
buf.WriteString(`,"endpoints":[`) if !channel {
for i, endpoint := range hook.Endpoints { buf.WriteString(`,"endpoints":[`)
if i > 0 { for i, endpoint := range hook.Endpoints {
buf.WriteByte(',') if i > 0 {
buf.WriteByte(',')
}
buf.WriteString(jsonString(endpoint))
} }
buf.WriteString(jsonString(endpoint))
} }
buf.WriteString(`],"command":[`) buf.WriteString(`],"command":[`)
for i, v := range hook.Message.Values { for i, v := range hook.Message.Values {
@ -303,7 +349,8 @@ func (c *Controller) cmdHooks(msg *server.Message) (res resp.Value, err error) {
} }
buf.WriteString(`}}`) buf.WriteString(`}}`)
} }
buf.WriteString(`],"elapsed":"` + time.Now().Sub(start).String() + "\"}") buf.WriteString(`],"elapsed":"` +
time.Now().Sub(start).String() + "\"}")
return resp.StringValue(buf.String()), nil return resp.StringValue(buf.String()), nil
case server.RESP: case server.RESP:
var vals []resp.Value var vals []resp.Value
@ -332,7 +379,6 @@ func (c *Controller) cmdHooks(msg *server.Message) (res resp.Value, err error) {
// Hook represents a hook. // Hook represents a hook.
type Hook struct { type Hook struct {
mu sync.Mutex
cond *sync.Cond cond *sync.Cond
Key string Key string
Name string Name string
@ -342,12 +388,15 @@ type Hook struct {
ScanWriter *scanWriter ScanWriter *scanWriter
Metas []FenceMeta Metas []FenceMeta
db *buntdb.DB db *buntdb.DB
channel bool
closed bool closed bool
opened bool opened bool
query string query string
epm *endpoint.Manager epm *endpoint.Manager
expires time.Time
} }
// Equals returns true if two hooks are equal
func (h *Hook) Equals(hook *Hook) bool { func (h *Hook) Equals(hook *Hook) bool {
if h.Key != hook.Key || if h.Key != hook.Key ||
h.Name != hook.Name || h.Name != hook.Name ||
@ -370,6 +419,7 @@ func (h *Hook) Equals(hook *Hook) bool {
resp.ArrayValue(hook.Message.Values)) resp.ArrayValue(hook.Message.Values))
} }
// FenceMeta is a meta key/value pair for fences
type FenceMeta struct { type FenceMeta struct {
Name, Value string Name, Value string
} }
@ -391,21 +441,28 @@ func (arr hookMetaByName) Swap(a, b int) {
// Open is called when a hook is first created. It calls the manager // Open is called when a hook is first created. It calls the manager
// function in a goroutine // function in a goroutine
func (h *Hook) Open() { func (h *Hook) Open() {
h.mu.Lock() if h.channel {
defer h.mu.Unlock() // nothing to open for channels
return
}
h.cond.L.Lock()
defer h.cond.L.Unlock()
if h.opened { if h.opened {
return return
} }
h.opened = true h.opened = true
b, _ := json.Marshal(h.Name) h.query = `{"hook":` + jsonString(h.Name) + `}`
h.query = `{"hook":` + string(b) + `}`
go h.manager() go h.manager()
} }
// Close closed the hook and stop the manager function // Close closed the hook and stop the manager function
func (h *Hook) Close() { func (h *Hook) Close() {
h.mu.Lock() if h.channel {
defer h.mu.Unlock() // nothing to close for channels
return
}
h.cond.L.Lock()
defer h.cond.L.Unlock()
if h.closed { if h.closed {
return return
} }
@ -416,30 +473,35 @@ func (h *Hook) Close() {
// Signal can be called at any point to wake up the hook and // Signal can be called at any point to wake up the hook and
// notify the manager that there may be something new in the queue. // notify the manager that there may be something new in the queue.
func (h *Hook) Signal() { func (h *Hook) Signal() {
h.mu.Lock() if h.channel {
// nothing to signal for channels
return
}
h.cond.L.Lock()
h.cond.Broadcast() h.cond.Broadcast()
h.mu.Unlock() h.cond.L.Unlock()
} }
// the manager is a forever loop that calls proc whenever there's a signal. // the manager is a forever loop that calls proc whenever there's a signal.
// it ends when the "closed" flag is set. // it ends when the "closed" flag is set.
func (h *Hook) manager() { func (h *Hook) manager() {
for { for {
h.mu.Lock() h.cond.L.Lock()
for { for {
if h.closed { if h.closed {
h.mu.Unlock() h.cond.L.Unlock()
return return
} }
if h.proc() { if h.proc() {
break break
} }
h.mu.Unlock() h.cond.L.Unlock()
time.Sleep(time.Second / 4) // proc failed. wait half a second and try again
h.mu.Lock() time.Sleep(time.Second / 2)
h.cond.L.Lock()
} }
h.cond.Wait() h.cond.Wait()
h.mu.Unlock() h.cond.L.Unlock()
} }
} }
@ -452,13 +514,15 @@ func (h *Hook) proc() (ok bool) {
start := time.Now() start := time.Now()
err := h.db.Update(func(tx *buntdb.Tx) error { err := h.db.Update(func(tx *buntdb.Tx) error {
// get keys and vals // get keys and vals
err := tx.AscendGreaterOrEqual("hooks", h.query, func(key, val string) bool { err := tx.AscendGreaterOrEqual("hooks",
if strings.HasPrefix(key, hookLogPrefix) { h.query, func(key, val string) bool {
keys = append(keys, key) if strings.HasPrefix(key, hookLogPrefix) {
vals = append(vals, val) keys = append(keys, key)
} vals = append(vals, val)
return true }
}) return true
},
)
if err != nil { if err != nil {
return err return err
} }
@ -494,7 +558,8 @@ func (h *Hook) proc() (ok bool) {
for _, endpoint := range h.Endpoints { for _, endpoint := range h.Endpoints {
err := h.epm.Send(endpoint, val) err := h.epm.Send(endpoint, val)
if err != nil { if err != nil {
log.Debugf("Endpoint connect/send error: %v: %v: %v", idx, endpoint, err) log.Debugf("Endpoint connect/send error: %v: %v: %v",
idx, endpoint, err)
continue continue
} }
log.Debugf("Endpoint send ok: %v: %v: %v", idx, endpoint, err) log.Debugf("Endpoint send ok: %v: %v: %v", idx, endpoint, err)
@ -502,7 +567,8 @@ func (h *Hook) proc() (ok bool) {
break break
} }
if !sent { if !sent {
// failed to send. try to reinsert the remaining. if this fails we lose log entries. // failed to send. try to reinsert the remaining.
// if this fails we lose log entries.
keys = keys[i:] keys = keys[i:]
vals = vals[i:] vals = vals[i:]
ttls = ttls[i:] ttls = ttls[i:]
@ -528,41 +594,3 @@ func (h *Hook) proc() (ok bool) {
} }
return true return true
} }
/*
// Do performs a hook.
func (hook *Hook) Do(details *commandDetailsT) error {
var lerrs []error
msgs := FenceMatch(hook.Name, hook.ScanWriter, hook.Fence, details)
nextMessage:
for _, msg := range msgs {
nextEndpoint:
for _, endpoint := range hook.Endpoints {
switch endpoint.Protocol {
case HTTP:
if err := sendHTTPMessage(endpoint, []byte(msg)); err != nil {
lerrs = append(lerrs, err)
continue nextEndpoint
}
continue nextMessage // sent
case Disque:
if err := sendDisqueMessage(endpoint, []byte(msg)); err != nil {
lerrs = append(lerrs, err)
continue nextEndpoint
}
continue nextMessage // sent
}
}
}
if len(lerrs) == 0 {
// log.Notice("YAY")
return nil
}
var errmsgs []string
for _, err := range lerrs {
errmsgs = append(errmsgs, err.Error())
}
err := errors.New("not sent: " + strings.Join(errmsgs, ","))
log.Error(err)
return err
}*/

View File

@ -43,7 +43,13 @@ func (c *Controller) processLives() {
} }
} }
func writeMessage(conn net.Conn, message []byte, wrapRESP bool, connType server.Type, websocket bool) error { func writeLiveMessage(
conn net.Conn,
message []byte,
wrapRESP bool,
connType server.Type,
websocket bool,
) error {
if len(message) == 0 { if len(message) == 0 {
return nil return nil
} }
@ -70,28 +76,34 @@ func (c *Controller) goLive(inerr error, conn net.Conn, rd *server.PipelineReade
defer func() { defer func() {
log.Info("not live " + addr) log.Info("not live " + addr)
}() }()
if s, ok := inerr.(liveAOFSwitches); ok { switch s := inerr.(type) {
default:
return errors.New("invalid live type switches")
case liveAOFSwitches:
return c.liveAOF(s.pos, conn, rd, msg) return c.liveAOF(s.pos, conn, rd, msg)
case liveSubscriptionSwitches:
return c.liveSubscription(conn, rd, msg, websocket)
case liveFenceSwitches:
// fallthrough
} }
// everything below is for live geofences
lb := &liveBuffer{ lb := &liveBuffer{
cond: sync.NewCond(&sync.Mutex{}), cond: sync.NewCond(&sync.Mutex{}),
} }
var err error var err error
var sw *scanWriter var sw *scanWriter
var wr bytes.Buffer var wr bytes.Buffer
switch s := inerr.(type) { s := inerr.(liveFenceSwitches)
default: lb.glob = s.glob
return errors.New("invalid switch") lb.key = s.key
case liveFenceSwitches: lb.fence = &s
lb.glob = s.glob c.mu.RLock()
lb.key = s.key sw, err = c.newScanWriter(
lb.fence = &s &wr, msg, s.key, s.output, s.precision, s.glob, false,
c.mu.RLock() s.cursor, s.limit, s.wheres, s.whereins, s.whereevals, s.nofields)
sw, err = c.newScanWriter( c.mu.RUnlock()
&wr, msg, s.key, s.output, s.precision, s.glob, false,
s.cursor, s.limit, s.wheres, s.whereins, s.whereevals, s.nofields)
c.mu.RUnlock()
}
// everything below if for live SCAN, NEARBY, WITHIN, INTERSECTS // everything below if for live SCAN, NEARBY, WITHIN, INTERSECTS
if err != nil { if err != nil {
return err return err
@ -149,7 +161,7 @@ func (c *Controller) goLive(inerr error, conn net.Conn, rd *server.PipelineReade
case server.RESP: case server.RESP:
livemsg = []byte("+OK\r\n") livemsg = []byte("+OK\r\n")
} }
if err := writeMessage(conn, livemsg, false, connType, websocket); err != nil { if err := writeLiveMessage(conn, livemsg, false, connType, websocket); err != nil {
return nil // nil return is fine here return nil // nil return is fine here
} }
for { for {
@ -168,7 +180,7 @@ func (c *Controller) goLive(inerr error, conn net.Conn, rd *server.PipelineReade
lb.cond.L.Unlock() lb.cond.L.Unlock()
msgs := FenceMatch("", sw, fence, nil, details) msgs := FenceMatch("", sw, fence, nil, details)
for _, msg := range msgs { for _, msg := range msgs {
if err := writeMessage(conn, []byte(msg), true, connType, websocket); err != nil { if err := writeLiveMessage(conn, []byte(msg), true, connType, websocket); err != nil {
return nil // nil return is fine here return nil // nil return is fine here
} }
} }

363
pkg/controller/pubsub.go Normal file
View File

@ -0,0 +1,363 @@
package controller
import (
"io"
"net"
"strconv"
"sync"
"time"
"github.com/tidwall/gjson"
"github.com/tidwall/match"
"github.com/tidwall/redcon"
"github.com/tidwall/resp"
"github.com/tidwall/tile38/pkg/log"
"github.com/tidwall/tile38/pkg/server"
)
const (
pubsubChannel = iota
pubsubPattern
)
type pubsub struct {
mu sync.RWMutex
hubs [2]map[string]*subhub
}
func newPubsub() *pubsub {
return &pubsub{
hubs: [2]map[string]*subhub{
make(map[string]*subhub),
make(map[string]*subhub),
},
}
}
// Publish a message to subscribers
func (c *Controller) Publish(channel string, message ...string) int {
var msgs []submsg
c.pubsub.mu.RLock()
if hub := c.pubsub.hubs[pubsubChannel][channel]; hub != nil {
for target := range hub.targets {
for _, message := range message {
msgs = append(msgs, submsg{
kind: pubsubChannel,
target: target,
channel: channel,
message: message,
})
}
}
}
for pattern, hub := range c.pubsub.hubs[pubsubPattern] {
if match.Match(channel, pattern) {
for target := range hub.targets {
for _, message := range message {
msgs = append(msgs, submsg{
kind: pubsubPattern,
target: target,
channel: channel,
pattern: pattern,
message: message,
})
}
}
}
}
c.pubsub.mu.RUnlock()
for _, msg := range msgs {
msg.target.cond.L.Lock()
msg.target.msgs = append(msg.target.msgs, msg)
msg.target.cond.Broadcast()
msg.target.cond.L.Unlock()
}
return len(msgs)
}
func (ps *pubsub) register(kind int, channel string, target *subtarget) {
ps.mu.Lock()
hub, ok := ps.hubs[kind][channel]
if !ok {
hub = newSubhub()
ps.hubs[kind][channel] = hub
}
hub.targets[target] = true
ps.mu.Unlock()
}
func (ps *pubsub) unregister(kind int, channel string, target *subtarget) {
ps.mu.Lock()
hub, ok := ps.hubs[kind][channel]
if ok {
delete(hub.targets, target)
if len(hub.targets) == 0 {
delete(ps.hubs[kind], channel)
}
}
ps.mu.Unlock()
}
type submsg struct {
kind byte
target *subtarget
pattern string
channel string
message string
}
type subtarget struct {
cond *sync.Cond
msgs []submsg
closed bool
}
func newSubtarget() *subtarget {
target := new(subtarget)
target.cond = sync.NewCond(&sync.Mutex{})
return target
}
type subhub struct {
targets map[*subtarget]bool
}
func newSubhub() *subhub {
hub := new(subhub)
hub.targets = make(map[*subtarget]bool)
return hub
}
type liveSubscriptionSwitches struct {
// no fields. everything is managed through the server.Message
}
func (sub liveSubscriptionSwitches) Error() string {
return goingLive
}
func (c *Controller) cmdSubscribe(msg *server.Message) (resp.Value, error) {
if len(msg.Values) < 2 {
return resp.Value{}, errInvalidNumberOfArguments
}
return server.NOMessage, liveSubscriptionSwitches{}
}
func (c *Controller) cmdPsubscribe(msg *server.Message) (resp.Value, error) {
if len(msg.Values) < 2 {
return resp.Value{}, errInvalidNumberOfArguments
}
return server.NOMessage, liveSubscriptionSwitches{}
}
func (c *Controller) cmdPublish(msg *server.Message) (resp.Value, error) {
start := time.Now()
if len(msg.Values) != 3 {
return resp.Value{}, errInvalidNumberOfArguments
}
channel := msg.Values[1].String()
message := msg.Values[2].String()
//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:
res = resp.StringValue(`{"ok":true` +
`,"published":` + strconv.FormatInt(int64(n), 10) +
`,"elapsed":"` + time.Now().Sub(start).String() + `"}`)
case server.RESP:
res = resp.IntegerValue(n)
}
return res, nil
}
func (c *Controller) liveSubscription(
conn net.Conn,
rd *server.PipelineReader,
msg *server.Message,
websocket bool,
) error {
defer conn.Close() // close connection when we are done
outputType := msg.OutputType
connType := msg.ConnType
if websocket {
outputType = server.JSON
}
var start time.Time
// write helpers
var writeLock sync.Mutex
write := func(data []byte) {
writeLock.Lock()
defer writeLock.Unlock()
writeLiveMessage(conn, data, false, connType, websocket)
}
writeOK := func() {
switch outputType {
case server.JSON:
write([]byte(`{"ok":true` +
`,"elapsed":"` + time.Now().Sub(start).String() + `"}`))
case server.RESP:
write([]byte(`+OK\r\n`))
}
}
writeWrongNumberOfArgsErr := func(command string) {
switch outputType {
case server.JSON:
write([]byte(`{"ok":false,"err":"invalid number of arguments"` +
`,"elapsed":"` + time.Now().Sub(start).String() + `"}`))
case server.RESP:
write([]byte(`-ERR wrong number of arguments ` +
`for '` + command + `' command\r\n`))
}
}
writeOnlyPubsubErr := func() {
switch outputType {
case server.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:
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:
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:
b := redcon.AppendArray(nil, 3)
b = redcon.AppendBulkString(b, command)
b = redcon.AppendBulkString(b, channel)
b = redcon.AppendInt(b, int64(num))
write(b)
}
}
writeMessage := func(msg submsg) {
if msg.kind == pubsubChannel {
switch outputType {
case server.JSON:
var data []byte
if !gjson.Valid(msg.message) {
data = appendJSONString(nil, msg.message)
} else {
data = []byte(msg.message)
}
write(data)
case server.RESP:
b := redcon.AppendArray(nil, 3)
b = redcon.AppendBulkString(b, "message")
b = redcon.AppendBulkString(b, msg.channel)
b = redcon.AppendBulkString(b, msg.message)
write(b)
}
} else {
switch outputType {
case server.JSON:
var data []byte
if !gjson.Valid(msg.message) {
data = appendJSONString(nil, msg.message)
} else {
data = []byte(msg.message)
}
write(data)
case server.RESP:
b := redcon.AppendArray(nil, 4)
b = redcon.AppendBulkString(b, "pmessage")
b = redcon.AppendBulkString(b, msg.pattern)
b = redcon.AppendBulkString(b, msg.channel)
b = redcon.AppendBulkString(b, msg.message)
write(b)
}
}
}
m := [2]map[string]bool{
make(map[string]bool),
make(map[string]bool),
}
target := newSubtarget()
defer func() {
for i := 0; i < 2; i++ {
for channel := range m[i] {
c.pubsub.unregister(i, channel, target)
}
}
target.cond.L.Lock()
target.closed = true
target.cond.Broadcast()
target.cond.L.Unlock()
}()
go func() {
log.Debugf("pubsub open")
defer log.Debugf("pubsub closed")
for {
var msgs []submsg
target.cond.L.Lock()
if len(target.msgs) > 0 {
msgs = target.msgs
target.msgs = nil
}
target.cond.L.Unlock()
for _, msg := range msgs {
writeMessage(msg)
}
target.cond.L.Lock()
if target.closed {
target.cond.L.Unlock()
return
}
target.cond.Wait()
target.cond.L.Unlock()
}
}()
msgs := []*server.Message{msg}
for {
for _, msg := range msgs {
start = time.Now()
var kind int
switch msg.Command {
case "quit":
writeOK()
return nil
case "psubscribe":
kind = pubsubPattern
case "subscribe":
kind = pubsubChannel
default:
writeOnlyPubsubErr()
}
if len(msg.Values) < 2 {
writeWrongNumberOfArgsErr(msg.Command)
}
for i := 1; i < len(msg.Values); i++ {
channel := msg.Values[i].String()
m[kind][channel] = true
c.pubsub.register(kind, channel, target)
writeSubscribe(msg.Command, channel, len(m[0])+len(m[1]))
}
}
var err error
msgs, err = rd.ReadMessages()
if err != nil {
if err == io.EOF {
return nil
}
return err
}
}
}

View File

@ -11,10 +11,15 @@ import (
"github.com/tidwall/tile38/pkg/server" "github.com/tidwall/tile38/pkg/server"
) )
func (c *Controller) cmdScanArgs(vs []resp.Value) (s liveFenceSwitches, err error) { func (c *Controller) cmdScanArgs(vs []resp.Value) (
if vs, s.searchScanBaseTokens, err = c.parseSearchScanBaseTokens("scan", vs); err != nil { s liveFenceSwitches, err error,
) {
var t searchScanBaseTokens
vs, t, err = c.parseSearchScanBaseTokens("scan", t, vs)
if err != nil {
return return
} }
s.searchScanBaseTokens = t
if len(vs) != 0 { if len(vs) != 0 {
err = errInvalidNumberOfArguments err = errInvalidNumberOfArguments
return return

View File

@ -38,7 +38,7 @@ type roamSwitches struct {
} }
func (s liveFenceSwitches) Error() string { func (s liveFenceSwitches) Error() string {
return "going live" return goingLive
} }
func (s liveFenceSwitches) Close() { func (s liveFenceSwitches) Close() {
@ -51,10 +51,18 @@ func (s liveFenceSwitches) usingLua() bool {
return len(s.whereevals) > 0 return len(s.whereevals) > 0
} }
func (c *Controller) cmdSearchArgs(cmd string, vs []resp.Value, types []string) (s liveFenceSwitches, err error) { func (c *Controller) cmdSearchArgs(
if vs, s.searchScanBaseTokens, err = c.parseSearchScanBaseTokens(cmd, vs); err != nil { fromFenceCmd bool, cmd string, vs []resp.Value, types []string,
) (s liveFenceSwitches, err error) {
var t searchScanBaseTokens
if fromFenceCmd {
t.fence = true
}
vs, t, err = c.parseSearchScanBaseTokens(cmd, t, vs)
if err != nil {
return return
} }
s.searchScanBaseTokens = t
var typ string var typ string
var ok bool var ok bool
if vs, typ, ok = tokenval(vs); !ok || typ == "" { if vs, typ, ok = tokenval(vs); !ok || typ == "" {
@ -350,7 +358,7 @@ func (c *Controller) cmdNearby(msg *server.Message) (res resp.Value, err error)
start := time.Now() start := time.Now()
vs := msg.Values[1:] vs := msg.Values[1:]
wr := &bytes.Buffer{} wr := &bytes.Buffer{}
s, err := c.cmdSearchArgs("nearby", vs, nearbyTypes) s, err := c.cmdSearchArgs(false, "nearby", vs, nearbyTypes)
if s.usingLua() { if s.usingLua() {
defer s.Close() defer s.Close()
defer func() { defer func() {
@ -473,7 +481,7 @@ func (c *Controller) cmdWithinOrIntersects(cmd string, msg *server.Message) (res
vs := msg.Values[1:] vs := msg.Values[1:]
wr := &bytes.Buffer{} wr := &bytes.Buffer{}
s, err := c.cmdSearchArgs(cmd, vs, withinOrIntersectsTypes) s, err := c.cmdSearchArgs(false, cmd, vs, withinOrIntersectsTypes)
if s.usingLua() { if s.usingLua() {
defer s.Close() defer s.Close()
defer func() { defer func() {
@ -533,11 +541,11 @@ func (c *Controller) cmdWithinOrIntersects(cmd string, msg *server.Message) (res
return true return true
} }
return sw.writeObject(ScanWriterParams{ return sw.writeObject(ScanWriterParams{
id: id, id: id,
o: o, o: o,
fields: fields, fields: fields,
noLock: true, noLock: true,
clip: s.clip, clip: s.clip,
clipbox: clipbox, clipbox: clipbox,
}) })
}, },
@ -552,10 +560,15 @@ func (c *Controller) cmdWithinOrIntersects(cmd string, msg *server.Message) (res
return sw.respOut, nil return sw.respOut, nil
} }
func (c *Controller) cmdSeachValuesArgs(vs []resp.Value) (s liveFenceSwitches, err error) { func (c *Controller) cmdSeachValuesArgs(vs []resp.Value) (
if vs, s.searchScanBaseTokens, err = c.parseSearchScanBaseTokens("search", vs); err != nil { s liveFenceSwitches, err error,
) {
var t searchScanBaseTokens
vs, t, err = c.parseSearchScanBaseTokens("search", t, vs)
if err != nil {
return return
} }
s.searchScanBaseTokens = t
if len(vs) != 0 { if len(vs) != 0 {
err = errInvalidNumberOfArguments err = errInvalidNumberOfArguments
return return

View File

@ -168,15 +168,15 @@ func (wherein whereinT) match(value float64) bool {
} }
type whereevalT struct { type whereevalT struct {
c *Controller c *Controller
luaState *lua.LState luaState *lua.LState
fn *lua.LFunction fn *lua.LFunction
} }
func (whereeval whereevalT) Close() { func (whereeval whereevalT) Close() {
luaSetRawGlobals( luaSetRawGlobals(
whereeval.luaState, map[string]lua.LValue{ whereeval.luaState, map[string]lua.LValue{
"ARGV": lua.LNil, "ARGV": lua.LNil,
}) })
whereeval.c.luapool.Put(whereeval.luaState) whereeval.c.luapool.Put(whereeval.luaState)
} }
@ -189,11 +189,11 @@ func (whereeval whereevalT) match(fieldsWithNames map[string]float64) bool {
luaSetRawGlobals( luaSetRawGlobals(
whereeval.luaState, map[string]lua.LValue{ whereeval.luaState, map[string]lua.LValue{
"FIELDS": fieldsTbl, "FIELDS": fieldsTbl,
}) })
defer luaSetRawGlobals( defer luaSetRawGlobals(
whereeval.luaState, map[string]lua.LValue{ whereeval.luaState, map[string]lua.LValue{
"FIELDS": lua.LNil, "FIELDS": lua.LNil,
}) })
whereeval.luaState.Push(whereeval.fn) whereeval.luaState.Push(whereeval.fn)
@ -219,41 +219,48 @@ func (whereeval whereevalT) match(fieldsWithNames map[string]float64) bool {
return true return true
} }
var match bool var match bool
tbl.ForEach(func(lk lua.LValue, lv lua.LValue) {match = true}) tbl.ForEach(func(lk lua.LValue, lv lua.LValue) { match = true })
return match return match
} }
panic(fmt.Sprintf("Script returned value of type %s", ret.Type())) panic(fmt.Sprintf("Script returned value of type %s", ret.Type()))
} }
type searchScanBaseTokens struct { type searchScanBaseTokens struct {
key string key string
cursor uint64 cursor uint64
output outputT output outputT
precision uint64 precision uint64
lineout string lineout string
fence bool fence bool
distance bool distance bool
detect map[string]bool detect map[string]bool
accept map[string]bool accept map[string]bool
glob string glob string
wheres []whereT wheres []whereT
whereins []whereinT whereins []whereinT
whereevals []whereevalT whereevals []whereevalT
nofields bool nofields bool
ulimit bool ulimit bool
limit uint64 limit uint64
usparse bool usparse bool
sparse uint8 sparse uint8
desc bool desc bool
clip bool clip bool
} }
func (c *Controller) parseSearchScanBaseTokens(cmd string, vs []resp.Value) (vsout []resp.Value, t searchScanBaseTokens, err error) { func (c *Controller) parseSearchScanBaseTokens(
cmd string, t searchScanBaseTokens, vs []resp.Value,
) (
vsout []resp.Value, tout searchScanBaseTokens, err error,
) {
var ok bool var ok bool
if vs, t.key, ok = tokenval(vs); !ok || t.key == "" { if vs, t.key, ok = tokenval(vs); !ok || t.key == "" {
err = errInvalidNumberOfArguments err = errInvalidNumberOfArguments
return return
} }
fromFence := t.fence
var slimit string var slimit string
var ssparse string var ssparse string
var scursor string var scursor string
@ -261,7 +268,8 @@ func (c *Controller) parseSearchScanBaseTokens(cmd string, vs []resp.Value) (vso
for { for {
nvs, wtok, ok := tokenval(vs) nvs, wtok, ok := tokenval(vs)
if ok && len(wtok) > 0 { if ok && len(wtok) > 0 {
if (wtok[0] == 'C' || wtok[0] == 'c') && strings.ToLower(wtok) == "cursor" { switch strings.ToLower(wtok) {
case "cursor":
vs = nvs vs = nvs
if scursor != "" { if scursor != "" {
err = errDuplicateArgument(strings.ToUpper(wtok)) err = errDuplicateArgument(strings.ToUpper(wtok))
@ -272,7 +280,7 @@ func (c *Controller) parseSearchScanBaseTokens(cmd string, vs []resp.Value) (vso
return return
} }
continue continue
} else if (wtok[0] == 'W' || wtok[0] == 'w') && strings.ToLower(wtok) == "where" { case "where":
vs = nvs vs = nvs
var field, smin, smax string var field, smin, smax string
if vs, field, ok = tokenval(vs); !ok || field == "" { if vs, field, ok = tokenval(vs); !ok || field == "" {
@ -317,7 +325,7 @@ func (c *Controller) parseSearchScanBaseTokens(cmd string, vs []resp.Value) (vso
} }
t.wheres = append(t.wheres, whereT{field, minx, min, maxx, max}) t.wheres = append(t.wheres, whereT{field, minx, min, maxx, max})
continue continue
} else if (wtok[0] == 'W' || wtok[0] == 'w') && strings.ToLower(wtok) == "wherein" { case "wherein":
vs = nvs vs = nvs
var field, nvalsStr, valStr string var field, nvalsStr, valStr string
if vs, field, ok = tokenval(vs); !ok || field == "" { if vs, field, ok = tokenval(vs); !ok || field == "" {
@ -349,7 +357,9 @@ func (c *Controller) parseSearchScanBaseTokens(cmd string, vs []resp.Value) (vso
} }
t.whereins = append(t.whereins, whereinT{field, valMap}) t.whereins = append(t.whereins, whereinT{field, valMap})
continue continue
} else if (wtok[0] == 'W' || wtok[0] == 'w') && strings.Contains(strings.ToLower(wtok), "whereeval") { case "whereevalsha":
fallthrough
case "whereeval":
scriptIsSha := strings.ToLower(wtok) == "whereevalsha" scriptIsSha := strings.ToLower(wtok) == "whereevalsha"
vs = nvs vs = nvs
var script, nargsStr, arg string var script, nargsStr, arg string
@ -392,7 +402,7 @@ func (c *Controller) parseSearchScanBaseTokens(cmd string, vs []resp.Value) (vso
luaSetRawGlobals( luaSetRawGlobals(
luaState, map[string]lua.LValue{ luaState, map[string]lua.LValue{
"ARGV": argsTbl, "ARGV": argsTbl,
}) })
compiled, ok := c.luascripts.Get(shaSum) compiled, ok := c.luascripts.Get(shaSum)
@ -417,9 +427,9 @@ func (c *Controller) parseSearchScanBaseTokens(cmd string, vs []resp.Value) (vso
} }
c.luascripts.Put(shaSum, fn.Proto) c.luascripts.Put(shaSum, fn.Proto)
} }
t.whereevals = append(t.whereevals, whereevalT{c,luaState, fn}) t.whereevals = append(t.whereevals, whereevalT{c, luaState, fn})
continue continue
} else if (wtok[0] == 'N' || wtok[0] == 'n') && strings.ToLower(wtok) == "nofields" { case "nofields":
vs = nvs vs = nvs
if t.nofields { if t.nofields {
err = errDuplicateArgument(strings.ToUpper(wtok)) err = errDuplicateArgument(strings.ToUpper(wtok))
@ -427,7 +437,7 @@ func (c *Controller) parseSearchScanBaseTokens(cmd string, vs []resp.Value) (vso
} }
t.nofields = true t.nofields = true
continue continue
} else if (wtok[0] == 'L' || wtok[0] == 'l') && strings.ToLower(wtok) == "limit" { case "limit":
vs = nvs vs = nvs
if slimit != "" { if slimit != "" {
err = errDuplicateArgument(strings.ToUpper(wtok)) err = errDuplicateArgument(strings.ToUpper(wtok))
@ -438,7 +448,7 @@ func (c *Controller) parseSearchScanBaseTokens(cmd string, vs []resp.Value) (vso
return return
} }
continue continue
} else if (wtok[0] == 'S' || wtok[0] == 's') && strings.ToLower(wtok) == "sparse" { case "sparse":
vs = nvs vs = nvs
if ssparse != "" { if ssparse != "" {
err = errDuplicateArgument(strings.ToUpper(wtok)) err = errDuplicateArgument(strings.ToUpper(wtok))
@ -449,15 +459,15 @@ func (c *Controller) parseSearchScanBaseTokens(cmd string, vs []resp.Value) (vso
return return
} }
continue continue
} else if (wtok[0] == 'F' || wtok[0] == 'f') && strings.ToLower(wtok) == "fence" { case "fence":
vs = nvs vs = nvs
if t.fence { if t.fence && !fromFence {
err = errDuplicateArgument(strings.ToUpper(wtok)) err = errDuplicateArgument(strings.ToUpper(wtok))
return return
} }
t.fence = true t.fence = true
continue continue
} else if (wtok[0] == 'C' || wtok[0] == 'c') && strings.ToLower(wtok) == "commands" { case "commands":
vs = nvs vs = nvs
if t.accept != nil { if t.accept != nil {
err = errDuplicateArgument(strings.ToUpper(wtok)) err = errDuplicateArgument(strings.ToUpper(wtok))
@ -481,7 +491,7 @@ func (c *Controller) parseSearchScanBaseTokens(cmd string, vs []resp.Value) (vso
t.accept = nil t.accept = nil
} }
continue continue
} else if (wtok[0] == 'D' || wtok[0] == 'd') && strings.ToLower(wtok) == "distance" { case "distance":
vs = nvs vs = nvs
if t.distance { if t.distance {
err = errDuplicateArgument(strings.ToUpper(wtok)) err = errDuplicateArgument(strings.ToUpper(wtok))
@ -489,7 +499,7 @@ func (c *Controller) parseSearchScanBaseTokens(cmd string, vs []resp.Value) (vso
} }
t.distance = true t.distance = true
continue continue
} else if (wtok[0] == 'D' || wtok[0] == 'd') && strings.ToLower(wtok) == "detect" { case "detect":
vs = nvs vs = nvs
if t.detect != nil { if t.detect != nil {
err = errDuplicateArgument(strings.ToUpper(wtok)) err = errDuplicateArgument(strings.ToUpper(wtok))
@ -525,7 +535,7 @@ func (c *Controller) parseSearchScanBaseTokens(cmd string, vs []resp.Value) (vso
} }
} }
continue continue
} else if (wtok[0] == 'D' || wtok[0] == 'd') && strings.ToLower(wtok) == "desc" { case "desc":
vs = nvs vs = nvs
if t.desc || asc { if t.desc || asc {
err = errDuplicateArgument(strings.ToUpper(wtok)) err = errDuplicateArgument(strings.ToUpper(wtok))
@ -533,7 +543,7 @@ func (c *Controller) parseSearchScanBaseTokens(cmd string, vs []resp.Value) (vso
} }
t.desc = true t.desc = true
continue continue
} else if (wtok[0] == 'A' || wtok[0] == 'a') && strings.ToLower(wtok) == "asc" { case "asc":
vs = nvs vs = nvs
if t.desc || asc { if t.desc || asc {
err = errDuplicateArgument(strings.ToUpper(wtok)) err = errDuplicateArgument(strings.ToUpper(wtok))
@ -541,7 +551,7 @@ func (c *Controller) parseSearchScanBaseTokens(cmd string, vs []resp.Value) (vso
} }
asc = true asc = true
continue continue
} else if (wtok[0] == 'M' || wtok[0] == 'm') && strings.ToLower(wtok) == "match" { case "match":
vs = nvs vs = nvs
if t.glob != "" { if t.glob != "" {
err = errDuplicateArgument(strings.ToUpper(wtok)) err = errDuplicateArgument(strings.ToUpper(wtok))
@ -552,7 +562,7 @@ func (c *Controller) parseSearchScanBaseTokens(cmd string, vs []resp.Value) (vso
return return
} }
continue continue
} else if (wtok[0] == 'C' || wtok[0] == 'c') && strings.ToLower(wtok) == "clip" { case "clip":
vs = nvs vs = nvs
if t.clip { if t.clip {
err = errDuplicateArgument(strings.ToUpper(wtok)) err = errDuplicateArgument(strings.ToUpper(wtok))
@ -666,5 +676,6 @@ func (c *Controller) parseSearchScanBaseTokens(cmd string, vs []resp.Value) (vso
t.limit = math.MaxUint64 t.limit = math.MaxUint64
} }
vsout = vs vsout = vs
tout = t
return return
} }

View File

@ -1,14 +1,12 @@
package endpoint package endpoint
import ( import (
"bufio"
"errors"
"fmt" "fmt"
"net"
"strconv"
"strings"
"sync" "sync"
"time" "time"
"github.com/garyburd/redigo/redis"
"github.com/tidwall/tile38/pkg/log"
) )
const ( const (
@ -21,8 +19,7 @@ type DisqueConn struct {
ep Endpoint ep Endpoint
ex bool ex bool
t time.Time t time.Time
conn net.Conn conn redis.Conn
rd *bufio.Reader
} }
func newDisqueConn(ep Endpoint) *DisqueConn { func newDisqueConn(ep Endpoint) *DisqueConn {
@ -52,7 +49,6 @@ func (conn *DisqueConn) close() {
conn.conn.Close() conn.conn.Close()
conn.conn = nil conn.conn = nil
} }
conn.rd = nil
} }
// Send sends a message // Send sends a message
@ -66,60 +62,23 @@ func (conn *DisqueConn) Send(msg string) error {
if conn.conn == nil { if conn.conn == nil {
addr := fmt.Sprintf("%s:%d", conn.ep.Disque.Host, conn.ep.Disque.Port) addr := fmt.Sprintf("%s:%d", conn.ep.Disque.Host, conn.ep.Disque.Port)
var err error var err error
conn.conn, err = net.Dial("tcp", addr) conn.conn, err = redis.Dial("tcp", addr)
if err != nil { if err != nil {
return err return err
} }
conn.rd = bufio.NewReader(conn.conn)
} }
var args []string
args = append(args, "ADDJOB", conn.ep.Disque.QueueName, msg, "0") var args []interface{}
args = append(args, conn.ep.Disque.QueueName, msg, 0)
if conn.ep.Disque.Options.Replicate > 0 { if conn.ep.Disque.Options.Replicate > 0 {
args = append(args, "REPLICATE", strconv.FormatInt(int64(conn.ep.Disque.Options.Replicate), 10)) args = append(args, "REPLICATE", conn.ep.Disque.Options.Replicate)
} }
cmd := buildRedisCommand(args)
if _, err := conn.conn.Write(cmd); err != nil { reply, err := redis.String(conn.conn.Do("ADDJOB", args...))
conn.close()
return err
}
c, err := conn.rd.ReadByte()
if err != nil { if err != nil {
conn.close() conn.close()
return err return err
} }
if c != '-' && c != '+' { log.Debugf("Disque: ADDJOB '%s'", reply)
conn.close()
return errors.New("invalid disque reply")
}
ln, err := conn.rd.ReadBytes('\n')
if err != nil {
conn.close()
return err
}
if len(ln) < 2 || ln[len(ln)-2] != '\r' {
conn.close()
return errors.New("invalid disque reply")
}
id := string(ln[:len(ln)-2])
p := strings.Split(id, "-")
if len(p) != 4 {
conn.close()
return errors.New("invalid disque reply")
}
return nil return nil
} }
func buildRedisCommand(args []string) []byte {
var cmd []byte
cmd = append(cmd, '*')
cmd = strconv.AppendInt(cmd, int64(len(args)), 10)
cmd = append(cmd, '\r', '\n')
for _, arg := range args {
cmd = append(cmd, '$')
cmd = strconv.AppendInt(cmd, int64(len(arg)), 10)
cmd = append(cmd, '\r', '\n')
cmd = append(cmd, arg...)
cmd = append(cmd, '\r', '\n')
}
return cmd
}

View File

@ -17,6 +17,8 @@ var errExpired = errors.New("expired")
type Protocol string type Protocol string
const ( const (
// Local protocol
Local = Protocol("local")
// HTTP protocol // HTTP protocol
HTTP = Protocol("http") HTTP = Protocol("http")
// Disque protocol // Disque protocol
@ -87,7 +89,6 @@ type Endpoint struct {
CertFile string CertFile string
KeyFile string KeyFile string
} }
SQS struct { SQS struct {
QueueID string QueueID string
Region string Region string
@ -95,7 +96,6 @@ type Endpoint struct {
CredProfile string CredProfile string
QueueName string QueueName string
} }
NATS struct { NATS struct {
Host string Host string
Port int Port int
@ -103,6 +103,9 @@ type Endpoint struct {
Pass string Pass string
Topic string Topic string
} }
Local struct {
Channel string
}
} }
// Conn is an endpoint connection // Conn is an endpoint connection
@ -113,14 +116,16 @@ type Conn interface {
// Manager manages all endpoints // Manager manages all endpoints
type Manager struct { type Manager struct {
mu sync.RWMutex mu sync.RWMutex
conns map[string]Conn conns map[string]Conn
publisher LocalPublisher
} }
// NewManager returns a new manager // NewManager returns a new manager
func NewManager() *Manager { func NewManager(publisher LocalPublisher) *Manager {
epc := &Manager{ epc := &Manager{
conns: make(map[string]Conn), conns: make(map[string]Conn),
publisher: publisher,
} }
go epc.Run() go epc.Run()
return epc return epc
@ -180,6 +185,8 @@ func (epc *Manager) Send(endpoint, msg string) error {
conn = newSQSConn(ep) conn = newSQSConn(ep)
case NATS: case NATS:
conn = newNATSConn(ep) conn = newNATSConn(ep)
case Local:
conn = newLocalConn(ep, epc.publisher)
} }
epc.conns[endpoint] = conn epc.conns[endpoint] = conn
} }
@ -204,6 +211,8 @@ func parseEndpoint(s string) (Endpoint, error) {
switch { switch {
default: default:
return endpoint, errors.New("unknown scheme") return endpoint, errors.New("unknown scheme")
case strings.HasPrefix(s, "local:"):
endpoint.Protocol = Local
case strings.HasPrefix(s, "http:"): case strings.HasPrefix(s, "http:"):
endpoint.Protocol = HTTP endpoint.Protocol = HTTP
case strings.HasPrefix(s, "https:"): case strings.HasPrefix(s, "https:"):
@ -237,9 +246,17 @@ func parseEndpoint(s string) (Endpoint, error) {
sp := strings.Split(sqp[0], "/") sp := strings.Split(sqp[0], "/")
s = sp[0] s = sp[0]
if s == "" { if s == "" {
if endpoint.Protocol == Local {
return endpoint, errors.New("missing channel")
}
return endpoint, errors.New("missing host") return endpoint, errors.New("missing host")
} }
// Local PubSub channel
// local://<channel>
if endpoint.Protocol == Local {
endpoint.Local.Channel = s
}
if endpoint.Protocol == GRPC { if endpoint.Protocol == GRPC {
dp := strings.Split(s, ":") dp := strings.Split(s, ":")
switch len(dp) { switch len(dp) {

38
pkg/endpoint/local.go Normal file
View File

@ -0,0 +1,38 @@
package endpoint
import (
"time"
)
const (
localExpiresAfter = time.Second * 30
)
// LocalPublisher is used to publish local notifcations
type LocalPublisher interface {
Publish(channel string, message ...string) int
}
// LocalConn is an endpoint connection
type LocalConn struct {
ep Endpoint
publisher LocalPublisher
}
func newLocalConn(ep Endpoint, publisher LocalPublisher) *LocalConn {
return &LocalConn{
ep: ep,
publisher: publisher,
}
}
// Expired returns true if the connection has expired
func (conn *LocalConn) Expired() bool {
return false
}
// Send sends a message
func (conn *LocalConn) Send(msg string) error {
conn.publisher.Publish(conn.ep.Local.Channel, msg)
return nil
}

View File

@ -1,12 +1,11 @@
package endpoint package endpoint
import ( import (
"bufio"
"errors"
"fmt" "fmt"
"net"
"sync" "sync"
"time" "time"
"github.com/garyburd/redigo/redis"
) )
const ( const (
@ -19,8 +18,7 @@ type RedisConn struct {
ep Endpoint ep Endpoint
ex bool ex bool
t time.Time t time.Time
conn net.Conn conn redis.Conn
rd *bufio.Reader
} }
func newRedisConn(ep Endpoint) *RedisConn { func newRedisConn(ep Endpoint) *RedisConn {
@ -50,7 +48,6 @@ func (conn *RedisConn) close() {
conn.conn.Close() conn.conn.Close()
conn.conn = nil conn.conn = nil
} }
conn.rd = nil
} }
// Send sends a message // Send sends a message
@ -61,48 +58,20 @@ func (conn *RedisConn) Send(msg string) error {
if conn.ex { if conn.ex {
return errExpired return errExpired
} }
conn.t = time.Now() conn.t = time.Now()
if conn.conn == nil { if conn.conn == nil {
addr := fmt.Sprintf("%s:%d", conn.ep.Redis.Host, conn.ep.Redis.Port) addr := fmt.Sprintf("%s:%d", conn.ep.Redis.Host, conn.ep.Redis.Port)
var err error var err error
conn.conn, err = net.Dial("tcp", addr) conn.conn, err = redis.Dial("tcp", addr)
if err != nil { if err != nil {
conn.close()
return err return err
} }
conn.rd = bufio.NewReader(conn.conn)
} }
_, err := redis.Int(conn.conn.Do("PUBLISH", conn.ep.Redis.Channel, msg))
var args []string
args = append(args, "PUBLISH", conn.ep.Redis.Channel, msg)
cmd := buildRedisCommand(args)
if _, err := conn.conn.Write(cmd); err != nil {
conn.close()
return err
}
c, err := conn.rd.ReadByte()
if err != nil { if err != nil {
conn.close() conn.close()
return err return err
} }
if c != ':' {
conn.close()
return errors.New("invalid redis reply")
}
ln, err := conn.rd.ReadBytes('\n')
if err != nil {
conn.close()
return err
}
if string(ln[0:1]) != "1" {
conn.close()
return errors.New("invalid redis reply")
}
return nil return nil
} }

View File

@ -145,7 +145,6 @@ func readNextHTTPCommand(packet []byte, argsIn [][]byte, msg *Message, wr io.Wri
accept := base64.StdEncoding.EncodeToString(sum[:]) 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" 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 { if _, err = wr.Write([]byte(wshead)); err != nil {
println(4)
return false, err return false, err
} }
} else if contentLength > 0 { } else if contentLength > 0 {