tidwall 9998e03f6f Optimization for non-cross geofence detection
This commit fixes a performance issue with the algorithm that
determines with geofences are potential candidates for
notifications following a SET operation.

Details

Prior to commit b471873 (10 commits ago) there was a bug where
the "cross" detection was not firing in all cases. This happened
because when looking for candidates for "cross" due to a SET
operation, only the geofences that overlapped the previous
position of the object and the geofences that overlapped the new
position where searched. But, in fac, all of the geofences that
overlapped the union rectangle of the old and new position should
have been searched.

That commit fixed the problem by searching a union rect of the
old and new positions. While this is an accurate solution, it
caused a slowdown on systems that have big/wild position changes
that might cross a huge number of geofences, even when those
geofences did not need actually need "cross" detection.

The fix

With this commit the geofences that have a "cross" detection
are stored in a seperated tree from those that do not. This
allows for a hybrid of the functionality prior and post b471873.

Fixes #583
2020-10-23 09:51:27 -07:00

591 lines
13 KiB
Go

package server
import (
"errors"
"fmt"
"io"
"math"
"net"
"os"
"sort"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/tidwall/buntdb"
"github.com/tidwall/gjson"
"github.com/tidwall/redcon"
"github.com/tidwall/resp"
"github.com/tidwall/tile38/internal/log"
)
type errAOFHook struct {
err error
}
func (err errAOFHook) Error() string {
return fmt.Sprintf("hook: %v", err.err)
}
var errInvalidAOF = errors.New("invalid aof file")
func (s *Server) loadAOF() (err error) {
fi, err := s.aof.Stat()
if err != nil {
return err
}
start := time.Now()
var count int
defer func() {
d := time.Now().Sub(start)
ps := float64(count) / (float64(d) / float64(time.Second))
suf := []string{"bytes/s", "KB/s", "MB/s", "GB/s", "TB/s"}
bps := float64(fi.Size()) / (float64(d) / float64(time.Second))
for i := 0; bps > 1024; i++ {
if len(suf) == 1 {
break
}
bps /= 1024
suf = suf[1:]
}
byteSpeed := fmt.Sprintf("%.0f %s", bps, suf[0])
log.Infof("AOF loaded %d commands: %.2fs, %.0f/s, %s",
count, float64(d)/float64(time.Second), ps, byteSpeed)
}()
var buf []byte
var args [][]byte
var packet [0xFFFF]byte
var zeros int
for {
n, err := s.aof.Read(packet[:])
if err != nil {
if err == io.EOF {
if len(buf) > 0 {
return io.ErrUnexpectedEOF
}
if zeros > 0 {
// Trailing zeros in AOF. Truncate the file so it's sane.
// See issue #230 for more information. Force a warning.
log.Infof("Truncating %d zeros from AOF (issue #230)", zeros)
s.aofsz -= zeros
if err := s.aof.Truncate(int64(s.aofsz)); err != nil {
return err
}
if _, err := s.aof.Seek(int64(s.aofsz), 0); err != nil {
return err
}
}
return nil
}
return err
}
s.aofsz += n
data := packet[:n]
if len(buf) > 0 {
data = append(buf, data...)
}
var complete bool
for {
if len(data) > 0 {
if data[0] == 0 {
zeros++
data = data[1:]
continue
}
if zeros > 0 {
return errors.New("Zeros found in AOF file (issue #230)")
}
}
complete, args, _, data, err = redcon.ReadNextCommand(data, args[:0])
if err != nil {
return err
}
if !complete {
break
}
if len(args) > 0 {
var msg Message
msg.Args = msg.Args[:0]
for _, arg := range args {
msg.Args = append(msg.Args, string(arg))
}
if _, _, err := s.command(&msg, nil); err != nil {
if commandErrIsFatal(err) {
return err
}
}
count++
}
}
if len(data) > 0 {
buf = append(buf[:0], data...)
} else if len(buf) > 0 {
buf = buf[:0]
}
}
}
func commandErrIsFatal(err error) bool {
// FSET (and other writable commands) may return errors that we need
// to ignore during the loading process. These errors may occur (though unlikely)
// due to the aof rewrite operation.
switch err {
case errKeyNotFound, errIDNotFound:
return false
}
return true
}
// flushAOF flushes all aof buffer data to disk. Set sync to true to sync the
// fsync the file.
func (s *Server) flushAOF(sync bool) {
if len(s.aofbuf) > 0 {
_, err := s.aof.Write(s.aofbuf)
if err != nil {
panic(err)
}
if sync {
if err := s.aof.Sync(); err != nil {
panic(err)
}
}
if cap(s.aofbuf) > 1024*1024*32 {
s.aofbuf = make([]byte, 0, 1024*1024*32)
} else {
s.aofbuf = s.aofbuf[:0]
}
}
}
func (s *Server) writeAOF(args []string, d *commandDetails) error {
if d != nil && !d.updated {
// just ignore writes if the command did not update
return nil
}
if s.shrinking {
nargs := make([]string, len(args))
copy(nargs, args)
s.shrinklog = append(s.shrinklog, nargs)
}
if s.aof != nil {
atomic.StoreInt32(&s.aofdirty, 1) // prewrite optimization flag
n := len(s.aofbuf)
s.aofbuf = redcon.AppendArray(s.aofbuf, len(args))
for _, arg := range args {
s.aofbuf = redcon.AppendBulkString(s.aofbuf, arg)
}
s.aofsz += len(s.aofbuf) - n
}
// notify aof live connections that we have new data
s.fcond.L.Lock()
s.fcond.Broadcast()
s.fcond.L.Unlock()
// process geofences
if d != nil {
// webhook geofences
if s.config.followHost() == "" {
// for leader only
if d.parent {
// queue children
for _, d := range d.children {
if err := s.queueHooks(d); err != nil {
return err
}
}
} else {
// queue parent
if err := s.queueHooks(d); err != nil {
return err
}
}
}
// live geofences
s.lcond.L.Lock()
if len(s.lives) > 0 {
if d.parent {
// queue children
for _, d := range d.children {
s.lstack = append(s.lstack, d)
}
} else {
// queue parent
s.lstack = append(s.lstack, d)
}
s.lcond.Broadcast()
}
s.lcond.L.Unlock()
}
return nil
}
func (s *Server) getQueueCandidates(d *commandDetails) []*Hook {
candidates := make(map[*Hook]bool)
// add the hooks with "outside" detection
for _, hook := range s.hooksOut {
if hook.Key == d.key {
candidates[hook] = true
}
}
// look for candidates that might "cross" geofences
if d.oldObj != nil && d.obj != nil && s.hookCross.Len() > 0 {
r1, r2 := d.oldObj.Rect(), d.obj.Rect()
s.hookCross.Search(
[2]float64{
math.Min(r1.Min.X, r2.Min.X),
math.Min(r1.Min.Y, r2.Min.Y),
},
[2]float64{
math.Max(r1.Max.X, r2.Max.X),
math.Max(r1.Max.Y, r2.Max.Y),
},
func(min, max [2]float64, value interface{}) bool {
hook := value.(*Hook)
if hook.Key == d.key {
candidates[hook] = true
}
return true
})
}
// look for candidates that overlap the old object
if d.oldObj != nil {
r1 := d.oldObj.Rect()
s.hookTree.Search(
[2]float64{r1.Min.X, r1.Min.Y},
[2]float64{r1.Max.X, r1.Max.Y},
func(min, max [2]float64, value interface{}) bool {
hook := value.(*Hook)
if hook.Key == d.key {
candidates[hook] = true
}
return true
})
}
// look for candidates that overlap the new object
if d.obj != nil {
r1 := d.obj.Rect()
s.hookTree.Search(
[2]float64{r1.Min.X, r1.Min.Y},
[2]float64{r1.Max.X, r1.Max.Y},
func(min, max [2]float64, value interface{}) bool {
hook := value.(*Hook)
if hook.Key == d.key {
candidates[hook] = true
}
return true
})
}
if len(candidates) == 0 {
return nil
}
// return the candidates as a slice
ret := make([]*Hook, 0, len(candidates))
for hook := range candidates {
ret = append(ret, hook)
}
return ret
}
func (s *Server) queueHooks(d *commandDetails) error {
// Create the slices that will store all messages and hooks
var cmsgs, wmsgs []string
var whooks []*Hook
// Compile a slice of potential hook recipients
candidates := s.getQueueCandidates(d)
for _, hook := range candidates {
// Calculate all matching fence messages for all candidates and append
// them to the appropriate message slice
msgs := FenceMatch(hook.Name, hook.ScanWriter, hook.Fence, hook.Metas, d)
if len(msgs) > 0 {
if hook.channel {
cmsgs = append(cmsgs, msgs...)
} else {
wmsgs = append(wmsgs, msgs...)
whooks = append(whooks, hook)
}
}
}
// Return nil if there are no messages to be sent
if len(cmsgs)+len(wmsgs) == 0 {
return nil
}
// Sort both message channel and webhook message slices
if len(cmsgs) > 1 {
sortMsgs(cmsgs)
}
if len(wmsgs) > 1 {
sortMsgs(wmsgs)
}
// Publish all channel messages if any exist
if len(cmsgs) > 0 {
for _, m := range cmsgs {
s.Publish(gjson.Get(m, "hook").String(), m)
}
}
// Queue the webhook messages in the buntdb database
err := s.qdb.Update(func(tx *buntdb.Tx) error {
for _, msg := range wmsgs {
s.qidx++ // increment the log id
key := hookLogPrefix + uint64ToString(s.qidx)
_, _, err := tx.Set(key, msg, hookLogSetDefaults)
if err != nil {
return err
}
log.Debugf("queued hook: %d", s.qidx)
}
_, _, err := tx.Set("hook:idx", uint64ToString(s.qidx), nil)
if err != nil {
return err
}
return nil
})
if err != nil {
return err
}
// all the messages have been queued.
// notify the hooks
for _, hook := range whooks {
hook.Signal()
}
return nil
}
// sortMsgs sorts passed notification messages by their detect and hook fields
func sortMsgs(msgs []string) {
sort.SliceStable(msgs, func(i, j int) bool {
detectI := msgDetectCode(gjson.Get(msgs[i], "detect").String())
detectJ := msgDetectCode(gjson.Get(msgs[j], "detect").String())
if detectI < detectJ {
return true
}
if detectI > detectJ {
return false
}
hookI := gjson.Get(msgs[i], "hook").String()
hookJ := gjson.Get(msgs[j], "hook").String()
return hookI < hookJ
})
}
// msgDetectCode returns a weight value for the passed detect value
func msgDetectCode(detect string) int {
switch detect {
case "exit":
return 1
case "outside":
return 2
case "enter":
return 3
case "inside":
return 4
default:
return 0
}
}
// Converts string to an integer
func stringToUint64(s string) uint64 {
n, _ := strconv.ParseUint(s, 10, 64)
return n
}
// Converts a uint to a string
func uint64ToString(u uint64) string {
s := strings.Repeat("0", 20) + strconv.FormatUint(u, 10)
return s[len(s)-20:]
}
type liveAOFSwitches struct {
pos int64
}
func (s liveAOFSwitches) Error() string {
return goingLive
}
func (s *Server) cmdAOFMD5(msg *Message) (res resp.Value, err error) {
start := time.Now()
vs := msg.Args[1:]
var ok bool
var spos, ssize string
if vs, spos, ok = tokenval(vs); !ok || spos == "" {
return NOMessage, errInvalidNumberOfArguments
}
if vs, ssize, ok = tokenval(vs); !ok || ssize == "" {
return NOMessage, errInvalidNumberOfArguments
}
if len(vs) != 0 {
return NOMessage, errInvalidNumberOfArguments
}
pos, err := strconv.ParseInt(spos, 10, 64)
if err != nil || pos < 0 {
return NOMessage, errInvalidArgument(spos)
}
size, err := strconv.ParseInt(ssize, 10, 64)
if err != nil || size < 0 {
return NOMessage, errInvalidArgument(ssize)
}
sum, err := s.checksum(pos, size)
if err != nil {
return NOMessage, err
}
switch msg.OutputType {
case JSON:
res = resp.StringValue(
fmt.Sprintf(`{"ok":true,"md5":"%s","elapsed":"%s"}`, sum, time.Now().Sub(start)))
case RESP:
res = resp.SimpleStringValue(sum)
}
return res, nil
}
func (s *Server) cmdAOF(msg *Message) (res resp.Value, err error) {
if s.aof == nil {
return NOMessage, errors.New("aof disabled")
}
vs := msg.Args[1:]
var ok bool
var spos string
if vs, spos, ok = tokenval(vs); !ok || spos == "" {
return NOMessage, errInvalidNumberOfArguments
}
if len(vs) != 0 {
return NOMessage, errInvalidNumberOfArguments
}
pos, err := strconv.ParseInt(spos, 10, 64)
if err != nil || pos < 0 {
return NOMessage, errInvalidArgument(spos)
}
f, err := os.Open(s.aof.Name())
if err != nil {
return NOMessage, err
}
defer f.Close()
n, err := f.Seek(0, 2)
if err != nil {
return NOMessage, err
}
if n < pos {
return NOMessage, errors.New("pos is too big, must be less that the aof_size of leader")
}
var ls liveAOFSwitches
ls.pos = pos
return NOMessage, ls
}
func (s *Server) liveAOF(pos int64, conn net.Conn, rd *PipelineReader, msg *Message) error {
s.mu.Lock()
s.aofconnM[conn] = true
s.mu.Unlock()
defer func() {
s.mu.Lock()
delete(s.aofconnM, conn)
s.mu.Unlock()
conn.Close()
}()
if _, err := conn.Write([]byte("+OK\r\n")); err != nil {
return err
}
s.mu.RLock()
f, err := os.Open(s.aof.Name())
s.mu.RUnlock()
if err != nil {
return err
}
defer f.Close()
if _, err := f.Seek(pos, 0); err != nil {
return err
}
cond := sync.NewCond(&sync.Mutex{})
var mustQuit bool
go func() {
defer func() {
cond.L.Lock()
mustQuit = true
cond.Broadcast()
cond.L.Unlock()
}()
for {
vs, err := rd.ReadMessages()
if err != nil {
if err != io.EOF {
log.Error(err)
}
return
}
for _, v := range vs {
switch v.Command() {
default:
log.Error("received a live command that was not QUIT")
return
case "quit", "":
return
}
}
}
}()
go func() {
defer func() {
cond.L.Lock()
mustQuit = true
cond.Broadcast()
cond.L.Unlock()
}()
err := func() error {
_, err := io.Copy(conn, f)
if err != nil {
return err
}
b := make([]byte, 4096)
// The reader needs to be OK with the eof not
for {
n, err := f.Read(b)
if err != io.EOF && n > 0 {
if err != nil {
return err
}
if _, err := conn.Write(b[:n]); err != nil {
return err
}
continue
}
s.fcond.L.Lock()
s.fcond.Wait()
s.fcond.L.Unlock()
}
}()
if err != nil {
if !strings.Contains(err.Error(), "use of closed network connection") &&
!strings.Contains(err.Error(), "bad file descriptor") {
log.Error(err)
}
return
}
}()
for {
cond.L.Lock()
if mustQuit {
cond.L.Unlock()
return nil
}
cond.Wait()
cond.L.Unlock()
}
}