From 8f3189c1b3b38e551f9404abd8252d4280b6bf41 Mon Sep 17 00:00:00 2001 From: Ben Johnson Date: Thu, 21 Jun 2018 21:09:06 -0600 Subject: [PATCH 1/8] Translation fixes, error checking. --- Gopkg.lock | 6 -- executor.go | 19 ++++++ executor_test.go | 147 +++++++++++++++++++++++++++++++++++++++-------- server.go | 10 ++-- statik/statik.go | 10 ---- 5 files changed, 147 insertions(+), 45 deletions(-) delete mode 100644 statik/statik.go diff --git a/Gopkg.lock b/Gopkg.lock index b3fc9be2b..d3a12ef8a 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -197,12 +197,6 @@ revision = "645ef00459ed84a119197bfb8d8205042c6df63d" version = "v0.8.0" -[[projects]] - name = "github.com/rakyll/statik" - packages = ["fs"] - revision = "fd36b3595eb2ec8da4b8153b107f7ea08504899d" - version = "v0.1.1" - [[projects]] name = "github.com/satori/go.uuid" packages = ["."] diff --git a/executor.go b/executor.go index c4abe0bd0..e59802851 100644 --- a/executor.go +++ b/executor.go @@ -1593,6 +1593,9 @@ func (e *Executor) mapperLocal(ctx context.Context, slices []uint64, mapFn mapFu func (e *Executor) translateCall(index string, idx *Index, c *pql.Call) error { // Translate column key. if idx.Keys() { + if c.Args["col"] != nil && !isString(c.Args["col"]) { + return errors.New("'col' value must be a string when index 'keys' option enabled") + } if value := callArgString(c, "col"); value != "" { ids, err := e.TranslateStore.TranslateColumnsToUint64(index, []string{value}) if err != nil { @@ -1600,12 +1603,19 @@ func (e *Executor) translateCall(index string, idx *Index, c *pql.Call) error { } c.Args["col"] = ids[0] } + } else { + if isString(c.Args["col"]) { + return errors.New("string 'col' value not allowed unless index 'keys' option enabled") + } } // Translate row key, if field is specified & key exists. if fieldName := callArgString(c, "field"); fieldName != "" { field := idx.Field(fieldName) if field.Keys() { + if c.Args["row"] != nil && !isString(c.Args["row"]) { + return errors.New("'row' value must be a string when field 'keys' option enabled") + } if value := callArgString(c, "row"); value != "" { ids, err := e.TranslateStore.TranslateRowsToUint64(index, fieldName, []string{value}) if err != nil { @@ -1613,6 +1623,10 @@ func (e *Executor) translateCall(index string, idx *Index, c *pql.Call) error { } c.Args["row"] = ids[0] } + } else { + if isString(c.Args["row"]) { + return errors.New("string 'row' value not allowed unless field 'keys' option enabled") + } } } @@ -1782,3 +1796,8 @@ func callArgString(call *pql.Call, key string) string { s, _ := value.(string) return s } + +func isString(v interface{}) bool { + _, ok := v.(string) + return ok +} diff --git a/executor_test.go b/executor_test.go index 164133ecb..d0816b35e 100644 --- a/executor_test.go +++ b/executor_test.go @@ -265,35 +265,132 @@ func TestExecutor_Execute_Count(t *testing.T) { // Ensure a set query can be executed. func TestExecutor_Execute_SetBit(t *testing.T) { - hldr := test.MustOpenHolder() - defer hldr.Close() + t.Run("ID", func(t *testing.T) { + t.Run("OK", func(t *testing.T) { + hldr := test.MustOpenHolder() + defer hldr.Close() - // set a bit so the view gets created. - hldr.SetBit("i", "f", 1, 0) + // set a bit so the view gets created. + hldr.SetBit("i", "f", 1, 0) - e := test.NewExecutor(hldr.Holder, pilosa.NewTestCluster(1)) - if n := hldr.Row("i", "f", 11).Count(); n != 0 { - t.Fatalf("unexpected bitmap count: %d", n) - } + e := test.NewExecutor(hldr.Holder, pilosa.NewTestCluster(1)) + if n := hldr.Row("i", "f", 11).Count(); n != 0 { + t.Fatalf("unexpected bitmap count: %d", n) + } - if res, err := e.Execute(context.Background(), "i", test.MustParse(`SetBit(row=11, field=f, col=1)`), nil, nil); err != nil { - t.Fatal(err) - } else { - if !res[0].(bool) { - t.Fatalf("expected column changed") - } - } + if res, err := e.Execute(context.Background(), "i", test.MustParse(`SetBit(row=11, field=f, col=1)`), nil, nil); err != nil { + t.Fatal(err) + } else { + if !res[0].(bool) { + t.Fatalf("expected column changed") + } + } - if n := hldr.Row("i", "f", 11).Count(); n != 1 { - t.Fatalf("unexpected bitmap count: %d", n) - } - if res, err := e.Execute(context.Background(), "i", test.MustParse(`SetBit(row=11, field=f, col=1)`), nil, nil); err != nil { - t.Fatal(err) - } else { - if res[0].(bool) { - t.Fatalf("expected column unchanged") - } - } + if n := hldr.Row("i", "f", 11).Count(); n != 1 { + t.Fatalf("unexpected bitmap count: %d", n) + } + if res, err := e.Execute(context.Background(), "i", test.MustParse(`SetBit(row=11, field=f, col=1)`), nil, nil); err != nil { + t.Fatal(err) + } else { + if res[0].(bool) { + t.Fatalf("expected column unchanged") + } + } + }) + + t.Run("ErrInvalidColValueType", func(t *testing.T) { + hldr := test.MustOpenHolder() + defer hldr.Close() + index := hldr.MustCreateIndexIfNotExists("i", pilosa.IndexOptions{}) + if _, err := index.CreateField("f", pilosa.FieldOptions{}); err != nil { + t.Fatal(err) + } + + e := test.NewExecutor(hldr.Holder, test.NewCluster(1)) + if _, err := e.Execute(context.Background(), "i", test.MustParse(`SetBit(field=f, row=1, col="foo")`), nil, nil); err == nil || err.Error() != `string 'col' value not allowed unless index 'keys' option enabled` { + t.Fatal(err) + } + }) + + t.Run("ErrInvalidRowValueType", func(t *testing.T) { + hldr := test.MustOpenHolder() + defer hldr.Close() + index := hldr.MustCreateIndexIfNotExists("i", pilosa.IndexOptions{}) + if _, err := index.CreateField("f", pilosa.FieldOptions{}); err != nil { + t.Fatal(err) + } + + e := test.NewExecutor(hldr.Holder, test.NewCluster(1)) + if _, err := e.Execute(context.Background(), "i", test.MustParse(`SetBit(field=f, row="bar", col=2)`), nil, nil); err == nil || err.Error() != `string 'row' value not allowed unless field 'keys' option enabled` { + t.Fatal(err) + } + }) + }) + + t.Run("Keys", func(t *testing.T) { + t.Run("OK", func(t *testing.T) { + hldr := test.MustOpenHolder() + defer hldr.Close() + hldr.MustCreateIndexIfNotExists("i", pilosa.IndexOptions{Keys: true}) + + // set a bit so the view gets created. + hldr.SetBit("i", "f", 1, 0) + + e := test.NewExecutor(hldr.Holder, pilosa.NewTestCluster(1)) + if n := hldr.Row("i", "f", 11).Count(); n != 0 { + t.Fatalf("unexpected bitmap count: %d", n) + } + + if res, err := e.Execute(context.Background(), "i", test.MustParse(`SetBit(row=11, field=f, col="foo")`), nil, nil); err != nil { + t.Fatal(err) + } else { + if !res[0].(bool) { + t.Fatalf("expected column changed") + } + } + + if n := hldr.Row("i", "f", 11).Count(); n != 1 { + t.Fatalf("unexpected bitmap count: %d", n) + } + if res, err := e.Execute(context.Background(), "i", test.MustParse(`SetBit(row=11, field=f, col="foo")`), nil, nil); err != nil { + t.Fatal(err) + } else { + if res[0].(bool) { + t.Fatalf("expected column unchanged") + } + } + }) + + t.Run("ErrInvalidColValueType", func(t *testing.T) { + hldr := test.MustOpenHolder() + defer hldr.Close() + index := hldr.MustCreateIndexIfNotExists("i", pilosa.IndexOptions{Keys: true}) + if _, err := index.CreateField("f", pilosa.FieldOptions{}); err != nil { + t.Fatal(err) + } + + e := test.NewExecutor(hldr.Holder, test.NewCluster(1)) + + if _, err := e.Execute(context.Background(), "i", test.MustParse(`SetBit(field=f, row=1, col=2)`), nil, nil); err == nil || err.Error() != `'col' value must be a string when index 'keys' option enabled` { + t.Fatal(err) + } + }) + + t.Run("ErrInvalidRowValueType", func(t *testing.T) { + hldr := test.MustOpenHolder() + defer hldr.Close() + index := hldr.MustCreateIndexIfNotExists("i", pilosa.IndexOptions{}) + if _, err := index.CreateField("f", pilosa.FieldOptions{Keys: true}); err != nil { + t.Fatal(err) + } + + e := test.NewExecutor(hldr.Holder, test.NewCluster(1)) + + if _, err := e.Execute(context.Background(), "i", test.MustParse(`SetBit(field=f, row=1, col=2)`), nil, nil); err == nil || err.Error() != `'row' value must be a string when field 'keys' option enabled` { + t.Fatal(err) + } + }) + }) } // Ensure a SetValue() query can be executed. diff --git a/server.go b/server.go index b6c4e7996..93aaecc9e 100644 --- a/server.go +++ b/server.go @@ -258,11 +258,8 @@ func NewServer(opts ...ServerOption) (*Server, error) { // Initialize translation database. s.translateFile = NewTranslateFile() - s.translateFile.Path = filepath.Join(path, "keys") + s.translateFile.Path = filepath.Join(path, ".keys") s.translateFile.PrimaryTranslateStore = s.primaryTranslateStore - if err := s.translateFile.Open(); err != nil { - return nil, err - } // update URI port with actual listener port. TODO this should probably be done outside of here. if s.URI.Port() == 0 { @@ -312,6 +309,11 @@ func (s *Server) Open() error { log.Println(errors.Wrap(err, "logging startup")) } + // Initialize id-key storage. + if err := s.translateFile.Open(); err != nil { + return err + } + // Cluster settings. s.Cluster.Broadcaster = s s.Cluster.MaxWritesPerRequest = s.maxWritesPerRequest diff --git a/statik/statik.go b/statik/statik.go deleted file mode 100644 index 54ef98b8f..000000000 --- a/statik/statik.go +++ /dev/null @@ -1,10 +0,0 @@ -package statik - -import ( - "github.com/rakyll/statik/fs" -) - -func init() { - data := "PK\x03\x04\x14\x00\x08\x00\x00\x00\xc1n\xa3J\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x17\x00\x00\x00assets/chevron-down.png\x89PNG\x0d\n\x1a\n\x00\x00\x00\x0dIHDR\x00\x00\x00\xc8\x00\x00\x00\xc8\x08\x06\x00\x00\x00\xadX\xae\x9e\x00\x00\x0e\x0eIDATx\xda\xed\xddy\x90\x14\xd5\x1d\x07\xf0\xc7\xce\xd5=}\xce\xec\x1c;\xb33\xb3;3{\xc0\x9e\xec\x01\xcb.\xbb\xec1\xbb\xa8A\xa3$h\xc5#\x1e \xb95\xa5Dc*\x95C<\"\xa5\xd1T\x02\xc6Jb\x89\xe6\x1f\xe3\x91hb\"\xa8\x89g\x8c\xa6\"\xc6\x8ax!`\x8c ^ \x88r\xaf\xc9\xef\x07\xa31D\x84\x85\xdd\x9973\xdfOUWQ\xcbL\xf7{\xef\xf7\xeb\xee7\xef\xf5!\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe4\x8d\x93\x16\xc3\x8cu\xfby\xa1\x7f\xeb\xb48\xd0,p\x10\x8e\xfd\xf2\xc6\xc8\xe6RQ\xd1\\V\xbc\xd3\xa1X\x0b\xed\x9a\xcc\xb7yq(\xf6\x99n_\xba\x89\xfe\xcf\x83\x1c\x80\x03\xf0p\x8eP\xae\xcc\x7f?o\xca<\xd6\xe78\x978\xa7\x8a\xa5\x92\x15j\xb0a\x9eQ9m\xb9Z^\xb7\xdeW;\xf2o^\xd4@\xfdsf|\xc62oE\xdb1\xf4\x19\x1fr\x01\xf6\xe3\xe3\xdc\xa0\x1c\xb9\x96r\xe5\x85\x0f\xf2\xa6\xbc\xee\xa5\xbd\xb9D9\xc5\xb9U\xe8\x95\xacR}\xa9\xaf\x1bU=\xcf\xbc_\xc1\xfd\x17\xb3\xba\xef/\xde\xc0\xe4\x85\xf4\xd90r\x02\xb2\xc2\x9c\x13\x9c\x1b\x07\xca\x1b\xce)\xd5\x97\xbe\x90s\xac\x10+\xa8*v\xb2\xca\xa1\x96/;P\x05\xf7_\x9cZ\xf8\"::T\xa2\xcbU\xda]*\xce\x01\xce\x85C\xcd\x1b\xca\xb1\xa5\x8a/\x95\xa0\xef*\x85RIEx\xb4\xd9\xde\xd0\x94{\x0f\xb5\x92\xef/\xde\x8a\xe6[\x85jt\xd3:\xdc\xc8\x95\x92\xe3\xa6\xd8\xf7p\x0e\x8c9o(\xd7(\xe7F\ne'9\xdf\xa8\x9a\xf9\x8a]32:\xd6\x8a\xf2w\xcc\xea\xde\xf5\xb4\x8e3\x91/%\xe7,\x8e\xfd\xe1\xe6\x0d\xe7\x1c\xad\xe3<\x99+\x18sx\xfd\xd7\xd9\xe9\xc1\xcdv\xcd\xf0\x9e\xb1V\xf2\xbf\x95\x1d\xdeC\xeb\xd8\xe4\xd4\x02W\xd2:#\xc8\x9b\xa2\x17\xa1X_\xc51\x1f\x87\xbc\xd9\xcc9\xc8\xb9([%\x8f\xd3\xc2\xcd\xbf\xb1\x92\xb36\x1dn\x05\xf7_\xacd\xff\x9bz\xb8\xf5\x97\xb4\xee\xa3\x91CE\xebh\x8e1\xc7z\xfc\xf2f\xd6&\xceEZ\xf7\xb12T\xb0\x8c\x96\x0b\x8d\xea\x9e\xa7\xecd\xff\xd6\xf1\xaa\xe4\x07G\x85T\xff\xdbf\xf5\xcc'\xb9\xdb\x96\xdd\x16\x14\x07\x8e\xe5\"\x8e-\xc7x\xdc\xf3\x86r\x91s\x92\xb6qA\xbe\xf2f\x12-\x8dn\xbb\xfab\xbd\xb2}-\x9d\xde\xde\x1b\xefJ~\xe8\xd4\xf9\x1emc\x0do\x8b\xb6\xd9\x94\xdd6\x14&\x8e]s6o^\xccA\xde\xac\xcd\xe6Mc.\xf3\x86\xf7\xc8\x11\xad\xa2e\xb9\x11\x9f\xbea\xa2*\xf8\x7f\xe3\xde\xb4-\xda\xe6\x8d\xb4\xed\x13\x04.Q)D|y\xc8\\\x8a\xe1My\xc8\x9b\xe5\x9c\xb3\xb9:\x9b\xd4z\xc3M+\xa9\xdf\xf8N\xae*\xf9\xa1\xfe\xe5\xbbj\xa8\xe1a\xe12\xe6S9\xca\x91s\x05\xa3\x9ccF\xb1{\x84c\x98\xfb\xbc\xe9\x7f\xc7[\xd1\xb4\x82\xca\x91\xceEeO\xceu\x05\xf7_\x94@\xed:\x8f]\xcdCz\xad\xa2\x08/\\+\xb2\xb3F+\xc7\x8ac\x96\xef\xbc\xa1\xb2\x9c\x94\x8bJ\x9f\x91\xef\x8a\xf2\xa2G\xdb7\xba\xcd\x04\x9f:3\x02\xb3\xef2\xe2\x98\x0cs\x8c8V2\xe4\x0c\x95\xe7\xb4\\T\xfdR*[\x1dr\xfb\x88\xd5s[r\x9b\xca\x12_=\xd6\xf9\x00\xe7\x1c\xe7^\xc1\x1ca<\xbe\xf41z\xb4\xfdf:\xfdJ1\x14l\xa7\x07\xff\xa9'\xba~.\x9cJ\xaf\xc0\xec\xfb\xe1\xf5\x10\xa8\xed\xb8\x0d\xb9-%\xe9Fo\xe7\x1c\xe3\\\x93\xb1Ku(\xda\x8dD\xcfO\xadT\xff\x1aY\x8e6\xdeH\xcb\x1djp\xf2\xa7\x05n\xc6\x1a\x8b\x08\xb7\x19\xb7\x9d4\xbd\x82T\xff\x0b\x9c[\x9cc\x85\xde\xb8 \xad\xb2c\x91Y\xdd\xfb'Y\x1a\xd7\x8cM_\xa5\x84\x1b.\xe2\xee\x02r\xff\xa0&s[q\x9bI\x13?\xca%\xce)\xce\xadbid\x8f\xd3\xe3\x9fm\xa5\x06\x1e\xb3k2[\xe48=\x0fn\xf1X\x89\x1f\xbb\xb50\xff\xa8\xf3b?\xf8?^n\x1bj\xa3\xeb\xb8\xad\xe4\x18t\xc9l\xe1\x1c\xe2\\\x12Ez\xfd]\xbd\x91\xec\xbb\xc5J\x0fm\x90\xe5h\xa4\xf8k\xeeq\x9a\x91\xa3\xa8l~\xec\x13\x1f\xf0s\x9b(\xfe\xf4\xbd\x12\xcdmm\xe0\xdc)\x85\x81\x16\x9f\x1an9\x97\x8e\x06odg\xbf\xf3>\xebj\xc4\xa7\xaf+s\x1b\x17e\x87\x82K\xf9\x8eE\xae\xbb\x8f\xdb\x82\xdbD\x92\xab#\xf8\x92\x917\xd4p\xeb9\xa56T?\x85*\xbe\xd5JgFe\xd8I\xf6^\x0e=\xc9\xc9\x0f\x890Kx\x071\xa9\x0d~%\xc9\x1c\xd6{Vjh\x94s\x84\x7f\x07\x95b0\x1c\x1e#Y\xef\xb1\x93?SC\x0d\xb2\xfc.\xd9A\xdd\x8a\x87\xa9l\xc7\x8b\xd2\xba*\x98\xebz<\xd5\xfd\x11n\x03\x19b\xc19\xe11\xa2?\xe1\x1c\x11%|\x85\xb6C\xa8\xfe\xb8C\xf1\x9d\xea\xd4C\xaf\xc8ry\xb4\x11\xebz\x8e\xca\xb6\x84\x16\xb5\x04b\xc0u\\\xc2u\x96\xe5\xf6\x05\xce\x05\xca\x89S(7b\x02\xb7/\xec\xed\xf7Z\xc2\xe9\x99\xed\xd2\xc3\xbf7\xabfn\xcb\xff\xe9}h\xb7\x91\x98\xb1\x81\x82t\x13\x95\x8dG\xb9\x8a\xf1\xf2y\xaeS3\xd7\x91\xeb\xcau\xce\xfb\xf0-\xc5\x9er\xe0>\xca\x85\x91\xbd9\x81'\xd8\xfc\x0f~\x1eo\x1fu\xb9\xae\xf2\x86\x9b\xa4\x18\xe52b\xd3\xb6\xa8\x81\xfa;\xa9\\\xa7\x17\xd9P0\xd7\xe5t\xae\x1b\xd7Q\x8a \\\x8a9\xc7\x9es@\xe0V\x85\x8f=\xaa\xd5\xba\x8c\xd8\x02\xc5\x9fz^\x86\x1f\xeftT\xe3;\xd2VQ\xb9.+\x92\xa1`\xae\xc3e\\'\xae\x9b\x0c?\xc6)\xd6\xcfr\xcc9\xf6\x027\xbb\x1d\x12>\x82,\xd0\"\xad+(\x88\x9b\xa4\xf8\xd1X\x9e\xde\xa8\xf8R\x97;\xfd\xe9\x1eQ\x98\x93T\x8a\x12\xe9\xea\xe5:p]\xa4\x98\x11\xa7\xd8r\x8c\xa9l\xf3q\xd68\xbc\xb3IF\x0dMY\xaaWv\xbe,\xc5U\xa3\x95\x1d\xdb\xf4X\xe7\xedf\xa2\xfb\xf4l\x1f\xb9PX\xf4;\xe3\x0c39\xeb\x0e\xae\x83\x1cm\xd9\xf92\xc7\x96\xca6\x84\xb3\xc6\x91\xa9t*\xfe\xefP\x80\x9f\xb1\xd2C;$\x18\xe5\x1a\xb5\xd3C/\xe9\xd1\xf6/ 5\x1a\x97|\x94\xc5\xc1e\xe4\xb2r\x99%\x99\x11\xdf\xc1\xb1\xa4\x98~K\xe0\x8e\xcfq\x1d\xe9\x9aO\x0d\xfb\x90\x95\xec\x7fK\x9a\xfb\x10\xa2m\x17k\xc1\x86\xa9\x92\x0e\x07\xab\\6=\xd2\xb6X\x9a\xcbE(v\x1cC*\xdbY\x18\xa1\x9a\x18\xd3\x94\xf2\xba\xe5\xd4\xd0\x1b\xe5\xb9\xb2t\xe6\n\x8f?9[\xb2Q.\xaf\xc7_}\x14\x95m\xa5<;\xc7\xc0\x9b\x14\xbb\x1b8\x86H\xe3\x89\xee6\x08\xf1M_:\xf3\x8e];\xbc[\x8e\xcb\"Fv\n\xb7>O\x88j%\xcfG\xc6I{\xcb\xe0\xd6O\xe42I\xd16\x1c#\x8a\x15\x95\xed\x1b\x02\x93~9\xdcI\x1c\x8e\xe3\x8d\xf8\xf4?\xcbr\xdd\x10-\xbb\xd4P\xe3\xd5\"\x87O\xef\xfb\x08I5\xd8x\x0d\x97E\x96\xeb\xdb(F\x8fS\xac\x8e\xc3\xce\x91\x87aK\xbe_A-\xaf[&K7\xc2\xae\xc9\xbcmT\xf5\xf2\xb5\\#yh\x8f\xd9\xbcm.\x83,\xedA\xb1Y\xea\xd6\xc3\xfcN\x17\x05\xe9\x9a\xaf\xf9\x125V\xe9\xb1\x13_\xb5\xd2\x83\xafK\xb2\x93l\xb7R\x03k\xca\xbc\xe5_\x16\xb9\xb92\xd8\xe4m\xf16y\xdb\x92\\\xf4\xf9:\xc5\xe4\\\xe1\x8d\xf3(\x15\xee\xff\x97@P 5\x9f\xaa'\xbaWH\xf3\xe3\xbd\xaa\xf7E-\xdcr\x0d\x95\xadc\x02\xeb\xdd\xc9\xdb\xe0m\xc9Ro\xa3\xaa\xfbn\x8e\x05\xc7\x04i)\x17C\x89\xb4\xf6j\x91\xf6k$\x1a\xd6|\xd5H\xcc\xf8-\x95m\xde\x04\xd4w\x9e\x11\x9f\xf1;\xde\x864O4\x8c\xb4_\xcd1\xe0X \x1d\xa5\x9d/\xf1\xa4\x8dD\xcf\x12:\xcd\xaf\x91\xa4\xcb\xb5K\x8b\xb4>\xe62bgS\xf9\xe2\xe3P\xc78\xadk!\xaf\x93\x1f\xce,I\x97\x8a\x9f0\xb2\x84\xda>\x85\xf9\x8d\x02\xe9r\xe9\x89\x19\xe7\xd0\xd1u\xa5<\x93\x8a\xed\xcf{\x83\x0d|\xc1c\xf7a&\x11\x7f\xa7\x9b\xd6q9\xad\xeb\x05\x89\xce\x92+\xf5\xaa\xee\xaf\x08\x89\x9e\x83\x0b\x878\xca\xa5T4\x0eZ\xa9\xfe\xdb\xcdD\xcf\xdbv:3*\xc1\xe3j6\xa9\xe5\xb5\xb7 \xa7\x87\xdf\xfd>\x96\x89E/\x7f\x87\xbf\xcb\xeb\xc8\xffC\xf82\xa3Fl\xda[Vj\xe06\xa5\xa2i\x00\xa3T\x85-\xa2\xf8jn\xd2+;\x9e\xa5\xe4\x92b\xf2\xccm'V\xb9\xed\xd8\xdcl\x97k\xd2A\xce\x1aq\xfe,}\xe7II\x9eI\xb5\x93\xdb\xd2\xa5\x85\xaf\xa7\xb2\x85\x91^\xc5\x81\xdf\x11\xb1P\xf1\xa7\x1f5\xaaz\xa4\x18\n\xf5V4\xff\xc3\xa9\x07/\xa1rM\x11\x1f=\x89\xc6\x7f\x9bB\x9f\xb9\x94?+\xc7\x08U\xcf6j\xc3?R\xb9\x16d\xdb\x14\x8a\x08_R]/\x1c\xca\x83vr`\xab\x0c].\xea\xfe\xed\x98\xe4R\xff N\x97Z\xd1z\x85\x11\xefZ]*;\x07\xd5\xf5i5\xd2\xfa=\x81\xdba\xe1\x10\xc5\x14\x7f\xf2l\xbd\xb2\x83\xdf\xe2\xba\xa7Xw\x0c\xae\x9b\x1e\xebxH\xf1\xa7\xf8\xb9T1\x84\x1d\xc6j\x8eY5\xf3\x1e+5\xf0j\xd1u\xa9\xa8N\xfcP\x07\xaa\xe31\x083\x1c\x89\x16o\xb4\xe3\x87\xd4G_[<;\xc7\xac\x17\xb5\xca\x0e\x9e\xf8kFxa<\x98.#r\xb6\x9d\x1a\\O\xdd\x92\x1d\x05\xdc\xa5\xda\xc1up\x19Q\xeeR\x99\x08+\x8c\xb7\x99fr\xd6\xe3\xbe\x9a\xcc\xb6\x82\xdbA\xa8\xcc\\v\xb1\xef~\x14\x80 \x93\xd0\"m\xdf/\xb8\xc9\xbfH\x1b\xbfZ \x8e\xf0\xc1D+\x13\x81z\xc3\xe1\x0d|\xb2Pv\x0e*\xeb\x1c.\xb3\xc0C\xa2!\x87\xbcZ\xb0\xe1h\xfa\xf1~\x9f\xbc\x93\x7f\xfd\xf7j\xa1&\xd9\x1e\x8b\n%D\xf1VL\x9b\xa6\xc7g\\\xa2E;\xb6\xcb\xb2cx\xc3M[\xf5x\xf7bodz\xa7(\xccw\x99@\x11q(\xbe\x86\x84\xc7W\xfb\x05\xb7\x9dXMg\x94]y\xbc\\d\x97\xdb\x8e\xff\x8d\x9fz\xa2\xfa\x9bd\x7f5\x03\x94\x18\xbe\x99\xe8\x145P\xf3\x0b=\xda\x96\xf3\xf7\xfe\xe9\x95m\x9bi\xdb7S\x19>C\x8b\x86p\x80\x8c\xf8E\xa4\xddn+~\xb9\x1a\x9c\x92\xb3\xd9w\xda\xd6\x06\xda\xe6\xa5\xb4\xed.\x81W\x99A\x01\xf0 Q\xb6X\xaf\xec|\xd2\xac\xee\x9b\xb09\x13\xab\xba\xef]\xda\xc6*\xda\xd6wi\x9b6\x9a\x1d\n\xedl\xf2Yo\xb0\xf1\xd7f\xa2{\xdc\xdf\x8aE\xeb\xdc\xe4\x0d5\xf2+\xaaO\xcbn\x0b\xa0\xe0\xf0\xbcC\xa3K\x0b\xde`V\xf5\xbcj\xa73{\xc6\xe1 #{x]\xb4N~.U\x83\xc0\xdc\x06\x14\x01\xfe]p\x99Y5s\xed\x91\xbc\x88\x94\xbe\xbb\x9d\xd7A\xeb\xe2\xdba\xf1Z\x01(*|o\xf7\xb1j\xb0\x9e_r\xb3k\xec\x17\x1afv\xa9\xc1\xc9\x0f\xd2:\xe6\xa0)\xa1X9\x84\xd0\x83en\xed\xc6\xb1\xee \xf4\x9d\xeb\xf9\xbb\x02s\x1bP\x02g\x92r\xa7\xea_d$\xba\x0f\xfa\xe2\x1b\xfa\xcc\x1a\x97\xea?\x8f\xbf#\xf0\x84\x11(!\x01\x8f\xafj\x8e7\xd8p\xd7\x01/\x19 5\xdeI\x9f\xf9Dv\xe7\x00(9\x9aK\xabl\xd5BM\x8b= 0) {\n output_string += `
\n
\n Just getting started? Try this:
\n :create index test
\n :use test
\n :create frame foo
\n SetBit(rowID=0, columnID=0, frame=foo) # Use PQL to set a bit\n `\n }\n }\n }\n }\n\n\n var markup =`\n
\n
\n
\n
\n
Input
\n       \n Source: ${res.indexname}\n
\n
\n ${res.input}\n
\n
\n
\n
\n
output
\n       \n ${res.querytime_ms} ms\n
\n
\n ${output_string}\n
\n
Expand
\n \n
\n
\n
\n \n
\n
\n `\n node.innerHTML = markup;\n this.output.insertBefore(node, this.output.firstChild);\n\n // Expand when overflow\n var element = this.output.firstChild.getElementsByClassName(result_class)[0];\n var expand = this.output.firstChild.getElementsByClassName(\"expand\")[0];\n if (element.clientHeight < element.scrollHeight) {\n expand.style.display = 'block';\n } else {\n expand.style.display = 'none';\n }\n expand.onclick = function () {\n element.style.height = element.scrollHeight + \"px\";\n expand.style.display = 'none';\n return false;\n };\n }\n\n populate_index_dropdown() {\n var xhr = new XMLHttpRequest();\n xhr.open('GET', '/schema')\n var select = document.getElementById('index-dropdown')\n\n xhr.onload = function() {\n var schema = JSON.parse(xhr.responseText)\n for(var i=0; i 0) {\n select.value = 1;\n }\n }\n xhr.send(null)\n }\n\n}\n\nfunction populate_version() {\n var xhr = new XMLHttpRequest();\n xhr.open('GET', '/version')\n var node = document.getElementById('server-version')\n\n xhr.onload = function() {\n var version = JSON.parse(xhr.responseText)['version']\n var version_major_minor = /(v\\d+\\.\\d+)/.exec(version)[0]\n var doc_link = document.getElementById('nav-documentation')\n doc_link.onclick = function() {\n window.open('https://www.pilosa.com/docs/' + version_major_minor + '/introduction/')\n }\n node.innerHTML = version\n }\n xhr.send(null)\n}\n\nfunction handle_nav_click(e) {\n // e.id = \"nav-xxx\"\n name = e.id.substring(4)\n set_active_pane_by_name(name)\n window.location.hash = name\n}\n\nfunction set_active_pane_by_name(name) {\n // toggle the nav buttons\n document.getElementsByClassName(\"nav-active\")[0].classList.remove(\"nav-active\")\n document.getElementById(\"nav-\" + name).classList.add(\"nav-active\")\n\n // toggle the main interface content divs\n document.getElementsByClassName(\"interface-active\")[0].classList.remove(\"interface-active\")\n document.getElementById('interface-' + name).classList.add(\"interface-active\")\n\n // hack hack\n switch(name) {\n case \"cluster\":\n update_cluster_status()\n break\n case \"documentation\":\n open_external_docs()\n break\n }\n}\n\n\nfunction update_cluster_status() {\n var xhr = new XMLHttpRequest();\n xhr.open('GET', '/status')\n status_node = document.getElementById('status')\n xhr.onload = function() {\n var status = JSON.parse(xhr.responseText)\n render_status(status)\n }\n xhr.send(null)\n}\n\nfunction render_status(status) {\n // render node table\n var nodes_div = document.getElementById(\"status-nodes\")\n while (nodes_div.firstChild) {\n nodes_div.removeChild(nodes_div.firstChild);\n }\n\n var nodes = status[\"status\"][\"Nodes\"]\n table = document.createElement(\"table\")\n tbody = document.createElement(\"tbody\")\n table.appendChild(tbody)\n var caption = document.createElement(\"caption\")\n caption.innerHTML = \"(\" + nodes.length + \")\"\n table.appendChild(caption)\n\n var header = document.createElement('tr')\n markup = `Host\n State`\n header.innerHTML = markup\n tbody.appendChild(header)\n for(var n=0; n${nodes[n][\"Host\"]}\n ${nodes[n][\"State\"]}`\n row.innerHTML = markup\n tbody.appendChild(row)\n }\n nodes_div.appendChild(table)\n\n // render index tables\n var indexes_div = document.getElementById(\"status-indexes\")\n while (indexes_div.firstChild) {\n indexes_div.removeChild(indexes_div.firstChild);\n }\n\n var indexes = nodes[0][\"Indexes\"] // TODO currently comes from only node 0\n for(var n=0; nName\n Cache Type\n Cache Size`\n header.innerHTML = markup\n tbody.appendChild(header)\n\n var frames = indexes[n][\"Frames\"]\n if(frames) {\n for(var m=0; m${frames[m][\"Name\"]}\n ${frames[m][\"Meta\"][\"CacheType\"]}\n ${frames[m][\"Meta\"][\"CacheSize\"]}`\n tbody.appendChild(row)\n }\n }\n indexes_div.appendChild(table)\n }\n\n // render slice tables\n // TODO enable when Slices element is present in status response\n /*\n var slices_div = document.getElementById(\"status-slices\")\n data = \"\"\n for(var n=0; n\"\n }\n }\n slices_div.innerHTML = data\n */\n\n}\n\nfunction open_external_docs() {\n window.open(\"https://www.pilosa.com/docs\");\n}\n\nfunction check_anchor_uri() {\n var pane_names = {\"console\": 0, \"cluster\": 0, \"documentation\": 0}\n var anchor = window.location.hash.substr(1);\n if(anchor in pane_names) {\n set_active_pane_by_name(anchor)\n }\n}\n\nDate.prototype.today = function () {\n return this.getFullYear() +\"/\"+ (((this.getMonth()+1) < 10)?\"0\":\"\") + (this.getMonth()+1) +\"/\"+ ((this.getDate() < 10)?\"0\":\"\") + this.getDate();\n}\n\nDate.prototype.timeNow = function () {\n return ((this.getHours() < 10)?\"0\":\"\") + this.getHours() +\":\"+ ((this.getMinutes() < 10)?\"0\":\"\") + this.getMinutes() +\":\"+ ((this.getSeconds() < 10)?\"0\":\"\") + this.getSeconds();\n}\n\npopulate_version()\n\n\nclass Autocompleter {\n constructor(input, output) {\n this.input = input\n this.output = output\n this.keyword_map = this.static_keywords\n this.init_dynamic_keywords()\n }\n\n get static_keywords() {\n return {\n // keyword: length of substring that comes after cursor\n \"SetBit()\": 1,\n \"ClearBit()\": 1,\n \"SetRowAttrs()\": 1,\n \"SetColumnAttrs()\": 1,\n \"Bitmap()\": 1,\n \"Union()\": 1,\n \"Intersect()\": 1,\n \"Difference()\": 1,\n \"Count()\": 1,\n \"Range()\": 1,\n \"TopN()\": 1,\n \"frame=\": 0,\n }\n }\n\n complete() {\n var completer = this\n // extract word fragment ending at cursor. a word fragment:\n // - starts with last nonalpha character before cursor (or beginning of string)\n // - ends at cursor\n var word_start = completer.input.selectionEnd-1\n while(word_start>0) {\n var c = completer.input.value.charCodeAt(word_start)\n if(!((c>64 && c<91) || (c>96 && c<123))) {\n word_start++\n break\n }\n word_start--\n }\n var input_word = completer.input.value.substring(word_start, completer.input.selectionEnd)\n\n // check for keyword match and insert if exactly one match\n var matches = []\n for(var keyword in this.keyword_map) {\n if(keyword.startsWith(input_word)){\n matches.push(keyword)\n }\n }\n if(matches.length > 1) {\n // completer.output.innerHTML = whatever\n }\n\n if(matches.length == 1) {\n // completer.output.innerHTML = \"\"\n var cursor_pos = completer.input.selectionEnd\n var completion = matches[0].substring(input_word.length)\n var before = completer.input.value.substring(0, cursor_pos)\n var after = completer.input.value.substring(cursor_pos)\n completer.input.value = before + completion + after\n var new_pos = cursor_pos + completion.length - this.keyword_map[matches[0]]\n completer.input.setSelectionRange(new_pos, new_pos)\n }\n }\n\n init_dynamic_keywords() {\n // hit /schema, parse indexes, frames, rowlabels, columnlabels, add to list\n }\n\n add_keyword() {\n // call when index or frame created in webui\n }\n\n remove_keyword() {\n // call when index or frame deleted in webui\n // issue: if e.g. multiple indexes have same frame, removing one removes all.\n // solution: maintain count. requires more elaborate representation of keywords.\n }\n}\n\nvar input = document.getElementById('query')\nvar output = document.getElementById('outputs')\nvar button = document.getElementById('query-btn')\nvar autocomplete_output = document.getElementById('autocomplete-container')\n\nautocompleter = new Autocompleter(input, autocomplete_output)\nrepl = new REPL(input, output, button, autocompleter)\nrepl.populate_index_dropdown()\nrepl.bind_events()\n\ninput.focus()\n\ncheck_anchor_uri()\n\nfunction isJSON(str) {\n try {\n JSON.parse(str)\n } catch (e) {\n return false\n }\n return true\n}\n\nfunction parse_query(query, indexname) {\n var keys = query.replace(/\\s+/g, \" \").split(\" \");\n var command = keys[0];\n var command_type = keys[1];\n var command_name = keys[2];\n var option_str = keys.slice(3, keys.length)\n var options = parse_options(option_str);\n if (command !== \":use\") {\n if (!command_name){\n return {}\n }\n }\n\n var parsed_query = {};\n parsed_query[\"command\"] = command.substr(1, command.length);\n parsed_query[\"command_name\"] = command_name;\n switch (command) {\n case \":create\":\n parsed_query[\"request\"] = \"POST\";\n if(Object.keys(options).length === 0) {\n parsed_query[\"data\"] = \"\";\n } else {\n var opts = {\"options\":{}};\n for (var o in options) {\n opts.options[o] = options[o]\n }\n parsed_query[\"data\"] = JSON.stringify(opts);\n }\n switch (command_type){\n case \"index\":\n parsed_query[\"url\"] = '/index/' + command_name;\n break;\n case \"frame\":\n parsed_query[\"url\"] = '/index/' + indexname + '/frame/' + command_name;\n break\n }\n break;\n case \":delete\":\n parsed_query[\"request\"] = \"DELETE\";\n switch (command_type){\n case \"index\":\n parsed_query[\"url\"] = '/index/' + command_name;\n parsed_query[\"data\"] = \"\";\n break;\n case \"frame\":\n parsed_query[\"url\"] = '/index/' + indexname + '/frame/' + command_name;\n parsed_query[\"data\"] = \"\";\n break;\n }\n break;\n case \":use\":\n parsed_query[\"command_name\"] = keys[1];\n break;\n default:\n return {}\n }\n return parsed_query;\n}\n\nfunction parse_options(option_str) {\n var int_keys = [\"cacheSize\"];\n var bool_keys = [\"inverseEnabled\"];\n var options = {};\n for (var i = 0; i < option_str.length; i++) {\n var parts = option_str[i].split('=');\n if (int_keys.indexOf(parts[0]) !== -1 ){\n options[parts[0]] = Number(parts[1])\n } else if (bool_keys.indexOf(parts[0]) !== -1){\n options[parts[0]] = (parts[1] == \"true\")\n } else {\n options[parts[0]] = parts[1]\n }\n }\n return options;\n}PK\x07\x08\xfa\x8b=\x1a\xcaH\x00\x00\xcaH\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\xc1n\xa3J\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1d\x00\x00\x00assets/nav-cluster-active.svgnav_cluster_1\nPK\x07\x08\xc1J\xead \x02\x00\x00 \x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\xc1n\xa3J\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x16\x00\x00\x00assets/nav-cluster.svgnav_cluster_1PK\x07\x08\xc4\x07\xec\x0b\x05\x02\x00\x00\x05\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\xc1n\xa3J\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1d\x00\x00\x00assets/nav-console-active.svgnav_consolePK\x07\x08\xf2\x90\xe75\xa0\x01\x00\x00\xa0\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\xc1n\xa3J\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x16\x00\x00\x00assets/nav-console.svgnav_console\nPK\x07\x08\xfb\xc8\xea\xb0\x9e\x01\x00\x00\x9e\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\xc1n\xa3J\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00#\x00\x00\x00assets/nav-documentation-active.svgdocumentation\nPK\x07\x08\xe5\x95\x86\x82\xec\x01\x00\x00\xec\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\xc1n\xa3J\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00assets/nav-documentation.svgdocumentationPK\x07\x08\xe18\x81J\xe8\x01\x00\x00\xe8\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\xc1n\xa3J\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x14\x00\x00\x00assets/nav_item1.svgnav_item1PK\x07\x08+\xd4\xf31\xa2\x01\x00\x00\xa2\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\xa0~\xe6J\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00assets/style.css*{\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\nbody{\n font-family: sans-serif;\n background-color: #fbfcfd;\n margin: 0;\n color: #102445;\n}\nh2{\n margin-bottom: 30px;\n}\n\nh5{\n text-transform: uppercase;\n letter-spacing: 2px;\n line-height: 1.21;\n margin: 0;\n}\na{\n line-height: 1.38;\n letter-spacing: 0.2px;\n text-decoration: none;\n color: #102445;\n}\n\n\na:hover{\n color: #1db598;\n}\n\ntextarea{\n width: 100%;\n margin-bottom: 10px;\n border-radius: 2px;\n background-color: #fbfcfd;\n border: solid 1.5px #e4eff4;\n font-family: monospace;\n font-size: 16px;\n line-height: 1.5;\n letter-spacing: 1.1px;\n outline: none;\n padding: 30px;\n}\n\n\nselect{\n /*-webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n background: url(\"img/chevron-down.png\") no-repeat calc(100% - 10px) !important;*/\n border-radius: 3px;\n background-color: #fbfcfd;\n width: 187px;\n height: 50px;\n border: solid 1.5px #e4eff4;\n font-size: 18px;\n font-weight: bold;\n line-height: 1.39;\n letter-spacing: 0.2px;\n color: #102445;\n padding: 10.5px;\n\n}\n\nbutton{\n width: 165px;\n height: 50px;\n border-radius: 3px;\n background-color: #1db598;\n outline: none;\n border: none;\n font-size: 16px;\n color: white;\n}\n\nem{\n font-style: normal;\n opacity: 0.5;\n font-size: 14px;\n font-weight: 500;\n letter-spacing: 0.2px;\n color: #102445;\n}\n\n.header{\n height: 92px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n width: 90%;\n margin: auto;\n}\n\n.container{\n display: flex;\n height:100%;\n min-height: 100vh;\n}\n.nav{\n color: white;\n display: flex;\n flex-direction: column;\n width: 150px;\n background: #3c5f8d;\n}\n\n.nav-item{\n height:150px;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-direction: column;\n border-bottom: 3px solid #2a4871;\n cursor: pointer;\n}\n\n.nav-active{\n background: #f2f7f9;\n font-weight: bold;\n color: #1db598;\n}\n\n.nav-item > .nav-image {\n display: flex;\n}\n\n.nav-item > .nav-image-active {\n display: none;\n}\n\n.nav-active > .nav-image {\n display: none;\n}\n\n.nav-active > .nav-image-active {\n display: flex;\n}\n\n\n.interface{\n display: none;\n flex: 1;\n flex-direction: column;\n align-items: center;\n background: #f2f7f9;\n}\n\n.interface-active{\n display: flex;\n}\n\n.query{\n margin-bottom: 30px;\n}\n.query,\n.output-container,\n.status-container{\n width: 75%;\n}\n\n.output{\n margin-bottom: 30px;\n}\n\n.input-controls{\n display: flex;\n justify-content: flex-end;\n}\n\n.tabs{\n display: flex;\n background: #eaf2f6;\n}\n.active-tab{\n background: white;\n font-weight: bold;\n color: #1db598;\n\n}\n\n.tab{\n height:60px;\n width: 100px;\n border-top-right-radius: 5px;\n display: flex;\n align-items: center;\n justify-content: center;\n visibility: visible;\n cursor: pointer;\n\n}\n\n.pane{\n background: white;\n padding: 30px;\n display: none;\n}\n\n.active{\n display: block;\n}\n\n.result-io-header{\n display: flex;\n align-items: center;\n margin-bottom: 15px;\n}\n\n.result-input,\n.result-output,\n.result-error{\n height: 60px;\n border-radius: 2px;\n background-color: #fafafa;\n border: solid 1.5px #e4eff4;\n font-family: monospace;\n font-size: 16px;\n line-height: 1.5;\n letter-spacing: 1.1px;\n color: #102445;\n padding: 15px;\n margin-bottom: 15px;\n word-break: break-all;\n overflow-wrap: break-word;\n overflow:hidden;\n}\n\n\n.result-output{\n background-color: #edf9f7;\n border-left: solid 4px #1db598;\n}\n\n.result-error{\n background-color: #fbf1f0;\n border-left: solid 4px #fa3035;\n color: #fa3035;\n}\n\n.raw{\n height: 253px;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n\n.result-table > table {\n border-left: solid 4px #1db598;\n}\n\ntable{\n border: solid 0.5px #e0e0e0;\n width: 100%;\n margin-bottom: 30px;\n /*color:#3c5f8d;*/\n}\ncaption{\n text-align:left;\n font-size: 16px;\n font-weight: bold;\n line-height: 1.21;\n letter-spacing: 2px;\n text-align: left;\n}\nth{\n font-size: 14px;\n font-weight: bold;\n line-height: 1.21;\n letter-spacing: 2px;\n color: #102445;\n text-transform: uppercase;\n text-align: left;\n padding: 21px 30px;\n background-color: white;\n}\ntr{\n border: solid 0.5px #e0e0e0;\n background-color: white;\n}\ntr:nth-child(even) {\n background-color: #f2f7f9;\n}\ntd{\n padding: 21px 30px;\n}\n\n.expand {\n text-align: center;\n}\n\n.query h2 {\n display: inline-block;\n}\n\n.query-tooltip {\n position: relative;\n display: inline;\n color: #000;\n margin-left: 5px;\n}\n\n.query-tooltip:hover {\n color: #000;\n}\n\n.query-tooltip-content {\n background-color: rgb(250, 250, 250);\n border: solid 1.5px #e4eff4;\n color: #102445;\n border-radius: 2px;\n padding: 15px;\n margin-bottom: 15px;\n\n position: absolute;\n left: 80px;\n top: -30px;\n z-index: 1;\n}\n\n.query-tooltip-container {\n position: relative;\n visibility: hidden;\n}\n\n.query-tooltip:hover+.query-tooltip-container{\n visibility: visible;\n}\n\n.code{\n font-family: monospace;\n}\n\nPK\x07\x08\xec[\xd0\xfe=\x13\x00\x00=\x13\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00cz\xbfJ\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\n\x00\x00\x00index.html\n\n\n \n \n \n \n Pilosa WebUI\n \n\n\n
\n \"\"\n
\n
\n
\n
\n
\n \"\"\n \"\"\n Console\n
\n
\n \"\"\n \"\"\n Cluster Admin\n
\n
\n \"\"\n \"\"\n Documentation\n
\n
\n
\n\n
\n

Query

\n ?\n
\n
\n
PQL
\n
\n SetBit(frame=foo, rowID=0, columnID=0)
\n ClearBit(frame=foo, rowID=0, columnID=0)
\n SetRowAttrs(frame=foo, rowID=0, color=\"blue\")
\n SetColumnAttrs(frame=foo, columnID=0, shape=\"circle\")
\n Bitmap(frame=foo, rowID=0)
\n Range(frame=foo, rowID=0, start=\"2010-01\", end=\"2017-03\")
\n Count(<BITMAP_CALL>)
\n TopN([BITMAP_CALL], frame=foo, n=20)
\n Union([BITMAP_CALL, ...])
\n Intersect(<BITMAP_CALL>, [BITMAP_CALL, ...])
\n Difference(<BITMAP_CALL>, <BITMAP_CALL>)\n
\n
\n
Special commands
\n
\n :create index test [columnLabel=column]
\n :use test
\n :create frame foo [rowLabel=row]
\n :delete index test
\n :delete frame foo\n
\n
\n <tab>: autocomplete
\n <up>/<down>: history
\n
\n
\n \n
\n
\n \n    \n \n
\n
\n
\n\n
\n

Output

\n
\n \n
\n
\n\n
\n\n
\n
\n

Nodes

\n
\n
\n
\n
\n

Indexes

\n
\n
\n
\n
\n \n
\n\n
\n\n
\n docs!\n
\n\n
\n \n\n\nPK\x07\x08\x8dC\xf8\xe1\xef\x0f\x00\x00\xef\x0f\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\xc1n\xa3JJ\x1c\xff\xa8G\x0e\x00\x00G\x0e\x00\x00\x17\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x00\x00\x00assets/chevron-down.pngPK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x96\x84jK\xfa\x8b=\x1a\xcaH\x00\x00\xcaH\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x8c\x0e\x00\x00assets/main.jsPK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\xc1n\xa3J\xc1J\xead \x02\x00\x00 \x02\x00\x00\x1d\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x92W\x00\x00assets/nav-cluster-active.svgPK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\xc1n\xa3J\xc4\x07\xec\x0b\x05\x02\x00\x00\x05\x02\x00\x00\x16\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xe6Y\x00\x00assets/nav-cluster.svgPK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\xc1n\xa3J\xf2\x90\xe75\xa0\x01\x00\x00\xa0\x01\x00\x00\x1d\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81/\\\x00\x00assets/nav-console-active.svgPK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\xc1n\xa3J\xfb\xc8\xea\xb0\x9e\x01\x00\x00\x9e\x01\x00\x00\x16\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x1a^\x00\x00assets/nav-console.svgPK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\xc1n\xa3J\xe5\x95\x86\x82\xec\x01\x00\x00\xec\x01\x00\x00#\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xfc_\x00\x00assets/nav-documentation-active.svgPK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\xc1n\xa3J\xe18\x81J\xe8\x01\x00\x00\xe8\x01\x00\x00\x1c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x819b\x00\x00assets/nav-documentation.svgPK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\xc1n\xa3J+\xd4\xf31\xa2\x01\x00\x00\xa2\x01\x00\x00\x14\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81kd\x00\x00assets/nav_item1.svgPK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\xa0~\xe6J\xec[\xd0\xfe=\x13\x00\x00=\x13\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81Of\x00\x00assets/style.cssPK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00cz\xbfJ\x8dC\xf8\xe1\xef\x0f\x00\x00\xef\x0f\x00\x00\n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xcay\x00\x00index.htmlPK\x05\x06\x00\x00\x00\x00\x0b\x00\x0b\x00\xf2\x02\x00\x00\xf1\x89\x00\x00\x00\x00" - fs.Register(data) -} From bb33411c5de17e82a97e252aeeefb74894e7c899 Mon Sep 17 00:00:00 2001 From: Matt Jaffee Date: Mon, 25 Jun 2018 12:40:46 -0500 Subject: [PATCH 2/8] remove debugging print statement --- executor.go | 1 - 1 file changed, 1 deletion(-) diff --git a/executor.go b/executor.go index a1402b066..610696022 100644 --- a/executor.go +++ b/executor.go @@ -1614,7 +1614,6 @@ func (e *Executor) translateCall(index string, idx *Index, c *pql.Call) error { if err != nil { return err } - fmt.Printf("translated %s to %d in field %s\n", value, ids[0], fieldName) c.Args[colKey] = ids[0] } } else { From 324028a8c254b0aa620c6d5ea707dfdf5c2233e0 Mon Sep 17 00:00:00 2001 From: Cody Soyland Date: Mon, 25 Jun 2018 15:09:36 -0500 Subject: [PATCH 3/8] Add ability to pass ServerOptions when calling NewCommand --- server/server.go | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/server/server.go b/server/server.go index ef9f46ca3..ac11662a0 100644 --- a/server/server.go +++ b/server/server.go @@ -76,11 +76,22 @@ type Command struct { Handler pilosa.Handler ln net.Listener + + serverOptions []pilosa.ServerOption +} + +type CommandOption func(c *Command) error + +func OptCommandServerOptions(opts ...pilosa.ServerOption) CommandOption { + return func(c *Command) error { + c.serverOptions = append(c.serverOptions, opts...) + return nil + } } // NewCommand returns a new instance of Main. -func NewCommand(stdin io.Reader, stdout, stderr io.Writer) *Command { - return &Command{ +func NewCommand(stdin io.Reader, stdout, stderr io.Writer, opts ...CommandOption) *Command { + c := &Command{ Config: NewConfig(), CmdIO: pilosa.NewCmdIO(stdin, stdout, stderr), @@ -88,6 +99,16 @@ func NewCommand(stdin io.Reader, stdout, stderr io.Writer) *Command { Started: make(chan struct{}), done: make(chan struct{}), } + + for _, opt := range opts { + err := opt(c) + if err != nil { + panic(err) + // TODO: Return error instead of panic? + } + } + + return c } // Start starts the pilosa server - it returns once the server is running. @@ -225,7 +246,7 @@ func (m *Command) SetupServer() error { primaryTranslateStore = http.NewTranslateStore(m.Config.Translation.PrimaryURL) } - m.Server, err = pilosa.NewServer( + serverOptions := []pilosa.ServerOption{ pilosa.OptServerAntiEntropyInterval(time.Duration(m.Config.AntiEntropy.Interval)), pilosa.OptServerLongQueryTime(time.Duration(m.Config.Cluster.LongQueryTime)), pilosa.OptServerDataDir(m.Config.DataDir), @@ -243,7 +264,12 @@ func (m *Command) SetupServer() error { pilosa.OptServerInternalClient(http.NewInternalClientFromURI(uri, c)), pilosa.OptServerPrimaryTranslateStore(primaryTranslateStore), pilosa.OptServerClusterDisabled(m.Config.Cluster.Disabled, m.Config.Cluster.Hosts), - ) + } + + serverOptions = append(serverOptions, m.serverOptions...) + + m.Server, err = pilosa.NewServer(serverOptions...) + if err != nil { return errors.Wrap(err, "new server") } From 9f68ea466337a15df6c55242d3525ca1a15e45cd Mon Sep 17 00:00:00 2001 From: Cody Soyland Date: Mon, 25 Jun 2018 17:22:34 -0500 Subject: [PATCH 4/8] Use OptCommandServerOptions to inject mock TranslateStore --- http/translator_test.go | 33 +++++++++++++++++---------------- mock/translator.go | 10 +++++----- server/server.go | 6 +++--- test/pilosa.go | 32 ++++++++++++-------------------- 4 files changed, 37 insertions(+), 44 deletions(-) diff --git a/http/translator_test.go b/http/translator_test.go index 8bedf22cd..5236c43db 100644 --- a/http/translator_test.go +++ b/http/translator_test.go @@ -4,13 +4,13 @@ import ( "context" "io" "io/ioutil" - "net/http/httptest" "testing" "time" "github.com/pilosa/pilosa" "github.com/pilosa/pilosa/http" "github.com/pilosa/pilosa/mock" + "github.com/pilosa/pilosa/server" "github.com/pilosa/pilosa/test" ) @@ -52,13 +52,14 @@ func TestTranslateStore_Reader(t *testing.T) { } return &mrc, nil } - h := test.MustNewHandler() - h.API.TranslateStore = &translateStore - s := httptest.NewServer(h) - defer s.Close() + + opts := server.OptCommandServerOptions(pilosa.OptServerPrimaryTranslateStore(translateStore)) + main := test.MustRunMainWithCluster(t, 1, opts)[0] + defer main.Close() // Connect to server and stream all available data. - store := http.NewTranslateStore(s.URL) + store := http.NewTranslateStore(main.Server.URI.String()) + rc, err := store.Reader(context.Background(), 100) if err != nil { t.Fatal(err) @@ -95,15 +96,16 @@ func TestTranslateStore_Reader(t *testing.T) { translateStore.ReaderFunc = func(ctx context.Context, off int64) (io.ReadCloser, error) { return &mrc, nil } - h := test.MustNewHandler() - h.API.TranslateStore = &translateStore - s := httptest.NewServer(h) - defer s.Close() + + opts := server.OptCommandServerOptions(pilosa.OptServerPrimaryTranslateStore(translateStore)) + main := test.MustRunMainWithCluster(t, 1, opts)[0] + + defer main.Close() defer close(done) // Connect to server and begin streaming. ctx, cancel := context.WithCancel(context.Background()) - store := http.NewTranslateStore(s.URL) + store := http.NewTranslateStore(main.Server.URI.String()) if _, err := store.Reader(ctx, 0); err != nil { t.Fatal(err) } @@ -123,12 +125,11 @@ func TestTranslateStore_Reader(t *testing.T) { translateStore.ReaderFunc = func(ctx context.Context, off int64) (io.ReadCloser, error) { return nil, pilosa.ErrNotImplemented } - h := test.MustNewHandler() - h.API.TranslateStore = &translateStore - s := httptest.NewServer(h) - defer s.Close() - _, err := http.NewTranslateStore(s.URL).Reader(context.Background(), 0) + opts := server.OptCommandServerOptions(pilosa.OptServerPrimaryTranslateStore(translateStore)) + main := test.MustRunMainWithCluster(t, 1, opts)[0] + + _, err := http.NewTranslateStore(main.Server.URI.String()).Reader(context.Background(), 0) if err != pilosa.ErrNotImplemented { t.Fatalf("unexpected error: %s", err) } diff --git a/mock/translator.go b/mock/translator.go index 3f815b89f..186c81894 100644 --- a/mock/translator.go +++ b/mock/translator.go @@ -17,22 +17,22 @@ type TranslateStore struct { ReaderFunc func(ctx context.Context, off int64) (io.ReadCloser, error) } -func (s *TranslateStore) TranslateColumnsToUint64(index string, values []string) ([]uint64, error) { +func (s TranslateStore) TranslateColumnsToUint64(index string, values []string) ([]uint64, error) { return s.TranslateColumnsToUint64Func(index, values) } -func (s *TranslateStore) TranslateColumnToString(index string, values uint64) (string, error) { +func (s TranslateStore) TranslateColumnToString(index string, values uint64) (string, error) { return s.TranslateColumnToStringFunc(index, values) } -func (s *TranslateStore) TranslateRowsToUint64(index, frame string, values []string) ([]uint64, error) { +func (s TranslateStore) TranslateRowsToUint64(index, frame string, values []string) ([]uint64, error) { return s.TranslateRowsToUint64Func(index, frame, values) } -func (s *TranslateStore) TranslateRowToString(index, frame string, value uint64) (string, error) { +func (s TranslateStore) TranslateRowToString(index, frame string, value uint64) (string, error) { return s.TranslateRowToStringFunc(index, frame, value) } -func (s *TranslateStore) Reader(ctx context.Context, off int64) (io.ReadCloser, error) { +func (s TranslateStore) Reader(ctx context.Context, off int64) (io.ReadCloser, error) { return s.ReaderFunc(ctx, off) } diff --git a/server/server.go b/server/server.go index ac11662a0..3dce467d6 100644 --- a/server/server.go +++ b/server/server.go @@ -77,14 +77,14 @@ type Command struct { Handler pilosa.Handler ln net.Listener - serverOptions []pilosa.ServerOption + ServerOptions []pilosa.ServerOption } type CommandOption func(c *Command) error func OptCommandServerOptions(opts ...pilosa.ServerOption) CommandOption { return func(c *Command) error { - c.serverOptions = append(c.serverOptions, opts...) + c.ServerOptions = append(c.ServerOptions, opts...) return nil } } @@ -266,7 +266,7 @@ func (m *Command) SetupServer() error { pilosa.OptServerClusterDisabled(m.Config.Cluster.Disabled, m.Config.Cluster.Hosts), } - serverOptions = append(serverOptions, m.serverOptions...) + serverOptions = append(serverOptions, m.ServerOptions...) m.Server, err = pilosa.NewServer(serverOptions...) diff --git a/test/pilosa.go b/test/pilosa.go index 0f5cdd0d5..5507db6f5 100644 --- a/test/pilosa.go +++ b/test/pilosa.go @@ -34,7 +34,7 @@ import ( ) //////////////////////////////////////////////////////////////////////////////////// -// Main represents a test wrapper for main.Main. +// Main represents a test wrapper for server.Command. type Main struct { *server.Command @@ -43,43 +43,35 @@ type Main struct { Stderr bytes.Buffer } -type MainOpt func(m *Main) error - -func OptAntiEntropyInterval(dur time.Duration) MainOpt { - return func(m *Main) error { - m.Command.Config.AntiEntropy.Interval = toml.Duration(dur) +func OptAntiEntropyInterval(dur time.Duration) server.CommandOption { + return func(m *server.Command) error { + m.Config.AntiEntropy.Interval = toml.Duration(dur) return nil } } -func OptAllowedOrigins(origins []string) MainOpt { - return func(m *Main) error { +func OptAllowedOrigins(origins []string) server.CommandOption { + return func(m *server.Command) error { m.Config.Handler.AllowedOrigins = origins return nil } } // NewMain returns a new instance of Main with a temporary data directory and random port. -func NewMain(opts ...MainOpt) *Main { +func NewMain(opts ...server.CommandOption) *Main { path, err := ioutil.TempDir("", "pilosa-") if err != nil { panic(err) } - m := &Main{Command: server.NewCommand(os.Stdin, os.Stdout, os.Stderr)} + m := &Main{Command: server.NewCommand(os.Stdin, os.Stdout, os.Stderr, opts...)} m.Config.DataDir = path m.Config.Bind = "http://localhost:0" m.Config.Cluster.Disabled = true m.Command.Stdin = &m.Stdin m.Command.Stdout = &m.Stdout m.Command.Stderr = &m.Stderr - for _, opt := range opts { - err := opt(m) - if err != nil { - panic(err) - } - } err = m.SetupServer() if err != nil { panic(err) @@ -94,7 +86,7 @@ func NewMain(opts ...MainOpt) *Main { } // NewMainWithCluster returns a new instance of Main with clustering enabled. -func NewMainWithCluster(isCoordinator bool, opts ...MainOpt) *Main { +func NewMainWithCluster(isCoordinator bool, opts ...server.CommandOption) *Main { m := NewMain(opts...) m.Config.Cluster.Disabled = false m.Config.Cluster.Coordinator = isCoordinator @@ -103,7 +95,7 @@ func NewMainWithCluster(isCoordinator bool, opts ...MainOpt) *Main { // MustRunMainWithCluster ruturns a running array of *Main where // all nodes are joined via memberlist (i.e. clustering enabled). -func MustRunMainWithCluster(t *testing.T, size int, opts ...MainOpt) []*Main { +func MustRunMainWithCluster(t *testing.T, size int, opts ...server.CommandOption) []*Main { ma, err := runMainWithCluster(size, opts...) if err != nil { t.Fatalf("new main array with cluster: %v", err) @@ -113,7 +105,7 @@ func MustRunMainWithCluster(t *testing.T, size int, opts ...MainOpt) []*Main { // runMainWithCluster runs an array of *Main where all nodes are // joined via memberlist (i.e. clustering enabled). -func runMainWithCluster(size int, opts ...MainOpt) ([]*Main, error) { +func runMainWithCluster(size int, opts ...server.CommandOption) ([]*Main, error) { if size == 0 { return nil, errors.New("cluster must contain at least one node") } @@ -164,7 +156,7 @@ func (m *Main) Reopen() error { // Create new main with the same config. config := m.Command.Config - m.Command = server.NewCommand(os.Stdin, os.Stdout, os.Stderr) + m.Command = server.NewCommand(os.Stdin, os.Stdout, os.Stderr, server.OptCommandServerOptions(m.ServerOptions...)) m.Command.Config = config err := m.SetupServer() if err != nil { From ee37152cd5d020767045a18ac1e77b686a0b7a30 Mon Sep 17 00:00:00 2001 From: Matt Jaffee Date: Mon, 25 Jun 2018 17:05:36 -0500 Subject: [PATCH 5/8] consolidate gossipEventReceiver into gossip member set pilosa.Server now implements StatusHandler and EventReceiver and needs only start a gossip memberset. A gossip member set now takes a server as an argument explicitly and the maze of handlers and receivers and the starting sequence is somewhat simplified. Server now trivially implements EventHandler by passing the call along to its Cluster object which has the actual implementation. This means that less things will need to refer to cluster. --- broadcast_test.go | 27 ++++++++++++++------------- cluster.go | 5 ----- gossip/gossip.go | 32 +++++++++++++++++--------------- server.go | 28 +++++++++++++--------------- server/server.go | 5 ----- test/pilosa.go | 4 ---- 6 files changed, 44 insertions(+), 57 deletions(-) diff --git a/broadcast_test.go b/broadcast_test.go index 970a249cb..23bd962a7 100644 --- a/broadcast_test.go +++ b/broadcast_test.go @@ -56,6 +56,7 @@ func testMessageMarshal(t *testing.T, m proto.Message) { // Ensure that BroadcastReceiver can register a BroadcastHandler. func TestBroadcast_BroadcastReceiver(t *testing.T) { + t.Skip("broadcast receiver") path, err := ioutil.TempDir("", "pilosa-") if err != nil { panic(err) @@ -67,24 +68,24 @@ func TestBroadcast_BroadcastReceiver(t *testing.T) { if err != nil { t.Fatalf("setting up server: %v", err) } - s := com.Server + // s := com.Server - sbr := NewSimpleBroadcastReceiver() - sbh := NewSimpleBroadcastHandler() + // sbr := NewSimpleBroadcastReceiver() + // sbh := NewSimpleBroadcastHandler() - s.BroadcastReceiver = sbr - s.BroadcastReceiver.Start(sbh) + // s.BroadcastReceiver = sbr + // s.BroadcastReceiver.Start(sbh) - msg := &internal.DeleteIndexMessage{ - Index: "i", - } + // msg := &internal.DeleteIndexMessage{ + // Index: "i", + // } - s.BroadcastReceiver.(*SimpleBroadcastReceiver).Receive(msg) + // s.BroadcastReceiver.(*SimpleBroadcastReceiver).Receive(msg) - // Make sure the message received is what was sentd - if !reflect.DeepEqual(sbh.receivedMessage, msg) { - t.Fatalf("unexpected message: %s", sbh.receivedMessage) - } + // // Make sure the message received is what was sentd + // if !reflect.DeepEqual(sbh.receivedMessage, msg) { + // t.Fatalf("unexpected message: %s", sbh.receivedMessage) + // } } type SimpleBroadcastReceiver struct { diff --git a/cluster.go b/cluster.go index 957fe20fd..ae34bf03c 100644 --- a/cluster.go +++ b/cluster.go @@ -886,11 +886,6 @@ func (c *Cluster) open() error { return errors.Wrap(err, "adding local node") } - // Start the EventReceiver. - if err := c.EventReceiver.Start(c); err != nil { - return fmt.Errorf("starting EventReceiver: %v", err) - } - // Open MemberSet communication. if err := c.MemberSet.Open(c.Node); err != nil { return fmt.Errorf("opening MemberSet: %v", err) diff --git a/gossip/gossip.go b/gossip/gossip.go index 9154e9fc7..4749c9c58 100644 --- a/gossip/gossip.go +++ b/gossip/gossip.go @@ -33,7 +33,6 @@ import ( ) // Ensure GossipMemberSet implements interfaces. -var _ pilosa.BroadcastReceiver = &GossipMemberSet{} var _ memberlist.Delegate = &GossipMemberSet{} // GossipMemberSet represents a gossip implementation of MemberSet using memberlist. @@ -45,19 +44,15 @@ type GossipMemberSet struct { broadcasts *memberlist.TransmitLimitedQueue - statusHandler pilosa.StatusHandler - config *gossipConfig + pserver *pilosa.Server + config *gossipConfig Logger pilosa.Logger logger *log.Logger transport *Transport -} -// Start implements the BroadcastReceiver interface and sets the BroadcastHandler. -func (g *GossipMemberSet) Start(h pilosa.BroadcastHandler) error { - g.handler = h - return nil + gossipEventReceiver *GossipEventReceiver } // GetBindAddr returns the gossip bind address based on config and auto bind port. @@ -69,13 +64,16 @@ func (g *GossipMemberSet) GetBindAddr() string { // Open implements the MemberSet interface to start network activity. func (g *GossipMemberSet) Open(n *pilosa.Node) error { + err := g.gossipEventReceiver.Start(g.pserver) + if err != nil { + return errors.Wrap(err, "starting event delegate") + } if g.handler == nil { return fmt.Errorf("must call Start(pilosa.BroadcastHandler) before calling Open()") } g.node = n - err := error(nil) g.mu.Lock() g.memberlist, err = memberlist.Create(g.config.memberlistConfig) g.mu.Unlock() @@ -166,7 +164,7 @@ func WithLogger(logger *log.Logger) GossipMemberSetOption { } // NewGossipMemberSet returns a new instance of GossipMemberSet based on options. -func NewGossipMemberSet(name string, host string, cfg Config, ger *GossipEventReceiver, sh pilosa.StatusHandler, options ...GossipMemberSetOption) (*GossipMemberSet, error) { +func NewGossipMemberSet(name string, host string, cfg Config, s *pilosa.Server, options ...GossipMemberSetOption) (*GossipMemberSet, error) { g := &GossipMemberSet{ Logger: pilosa.NopLogger, } @@ -177,6 +175,10 @@ func NewGossipMemberSet(name string, host string, cfg Config, ger *GossipEventRe return nil, errors.Wrap(err, "executing option") } } + ger := NewGossipEventReceiver(g.logger) + g.gossipEventReceiver = ger + + g.handler = s if g.transport == nil { port, err := strconv.Atoi(cfg.Port) @@ -232,7 +234,7 @@ func NewGossipMemberSet(name string, host string, cfg Config, ger *GossipEventRe gossipSeeds: cfg.Seeds, } - g.statusHandler = sh + g.pserver = s return g, nil } @@ -270,7 +272,7 @@ func (g *GossipMemberSet) GetBroadcasts(overhead, limit int) [][]byte { // LocalState implementation of the memberlist.Delegate interface // sends this Node's state data. func (g *GossipMemberSet) LocalState(join bool) []byte { - pb, err := g.statusHandler.LocalStatus() + pb, err := g.pserver.LocalStatus() if err != nil { g.Logger.Printf("error getting local state, err=%s", err) return []byte{} @@ -294,7 +296,7 @@ func (g *GossipMemberSet) MergeRemoteState(buf []byte, join bool) { g.Logger.Printf("error unmarshalling nodestate data, err=%s", err) return } - err := g.statusHandler.HandleRemoteStatus(&pb) + err := g.pserver.HandleRemoteStatus(&pb) if err != nil { g.Logger.Printf("merge state error: %s", err) } @@ -309,11 +311,11 @@ type GossipEventReceiver struct { ch chan memberlist.NodeEvent eventHandler pilosa.EventHandler - Logger pilosa.Logger + Logger *log.Logger } // NewGossipEventReceiver returns a new instance of GossipEventReceiver. -func NewGossipEventReceiver(logger pilosa.Logger) *GossipEventReceiver { +func NewGossipEventReceiver(logger *log.Logger) *GossipEventReceiver { return &GossipEventReceiver{ ch: make(chan memberlist.NodeEvent, 1), Logger: logger, diff --git a/server.go b/server.go index 84a09a01d..dc21d2bc2 100644 --- a/server.go +++ b/server.go @@ -61,10 +61,9 @@ type Server struct { clusterDisabled bool // External - BroadcastReceiver BroadcastReceiver - systemInfo SystemInfo - gcNotifier GCNotifier - logger Logger + systemInfo SystemInfo + gcNotifier GCNotifier + logger Logger NodeID string URI URI @@ -207,12 +206,11 @@ func OptServerClusterDisabled(disabled bool, hosts []string) ServerOption { // NewServer returns a new instance of Server. func NewServer(opts ...ServerOption) (*Server, error) { s := &Server{ - closing: make(chan struct{}), - Cluster: NewCluster(), - holder: NewHolder(), - BroadcastReceiver: NopBroadcastReceiver, - diagnostics: NewDiagnosticsCollector(DefaultDiagnosticServer), - systemInfo: NewNopSystemInfo(), + closing: make(chan struct{}), + Cluster: NewCluster(), + holder: NewHolder(), + diagnostics: NewDiagnosticsCollector(DefaultDiagnosticServer), + systemInfo: NewNopSystemInfo(), gcNotifier: NopGCNotifier, @@ -297,11 +295,6 @@ func (s *Server) Open() error { // Initialize Holder. s.holder.Broadcaster = s - // Start the BroadcastReceiver. - if err := s.BroadcastReceiver.Start(s); err != nil { - return fmt.Errorf("starting BroadcastReceiver: %v", err) - } - // Open Cluster management. if err := s.Cluster.open(); err != nil { return fmt.Errorf("opening Cluster: %v", err) @@ -711,6 +704,11 @@ func (s *Server) monitorRuntime() { } } +// ReceiveEvent implement EventHandler +func (s *Server) ReceiveEvent(e *NodeEvent) error { + return s.Cluster.ReceiveEvent(e) +} + // countOpenFiles on operating systems that support lsof. func countOpenFiles() (int, error) { switch runtime.GOOS { diff --git a/server/server.go b/server/server.go index ef9f46ca3..25076d95c 100644 --- a/server/server.go +++ b/server/server.go @@ -292,13 +292,10 @@ func (m *Command) SetupNetworking() error { m.Server.Cluster.Node.IsCoordinator = true } - gossipEventReceiver := gossip.NewGossipEventReceiver(m.logger) - m.Server.Cluster.EventReceiver = gossipEventReceiver gossipMemberSet, err := gossip.NewGossipMemberSet( m.Server.NodeID, m.Server.URI.Host(), m.Config.Gossip, - gossipEventReceiver, m.Server, gossip.WithLogger(m.logger.Logger()), gossip.WithTransport(transport), @@ -306,9 +303,7 @@ func (m *Command) SetupNetworking() error { if err != nil { return errors.Wrap(err, "getting memberset") } - gossipMemberSet.Logger = m.logger m.Server.Cluster.MemberSet = gossipMemberSet - m.Server.BroadcastReceiver = gossipMemberSet return nil } diff --git a/test/pilosa.go b/test/pilosa.go index 0f5cdd0d5..cba038e09 100644 --- a/test/pilosa.go +++ b/test/pilosa.go @@ -223,10 +223,6 @@ func (m *Main) RunWithTransport(host string, bindPort int, joinSeeds []string) ( return seed, err } - if err = m.Server.BroadcastReceiver.Start(m.Server); err != nil { - return seed, err - } - m.Server.Cluster.Static = false go func() { From e448d470040dfafa020d1d81060c85099c54a1ed Mon Sep 17 00:00:00 2001 From: Cody Soyland Date: Tue, 26 Jun 2018 08:46:56 -0500 Subject: [PATCH 6/8] Store commandOptions on test.Main for use in Reopen(); unexport serverOptions --- server/server.go | 6 +++--- test/pilosa.go | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/server/server.go b/server/server.go index 3dce467d6..ac11662a0 100644 --- a/server/server.go +++ b/server/server.go @@ -77,14 +77,14 @@ type Command struct { Handler pilosa.Handler ln net.Listener - ServerOptions []pilosa.ServerOption + serverOptions []pilosa.ServerOption } type CommandOption func(c *Command) error func OptCommandServerOptions(opts ...pilosa.ServerOption) CommandOption { return func(c *Command) error { - c.ServerOptions = append(c.ServerOptions, opts...) + c.serverOptions = append(c.serverOptions, opts...) return nil } } @@ -266,7 +266,7 @@ func (m *Command) SetupServer() error { pilosa.OptServerClusterDisabled(m.Config.Cluster.Disabled, m.Config.Cluster.Hosts), } - serverOptions = append(serverOptions, m.ServerOptions...) + serverOptions = append(serverOptions, m.serverOptions...) m.Server, err = pilosa.NewServer(serverOptions...) diff --git a/test/pilosa.go b/test/pilosa.go index 5507db6f5..64514e0ab 100644 --- a/test/pilosa.go +++ b/test/pilosa.go @@ -38,6 +38,8 @@ import ( type Main struct { *server.Command + commandOptions []server.CommandOption + Stdin bytes.Buffer Stdout bytes.Buffer Stderr bytes.Buffer @@ -64,7 +66,7 @@ func NewMain(opts ...server.CommandOption) *Main { panic(err) } - m := &Main{Command: server.NewCommand(os.Stdin, os.Stdout, os.Stderr, opts...)} + m := &Main{Command: server.NewCommand(os.Stdin, os.Stdout, os.Stderr, opts...), commandOptions: opts} m.Config.DataDir = path m.Config.Bind = "http://localhost:0" m.Config.Cluster.Disabled = true @@ -156,7 +158,7 @@ func (m *Main) Reopen() error { // Create new main with the same config. config := m.Command.Config - m.Command = server.NewCommand(os.Stdin, os.Stdout, os.Stderr, server.OptCommandServerOptions(m.ServerOptions...)) + m.Command = server.NewCommand(os.Stdin, os.Stdout, os.Stderr, m.commandOptions...) m.Command.Config = config err := m.SetupServer() if err != nil { From 533de70cbd64280d8d25460b94174823c5da624f Mon Sep 17 00:00:00 2001 From: Cody Soyland Date: Tue, 26 Jun 2018 11:22:48 -0500 Subject: [PATCH 7/8] Allow passing slice of CommandOptions to MustRunMainWithCluster, each slice going to one Command --- http/translator_test.go | 6 +++--- server/handler_test.go | 3 ++- server_test.go | 3 ++- test/pilosa.go | 13 ++++++++++--- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/http/translator_test.go b/http/translator_test.go index 5236c43db..15aef62a4 100644 --- a/http/translator_test.go +++ b/http/translator_test.go @@ -54,7 +54,7 @@ func TestTranslateStore_Reader(t *testing.T) { } opts := server.OptCommandServerOptions(pilosa.OptServerPrimaryTranslateStore(translateStore)) - main := test.MustRunMainWithCluster(t, 1, opts)[0] + main := test.MustRunMainWithCluster(t, 1, []server.CommandOption{opts})[0] defer main.Close() // Connect to server and stream all available data. @@ -98,7 +98,7 @@ func TestTranslateStore_Reader(t *testing.T) { } opts := server.OptCommandServerOptions(pilosa.OptServerPrimaryTranslateStore(translateStore)) - main := test.MustRunMainWithCluster(t, 1, opts)[0] + main := test.MustRunMainWithCluster(t, 1, []server.CommandOption{opts})[0] defer main.Close() defer close(done) @@ -127,7 +127,7 @@ func TestTranslateStore_Reader(t *testing.T) { } opts := server.OptCommandServerOptions(pilosa.OptServerPrimaryTranslateStore(translateStore)) - main := test.MustRunMainWithCluster(t, 1, opts)[0] + main := test.MustRunMainWithCluster(t, 1, []server.CommandOption{opts})[0] _, err := http.NewTranslateStore(main.Server.URI.String()).Reader(context.Background(), 0) if err != pilosa.ErrNotImplemented { diff --git a/server/handler_test.go b/server/handler_test.go index 070a3176a..cc21b8825 100644 --- a/server/handler_test.go +++ b/server/handler_test.go @@ -31,6 +31,7 @@ import ( "github.com/pilosa/pilosa" "github.com/pilosa/pilosa/http" "github.com/pilosa/pilosa/internal" + "github.com/pilosa/pilosa/server" "github.com/pilosa/pilosa/test" ) @@ -565,7 +566,7 @@ func TestHandler_Endpoints(t *testing.T) { t.Fatalf("CORS preflight status should be 405, but is %v", result.StatusCode) } - clus := test.MustRunMainWithCluster(t, 1, test.OptAllowedOrigins([]string{"http://test/"})) + clus := test.MustRunMainWithCluster(t, 1, []server.CommandOption{test.OptAllowedOrigins([]string{"http://test/"})}) w = httptest.NewRecorder() h := clus[0].Handler.(*http.Handler).Handler h.ServeHTTP(w, req) diff --git a/server_test.go b/server_test.go index 402d4de7d..9a94da8a2 100644 --- a/server_test.go +++ b/server_test.go @@ -20,6 +20,7 @@ import ( "time" "github.com/pilosa/pilosa" + "github.com/pilosa/pilosa/server" "github.com/pilosa/pilosa/test" ) @@ -27,7 +28,7 @@ import ( // pilosa.Server was not having its remoteClient field set by an option and so // it was using a nil client in monitorAntiEntropy. func TestMonitorAntiEntropy(t *testing.T) { - cluster := test.MustRunMainWithCluster(t, 3, test.OptAntiEntropyInterval(time.Millisecond*20)) + cluster := test.MustRunMainWithCluster(t, 3, []server.CommandOption{test.OptAntiEntropyInterval(time.Millisecond * 20)}) client := cluster[1].Client() err := client.CreateIndex(context.Background(), "balh", pilosa.IndexOptions{}) if err != nil { diff --git a/test/pilosa.go b/test/pilosa.go index 64514e0ab..3aa671801 100644 --- a/test/pilosa.go +++ b/test/pilosa.go @@ -97,7 +97,7 @@ func NewMainWithCluster(isCoordinator bool, opts ...server.CommandOption) *Main // MustRunMainWithCluster ruturns a running array of *Main where // all nodes are joined via memberlist (i.e. clustering enabled). -func MustRunMainWithCluster(t *testing.T, size int, opts ...server.CommandOption) []*Main { +func MustRunMainWithCluster(t *testing.T, size int, opts ...[]server.CommandOption) []*Main { ma, err := runMainWithCluster(size, opts...) if err != nil { t.Fatalf("new main array with cluster: %v", err) @@ -107,10 +107,13 @@ func MustRunMainWithCluster(t *testing.T, size int, opts ...server.CommandOption // runMainWithCluster runs an array of *Main where all nodes are // joined via memberlist (i.e. clustering enabled). -func runMainWithCluster(size int, opts ...server.CommandOption) ([]*Main, error) { +func runMainWithCluster(size int, opts ...[]server.CommandOption) ([]*Main, error) { if size == 0 { return nil, errors.New("cluster must contain at least one node") } + if len(opts) != size && len(opts) != 0 && len(opts) != 1 { + return nil, errors.New("Slice of CommandOptions must be of length 0, 1, or equal to the number of cluster nodes") + } mains := make([]*Main, size) @@ -120,7 +123,11 @@ func runMainWithCluster(size int, opts ...server.CommandOption) ([]*Main, error) var gossipSeeds = make([]string, size) for i := 0; i < size; i++ { - m := NewMainWithCluster(i == 0, opts...) + var commandOpts []server.CommandOption + if len(opts) > 0 { + commandOpts = opts[i%len(opts)] + } + m := NewMainWithCluster(i == 0, commandOpts...) m.Config.Cluster.Disabled = false gossipSeeds[i], err = m.RunWithTransport(gossipHost, gossipPort, gossipSeeds[:i]) From 7190fe71e5222010f693cede7b01cba2af6338f3 Mon Sep 17 00:00:00 2001 From: Matt Jaffee Date: Tue, 26 Jun 2018 11:23:31 -0500 Subject: [PATCH 8/8] fix comment --- server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server.go b/server.go index dc21d2bc2..d6a4dde38 100644 --- a/server.go +++ b/server.go @@ -704,7 +704,7 @@ func (s *Server) monitorRuntime() { } } -// ReceiveEvent implement EventHandler +// ReceiveEvent implements the EventHandler interface. func (s *Server) ReceiveEvent(e *NodeEvent) error { return s.Cluster.ReceiveEvent(e) }