tile38/controller/hooks.go

511 lines
11 KiB
Go
Raw Normal View History

2016-03-19 17:16:19 +03:00
package controller
import (
"bytes"
2016-09-11 17:49:48 +03:00
"encoding/json"
2016-03-19 17:16:19 +03:00
"errors"
"sort"
2016-09-12 05:20:53 +03:00
"strconv"
2016-03-19 17:16:19 +03:00
"strings"
2016-09-11 17:49:48 +03:00
"sync"
2016-03-19 17:16:19 +03:00
"time"
2016-09-11 17:49:48 +03:00
"github.com/tidwall/buntdb"
2016-03-29 00:16:21 +03:00
"github.com/tidwall/resp"
2016-09-12 05:01:24 +03:00
"github.com/tidwall/tile38/controller/endpoint"
2016-07-12 22:18:16 +03:00
"github.com/tidwall/tile38/controller/glob"
2016-03-19 17:16:19 +03:00
"github.com/tidwall/tile38/controller/log"
2016-03-29 00:16:21 +03:00
"github.com/tidwall/tile38/controller/server"
2016-03-19 17:16:19 +03:00
)
2016-09-11 17:49:48 +03:00
const hookLogTTL = time.Second * 30
2016-03-19 17:16:19 +03:00
2016-09-11 17:49:48 +03:00
func hookLogSetDefaults() *buntdb.SetOptions {
if hookLogTTL > 0 {
return &buntdb.SetOptions{
Expires: true, // automatically delete after 30 seconds
TTL: hookLogTTL,
2016-03-20 18:24:20 +03:00
}
}
2016-09-11 17:49:48 +03:00
return nil
2016-03-19 17:16:19 +03:00
}
type hooksByName []*Hook
func (a hooksByName) Len() int {
return len(a)
}
func (a hooksByName) Less(i, j int) bool {
return a[i].Name < a[j].Name
}
func (a hooksByName) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
2016-03-29 22:29:15 +03:00
func (c *Controller) cmdSetHook(msg *server.Message) (res string, d commandDetailsT, err error) {
start := time.Now()
vs := msg.Values[1:]
2016-09-11 17:49:48 +03:00
var name, urls, cmd string
2016-03-29 22:29:15 +03:00
var ok bool
if vs, name, ok = tokenval(vs); !ok || name == "" {
return "", d, errInvalidNumberOfArguments
2016-03-19 17:16:19 +03:00
}
2016-09-11 17:49:48 +03:00
if vs, urls, ok = tokenval(vs); !ok || urls == "" {
2016-03-29 22:29:15 +03:00
return "", d, errInvalidNumberOfArguments
2016-03-19 17:16:19 +03:00
}
2016-09-11 17:49:48 +03:00
var endpoints []string
for _, url := range strings.Split(urls, ",") {
url = strings.TrimSpace(url)
err := c.epc.Validate(url)
2016-03-20 18:24:20 +03:00
if err != nil {
log.Errorf("sethook: %v", err)
2016-09-11 17:49:48 +03:00
return "", d, errInvalidArgument(url)
2016-03-20 18:24:20 +03:00
}
2016-09-11 17:49:48 +03:00
endpoints = append(endpoints, url)
2016-03-19 17:16:19 +03:00
}
2016-04-01 22:46:39 +03:00
2016-03-29 22:29:15 +03:00
commandvs := vs
if vs, cmd, ok = tokenval(vs); !ok || cmd == "" {
return "", d, errInvalidNumberOfArguments
2016-03-19 17:16:19 +03:00
}
2016-04-01 22:46:39 +03:00
2016-03-19 17:16:19 +03:00
cmdlc := strings.ToLower(cmd)
var types []string
switch cmdlc {
default:
2016-03-29 22:29:15 +03:00
return "", d, errInvalidArgument(cmd)
2016-03-19 17:16:19 +03:00
case "nearby":
types = nearbyTypes
case "within", "intersects":
types = withinOrIntersectsTypes
}
2016-03-29 00:16:21 +03:00
s, err := c.cmdSearchArgs(cmdlc, vs, types)
2016-03-19 17:16:19 +03:00
if err != nil {
2016-03-29 22:29:15 +03:00
return "", d, err
2016-03-19 17:16:19 +03:00
}
if !s.fence {
2016-03-29 22:29:15 +03:00
return "", d, errors.New("missing FENCE argument")
2016-03-19 17:16:19 +03:00
}
s.cmd = cmdlc
2016-03-29 22:29:15 +03:00
cmsg := &server.Message{}
*cmsg = *msg
cmsg.Values = commandvs
cmsg.Command = strings.ToLower(cmsg.Values[0].String())
2016-03-19 17:16:19 +03:00
hook := &Hook{
2016-03-20 18:24:20 +03:00
Key: s.key,
Name: name,
Endpoints: endpoints,
Fence: &s,
2016-03-29 22:29:15 +03:00
Message: cmsg,
2016-09-11 17:49:48 +03:00
db: c.qdb,
epm: c.epc,
2016-03-19 17:16:19 +03:00
}
2016-09-11 17:49:48 +03:00
hook.cond = sync.NewCond(&hook.mu)
2016-03-19 17:16:19 +03:00
var wr bytes.Buffer
2016-07-13 06:11:02 +03:00
hook.ScanWriter, err = c.newScanWriter(&wr, cmsg, s.key, s.output, s.precision, s.glob, false, s.limit, s.wheres, s.nofields)
2016-03-19 17:16:19 +03:00
if err != nil {
2016-03-29 22:29:15 +03:00
return "", d, err
2016-03-19 17:16:19 +03:00
}
if h, ok := c.hooks[name]; ok {
2016-03-29 22:29:15 +03:00
// lets see if the previous hook matches the new hook
if h.Key == hook.Key && h.Name == hook.Name {
if len(h.Endpoints) == len(hook.Endpoints) {
match := true
for i, endpoint := range h.Endpoints {
2016-09-11 17:49:48 +03:00
if endpoint != hook.Endpoints[i] {
2016-03-29 22:29:15 +03:00
match = false
break
}
}
if match && resp.ArrayValue(h.Message.Values).Equals(resp.ArrayValue(hook.Message.Values)) {
2016-09-11 17:49:48 +03:00
// it was a match so we do nothing. But let's signal just
// for good measure.
h.Signal()
2016-03-29 22:29:15 +03:00
switch msg.OutputType {
case server.JSON:
return server.OKMessage(msg, start), d, nil
case server.RESP:
return ":0\r\n", d, nil
}
}
}
}
2016-09-11 17:49:48 +03:00
h.Close()
2016-04-01 22:46:39 +03:00
// delete the previous hook
2016-03-19 17:16:19 +03:00
if hm, ok := c.hookcols[h.Key]; ok {
delete(hm, h.Name)
}
delete(c.hooks, h.Name)
}
2016-03-29 22:29:15 +03:00
d.updated = true
2016-04-02 17:20:30 +03:00
d.timestamp = time.Now()
2016-03-19 17:16:19 +03:00
c.hooks[name] = hook
hm, ok := c.hookcols[hook.Key]
if !ok {
hm = make(map[string]*Hook)
c.hookcols[hook.Key] = hm
}
hm[name] = hook
2016-09-11 17:49:48 +03:00
hook.Open()
2016-03-29 22:29:15 +03:00
switch msg.OutputType {
case server.JSON:
return server.OKMessage(msg, start), d, nil
case server.RESP:
return ":1\r\n", d, nil
}
return "", d, nil
2016-03-19 17:16:19 +03:00
}
2016-03-29 22:29:15 +03:00
func (c *Controller) cmdDelHook(msg *server.Message) (res string, d commandDetailsT, err error) {
start := time.Now()
vs := msg.Values[1:]
2016-03-19 17:16:19 +03:00
var name string
2016-03-29 22:29:15 +03:00
var ok bool
if vs, name, ok = tokenval(vs); !ok || name == "" {
return "", d, errInvalidNumberOfArguments
2016-03-19 17:16:19 +03:00
}
2016-03-29 22:29:15 +03:00
if len(vs) != 0 {
return "", d, errInvalidNumberOfArguments
2016-03-19 17:16:19 +03:00
}
if h, ok := c.hooks[name]; ok {
2016-09-11 17:49:48 +03:00
h.Close()
2016-03-19 17:16:19 +03:00
if hm, ok := c.hookcols[h.Key]; ok {
delete(hm, h.Name)
}
delete(c.hooks, h.Name)
2016-03-29 22:29:15 +03:00
d.updated = true
}
2016-04-02 17:20:30 +03:00
d.timestamp = time.Now()
2016-03-29 22:29:15 +03:00
switch msg.OutputType {
case server.JSON:
return server.OKMessage(msg, start), d, nil
case server.RESP:
if d.updated {
return ":1\r\n", d, nil
}
2016-04-03 05:16:36 +03:00
return ":0\r\n", d, nil
2016-03-19 17:16:19 +03:00
}
return
}
2016-09-12 05:20:53 +03:00
func (c *Controller) cmdPDelHook(msg *server.Message) (res string, d commandDetailsT, err error) {
start := time.Now()
vs := msg.Values[1:]
var pattern string
var ok bool
if vs, pattern, ok = tokenval(vs); !ok || pattern == "" {
return "", d, errInvalidNumberOfArguments
}
if len(vs) != 0 {
return "", d, errInvalidNumberOfArguments
}
count := 0
for name := range c.hooks {
match, _ := glob.Match(pattern, name)
if match {
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)
count++
}
}
}
d.timestamp = time.Now()
switch msg.OutputType {
case server.JSON:
return server.OKMessage(msg, start), d, nil
case server.RESP:
return ":" + strconv.FormatInt(int64(count), 10) + "\r\n", d, nil
}
return
}
2016-03-29 22:29:15 +03:00
func (c *Controller) cmdHooks(msg *server.Message) (res string, err error) {
2016-03-19 17:16:19 +03:00
start := time.Now()
2016-03-29 22:29:15 +03:00
vs := msg.Values[1:]
2016-03-19 17:16:19 +03:00
var pattern string
2016-03-29 22:29:15 +03:00
var ok bool
if vs, pattern, ok = tokenval(vs); !ok || pattern == "" {
return "", errInvalidNumberOfArguments
2016-03-19 17:16:19 +03:00
}
2016-03-29 22:29:15 +03:00
if len(vs) != 0 {
return "", errInvalidNumberOfArguments
2016-03-19 17:16:19 +03:00
}
var hooks []*Hook
for name, hook := range c.hooks {
2016-07-12 22:18:16 +03:00
match, _ := glob.Match(pattern, name)
2016-03-29 22:29:15 +03:00
if match {
2016-03-19 17:16:19 +03:00
hooks = append(hooks, hook)
}
}
sort.Sort(hooksByName(hooks))
2016-03-29 22:29:15 +03:00
switch msg.OutputType {
case server.JSON:
buf := &bytes.Buffer{}
buf.WriteString(`{"ok":true,"hooks":[`)
for i, hook := range hooks {
2016-03-20 18:24:20 +03:00
if i > 0 {
buf.WriteByte(',')
}
2016-03-29 22:29:15 +03:00
buf.WriteString(`{`)
buf.WriteString(`"name":` + jsonString(hook.Name))
buf.WriteString(`,"key":` + jsonString(hook.Key))
buf.WriteString(`,"endpoints":[`)
for i, endpoint := range hook.Endpoints {
if i > 0 {
buf.WriteByte(',')
}
2016-09-11 17:49:48 +03:00
buf.WriteString(jsonString(endpoint))
2016-03-29 22:29:15 +03:00
}
buf.WriteString(`],"command":[`)
for i, v := range hook.Message.Values {
if i > 0 {
buf.WriteString(`,`)
}
buf.WriteString(jsonString(v.String()))
}
buf.WriteString(`]}`)
}
buf.WriteString(`],"elapsed":"` + time.Now().Sub(start).String() + "\"}")
return buf.String(), nil
case server.RESP:
var vals []resp.Value
for _, hook := range hooks {
var hvals []resp.Value
hvals = append(hvals, resp.StringValue(hook.Name))
hvals = append(hvals, resp.StringValue(hook.Key))
var evals []resp.Value
for _, endpoint := range hook.Endpoints {
2016-09-11 17:49:48 +03:00
evals = append(evals, resp.StringValue(endpoint))
2016-03-29 22:29:15 +03:00
}
hvals = append(hvals, resp.ArrayValue(evals))
hvals = append(hvals, resp.ArrayValue(hook.Message.Values))
vals = append(vals, resp.ArrayValue(hvals))
2016-03-19 17:16:19 +03:00
}
2016-03-29 22:29:15 +03:00
data, err := resp.ArrayValue(vals).MarshalRESP()
if err != nil {
return "", err
}
return string(data), nil
2016-03-19 17:16:19 +03:00
}
2016-03-29 22:29:15 +03:00
return "", nil
2016-03-19 17:16:19 +03:00
}
2016-09-11 17:49:48 +03:00
// Hook represents a hook.
type Hook struct {
mu sync.Mutex
cond *sync.Cond
Key string
Name string
Endpoints []string
Message *server.Message
Fence *liveFenceSwitches
ScanWriter *scanWriter
db *buntdb.DB
closed bool
opened bool
query string
2016-09-12 05:01:24 +03:00
epm *endpoint.EndpointManager
2016-09-11 17:49:48 +03:00
}
// Open is called when a hook is first created. It calls the manager
// function in a goroutine
func (h *Hook) Open() {
h.mu.Lock()
defer h.mu.Unlock()
if h.opened {
return
}
h.opened = true
b, _ := json.Marshal(h.Name)
h.query = `{"hook":` + string(b) + `}`
go h.manager()
}
// Close closed the hook and stop the manager function
func (h *Hook) Close() {
h.mu.Lock()
defer h.mu.Unlock()
if h.closed {
return
}
h.closed = true
h.cond.Broadcast()
}
// 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.
func (h *Hook) Signal() {
h.mu.Lock()
defer h.mu.Unlock()
h.cond.Broadcast()
}
// the manager is a forever loop that calls proc whenever there's a signal.
// it ends when the "closed" flag is set.
func (h *Hook) manager() {
for {
h.mu.Lock()
for {
if h.closed {
h.mu.Unlock()
return
}
if h.proc() {
break
}
h.mu.Unlock()
time.Sleep(time.Second / 4)
h.mu.Lock()
}
h.cond.Wait()
h.mu.Unlock()
}
}
2016-09-12 05:01:24 +03:00
// proc processes queued hook logs.
// returning true will indicate that all log entries have been
// successfully handled.
2016-09-11 17:49:48 +03:00
func (h *Hook) proc() (ok bool) {
var keys, vals []string
var ttls []time.Duration
err := h.db.Update(func(tx *buntdb.Tx) error {
2016-09-12 05:01:24 +03:00
2016-09-11 17:49:48 +03:00
// get keys and vals
err := tx.AscendGreaterOrEqual("hooks", h.query, func(key, val string) bool {
if strings.HasPrefix(key, hookLogPrefix) {
keys = append(keys, key)
vals = append(vals, val)
}
return true
})
if err != nil {
return err
}
2016-09-12 05:01:24 +03:00
2016-09-11 17:49:48 +03:00
// delete the keys
for _, key := range keys {
if hookLogTTL > 0 {
ttl, err := tx.TTL(key)
if err != nil {
if err != buntdb.ErrNotFound {
return err
}
}
ttls = append(ttls, ttl)
}
_, err = tx.Delete(key)
if err != nil {
if err != buntdb.ErrNotFound {
return err
}
}
}
return nil
})
if err != nil {
log.Error(err)
return false
}
// send each val. on failure reinsert that one and all of the following
for i, key := range keys {
val := vals[i]
idx := stringToUint64(key[len(hookLogPrefix):])
var sent bool
for _, endpoint := range h.Endpoints {
err := h.epm.Send(endpoint, val)
if err != nil {
log.Debugf("could not send log: %v: %v: %v", idx, endpoint, err)
continue
}
sent = true
break
}
if !sent {
// failed to send. try to reinsert the remaining. if this fails we lose log entries.
keys = keys[i:]
vals = vals[i:]
if hookLogTTL > 0 {
ttls = ttls[i:]
}
h.db.Update(func(tx *buntdb.Tx) error {
for i, key := range keys {
val := vals[i]
var opts *buntdb.SetOptions
if hookLogTTL > 0 {
opts = &buntdb.SetOptions{
Expires: true,
TTL: ttls[i],
}
}
_, _, err := tx.Set(key, val, opts)
if err != nil {
return err
}
}
return nil
})
return false
}
}
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
}*/