diff --git a/.travis.yml b/.travis.yml index 6e83872ad..1a4691b8c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ env: - secure: "VnBFmFfBOrrf7ONLN9WpAFCcV8SEt5G5VPnnHv97TP7PlJG8LWR6k6O+vRJOvf8V4vDMfKCTDonwWLgbssVf3yygo3C8ZoftY2phehEkWGffCgsd9ML/YBNbGq4LYLSE5HKvBqrZjQaOrVby71BAsP8W7RhC6hqzFQ00M/z8dZVfwaQQFwew2eEcSxLEaaDFS8Wgc3/UuwxDRPBq6u3cCN5RxfB+q70HvGVq4TT+0dqS4eCvz688+Z0GIGYx9olNjh0F2Kc8R2Po0lnUNa0GiHrZ21zeQ1DxIK04QABrWWmjL4h+bx3VHNKPFR4GYSKDf+pj1kfaqbfrAg6rMAJdGejgoS+QyjhgCoN4d3qRp8s+1nrxtp0TvezEdjwyxt4quGHbP5TxWUszssbGhWqf4mx6OeJ8MmdTaJjfu0f3NWJXMycqT6J73WKORk4rHeIqF9CIdxdmcpkwYj8rk0TEMTPTsd7WA8w2HIDsCz/jQnRmEgLUiNnTAofYc/uUi/Wg/T2hllkp+oBDTzxk9NTelkqx8TJ0bDmYYL9JWUi1siFHTHiVYTJgyirSfGNpe61u8OLmT0Hak/D399IfL7qgFLlMXk8q92typfO2xEduq6G+8KygeqiOMSsOY+xcDvZf5xtcEihYd21vjtrxRSqFsup/o8DIxEurQnfXBx1B+WA=" - secure: "U4fpHWDVOG4viqZsiVgUDW7OW1JW60uPOZy0q9pfbs86iHvmZq0PaScsZ+YdlYaN2GETVr7endDf6DCcZs1PWfg0F6VQfkOXcShX8HVS9O58lUZA5tyvbDVql9DQs4PbnkZo+ktz+Z0YaXqq2RdtMDOUz4bgZwspLPMA14if+N6w0tqCFpB7bEtpptTGsdbIQPG1n07yvSeNmK4mvrEEs77tWmhulN5iilpOqhpIvD39bJvtCYVALuJpzLd/OjLTPV9l/fl+hJkMXSj+X5ilO1DHINAcCM648iEX2phXAIWmi0O0Rbg2cI4kV9T5ysOIw8ux+YCm9bZDGTCt+VGBW5Fg+Z5iaXXexyKYCGiHleOJ7kCj9kXxh2u8NiYVNgb19dGJV5/HgQ6pcGWjeVEqr8yY1546zMjpTX+SYGQF+XZe+uggEjeAsk53ueXa0pyZTrlrqSvR7BBtWPx47s/dTg2L19FQYv3XpGMxEXLw92RplExQKi1h7QgihRxFpjGgURHhrt7d9eiNiNqBt3ZsHjmh2AkXZHnaDjlgSnFFWaMqP3UtDBWIuO+2BMbZUJVfP+gpQGBZ4gtpUSmV2JDCHgZgX5OAnLD4usxh+ATQ4rvUXF/tf8nMqEKHlGKd8hxpYSyMX21BoqfSfY4/IA0ejVE9BITqlrvqewqkP1yxe7o=" install: - - make vendor + - make vendor generate-statik script: - make test # TODO: When we drop support for Go <1.10, we should use `-coverprofile=` on both `go test` and `goveralls` so the test suite doesn't run twice. See https://github.com/pilosa/pilosa/issues/1009 @@ -19,10 +19,11 @@ before_deploy: - pip install awscli --user `whoami` deploy: - provider: script - script: make prerelease-upload + script: make prerelease-upload GOOS=linux GOARCH=amd64 skip_cleanup: true on: - branch: master + all_branches: true + go: 1.9 matrix: allow_failures: - go: master diff --git a/CHANGELOG.md b/CHANGELOG.md index ac62abc67..2a4d17174 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,65 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## Unreleased + +### Added + +- Add ability to dynamically resize clusters ([#982](https://github.com/pilosa/pilosa/pull/982), [#946](https://github.com/pilosa/pilosa/pull/946), [#929](https://github.com/pilosa/pilosa/pull/929), [#927](https://github.com/pilosa/pilosa/pull/927), [#917](https://github.com/pilosa/pilosa/pull/917), [#913](https://github.com/pilosa/pilosa/pull/913), [#912](https://github.com/pilosa/pilosa/pull/912), [#908](https://github.com/pilosa/pilosa/pull/908)) +- Update docs to include cluster-resize config and instructions ([#1088](https://github.com/pilosa/pilosa/pull/1088)) +- Add support for lists of gossip seeds for redundancy ([#1133](https://github.com/pilosa/pilosa/pull/1133)) +- Add HTTP Handler validation ([#1140](https://github.com/pilosa/pilosa/pull/1140), [#1121](https://github.com/pilosa/pilosa/pull/1121)) +- Add validation around node-remove conditions ([#1138](https://github.com/pilosa/pilosa/pull/1138)) +- broadcast.SendSync field creation and deletion to all nodes ([#1132](https://github.com/pilosa/pilosa/pull/1132)) +- Spread recalculate caches to all nodes. Fixes #1069 ([#1109](https://github.com/pilosa/pilosa/pull/1109)) +- Add QueryResult.Type to protobuf message to distiguish results at the client ([#1064](https://github.com/pilosa/pilosa/pull/1064)) +- Modify `pilosa import` to support string rows/columns ([#1063](https://github.com/pilosa/pilosa/pull/1063)) +- Add some statsd calls to HolderSyncer ([#1048](https://github.com/pilosa/pilosa/pull/1048)) +- Adds support for memberlist gossip configuration via pilosa.Config ([#1014](https://github.com/pilosa/pilosa/pull/1014)) +- Add local and cluster IDs ([#1013](https://github.com/pilosa/pilosa/pull/1013)) +- Add HolderCleaner and view.DeleteFragment ([#985](https://github.com/pilosa/pilosa/pull/985)) +- Add set-coordinator endpoint ([#963](https://github.com/pilosa/pilosa/pull/963)) +- Documentation improvements ([#1135](https://github.com/pilosa/pilosa/pull/1135), [#1154](https://github.com/pilosa/pilosa/pull/1154), [#1091](https://github.com/pilosa/pilosa/pull/1091), [#1108](https://github.com/pilosa/pilosa/pull/1108), [#1087](https://github.com/pilosa/pilosa/pull/1087), [#1086](https://github.com/pilosa/pilosa/pull/1086), [#1026](https://github.com/pilosa/pilosa/pull/1026), [#1022](https://github.com/pilosa/pilosa/pull/1022), [#1007](https://github.com/pilosa/pilosa/pull/1007), [#981](https://github.com/pilosa/pilosa/pull/981), [#901](https://github.com/pilosa/pilosa/pull/901), [#972](https://github.com/pilosa/pilosa/pull/972)) + +### Changed + +- Put Statik behind an interface ([#1163](https://github.com/pilosa/pilosa/pull/1163)) +- Refactor diagnostics, inject gopsutil dependency ([#1166](https://github.com/pilosa/pilosa/pull/1166)) +- Use boolean instead of address to configure coordinator ([#1158](https://github.com/pilosa/pilosa/pull/1158)) +- Put GCNotify behind an interface ([#1148](https://github.com/pilosa/pilosa/pull/1148)) +- Replace custom assembly bit functions with standard go ([#797](https://github.com/pilosa/pilosa/pull/797)) +- Improve roaring tests ([#1115](https://github.com/pilosa/pilosa/pull/1115)) +- Change configuration cluster.type (string) to cluster.disabled (bool) ([#1099](https://github.com/pilosa/pilosa/pull/1099)) +- Use NodeID instead of URI for node identification ([#1077](https://github.com/pilosa/pilosa/pull/1077)) +- Change gossip config from DefaultLocalConfig to DefaultWANConfig ([#1032](https://github.com/pilosa/pilosa/pull/1032)) +- Use binary search in runAdd ([#1027](https://github.com/pilosa/pilosa/pull/1027)) +- Use HTTP handler for gossip SendSync ([#1001](https://github.com/pilosa/pilosa/pull/1001)) +- Group the write operations in syncBlock by MaxWritesPerRequest ([#950](https://github.com/pilosa/pilosa/pull/950)) +- Refactored HTTPClient handling ([#991](https://github.com/pilosa/pilosa/pull/991)) +- Remove FrameSchema. Move Fields to the Frame struct ([#907](https://github.com/pilosa/pilosa/pull/907)) + +### Removed + +- Remove the Gossip stutter from memberlist-related config options ([#1171](https://github.com/pilosa/pilosa/pull/1171)) +- Remove old GossipPort and GossipSeed config options ([#1142](https://github.com/pilosa/pilosa/pull/1142)) +- Remove cluster type `http` from docs ([#1130](https://github.com/pilosa/pilosa/pull/1130)) + +### Fixed + +- Handle the scheme correctly in config.Bind ([#1143](https://github.com/pilosa/pilosa/pull/1143)) +- Prevent excessive sendSync (createView) messages. ([#1139](https://github.com/pilosa/pilosa/pull/1139)) +- Fix a shift logic bug in bitmapZeroRange ([#1110](https://github.com/pilosa/pilosa/pull/1110)) +- Fix node id validation on set-coordinator ([#1102](https://github.com/pilosa/pilosa/pull/1102)) +- Avoid overflow bug in differenceRunArray ([#1105](https://github.com/pilosa/pilosa/pull/1105)) +- Fix bug in NewServerCluster where each host was its own coordinator ([#1101](https://github.com/pilosa/pilosa/pull/1101)) +- Fix count/bitmap mismatch bug ([#1084](https://github.com/pilosa/pilosa/pull/1084)) +- Fix edge case with Range() calls outside field Min/Max. Fixes #876. ([#979](https://github.com/pilosa/pilosa/pull/979)) +- Bind the handler to all interfaces (0.0.0.0) in Dockerfile. Fixes #977. ([#980](https://github.com/pilosa/pilosa/pull/980)) + +### Performance + +- Add benchmark for various container usage patterns ([#1017](https://github.com/pilosa/pilosa/pull/1017)) + ## [0.8.8] - 2018-02-19 This version contains 1 contribution from 2 contributors. There are 4 files changed, 1,153 insertions, and 618 deletions. diff --git a/Gopkg.lock b/Gopkg.lock index c77dc951a..af6174c08 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -205,12 +205,6 @@ packages = ["."] revision = "bb4de0191aa41b5507caa14b0650cdbddcd9280b" -[[projects]] - name = "github.com/sony/gobreaker" - packages = ["."] - revision = "e9556a45379ef1da12e54847edb2fb3d7d566f36" - version = "0.3.0" - [[projects]] branch = "master" name = "github.com/spf13/afero" diff --git a/Makefile b/Makefile index 48c0075a8..8fdbcea3d 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: dep docker pilosa release-build prerelease-build release prerelease prerelease-upload install generate statik test cover cover-pkg cover-viz clean docker-build docker-test +.PHONY: dep docker pilosa release-build prerelease-build release prerelease prerelease-upload install generate generate-statik generate-protoc statik test cover cover-pkg cover-viz clean docker-build docker-test DEP := $(shell command -v dep 2>/dev/null) STATIK := $(shell command -v statik 2>/dev/null) @@ -11,6 +11,9 @@ PKGS := $(shell cd $(GOPATH)/src/$(CLONE_URL); go list ./... | grep -v vendor) BUILD_TIME=`date -u +%FT%T%z` LDFLAGS="-X github.com/pilosa/pilosa.Version=$(VERSION) -X github.com/pilosa/pilosa.BuildTime=$(BUILD_TIME)" DOCKER_GOLANG_IMAGE=golang:latest +GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD) +BRANCH := $(if $(TRAVIS_BRANCH),$(TRAVIS_BRANCH),$(GIT_BRANCH)) +BRANCH_IDENTIFIER := $(BRANCH)-$(GOOS)-$(GOARCH) default: test pilosa @@ -75,16 +78,16 @@ else endif prerelease-build: vendor - make pilosa FLAGS="-o build/pilosa-master-$(GOOS)-$(GOARCH)/pilosa" - cp LICENSE README.md build/pilosa-master-$(GOOS)-$(GOARCH) - tar -cvz -C build -f build/pilosa-master-$(GOOS)-$(GOARCH).tar.gz pilosa-master-$(GOOS)-$(GOARCH)/ - @echo "Created pre-release build: build/pilosa-master-$(GOOS)-$(GOARCH).tar.gz" + make pilosa FLAGS="-o build/pilosa-$(BRANCH_IDENTIFIER)/pilosa" + cp LICENSE README.md build/pilosa-$(BRANCH_IDENTIFIER) + tar -cvz -C build -f build/pilosa-$(BRANCH_IDENTIFIER).tar.gz pilosa-$(BRANCH_IDENTIFIER)/ + @echo "Created pre-release build: build/pilosa-$(BRANCH_IDENTIFIER).tar.gz" prerelease: make prerelease-build GOOS=linux GOARCH=amd64 prerelease-upload: prerelease - aws s3 cp build/pilosa-master-linux-amd64.tar.gz s3://build.pilosa.com/pilosa-master-linux-amd64.tar.gz --acl public-read + aws s3 cp build/pilosa-$(BRANCH_IDENTIFIER).tar.gz s3://build.pilosa.com/pilosa-$(BRANCH_IDENTIFIER).tar.gz --acl public-read install: vendor go install -ldflags $(LDFLAGS) $(FLAGS) $(CLONE_URL)/cmd/pilosa @@ -100,7 +103,7 @@ generate-protoc: .protoc-gen-gofast go generate github.com/pilosa/pilosa/internal generate-statik: statik - go generate github.com/pilosa/pilosa + go generate github.com/pilosa/pilosa/statik generate: generate-protoc generate-statik diff --git a/attr.go b/attr.go index 437cab433..03ea4f43f 100644 --- a/attr.go +++ b/attr.go @@ -16,23 +16,12 @@ package pilosa import ( "bytes" - - "encoding/binary" - "fmt" "sort" - "sync" - "time" - "github.com/cespare/xxhash" - - "github.com/boltdb/bolt" "github.com/gogo/protobuf/proto" "github.com/pilosa/pilosa/internal" ) -// AttrBlockSize is the size of attribute blocks for anti-entropy. -const AttrBlockSize = 100 - // Attribute data type enum. const ( AttrTypeString = 1 @@ -41,313 +30,111 @@ const ( AttrTypeFloat = 4 ) -// AttrCache represents a cache for attributes. -type AttrCache struct { - mu sync.RWMutex - attrs map[uint64]map[string]interface{} +// AttrStore represents an interface for handling row/column attributes. +type AttrStore interface { + Path() string + Open() error + Close() error + Attrs(id uint64) (m map[string]interface{}, err error) + SetAttrs(id uint64, m map[string]interface{}) error + SetBulkAttrs(m map[uint64]map[string]interface{}) error + Blocks() ([]AttrBlock, error) + BlockData(i uint64) (map[uint64]map[string]interface{}, error) } -// Get returns the cached attributes for a given id. -func (c *AttrCache) Get(id uint64) map[string]interface{} { - c.mu.RLock() - defer c.mu.RUnlock() - attrs := c.attrs[id] - if attrs == nil { - return nil - } - - // Make a copy for safety - ret := make(map[string]interface{}) - for k, v := range attrs { - ret[k] = v - } - return ret +func init() { + NopAttrStore = &nopAttrStore{} } -// Set updates the cached attributes for a given id. -func (c *AttrCache) Set(id uint64, attrs map[string]interface{}) { - c.mu.Lock() - defer c.mu.Unlock() - c.attrs[id] = attrs +// NopAttrStore represents an AttrStore that doesn't do anything. +var NopAttrStore AttrStore + +func NewNopAttrStore(string) AttrStore { + return &nopAttrStore{} } -// AttrStore represents a storage layer for attributes. -type AttrStore struct { - mu sync.RWMutex - path string - db *bolt.DB - attrCache *AttrCache -} +// nopAttrStore represents a no-op implementation of the AttrStore interface. +type nopAttrStore struct{} -// NewAttrCache returns a new instance of AttrCache. -func NewAttrCache() *AttrCache { - return &AttrCache{ - attrs: make(map[uint64]map[string]interface{}), - } -} - -// NewAttrStore returns a new instance of AttrStore. -func NewAttrStore(path string) *AttrStore { - return &AttrStore{ - path: path, - attrCache: NewAttrCache(), - } -} - -// Path returns path to the store's data file. -func (s *AttrStore) Path() string { return s.path } - -// Open opens and initializes the store. -func (s *AttrStore) Open() error { - // Open storage. - db, err := bolt.Open(s.path, 0666, &bolt.Options{Timeout: 1 * time.Second}) - if err != nil { - return err - } - s.db = db - - // Initialize database. - if err := s.db.Update(func(tx *bolt.Tx) error { - if _, err := tx.CreateBucketIfNotExists([]byte("attrs")); err != nil { - return err - } - return nil - }); err != nil { - return err - } +// Path is a no-op implementation of AttrStore Path method. +func (s *nopAttrStore) Path() string { return "" } +// Open is a no-op implementation of AttrStore Open method. +func (s *nopAttrStore) Open() error { return nil } -// Close closes the store. -func (s *AttrStore) Close() error { - if s.db != nil { - s.db.Close() - } +// Close is a no-op implementation of AttrStore Close method. +func (s *nopAttrStore) Close() error { return nil } -// Attrs returns a set of attributes by ID. -func (s *AttrStore) Attrs(id uint64) (m map[string]interface{}, err error) { - s.mu.RLock() - defer s.mu.RUnlock() - - // Check cache for map. - if m = s.attrCache.Get(id); m != nil { - return m, nil - } - - // Find attributes from storage. - if err = s.db.View(func(tx *bolt.Tx) error { - m, err = txAttrs(tx, id) - if err != nil { - return err - } - return nil - }); err != nil { - return nil, err - } - - // Add to cache. - s.attrCache.Set(id, m) - - return +// Attrs is a no-op implementation of AttrStore Attrs method. +func (s *nopAttrStore) Attrs(id uint64) (m map[string]interface{}, err error) { + return nil, nil } -// SetAttrs sets attribute values for a given ID. -func (s *AttrStore) SetAttrs(id uint64, m map[string]interface{}) error { - // Ignore empty maps. - if len(m) == 0 { - return nil - } - - // Check if the attributes already exist under a read-only lock. - if attr, err := s.Attrs(id); err != nil { - return err - } else if attr != nil && mapContains(attr, m) { - return nil - } - - // Obtain write lock. - s.mu.Lock() - defer s.mu.Unlock() - - var attr map[string]interface{} - if err := s.db.Update(func(tx *bolt.Tx) error { - tmp, err := txUpdateAttrs(tx, id, m) - if err != nil { - return err - } - attr = tmp - - return nil - }); err != nil { - return err - } - - // Swap attributes map in cache. - s.attrCache.Set(id, attr) - +// SetAttrs is a no-op implementation of AttrStore SetAttrs method. +func (s *nopAttrStore) SetAttrs(id uint64, m map[string]interface{}) error { return nil } -// SetBulkAttrs sets attribute values for a set of ids. -func (s *AttrStore) SetBulkAttrs(m map[uint64]map[string]interface{}) error { - s.mu.Lock() - defer s.mu.Unlock() +// SetBulkAttrs is a no-op implementation of AttrStore SetBulkAttrs method. +func (s *nopAttrStore) SetBulkAttrs(m map[uint64]map[string]interface{}) error { + return nil +} - attrs := make(map[uint64]map[string]interface{}) - if err := s.db.Update(func(tx *bolt.Tx) error { - // Collect and sort keys. - ids := make([]uint64, 0, len(m)) - for id := range m { - ids = append(ids, id) +// Blocks is a no-op implementation of AttrStore Blocks method. +func (s *nopAttrStore) Blocks() ([]AttrBlock, error) { + return nil, nil +} + +// BlockData is a no-op implementation of AttrStore BlockData method. +func (s *nopAttrStore) BlockData(i uint64) (map[uint64]map[string]interface{}, error) { + return nil, nil +} + +// AttrBlock represents a checksummed block of the attribute store. +type AttrBlock struct { + ID uint64 `json:"id"` + Checksum []byte `json:"checksum"` +} + +// AttrBlocks represents a list of blocks. +type AttrBlocks []AttrBlock + +// Diff returns a list of block ids that are different or are new in other. +// Block lists must be in sorted order. +func (a AttrBlocks) Diff(other []AttrBlock) []uint64 { + var ids []uint64 + for { + // Read next block from each list. + var blk0, blk1 *AttrBlock + if len(a) > 0 { + blk0 = &a[0] + } + if len(other) > 0 { + blk1 = &other[0] } - sort.Sort(uint64Slice(ids)) - // Update attributes for each id. - for _, id := range ids { - attr, err := txUpdateAttrs(tx, id, m[id]) - if err != nil { - return err + // Exit if "a" contains no more blocks. + if blk0 == nil { + return ids + } + + // Add block ID if it's different or if it's only in "a". + if blk1 == nil || blk0.ID < blk1.ID { + ids = append(ids, blk0.ID) + a = a[1:] + } else if blk1.ID < blk0.ID { + other = other[1:] + } else { + if !bytes.Equal(blk0.Checksum, blk1.Checksum) { + ids = append(ids, blk0.ID) } - attrs[id] = attr - } - - return nil - }); err != nil { - return err - } - - // Swap attributes map in cache. - for id, attr := range attrs { - s.attrCache.Set(id, attr) - } - - return nil -} - -// Blocks returns a list of all blocks in the store. -func (s *AttrStore) Blocks() ([]AttrBlock, error) { - tx, err := s.db.Begin(false) - if err != nil { - return nil, err - } - defer tx.Rollback() - - // Wrap cursor to segment by block. - cur := newBlockCursor(tx.Bucket([]byte("attrs")).Cursor(), AttrBlockSize) - - // Iterate over each block. - var blocks []AttrBlock - for cur.nextBlock() { - block := AttrBlock{ID: cur.blockID()} - - // Compute checksum of every key/value in block. - h := xxhash.New() - for k, v := cur.next(); k != nil; k, v = cur.next() { - h.Write(k) - h.Write(v) - } - block.Checksum = h.Sum(nil) - - // Append block. - blocks = append(blocks, block) - } - - return blocks, nil -} - -// BlockData returns all data for a single block. -func (s *AttrStore) BlockData(i uint64) (map[uint64]map[string]interface{}, error) { - m := make(map[uint64]map[string]interface{}) - - // Start read-only transaction. - tx, err := s.db.Begin(false) - if err != nil { - return nil, err - } - defer tx.Rollback() - - // Move to the start of the block. - min := u64tob(uint64(i) * AttrBlockSize) - max := u64tob(uint64(i+1) * AttrBlockSize) - cur := tx.Bucket([]byte("attrs")).Cursor() - for k, v := cur.Seek(min); k != nil; k, v = cur.Next() { - // Exit if we're past the end of the block. - if bytes.Compare(k, max) != -1 { - break - } - - // Decode attribute map and associate with id. - var pb internal.AttrMap - if err := proto.Unmarshal(v, &pb); err != nil { - return nil, err - } - m[btou64(k)] = decodeAttrs(pb.GetAttrs()) - } - - return m, nil -} - -// txAttrs returns a map of attributes for an id. -func txAttrs(tx *bolt.Tx, id uint64) (map[string]interface{}, error) { - v := tx.Bucket([]byte("attrs")).Get(u64tob(id)) - if v == nil { - return emptyMap, nil - } - - var pb internal.AttrMap - if err := proto.Unmarshal(v, &pb); err != nil { - return nil, err - } - return decodeAttrs(pb.GetAttrs()), nil -} - -// txUpdateAttrs updates the attributes for an id. -// Returns the new combined set of attributes for the id. -func txUpdateAttrs(tx *bolt.Tx, id uint64, m map[string]interface{}) (map[string]interface{}, error) { - attr, err := txAttrs(tx, id) - if err != nil { - return nil, err - } - - // Create a new map if it is empty so we don't update emptyMap. - if len(attr) == 0 { - attr = make(map[string]interface{}, len(m)) - } - - // Merge attributes with original values. - // Nil values should delete keys. - for k, v := range m { - if v == nil { - delete(attr, k) - continue - } - - switch v := v.(type) { - case int: - attr[k] = int64(v) - case uint: - attr[k] = int64(v) - case uint64: - attr[k] = int64(v) - case string, int64, bool, float64: - attr[k] = v - default: - return nil, fmt.Errorf("invalid attr type: %T", v) + a, other = a[1:], other[1:] } } - - // Marshal and save new values. - buf, err := proto.Marshal(&internal.AttrMap{Attrs: encodeAttrs(attr)}) - if err != nil { - return nil, err - } - if err := tx.Bucket([]byte("attrs")).Put(u64tob(id), buf); err != nil { - return nil, err - } - return attr, nil } func encodeAttrs(m map[string]interface{}) []*internal.Attr { @@ -421,136 +208,16 @@ func cloneAttrs(m map[string]interface{}) map[string]interface{} { return other } -// u64tob encodes v to big endian encoding. -func u64tob(v uint64) []byte { - b := make([]byte, 8) - binary.BigEndian.PutUint64(b, v) - return b +// EncodeAttrs encodes an attribute map into a byte slice. +func EncodeAttrs(attr map[string]interface{}) ([]byte, error) { + return proto.Marshal(&internal.AttrMap{Attrs: encodeAttrs(attr)}) } -// btou64 decodes b from big endian encoding. -func btou64(b []byte) uint64 { return binary.BigEndian.Uint64(b) } - -// emptyMap is a reusable map that contains no keys. -var emptyMap = make(map[string]interface{}) - -// AttrBlock represents a checksummed block of the attribute store. -type AttrBlock struct { - ID uint64 `json:"id"` - Checksum []byte `json:"checksum"` -} - -// AttrBlocks represents a list of blocks. -type AttrBlocks []AttrBlock - -// Diff returns a list of block ids that are different or are new in other. -// Block lists must be in sorted order. -func (a AttrBlocks) Diff(other []AttrBlock) []uint64 { - var ids []uint64 - for { - // Read next block from each list. - var blk0, blk1 *AttrBlock - if len(a) > 0 { - blk0 = &a[0] - } - if len(other) > 0 { - blk1 = &other[0] - } - - // Exit if "a" contains no more blocks. - if blk0 == nil { - return ids - } - - // Add block ID if it's different or if it's only in "a". - if blk1 == nil || blk0.ID < blk1.ID { - ids = append(ids, blk0.ID) - a = a[1:] - } else if blk1.ID < blk0.ID { - other = other[1:] - } else { - if !bytes.Equal(blk0.Checksum, blk1.Checksum) { - ids = append(ids, blk0.ID) - } - a, other = a[1:], other[1:] - } - } -} - -// blockCursor represents a cursor for iterating over blocks of a bolt bucket. -type blockCursor struct { - cur *bolt.Cursor - base uint64 - n uint64 - - buf struct { - key []byte - value []byte - filled bool - } -} - -// newBlockCursor returns a new block cursor that wraps cur using n sized blocks. -func newBlockCursor(c *bolt.Cursor, n int) blockCursor { - cur := blockCursor{ - cur: c, - n: uint64(n), - } - cur.buf.key, cur.buf.value = c.First() - cur.buf.filled = true - return cur -} - -// blockID returns the current block ID. Only valid after call to nextBlock(). -func (cur *blockCursor) blockID() uint64 { return cur.base } - -// nextBlock moves the cursor to the next block. -// Returns true if another block exists, otherwise returns false. -func (cur *blockCursor) nextBlock() bool { - if cur.buf.key == nil { - return false - } - - cur.base = binary.BigEndian.Uint64(cur.buf.key) / cur.n - return true -} - -// next returns the next key/value within the block. -// Returns nils at the end of the block. -func (cur *blockCursor) next() (key, value []byte) { - // Use buffered value, if set. - if cur.buf.filled { - key, value = cur.buf.key, cur.buf.value - cur.buf.filled = false - return key, value - } - - // Read next key. - key, value = cur.cur.Next() - - // Fill buffer for EOF. - if key == nil { - cur.buf.key, cur.buf.value, cur.buf.filled = key, value, false - return nil, nil - } - - // Parse key and buffer if outside of block. - id := binary.BigEndian.Uint64(key) - if id/cur.n > cur.base { - cur.buf.key, cur.buf.value, cur.buf.filled = key, value, true - return nil, nil - } - - return key, value -} - -// mapContains returns true if all keys & values of subset are in m. -func mapContains(m, subset map[string]interface{}) bool { - for k, v := range subset { - value, ok := m[k] - if !ok || value != v { - return false - } - } - return true +// DecodeAttrs decodes a byte slice into an attribute map. +func DecodeAttrs(v []byte) (map[string]interface{}, error) { + var pb internal.AttrMap + if err := proto.Unmarshal(v, &pb); err != nil { + return nil, err + } + return decodeAttrs(pb.GetAttrs()), nil } diff --git a/boltdb/attrstore.go b/boltdb/attrstore.go new file mode 100644 index 000000000..ebb539903 --- /dev/null +++ b/boltdb/attrstore.go @@ -0,0 +1,465 @@ +// Copyright 2017 Pilosa Corp. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package boltdb + +import ( + "bytes" + + "encoding/binary" + "fmt" + "sort" + "sync" + "time" + + "github.com/cespare/xxhash" + + "github.com/boltdb/bolt" + "github.com/pilosa/pilosa" +) + +// AttrBlockSize is the size of attribute blocks for anti-entropy. +const AttrBlockSize = 100 + +// AttrCache represents a cache for attributes. +type AttrCache struct { + mu sync.RWMutex + attrs map[uint64]map[string]interface{} +} + +// Get returns the cached attributes for a given id. +func (c *AttrCache) Get(id uint64) map[string]interface{} { + c.mu.RLock() + defer c.mu.RUnlock() + attrs := c.attrs[id] + if attrs == nil { + return nil + } + + // Make a copy for safety + ret := make(map[string]interface{}) + for k, v := range attrs { + ret[k] = v + } + return ret +} + +// Set updates the cached attributes for a given id. +func (c *AttrCache) Set(id uint64, attrs map[string]interface{}) { + c.mu.Lock() + defer c.mu.Unlock() + c.attrs[id] = attrs +} + +// AttrStore represents a storage layer for attributes. +type AttrStore struct { + mu sync.RWMutex + path string + db *bolt.DB + attrCache *AttrCache +} + +// NewAttrCache returns a new instance of AttrCache. +func NewAttrCache() *AttrCache { + return &AttrCache{ + attrs: make(map[uint64]map[string]interface{}), + } +} + +// NewAttrStore returns a new instance of AttrStore. +func NewAttrStore(path string) pilosa.AttrStore { + return &AttrStore{ + path: path, + attrCache: NewAttrCache(), + } +} + +// Path returns path to the store's data file. +func (s *AttrStore) Path() string { return s.path } + +// Open opens and initializes the store. +func (s *AttrStore) Open() error { + // Open storage. + db, err := bolt.Open(s.path, 0666, &bolt.Options{Timeout: 1 * time.Second}) + if err != nil { + return err + } + s.db = db + + // Initialize database. + if err := s.db.Update(func(tx *bolt.Tx) error { + if _, err := tx.CreateBucketIfNotExists([]byte("attrs")); err != nil { + return err + } + return nil + }); err != nil { + return err + } + + return nil +} + +// Close closes the store. +func (s *AttrStore) Close() error { + if s.db != nil { + s.db.Close() + } + return nil +} + +// Attrs returns a set of attributes by ID. +func (s *AttrStore) Attrs(id uint64) (m map[string]interface{}, err error) { + s.mu.RLock() + defer s.mu.RUnlock() + + // Check cache for map. + if m = s.attrCache.Get(id); m != nil { + return m, nil + } + + // Find attributes from storage. + if err = s.db.View(func(tx *bolt.Tx) error { + m, err = txAttrs(tx, id) + if err != nil { + return err + } + return nil + }); err != nil { + return nil, err + } + + // Add to cache. + s.attrCache.Set(id, m) + + return +} + +// SetAttrs sets attribute values for a given ID. +func (s *AttrStore) SetAttrs(id uint64, m map[string]interface{}) error { + // Ignore empty maps. + if len(m) == 0 { + return nil + } + + // Check if the attributes already exist under a read-only lock. + if attr, err := s.Attrs(id); err != nil { + return err + } else if attr != nil && mapContains(attr, m) { + return nil + } + + // Obtain write lock. + s.mu.Lock() + defer s.mu.Unlock() + + var attr map[string]interface{} + if err := s.db.Update(func(tx *bolt.Tx) error { + tmp, err := txUpdateAttrs(tx, id, m) + if err != nil { + return err + } + attr = tmp + + return nil + }); err != nil { + return err + } + + // Swap attributes map in cache. + s.attrCache.Set(id, attr) + + return nil +} + +// SetBulkAttrs sets attribute values for a set of ids. +func (s *AttrStore) SetBulkAttrs(m map[uint64]map[string]interface{}) error { + s.mu.Lock() + defer s.mu.Unlock() + + attrs := make(map[uint64]map[string]interface{}) + if err := s.db.Update(func(tx *bolt.Tx) error { + // Collect and sort keys. + ids := make([]uint64, 0, len(m)) + for id := range m { + ids = append(ids, id) + } + sort.Sort(uint64Slice(ids)) + + // Update attributes for each id. + for _, id := range ids { + attr, err := txUpdateAttrs(tx, id, m[id]) + if err != nil { + return err + } + attrs[id] = attr + } + + return nil + }); err != nil { + return err + } + + // Swap attributes map in cache. + for id, attr := range attrs { + s.attrCache.Set(id, attr) + } + + return nil +} + +// Blocks returns a list of all blocks in the store. +func (s *AttrStore) Blocks() ([]pilosa.AttrBlock, error) { + tx, err := s.db.Begin(false) + if err != nil { + return nil, err + } + defer tx.Rollback() + + // Wrap cursor to segment by block. + cur := newBlockCursor(tx.Bucket([]byte("attrs")).Cursor(), AttrBlockSize) + + // Iterate over each block. + var blocks []pilosa.AttrBlock + for cur.nextBlock() { + block := pilosa.AttrBlock{ID: cur.blockID()} + + // Compute checksum of every key/value in block. + h := xxhash.New() + for k, v := cur.next(); k != nil; k, v = cur.next() { + h.Write(k) + h.Write(v) + } + block.Checksum = h.Sum(nil) + + // Append block. + blocks = append(blocks, block) + } + + return blocks, nil +} + +// BlockData returns all data for a single block. +func (s *AttrStore) BlockData(i uint64) (map[uint64]map[string]interface{}, error) { + m := make(map[uint64]map[string]interface{}) + + // Start read-only transaction. + tx, err := s.db.Begin(false) + if err != nil { + return nil, err + } + defer tx.Rollback() + + // Move to the start of the block. + min := u64tob(uint64(i) * AttrBlockSize) + max := u64tob(uint64(i+1) * AttrBlockSize) + cur := tx.Bucket([]byte("attrs")).Cursor() + for k, v := cur.Seek(min); k != nil; k, v = cur.Next() { + // Exit if we're past the end of the block. + if bytes.Compare(k, max) != -1 { + break + } + + // Decode attribute map and associate with id. + attrs, err := pilosa.DecodeAttrs(v) + if err != nil { + return nil, err + } + m[btou64(k)] = attrs + + } + + return m, nil +} + +// txAttrs returns a map of attributes for an id. +func txAttrs(tx *bolt.Tx, id uint64) (map[string]interface{}, error) { + v := tx.Bucket([]byte("attrs")).Get(u64tob(id)) + if v == nil { + return emptyMap, nil + } + return pilosa.DecodeAttrs(v) +} + +// txUpdateAttrs updates the attributes for an id. +// Returns the new combined set of attributes for the id. +func txUpdateAttrs(tx *bolt.Tx, id uint64, m map[string]interface{}) (map[string]interface{}, error) { + attr, err := txAttrs(tx, id) + if err != nil { + return nil, err + } + + // Create a new map if it is empty so we don't update emptyMap. + if len(attr) == 0 { + attr = make(map[string]interface{}, len(m)) + } + + // Merge attributes with original values. + // Nil values should delete keys. + for k, v := range m { + if v == nil { + delete(attr, k) + continue + } + + switch v := v.(type) { + case int: + attr[k] = int64(v) + case uint: + attr[k] = int64(v) + case uint64: + attr[k] = int64(v) + case string, int64, bool, float64: + attr[k] = v + default: + return nil, fmt.Errorf("invalid attr type: %T", v) + } + } + + // Marshal and save new values. + buf, err := pilosa.EncodeAttrs(attr) + if err != nil { + return nil, err + } + if err := tx.Bucket([]byte("attrs")).Put(u64tob(id), buf); err != nil { + return nil, err + } + return attr, nil +} + +// u64tob encodes v to big endian encoding. +func u64tob(v uint64) []byte { + b := make([]byte, 8) + binary.BigEndian.PutUint64(b, v) + return b +} + +// btou64 decodes b from big endian encoding. +func btou64(b []byte) uint64 { return binary.BigEndian.Uint64(b) } + +// emptyMap is a reusable map that contains no keys. +var emptyMap = make(map[string]interface{}) + +// mapContains returns true if all keys & values of subset are in m. +func mapContains(m, subset map[string]interface{}) bool { + for k, v := range subset { + value, ok := m[k] + if !ok || value != v { + return false + } + } + return true +} + +// uint64Slice represents a sortable slice of uint64 numbers. +type uint64Slice []uint64 + +func (p uint64Slice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } +func (p uint64Slice) Len() int { return len(p) } +func (p uint64Slice) Less(i, j int) bool { return p[i] < p[j] } + +// merge combines p and other to a unique sorted set of values. +// p and other must both have unique sets and be sorted. +func (p uint64Slice) merge(other []uint64) []uint64 { + ret := make([]uint64, 0, len(p)) + + i, j := 0, 0 + for i < len(p) && j < len(other) { + a, b := p[i], other[j] + if a == b { + ret = append(ret, a) + i, j = i+1, j+1 + } else if a < b { + ret = append(ret, a) + i++ + } else { + ret = append(ret, b) + j++ + } + } + + if i < len(p) { + ret = append(ret, p[i:]...) + } else if j < len(other) { + ret = append(ret, other[j:]...) + } + + return ret +} + +// blockCursor represents a cursor for iterating over blocks of a bolt bucket. +type blockCursor struct { + cur *bolt.Cursor + base uint64 + n uint64 + + buf struct { + key []byte + value []byte + filled bool + } +} + +// newBlockCursor returns a new block cursor that wraps cur using n sized blocks. +func newBlockCursor(c *bolt.Cursor, n int) blockCursor { + cur := blockCursor{ + cur: c, + n: uint64(n), + } + cur.buf.key, cur.buf.value = c.First() + cur.buf.filled = true + return cur +} + +// blockID returns the current block ID. Only valid after call to nextBlock(). +func (cur *blockCursor) blockID() uint64 { return cur.base } + +// nextBlock moves the cursor to the next block. +// Returns true if another block exists, otherwise returns false. +func (cur *blockCursor) nextBlock() bool { + if cur.buf.key == nil { + return false + } + + cur.base = binary.BigEndian.Uint64(cur.buf.key) / cur.n + return true +} + +// next returns the next key/value within the block. +// Returns nils at the end of the block. +func (cur *blockCursor) next() (key, value []byte) { + // Use buffered value, if set. + if cur.buf.filled { + key, value = cur.buf.key, cur.buf.value + cur.buf.filled = false + return key, value + } + + // Read next key. + key, value = cur.cur.Next() + + // Fill buffer for EOF. + if key == nil { + cur.buf.key, cur.buf.value, cur.buf.filled = key, value, false + return nil, nil + } + + // Parse key and buffer if outside of block. + id := binary.BigEndian.Uint64(key) + if id/cur.n > cur.base { + cur.buf.key, cur.buf.value, cur.buf.filled = key, value, true + return nil, nil + } + + return key, value +} diff --git a/broadcast.go b/broadcast.go index bc3593a2c..de43f3b85 100644 --- a/broadcast.go +++ b/broadcast.go @@ -22,38 +22,27 @@ import ( "github.com/pilosa/pilosa/internal" ) -// NodeSet represents an interface for Node membership and inter-node communication. -type NodeSet interface { - // Returns a list of all Nodes in the cluster - Nodes() []*Node - - // Open starts any network activity implemented by the NodeSet - Open() error +// MemberSet represents an interface for Node membership and inter-node communication. +type MemberSet interface { + // Open starts any network activity implemented by the MemberSet + // Node is the local node, used for membership broadcasts. + Open(n *Node) error } -// StaticNodeSet represents a basic NodeSet for testing. -type StaticNodeSet struct { +// StaticMemberSet represents a basic MemberSet for testing. +type StaticMemberSet struct { nodes []*Node } -// NewStaticNodeSet creates a statically defined NodeSet. -func NewStaticNodeSet() *StaticNodeSet { - return &StaticNodeSet{} +// NewStaticMemberSet creates a statically defined MemberSet. +func NewStaticMemberSet(nodes []*Node) *StaticMemberSet { + return &StaticMemberSet{ + nodes: nodes, + } } -// Nodes implements the NodeSet interface and returns a list of nodes in the cluster. -func (s *StaticNodeSet) Nodes() []*Node { - return s.nodes -} - -// Open implements the NodeSet interface to start network activity, but for a static NodeSet it does nothing. -func (s *StaticNodeSet) Open() error { - return nil -} - -// Join sets the NodeSet nodes to the slice of Nodes passed in. -func (s *StaticNodeSet) Join(nodes []*Node) error { - s.nodes = nodes +// Open implements the MemberSet interface to start network activity, but for a static MemberSet it does nothing. +func (s *StaticMemberSet) Open(n *Node) error { return nil } @@ -61,6 +50,7 @@ func (s *StaticNodeSet) Join(nodes []*Node) error { type Broadcaster interface { SendSync(pb proto.Message) error SendAsync(pb proto.Message) error + SendTo(to *Node, pb proto.Message) error } func init() { @@ -73,16 +63,21 @@ var NopBroadcaster Broadcaster type nopBroadcaster struct{} -// SendSync A no-op implemenetation of Broadcaster SendSync method. +// SendSync A no-op implementation of Broadcaster SendSync method. func (n *nopBroadcaster) SendSync(pb proto.Message) error { return nil } -// SendAsync A no-op implemenetation of Broadcaster SendAsync method. +// SendAsync A no-op implementation of Broadcaster SendAsync method. func (n *nopBroadcaster) SendAsync(pb proto.Message) error { return nil } +// SendTo is a no-op implementation of Broadcaster SendTo method. +func (c *nopBroadcaster) SendTo(to *Node, pb proto.Message) error { + return nil +} + // BroadcastHandler is the interface for the pilosa object which knows how to // handle broadcast messages. (Hint: this is implemented by pilosa.Server) type BroadcastHandler interface { @@ -117,23 +112,32 @@ var NopGossiper Gossiper type nopGossiper struct{} -// SendAsync A no-op implemenetation of Gossiper SendAsync method. +// SendAsync A no-op implementation of Gossiper SendAsync method. func (n *nopGossiper) SendAsync(pb proto.Message) error { return nil } // Broadcast message types. const ( - MessageTypeCreateSlice = 1 - MessageTypeCreateIndex = 2 - MessageTypeDeleteIndex = 3 - MessageTypeCreateFrame = 4 - MessageTypeDeleteFrame = 5 - MessageTypeCreateInputDefinition = 6 - MessageTypeDeleteInputDefinition = 7 - MessageTypeDeleteView = 8 - MessageTypeCreateField = 9 - MessageTypeDeleteField = 10 + MessageTypeCreateSlice = iota + MessageTypeCreateIndex + MessageTypeDeleteIndex + MessageTypeCreateFrame + MessageTypeDeleteFrame + MessageTypeCreateView + MessageTypeDeleteView + MessageTypeCreateField + MessageTypeDeleteField + MessageTypeCreateInputDefinition + MessageTypeDeleteInputDefinition + MessageTypeClusterStatus + MessageTypeResizeInstruction + MessageTypeResizeInstructionComplete + MessageTypeSetCoordinator + MessageTypeUpdateCoordinator + MessageTypeNodeState + MessageTypeRecalculateCaches + MessageTypeNodeEvent ) // MarshalMessage encodes the protobuf message into a byte slice. @@ -150,6 +154,10 @@ func MarshalMessage(m proto.Message) ([]byte, error) { typ = MessageTypeCreateFrame case *internal.DeleteFrameMessage: typ = MessageTypeDeleteFrame + case *internal.CreateViewMessage: + typ = MessageTypeCreateView + case *internal.DeleteViewMessage: + typ = MessageTypeDeleteView case *internal.CreateFieldMessage: typ = MessageTypeCreateField case *internal.DeleteFieldMessage: @@ -158,8 +166,22 @@ func MarshalMessage(m proto.Message) ([]byte, error) { typ = MessageTypeCreateInputDefinition case *internal.DeleteInputDefinitionMessage: typ = MessageTypeDeleteInputDefinition - case *internal.DeleteViewMessage: - typ = MessageTypeDeleteView + case *internal.ClusterStatus: + typ = MessageTypeClusterStatus + case *internal.ResizeInstruction: + typ = MessageTypeResizeInstruction + case *internal.ResizeInstructionComplete: + typ = MessageTypeResizeInstructionComplete + case *internal.SetCoordinatorMessage: + typ = MessageTypeSetCoordinator + case *internal.UpdateCoordinatorMessage: + typ = MessageTypeUpdateCoordinator + case *internal.NodeStateMessage: + typ = MessageTypeNodeState + case *internal.RecalculateCaches: + typ = MessageTypeRecalculateCaches + case *internal.NodeEventMessage: + typ = MessageTypeNodeEvent default: return nil, fmt.Errorf("message type not implemented for marshalling: %s", reflect.TypeOf(obj)) } @@ -186,6 +208,10 @@ func UnmarshalMessage(buf []byte) (proto.Message, error) { m = &internal.CreateFrameMessage{} case MessageTypeDeleteFrame: m = &internal.DeleteFrameMessage{} + case MessageTypeCreateView: + m = &internal.CreateViewMessage{} + case MessageTypeDeleteView: + m = &internal.DeleteViewMessage{} case MessageTypeCreateField: m = &internal.CreateFieldMessage{} case MessageTypeDeleteField: @@ -194,8 +220,22 @@ func UnmarshalMessage(buf []byte) (proto.Message, error) { m = &internal.CreateInputDefinitionMessage{} case MessageTypeDeleteInputDefinition: m = &internal.DeleteInputDefinitionMessage{} - case MessageTypeDeleteView: - m = &internal.DeleteViewMessage{} + case MessageTypeClusterStatus: + m = &internal.ClusterStatus{} + case MessageTypeResizeInstruction: + m = &internal.ResizeInstruction{} + case MessageTypeResizeInstructionComplete: + m = &internal.ResizeInstructionComplete{} + case MessageTypeSetCoordinator: + m = &internal.SetCoordinatorMessage{} + case MessageTypeUpdateCoordinator: + m = &internal.UpdateCoordinatorMessage{} + case MessageTypeNodeState: + m = &internal.NodeStateMessage{} + case MessageTypeRecalculateCaches: + m = &internal.RecalculateCaches{} + case MessageTypeNodeEvent: + m = &internal.NodeEventMessage{} default: return nil, fmt.Errorf("invalid message type: %d", typ) } diff --git a/client.go b/client.go index bbf118399..c2e88c77b 100644 --- a/client.go +++ b/client.go @@ -89,9 +89,6 @@ func (c *InternalHTTPClient) MaxInverseSliceByIndex(ctx context.Context) (map[st func (c *InternalHTTPClient) maxSliceByIndex(ctx context.Context, inverse bool) (map[string]uint64, error) { // Execute request against the host. u := uriPathToURL(c.clientURI(ctx), "/slices/max") - u.RawQuery = (&url.Values{ - "inverse": {strconv.FormatBool(inverse)}, - }).Encode() // Build request. req, err := http.NewRequest("GET", u.String(), nil) @@ -108,14 +105,17 @@ func (c *InternalHTTPClient) maxSliceByIndex(ctx context.Context, inverse bool) } defer resp.Body.Close() - var rsp sliceMaxResponse + var rsp getSlicesMaxResponse if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("http: status=%d", resp.StatusCode) } else if err := json.NewDecoder(resp.Body).Decode(&rsp); err != nil { return nil, fmt.Errorf("json decode: %s", err) } - return rsp.MaxSlices, nil + if inverse { + return rsp.Inverse, nil + } + return rsp.Standard, nil } // Schema returns all index and frame schema information. @@ -296,7 +296,7 @@ func (c *InternalHTTPClient) Import(ctx context.Context, index, frame string, sl // Import to each node. for _, node := range nodes { if err := c.importNode(ctx, node, buf); err != nil { - return fmt.Errorf("import node: host=%s, err=%s", node.Host, err) + return fmt.Errorf("import node: host=%s, err=%s", node.URI, err) } } @@ -317,13 +317,12 @@ func (c *InternalHTTPClient) ImportK(ctx context.Context, index, frame string, b } node := &Node{ - Scheme: c.defaultURI.Scheme(), - Host: c.defaultURI.HostPort(), + URI: *c.defaultURI, } // Import to node. if err := c.importNode(ctx, node, buf); err != nil { - return fmt.Errorf("import node: host=%s, err=%s", node.Host, err) + return fmt.Errorf("import node: host=%s, err=%s", node.URI, err) } return nil @@ -448,7 +447,7 @@ func (c *InternalHTTPClient) ImportValue(ctx context.Context, index, frame, fiel // Import to each node. for _, node := range nodes { if err := c.importValueNode(ctx, node, buf); err != nil { - return fmt.Errorf("import node: host=%s, err=%s", node.Host, err) + return fmt.Errorf("import node: host=%s, err=%s", node.URI, err) } } @@ -536,7 +535,7 @@ func (c *InternalHTTPClient) ExportCSV(ctx context.Context, index, frame, view s node := nodes[i] if err := c.exportNodeCSV(ctx, node, index, frame, view, slice, w); err != nil { - e = fmt.Errorf("export node: host=%s, err=%s", node.Host, err) + e = fmt.Errorf("export node: host=%s, err=%s", node.URI, err) continue } else { return nil @@ -688,6 +687,13 @@ func (c *InternalHTTPClient) BackupSlice(ctx context.Context, index, frame, view return nil, fmt.Errorf("unable to connect to any owner") } +func (c *InternalHTTPClient) RetrieveSliceFromURI(ctx context.Context, index, frame, view string, slice uint64, uri URI) (io.ReadCloser, error) { + node := &Node{ + URI: uri, + } + return c.backupSliceNode(ctx, index, frame, view, slice, node) +} + func (c *InternalHTTPClient) backupSliceNode(ctx context.Context, index, frame, view string, slice uint64, node *Node) (io.ReadCloser, error) { u := nodePathToURL(node, "/fragment/data") u.RawQuery = url.Values{ @@ -717,7 +723,7 @@ func (c *InternalHTTPClient) backupSliceNode(ctx context.Context, index, frame, return nil, ErrFragmentNotFound } else if resp.StatusCode != http.StatusOK { resp.Body.Close() - return nil, fmt.Errorf("unexpected backup status code: host=%s, code=%d", node.Host, resp.StatusCode) + return nil, fmt.Errorf("unexpected backup status code: host=%s, code=%d", node.URI, resp.StatusCode) } return resp.Body, nil @@ -796,7 +802,7 @@ func (c *InternalHTTPClient) restoreSliceFrom(ctx context.Context, buf []byte, i // Return error if response not OK. if resp.StatusCode != http.StatusOK { - return fmt.Errorf("unexpected status code: host=%s, code=%d", node.Host, resp.StatusCode) + return fmt.Errorf("unexpected status code: host=%s, code=%d", node.URI, resp.StatusCode) } } @@ -1130,6 +1136,8 @@ func (c *InternalHTTPClient) clientURI(ctx context.Context) *URI { clientURI := c.defaultURI if contextURI, ok := ctx.Value("uri").(*URI); ok { clientURI = contextURI + } else if contextURI, ok := ctx.Value("uri").(URI); ok { + clientURI = &contextURI } return clientURI } @@ -1318,8 +1326,8 @@ func uriPathToURL(uri *URI, path string) url.URL { func nodePathToURL(node *Node, path string) url.URL { return url.URL{ - Scheme: node.Scheme, - Host: node.Host, + Scheme: node.URI.Scheme(), + Host: node.URI.HostPort(), Path: path, } } diff --git a/client_test.go b/client_test.go index dfcc3072f..f2bd18e50 100644 --- a/client_test.go +++ b/client_test.go @@ -36,10 +36,10 @@ func createCluster(c *pilosa.Cluster) ([]*test.Server, []*test.Holder) { for i := 0; i < numNodes; i++ { hldr[i] = test.MustOpenHolder() server[i] = test.NewServer() - server[i].Handler.URI = server[i].HostURI() server[i].Handler.Cluster = c - server[i].Handler.Cluster.Nodes[i].Host = server[i].Host() + server[i].Handler.Cluster.Nodes[i].URI = server[i].HostURI() server[i].Handler.Holder = hldr[i].Holder + server[i].Handler.Node = server[i].Handler.Cluster.Nodes[i] } return server, hldr } @@ -64,24 +64,21 @@ func TestClient_MultiNode(t *testing.T) { s[0].Handler.Executor.ExecuteFn = func(ctx context.Context, index string, query *pql.Query, slices []uint64, opt *pilosa.ExecOptions) ([]interface{}, error) { e := pilosa.NewExecutor(defaultClient) e.Holder = hldr[0].Holder - e.Scheme = cluster.Nodes[0].Scheme - e.Host = cluster.Nodes[0].Host + e.Node = cluster.Nodes[0] e.Cluster = cluster return e.Execute(ctx, index, query, slices, opt) } s[1].Handler.Executor.ExecuteFn = func(ctx context.Context, index string, query *pql.Query, slices []uint64, opt *pilosa.ExecOptions) ([]interface{}, error) { e := pilosa.NewExecutor(defaultClient) e.Holder = hldr[1].Holder - e.Scheme = cluster.Nodes[1].Scheme - e.Host = cluster.Nodes[1].Host + e.Node = cluster.Nodes[1] e.Cluster = cluster return e.Execute(ctx, index, query, slices, opt) } s[2].Handler.Executor.ExecuteFn = func(ctx context.Context, index string, query *pql.Query, slices []uint64, opt *pilosa.ExecOptions) ([]interface{}, error) { e := pilosa.NewExecutor(defaultClient) e.Holder = hldr[2].Holder - e.Scheme = cluster.Nodes[2].Scheme - e.Host = cluster.Nodes[2].Host + e.Node = cluster.Nodes[2] e.Cluster = cluster return e.Execute(ctx, index, query, slices, opt) } @@ -89,7 +86,7 @@ func TestClient_MultiNode(t *testing.T) { // Create a dispersed set of bitmaps across 3 nodes such that each individual node and slice width increment would reveal a different TopN. sliceNums := []uint64{1, 2, 6} for i, num := range sliceNums { - owns := s[i].Handler.Handler.Cluster.OwnsSlices("i", 20, s[i].Host()) + owns := s[i].Handler.Handler.Cluster.OwnsSlices("i", 20, s[i].HostURI()) ownsNum := false for _, ownNum := range owns { if ownNum == num { @@ -220,10 +217,10 @@ func TestClient_Import(t *testing.T) { s := test.NewServer() defer s.Close() - s.Handler.URI = s.HostURI() s.Handler.Cluster = test.NewCluster(1) - s.Handler.Cluster.Nodes[0].Host = s.Host() + s.Handler.Cluster.Nodes[0].URI = s.HostURI() s.Handler.Holder = hldr.Holder + s.Handler.Node = s.Handler.Cluster.Nodes[0] // Send import request. c := test.MustNewClient(s.Host(), defaultClient) @@ -271,10 +268,10 @@ func TestClient_ImportInverseEnabled(t *testing.T) { s := test.NewServer() defer s.Close() - s.Handler.URI = s.HostURI() s.Handler.Cluster = test.NewCluster(1) - s.Handler.Cluster.Nodes[0].Host = s.Host() + s.Handler.Cluster.Nodes[0].URI = s.HostURI() s.Handler.Holder = hldr.Holder + s.Handler.Node = s.Handler.Cluster.Nodes[0] // Send import request. c := test.MustNewClient(s.Host(), defaultClient) @@ -320,10 +317,10 @@ func TestClient_ImportValue(t *testing.T) { s := test.NewServer() defer s.Close() - s.Handler.URI = s.HostURI() s.Handler.Cluster = test.NewCluster(1) - s.Handler.Cluster.Nodes[0].Host = s.Host() + s.Handler.Cluster.Nodes[0].URI = s.HostURI() s.Handler.Holder = hldr.Holder + s.Handler.Node = s.Handler.Cluster.Nodes[0] // Send import request. c := test.MustNewClient(s.Host(), defaultClient) @@ -358,10 +355,10 @@ func TestClient_BackupRestore(t *testing.T) { s := test.NewServer() defer s.Close() - s.Handler.URI = s.HostURI() s.Handler.Cluster = test.NewCluster(1) - s.Handler.Cluster.Nodes[0].Host = s.Host() + s.Handler.Cluster.Nodes[0].URI = s.HostURI() s.Handler.Holder = hldr.Holder + s.Handler.Node = s.Handler.Cluster.Nodes[0] c := test.MustNewClient(s.Host(), defaultClient) @@ -423,10 +420,10 @@ func TestClient_BackupInverseView(t *testing.T) { s := test.NewServer() defer s.Close() - s.Handler.URI = s.HostURI() s.Handler.Cluster = test.NewCluster(1) - s.Handler.Cluster.Nodes[0].Host = s.Host() + s.Handler.Cluster.Nodes[0].URI = s.HostURI() s.Handler.Holder = hldr.Holder + s.Handler.Node = s.Handler.Cluster.Nodes[0] c := test.MustNewClient(s.Host(), defaultClient) @@ -460,10 +457,10 @@ func TestClient_BackupInvalidView(t *testing.T) { s := test.NewServer() defer s.Close() - s.Handler.URI = s.HostURI() s.Handler.Cluster = test.NewCluster(1) - s.Handler.Cluster.Nodes[0].Host = s.Host() + s.Handler.Cluster.Nodes[0].URI = s.HostURI() s.Handler.Holder = hldr.Holder + s.Handler.Node = s.Handler.Cluster.Nodes[0] c := test.MustNewClient(s.Host(), defaultClient) @@ -489,10 +486,10 @@ func TestClient_FragmentBlocks(t *testing.T) { s := test.NewServer() defer s.Close() - s.Handler.URI = s.HostURI() s.Handler.Cluster = test.NewCluster(1) - s.Handler.Cluster.Nodes[0].Host = s.Host() + s.Handler.Cluster.Nodes[0].URI = s.HostURI() s.Handler.Holder = hldr.Holder + s.Handler.Node = s.Handler.Cluster.Nodes[0] // Retrieve blocks. c := test.MustNewClient(s.Host(), defaultClient) diff --git a/cluster.go b/cluster.go index d51e1f88f..a5b7b654f 100644 --- a/cluster.go +++ b/cluster.go @@ -15,12 +15,27 @@ package pilosa import ( + "context" "encoding/binary" + "errors" + "fmt" "hash/fnv" + "io" + "io/ioutil" + "log" + "math/rand" + "net/http" + "os" + "path/filepath" + "sort" "sync" "time" + "golang.org/x/sync/errgroup" + + "github.com/gogo/protobuf/proto" "github.com/pilosa/pilosa/internal" + uuid "github.com/satori/go.uuid" ) const ( @@ -29,55 +44,81 @@ const ( // DefaultReplicaN is the default number of replicas per partition. DefaultReplicaN = 1 -) -// NodeState represents node state returned in /status endpoint for a node in the cluster. -const ( - NodeStateUp = "UP" - NodeStateDown = "DOWN" + // ClusterState represents the state returned in the /status endpoint. + ClusterStateStarting = "STARTING" + ClusterStateNormal = "NORMAL" + ClusterStateResizing = "RESIZING" + + // NodeState represents the state of a node during startup. + NodeStateLoading = "LOADING" + NodeStateReady = "READY" + + // ResizeJob states. + ResizeJobStateRunning = "RUNNING" + // Final states. + ResizeJobStateDone = "DONE" + ResizeJobStateAborted = "ABORTED" + + ResizeJobActionAdd = "ADD" + ResizeJobActionRemove = "REMOVE" ) // Node represents a node in the cluster. type Node struct { - Scheme string `json:"scheme"` - Host string `json:"host"` - - mu sync.RWMutex - status *internal.NodeStatus `json:"status"` + ID string `json:"id"` + URI URI `json:"uri"` + IsCoordinator bool `json:"isCoordinator"` } -// Status gets the NodeStatus. -func (n *Node) Status() *internal.NodeStatus { - n.mu.RLock() - defer n.mu.RUnlock() - return n.status +func (n Node) String() string { + return fmt.Sprintf("Node: %s", n.ID) } -// SetStatus sets the NodeStatus. -func (n *Node) SetStatus(s *internal.NodeStatus) { - n.mu.Lock() - n.status = s - n.mu.Unlock() -} - -// SetState sets the Node.status.state. -func (n *Node) SetState(s string) { - n.mu.Lock() - defer n.mu.Unlock() - if n.status == nil { - n.status = &internal.NodeStatus{} +// EncodeNodes converts a slice of Nodes into its internal representation. +func EncodeNodes(a []*Node) []*internal.Node { + other := make([]*internal.Node, len(a)) + for i := range a { + other[i] = EncodeNode(a[i]) } - n.status.State = s + return other } -// URI returns the pilosa.URI corresponding to this node -func (n *Node) URI() (*URI, error) { - uri, err := NewURIFromAddress(n.Host) - if err != nil { - return nil, err +// EncodeNode converts a Node into its internal representation. +func EncodeNode(n *Node) *internal.Node { + return &internal.Node{ + ID: n.ID, + URI: n.URI.Encode(), + IsCoordinator: n.IsCoordinator, + } +} + +// DecodeNodes converts a proto message into a slice of Nodes. +func DecodeNodes(a []*internal.Node) []*Node { + if len(a) == 0 { + return nil + } + other := make([]*Node, len(a)) + for i := range a { + other[i] = DecodeNode(a[i]) + } + return other +} + +// DecodeNode converts a proto message into a Node. +func DecodeNode(node *internal.Node) *Node { + return &Node{ + ID: node.ID, + URI: decodeURI(node.URI), + IsCoordinator: node.IsCoordinator, + } +} + +func DecodeNodeEvent(ne *internal.NodeEventMessage) *NodeEvent { + return &NodeEvent{ + Event: NodeEventType(ne.Event), + Node: DecodeNode(ne.Node), } - uri.SetScheme(n.Scheme) - return uri, nil } // Nodes represents a list of nodes. @@ -93,10 +134,10 @@ func (a Nodes) Contains(n *Node) bool { return false } -// ContainsHost returns true if host matches one of the node's host. -func (a Nodes) ContainsHost(host string) bool { +// ContainsID returns true if host matches one of the node's id. +func (a Nodes) ContainsID(id string) bool { for _, n := range a { - if n.Host == host { + if n.ID == id { return true } } @@ -114,24 +155,44 @@ func (a Nodes) Filter(n *Node) []*Node { return other } -// FilterHost returns a new list of nodes with host removed. -func (a Nodes) FilterHost(host string) []*Node { +// FilterID returns a new list of nodes with ID removed. +func (a Nodes) FilterID(id string) []*Node { other := make([]*Node, 0, len(a)) for _, node := range a { - if node.Host != host { + if node.ID != id { other = append(other, node) } } return other } -// Hosts returns a list of all hostnames. -func (a Nodes) Hosts() []string { - hosts := make([]string, len(a)) - for i, n := range a { - hosts[i] = n.Host +// FilterURI returns a new list of nodes with URI removed. +func (a Nodes) FilterURI(uri URI) []*Node { + other := make([]*Node, 0, len(a)) + for _, node := range a { + if node.URI != uri { + other = append(other, node) + } } - return hosts + return other +} + +// IDs returns a list of all node IDs. +func (a Nodes) IDs() []string { + ids := make([]string, len(a)) + for i, n := range a { + ids[i] = n.ID + } + return ids +} + +// URIs returns a list of all uris. +func (a Nodes) URIs() []URI { + uris := make([]URI, len(a)) + for i, n := range a { + uris[i] = n.URI + } + return uris } // Clone returns a shallow copy of nodes. @@ -141,10 +202,26 @@ func (a Nodes) Clone() []*Node { return other } +// byID implements sort.Interface for []Node based on +// the ID field. +type byID []*Node + +func (h byID) Len() int { return len(h) } +func (h byID) Swap(i, j int) { h[i], h[j] = h[j], h[i] } +func (h byID) Less(i, j int) bool { return h[i].ID < h[j].ID } + +// nodeAction represents a node that is joining or leaving the cluster. +type nodeAction struct { + node *Node + action string +} + // Cluster represents a collection of nodes. type Cluster struct { - Nodes []*Node - NodeSet NodeSet + ID string + Node *Node + Nodes []*Node // TODO phase this out? + MemberSet MemberSet // Hashing algorithm used to assign partitions to nodes. Hasher Hasher @@ -160,6 +237,42 @@ type Cluster struct { // Maximum number of SetBit() or ClearBit() commands per request. MaxWritesPerRequest int + + // EventReceiver receives NodeEvents pertaining to node membership. + EventReceiver EventReceiver + + // Data directory path. + Path string + Topology *Topology + + // Required for cluster Resize. + Static bool // Static is primarily used for testing in a non-gossip environment. + state string + Coordinator string + Holder *Holder + Broadcaster Broadcaster + + joiningLeavingNodes chan nodeAction + + // joining is held open until this node + // receives ClusterStatus from the coordinator. + joining chan struct{} + joined bool + + mu sync.RWMutex + jobs map[int64]*ResizeJob + currentJob *ResizeJob + + // Close management + wg sync.WaitGroup + closing chan struct{} + prefect SecurityManager + + // The writer for any logging. + LogOutput io.Writer + + // + RemoteClient *http.Client } // NewCluster returns a new instance of Cluster with defaults. @@ -169,62 +282,527 @@ func NewCluster() *Cluster { PartitionN: DefaultPartitionN, ReplicaN: DefaultReplicaN, MaxWritesPerRequest: DefaultMaxWritesPerRequest, + EventReceiver: NopEventReceiver, + + joiningLeavingNodes: make(chan nodeAction, 10), // buffered channel + jobs: make(map[int64]*ResizeJob), + closing: make(chan struct{}), + joining: make(chan struct{}), + + LogOutput: os.Stderr, + prefect: &NopSecurityManager{}, } } -// NodeSetHosts returns the list of host strings for NodeSet members. -func (c *Cluster) NodeSetHosts() []string { - if c.NodeSet == nil { - return []string{} - } - a := make([]string, 0, len(c.NodeSet.Nodes())) - for _, m := range c.NodeSet.Nodes() { - a = append(a, m.Host) - } - return a +// logger returns a logger for the cluster. +func (c *Cluster) logger() *log.Logger { + return log.New(c.LogOutput, "", log.LstdFlags) } -// NodeStates returns a map of nodes in the cluster with each node's state (UP/DOWN) as the value. -func (c *Cluster) NodeStates() map[string]string { - h := make(map[string]string) - for _, n := range c.Nodes { - h[n.Host] = NodeStateDown +// Coordinator returns the coordinator node. +func (c *Cluster) CoordinatorNode() *Node { + return c.nodeByID(c.Coordinator) +} + +// IsCoordinator is true if this node is the coordinator. +func (c *Cluster) IsCoordinator() bool { + return c.Coordinator == c.Node.ID +} + +// SetCoordinator tells the current node to become the +// Coordinator. In response to this, the current node +// will consider itself coordinator and update the other +// nodes with its version of Cluster.Status. +func (c *Cluster) SetCoordinator(n *Node) error { + // Verify that the new Coordinator value matches + // this node. + if c.Node.ID != n.ID { + return fmt.Errorf("coordinator node does not match this node") } - // we are assuming that NodeSetHosts is a subset of c.Nodes - for _, m := range c.NodeSetHosts() { - if _, ok := h[m]; ok { - h[m] = NodeStateUp + + // Update IsCoordinator on all nodes (locally). + _ = c.UpdateCoordinator(n) + + // Send the update coordinator message to all nodes. + err := c.Broadcaster.SendSync( + &internal.UpdateCoordinatorMessage{ + New: EncodeNode(n), + }) + if err != nil { + return fmt.Errorf("problem sending UpdateCoordinator message: %v", err) + } + + // Broadcast cluster status. + return c.Broadcaster.SendSync(c.Status()) +} + +// UpdateCoordinator updates this nodes Coordinator value as well as +// changing the corresponding node's IsCoordinator value +// to true, and sets all other nodes to false. Returns true if the value +// changed. +func (c *Cluster) UpdateCoordinator(n *Node) bool { + var changed bool + if c.Coordinator != n.ID { + c.Coordinator = n.ID + changed = true + } + for _, node := range c.Nodes { + if node.ID == n.ID { + node.IsCoordinator = true + } else { + node.IsCoordinator = false } } - return h + return changed } +// AddNode adds a node to the Cluster and updates and saves the +// new topology. +func (c *Cluster) AddNode(node *Node) error { + c.logger().Printf("add node %s to cluster on %s", node, c.Node) + + // If the node being added is the coordinator, set it for this node. + if node.IsCoordinator { + c.Coordinator = node.ID + } + + // add to cluster + if !c.addNodeBasicSorted(node) { + return nil + } + + // add to topology + if c.Topology == nil { + return fmt.Errorf("Cluster.Topology is nil") + } + if !c.Topology.AddID(node.ID) { + return nil + } + + // save topology + return c.saveTopology() +} + +// RemoveNode removes a node from the Cluster and updates and saves the +// new topology. +func (c *Cluster) RemoveNode(node *Node) error { + // remove from cluster + if !c.removeNodeBasicSorted(node) { + return nil + } + + // remove from topology + if c.Topology == nil { + return fmt.Errorf("Cluster.Topology is nil") + } + if !c.Topology.RemoveID(node.ID) { + return nil + } + + // save topology + return c.saveTopology() +} + +// NodeIDs returns the list of IDs in the cluster. +func (c *Cluster) NodeIDs() []string { + return Nodes(c.Nodes).IDs() +} + +func (c *Cluster) setID(id string) { + // Don't overwrite ClusterID. + if c.ID != "" { + return + } + c.ID = id + + // Make sure the Topology is updated. + c.Topology.ClusterID = c.ID +} + +func (c *Cluster) State() string { + c.mu.RLock() + defer c.mu.RUnlock() + return c.state +} + +func (c *Cluster) SetState(state string) { + c.mu.Lock() + c.setState(state) + c.mu.Unlock() +} + +func (c *Cluster) setState(state string) { + // Ignore cases where the state hasn't changed. + if state == c.state { + return + } + + c.logger().Printf("change cluster state from %s to %s on %s", c.state, state, c.Node.ID) + + var doCleanup bool + + switch state { + case ClusterStateResizing: + c.prefect.SetRestricted() + case ClusterStateNormal: + c.prefect.SetNormal() + // Don't change routing for these states: + // - ClusterStateStarting + + // If state is RESIZING -> NORMAL then run cleanup. + if c.state == ClusterStateResizing { + doCleanup = true + } + default: + panic(fmt.Sprintf("invalid cluster state: %s", state)) + } + + c.state = state + + // TODO: consider NOT running cleanup on an active node that has + // been removed. + // It's safe to do a cleanup after state changes back to normal. + if doCleanup { + var cleaner HolderCleaner + cleaner.Node = c.Node + cleaner.Holder = c.Holder + cleaner.Cluster = c + cleaner.Closing = c.closing + + // Clean holder. + if err := cleaner.CleanHolder(); err != nil { + c.logger().Printf("holder clean error: err=%s", err) + } + } +} + +func (c *Cluster) SetNodeState(state string) error { + if c.IsCoordinator() { + return c.ReceiveNodeState(c.Node.ID, state) + } + + // Send node state to coordinator. + ns := &internal.NodeStateMessage{ + NodeID: c.Node.ID, + State: state, + } + + c.logger().Printf("Sending State %s (%s)", state, c.Coordinator) + if err := c.sendTo(c.CoordinatorNode(), ns); err != nil { + return fmt.Errorf("sending node state error: err=%s", err) + } + + return nil +} + +// ReceiveNodeState sets node state in Topology in order for the +// Coordinator to keep track of, during startup, which nodes have +// finished opening their Holder. +func (c *Cluster) ReceiveNodeState(nodeID string, state string) error { + if !c.IsCoordinator() { + return nil + } + + // This method is really only useful during initial startup. + if c.State() != ClusterStateStarting { + return nil + } + + c.Topology.nodeStates[nodeID] = state + c.logger().Printf("received state %s (%s)", state, nodeID) + + // Set cluster state to NORMAL. + if c.haveTopologyAgreement() && c.allNodesReady() { + return c.setStateAndBroadcast(ClusterStateNormal) + } + + return nil +} + +// localNode is not being used. +//func (c *Cluster) localNode() *Node { +// return c.NodeByURI(c.URI) +//} + // Status returns the internal ClusterStatus representation. func (c *Cluster) Status() *internal.ClusterStatus { return &internal.ClusterStatus{ - Nodes: encodeClusterStatus(c.Nodes), + ClusterID: c.ID, + State: c.state, + Nodes: EncodeNodes(c.Nodes), } } -// encodeClusterStatus converts a into its internal representation. -func encodeClusterStatus(a []*Node) []*internal.NodeStatus { - other := make([]*internal.NodeStatus, len(a)) - for i := range a { - other[i] = a[i].Status() - } - return other -} - -// NodeByHost returns a node reference by host. -func (c *Cluster) NodeByHost(host string) *Node { +// nodeByID returns a node reference by ID. +func (c *Cluster) nodeByID(id string) *Node { for _, n := range c.Nodes { - if n.Host == host { + if n.ID == id { return n } } return nil } +// nodeByURI returns a node reference by node URI. +func (c *Cluster) nodeByURI(uri URI) *Node { + for _, n := range c.Nodes { + if n.URI == uri { + return n + } + } + return nil +} + +// nodePositionByID returns the position of the node in slice c.Nodes. +func (c *Cluster) nodePositionByID(nodeID string) int { + for i, n := range c.Nodes { + if n.ID == nodeID { + return i + } + } + return -1 +} + +// addNodeBasicSorted adds a node to the cluster, sorted by id. +// Returns a pointer to the node and true if the node was added. +func (c *Cluster) addNodeBasicSorted(node *Node) bool { + n := c.nodeByID(node.ID) + if n != nil { + return false + } + + c.Nodes = append(c.Nodes, node) + + // All hosts must be merged in the same order on all nodes in the cluster. + sort.Sort(byID(c.Nodes)) + + return true +} + +// removeNodeBasicSorted removes a node from the cluster, maintaining +// the sort order. Returns true if the node was removed. +func (c *Cluster) removeNodeBasicSorted(node *Node) bool { + i := c.nodePositionByID(node.ID) + if i < 0 { + return false + } + + copy(c.Nodes[i:], c.Nodes[i+1:]) + c.Nodes[len(c.Nodes)-1] = nil + c.Nodes = c.Nodes[:len(c.Nodes)-1] + + return true +} + +// frag is a struct of basic fragment information. +type frag struct { + frame string + view string + slice uint64 +} + +func fragsDiff(a, b []frag) []frag { + m := make(map[frag]uint64) + + for _, y := range b { + m[y]++ + } + + var ret []frag + for _, x := range a { + if m[x] > 0 { + m[x]-- + continue + } + ret = append(ret, x) + } + + return ret +} + +type fragsByHost map[string][]frag + +func (a fragsByHost) add(b fragsByHost) fragsByHost { + for k, v := range b { + for _, vv := range v { + a[k] = append(a[k], vv) + } + } + return a +} + +type viewsByFrame map[string][]string + +func (a viewsByFrame) addView(frame, view string) { + a[frame] = append(a[frame], view) +} + +func (c *Cluster) fragsByHost(idx *Index) fragsByHost { + // frameViews is a map of frame to slice of views. + frameViews := make(viewsByFrame) + inverseFrameViews := make(viewsByFrame) + + for _, frame := range idx.Frames() { + for _, view := range frame.Views() { + if IsInverseView(view.Name()) { + inverseFrameViews.addView(frame.Name(), view.Name()) + } else { + frameViews.addView(frame.Name(), view.Name()) + } + } + } + + std := c.fragCombos(idx.Name(), idx.MaxSlice(), frameViews) + inv := c.fragCombos(idx.Name(), idx.MaxInverseSlice(), inverseFrameViews) + return std.add(inv) +} + +// fragCombos returns a map (by uri) of lists of fragments for a given index +// by creating every combination of frame/view specified in `frameViews` up to maxSlice. +func (c *Cluster) fragCombos(idx string, maxSlice uint64, frameViews viewsByFrame) fragsByHost { + t := make(fragsByHost) + for i := uint64(0); i <= maxSlice; i++ { + nodes := c.FragmentNodes(idx, i) + for _, n := range nodes { + // for each frame/view combination: + for frame, views := range frameViews { + for _, view := range views { + t[n.ID] = append(t[n.ID], frag{frame, view, i}) + } + } + } + } + return t +} + +// diff compares c with another cluster and determines if a node is being +// added or removed. An error is returned for any case other than where +// exactly one node is added or removed. +func (c *Cluster) diff(other *Cluster) (action string, nodeID string, err error) { + lenFrom := len(c.Nodes) + lenTo := len(other.Nodes) + // Determine if a node is being added or removed. + if lenFrom == lenTo { + return "", "", errors.New("clusters are the same size") + } + if lenFrom < lenTo { + // Adding a node. + if lenTo-lenFrom > 1 { + return "", "", errors.New("adding more than one node at a time is not supported") + } + action = ResizeJobActionAdd + // Determine the node ID that is being added. + for _, n := range other.Nodes { + if c.nodeByID(n.ID) == nil { + nodeID = n.ID + break + } + } + } else if lenFrom > lenTo { + // Removing a node. + if lenFrom-lenTo > 1 { + return "", "", errors.New("removing more than one node at a time is not supported") + } + action = ResizeJobActionRemove + // Determine the node ID that is being removed. + for _, n := range c.Nodes { + if other.nodeByID(n.ID) == nil { + nodeID = n.ID + break + } + } + } + return action, nodeID, nil +} + +// fragSources returns a list of ResizeSources - for each node in the `to` cluster - +// required to move from cluster `c` to cluster `to`. +func (c *Cluster) fragSources(to *Cluster, idx *Index) (map[string][]*internal.ResizeSource, error) { + m := make(map[string][]*internal.ResizeSource) + + // Determine if a node is being added or removed. + action, diffNodeID, err := c.diff(to) + if err != nil { + return nil, err + } + + // Initialize the map with all the nodes in `to`. + for _, n := range to.Nodes { + m[n.ID] = nil + } + + // If a node is being added, the source can be confined to the + // primary fragments (i.e. no need to use replicas as source data). + // In this case, source fragments can be based on a cluster with + // replica = 1. + // If a node is being removed, however, then it will most likely + // require that a replica fragment be the source data. + srcCluster := c + if action == ResizeJobActionAdd && c.ReplicaN > 1 { + srcCluster = NewCluster() + srcCluster.Nodes = Nodes(c.Nodes).Clone() + srcCluster.Hasher = c.Hasher + srcCluster.PartitionN = c.PartitionN + srcCluster.ReplicaN = 1 + } + + // Represents the fragment location for the from/to clusters. + fFrags := c.fragsByHost(idx) + tFrags := to.fragsByHost(idx) + + // srcFrags is the frag map based on a source cluster of replica = 1. + srcFrags := srcCluster.fragsByHost(idx) + + // srcNodesByFrag is the inverse representation of srcFrags. + srcNodesByFrag := make(map[frag]string) + for nodeID, frags := range srcFrags { + // If a node is being removed, don't consider it as a source. + if action == ResizeJobActionRemove && nodeID == diffNodeID { + continue + } + for _, frag := range frags { + srcNodesByFrag[frag] = nodeID + } + } + + // Get the frag diff for each nodeID. + diffs := make(fragsByHost) + for nodeID, frags := range tFrags { + if _, ok := fFrags[nodeID]; ok { + diffs[nodeID] = fragsDiff(frags, fFrags[nodeID]) + } else { + diffs[nodeID] = frags + } + } + + // Get the ResizeSource for each diff. + for nodeID, diff := range diffs { + m[nodeID] = []*internal.ResizeSource{} + for _, frag := range diff { + // If there is no valid source node ID for a fragment, + // it likely means that the replica factor was not + // high enough for the remaining nodes to contain + // the fragment. + srcNodeID, ok := srcNodesByFrag[frag] + if !ok { + return nil, errors.New("not enough data to perform resize (replica factor may need to be increased)") + } + + src := &internal.ResizeSource{ + Node: EncodeNode(c.nodeByID(srcNodeID)), + Index: idx.Name(), + Frame: frag.frame, + View: frag.view, + Slice: frag.slice, + } + + m[nodeID] = append(m[nodeID], src) + } + } + + return m, nil +} + // Partition returns the partition that a slice belongs to. func (c *Cluster) Partition(index string, slice uint64) int { var buf [8]byte @@ -243,8 +821,8 @@ func (c *Cluster) FragmentNodes(index string, slice uint64) []*Node { } // OwnsFragment returns true if a host owns a fragment. -func (c *Cluster) OwnsFragment(host string, index string, slice uint64) bool { - return Nodes(c.FragmentNodes(index, slice)).ContainsHost(host) +func (c *Cluster) OwnsFragment(nodeID string, index string, slice uint64) bool { + return Nodes(c.FragmentNodes(index, slice)).ContainsID(nodeID) } // PartitionNodes returns a list of nodes that own a partition. @@ -270,20 +848,36 @@ func (c *Cluster) PartitionNodes(partitionID int) []*Node { return nodes } -// OwnsSlices find the set of slices owned by the node per Index -func (c *Cluster) OwnsSlices(index string, maxSlice uint64, host string) []uint64 { +// OwnsSlices finds the set of slices owned by the node per Index +func (c *Cluster) OwnsSlices(index string, maxSlice uint64, uri URI) []uint64 { var slices []uint64 for i := uint64(0); i <= maxSlice; i++ { p := c.Partition(index, i) // Determine primary owner node. nodeIndex := c.Hasher.Hash(uint64(p), len(c.Nodes)) - if c.Nodes[nodeIndex].Host == host { + if c.Nodes[nodeIndex].URI == uri { slices = append(slices, i) } } return slices } +// ContainsSlices is like OwnsSlices, but it includes replicas. +func (c *Cluster) ContainsSlices(index string, maxSlice uint64, node *Node) []uint64 { + var slices []uint64 + for i := uint64(0); i <= maxSlice; i++ { + p := c.Partition(index, i) + // Determine the nodes for partition. + nodes := c.PartitionNodes(p) + for _, n := range nodes { + if n.ID == node.ID { + slices = append(slices, i) + } + } + } + return slices +} + // Hasher represents an interface to hash integers into buckets. type Hasher interface { // Hashes the key into a number between [0,N). @@ -306,3 +900,964 @@ func (h *jmphasher) Hash(key uint64, n int) int { } return int(b) } + +func (c *Cluster) Open() error { + // Cluster always comes up in state STARTING until cluster membership is determined. + c.state = ClusterStateStarting + + // Load topology file if it exists. + if err := c.loadTopology(); err != nil { + return fmt.Errorf("load topology: %v", err) + } + + c.ID = c.Topology.ClusterID + + // Only the coordinator needs to consider the .topology file. + if c.IsCoordinator() { + err := c.considerTopology() + if err != nil { + return fmt.Errorf("considerTopology: %v", err) + } + } + + // Add the local node to the cluster. + c.AddNode(c.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) + } + + // If not coordinator then wait for ClusterStatus from coordinator. + if !c.IsCoordinator() { + // In the case where a node has been restarted and memberlist has + // not had enough time to determine the node went down/up, then + // the coorninator needs to be alerted that this node is back up + // (and now in a state of STARTING) so that it can be put to the correct + // cluster state. + // TODO: Because the normal code path already sends a NodeJoin event (via + // memberlist), this it a bit redundant in most cases. Perhaps determine + // that the node has been restarted and don't do this step. + msg := &internal.NodeEventMessage{ + Event: uint32(NodeJoin), + Node: EncodeNode(c.Node), + } + if err := c.Broadcaster.SendAsync(msg); err != nil { + return fmt.Errorf("sending restart NodeJoin: %v", err) + } + + c.logger().Printf("wait for joining to complete") + <-c.joining + c.logger().Printf("joining has completed") + } + + return nil +} + +func (c *Cluster) Close() error { + // Notify goroutines of closing and wait for completion. + close(c.closing) + c.wg.Wait() + + return nil +} + +func (c *Cluster) markAsJoined() { + c.logger().Printf("mark node as joined (received coordinator update)") + if !c.joined { + c.joined = true + close(c.joining) + } +} + +func (c *Cluster) needTopologyAgreement() bool { + return c.State() == ClusterStateStarting && !StringSlicesAreEqual(c.Topology.NodeIDs, c.NodeIDs()) +} + +func (c *Cluster) haveTopologyAgreement() bool { + if c.Static { + return true + } + return StringSlicesAreEqual(c.Topology.NodeIDs, c.NodeIDs()) +} + +func (c *Cluster) allNodesReady() bool { + if c.Static { + return true + } + for _, uri := range c.Topology.NodeIDs { + if c.Topology.nodeStates[uri] != NodeStateReady { + return false + } + } + return true +} + +func (c *Cluster) handleNodeAction(nodeAction nodeAction) error { + j, err := c.generateResizeJob(nodeAction) + if err != nil { + c.logger().Printf("generateResizeJob error: err=%s", err) + if err := c.setStateAndBroadcast(ClusterStateNormal); err != nil { + c.logger().Printf("setStateAndBroadcast error: err=%s", err) + } + return err + } + + // j.Run() runs in a goroutine because in the case where the + // job requires no action, it immediately writes to the j.result + // channel, which is not consumed until the code below. + var eg errgroup.Group + eg.Go(func() error { + return j.Run() + }) + + // Wait for the ResizeJob to finish or be aborted. + c.logger().Printf("wait for jobResult") + jobResult := <-j.result + + // Make sure j.Run() didn't return an error. + if eg.Wait() != nil { + return err + } + + c.logger().Printf("received jobResult: %s", jobResult) + switch jobResult { + case ResizeJobStateDone: + if err := c.CompleteCurrentJob(ResizeJobStateDone); err != nil { + return err + } + // Add/remove uri to/from the cluster. + if j.action == ResizeJobActionRemove { + return c.RemoveNode(nodeAction.node) + } else if j.action == ResizeJobActionAdd { + return c.AddNode(nodeAction.node) + } + case ResizeJobStateAborted: + if err := c.CompleteCurrentJob(ResizeJobStateAborted); err != nil { + return err + } + } + return nil +} + +func (c *Cluster) setStateAndBroadcast(state string) error { + c.SetState(state) + // Broadcast cluster status changes to the cluster. + c.logger().Printf("broadcasting ClusterStatus: %s", state) + return c.Broadcaster.SendSync(c.Status()) +} + +func (c *Cluster) sendTo(node *Node, msg proto.Message) error { + if err := c.Broadcaster.SendTo(node, msg); err != nil { + return err + } + return nil +} + +// ListenForJoins handles cluster-resize events. +func (c *Cluster) ListenForJoins() { + c.wg.Add(1) + go func() { defer c.wg.Done(); c.listenForJoins() }() +} + +func (c *Cluster) listenForJoins() { + // When a cluster starts, the state is STARTING. + // We first want to wait for at least one node to join. + // Then we want to clear out the joiningLeavingNodes queue (buffered channel). + // Then we want to set the cluster state to NORMAL and resume processing of joiningLeavingNodes events. + // We use a bool `setNormal` to indicate when at least one node has joined. + + var setNormal bool + + for { + + // Handle all pending joins before changing state back to NORMAL. + select { + case nodeAction := <-c.joiningLeavingNodes: + err := c.handleNodeAction(nodeAction) + if err != nil { + c.logger().Printf("handleNodeAction error: err=%s", err) + continue + } + setNormal = true + continue + default: + } + + // Only change state to NORMAL if we have successfully added at least one host. + if setNormal { + // Put the cluster back to state NORMAL and broadcast. + if err := c.setStateAndBroadcast(ClusterStateNormal); err != nil { + c.logger().Printf("setStateAndBroadcast error: err=%s", err) + } + } + + // Wait for a joining host or a close. + select { + case <-c.closing: + return + case nodeAction := <-c.joiningLeavingNodes: + err := c.handleNodeAction(nodeAction) + if err != nil { + c.logger().Printf("handleNodeAction error: err=%s", err) + continue + } + setNormal = true + continue + } + } +} + +// generateResizeJob creates a new ResizeJob based on the new node being +// added/removed. It also saves a reference to the ResizeJob in the `jobs` map +// for future lookup by JobID. +func (c *Cluster) generateResizeJob(nodeAction nodeAction) (*ResizeJob, error) { + c.logger().Printf("generateResizeJob: %v", nodeAction) + c.mu.Lock() + defer c.mu.Unlock() + + j, err := c.generateResizeJobByAction(nodeAction) + if err != nil { + return nil, err + } + c.logger().Printf("generated ResizeJob: %d", j.ID) + + // Save job in jobs map for future reference. + c.jobs[j.ID] = j + + // Set job as currentJob. + if c.currentJob != nil { + return nil, fmt.Errorf("there is currently a resize job running") + } + c.currentJob = j + + return j, nil +} + +// generateResizeJobByAction returns a ResizeJob with instructions based on +// the difference between Cluster and a new Cluster with/without uri. +// Broadcaster is associated to the ResizeJob here for use in broadcasting +// the resize instructions to other nodes in the cluster. +func (c *Cluster) generateResizeJobByAction(nodeAction nodeAction) (*ResizeJob, error) { + j := NewResizeJob(c.Nodes, nodeAction.node, nodeAction.action) + j.Broadcaster = c.Broadcaster + + // toCluster is a clone of Cluster with the new node added/removed for comparison. + toCluster := NewCluster() + toCluster.Nodes = Nodes(c.Nodes).Clone() + toCluster.Hasher = c.Hasher + toCluster.PartitionN = c.PartitionN + toCluster.ReplicaN = c.ReplicaN + if nodeAction.action == ResizeJobActionRemove { + toCluster.removeNodeBasicSorted(nodeAction.node) + } else if nodeAction.action == ResizeJobActionAdd { + toCluster.addNodeBasicSorted(nodeAction.node) + } + + // multiIndex is a map of sources initialized with all the nodes in toCluster. + multiIndex := make(map[string][]*internal.ResizeSource) + + for _, n := range toCluster.Nodes { + multiIndex[n.ID] = nil + } + + // Add to multiIndex the instructions for each index. + for _, idx := range c.Holder.Indexes() { + fragSources, err := c.fragSources(toCluster, idx) + if err != nil { + return nil, err + } + + for id, sources := range fragSources { + for _, src := range sources { + multiIndex[id] = append(multiIndex[id], src) + } + } + } + + for id, sources := range multiIndex { + // If a host doesn't need to request data, mark it as complete. + if len(sources) == 0 { + j.IDs[id] = true + continue + } + instr := &internal.ResizeInstruction{ + JobID: j.ID, + Node: EncodeNode(toCluster.nodeByID(id)), + Coordinator: EncodeNode(c.CoordinatorNode()), + Sources: sources, + Schema: c.Holder.EncodeSchema(), // Include the schema to ensure it's in sync on the receiving node. + ClusterStatus: c.Status(), + } + j.Instructions = append(j.Instructions, instr) + } + + return j, nil +} + +// CompleteCurrentJob sets the state of the current ResizeJob +// then removes the pointer to currentJob. +func (c *Cluster) CompleteCurrentJob(state string) error { + c.mu.Lock() + defer c.mu.Unlock() + if c.currentJob == nil { + return fmt.Errorf("no resize job currently running") + } + c.currentJob.SetState(state) + c.currentJob = nil + return nil +} + +// FollowResizeInstruction is run by any node that receives a ResizeInstruction. +func (c *Cluster) FollowResizeInstruction(instr *internal.ResizeInstruction) error { + c.logger().Printf("follow resize instruction on %s", c.Node.ID) + // Make sure the cluster status on this node agrees with the Coordinator + // before attempting a resize. + if err := c.MergeClusterStatus(instr.ClusterStatus); err != nil { + return err + } + + c.logger().Printf("MergeClusterStatus done, start goroutine") + + // The actual resizing runs in a goroutine because we don't want to block + // the distribution of other ResizeInstructions to the rest of the cluster. + go func() { + + // Make sure the holder has opened. + <-c.Holder.opened + + // Prepare the return message. + complete := &internal.ResizeInstructionComplete{ + JobID: instr.JobID, + Node: instr.Node, + Error: "", + } + + // Stop processing on any error. + if err := func() error { + + // Sync the schema received in the resize instruction. + c.logger().Printf("Holder ApplySchema") + if err := c.Holder.ApplySchema(instr.Schema); err != nil { + return err + } + + // Create a client for calling remote nodes. + client := NewInternalHTTPClientFromURI(&c.Node.URI, c.RemoteClient) // TODO: ClientOptions + + // Request each source file in ResizeSources. + for _, src := range instr.Sources { + c.logger().Printf("get slice %d for index %s from host %s", src.Slice, src.Index, src.Node.URI) + + srcURI := decodeURI(src.Node.URI) + + // Retrieve frame. + f := c.Holder.Frame(src.Index, src.Frame) + if f == nil { + return ErrFrameNotFound + } + + // Create view. + v, err := f.CreateViewIfNotExists(src.View) + if err != nil { + return err + } + + // Create the local fragment. + frag, err := v.CreateFragmentIfNotExists(src.Slice) + if err != nil { + return err + } + + // Stream slice from remote node. + c.logger().Printf("retrieve slice %d for index %s from host %s", src.Slice, src.Index, src.Node.URI) + rd, err := client.RetrieveSliceFromURI(context.Background(), src.Index, src.Frame, src.View, src.Slice, srcURI) + if err != nil { + // For now it is an acceptable error if the fragment is not found + // on the remote node. This occurs when a slice has been skipped and + // therefore doesn't contain data. The coordinator correctly determined + // the resize instruction to retrieve the slice, but it doesn't have data. + // TODO: figure out a way to distinguish from "fragment not found" errors + // which are true errors and which simply mean the fragment doesn't have data. + if err == ErrFragmentNotFound { + return nil + } + return err + } else if rd == nil { + return fmt.Errorf("slice %v doesn't exist on host: %s", src.Slice, src.Node.URI) + } + + // Write to local frame and always close reader. + if err := func() error { + defer rd.Close() + if _, err := frag.ReadFrom(rd); err != nil { + return err + } + return nil + }(); err != nil { + return err + } + } + return nil + }(); err != nil { + complete.Error = err.Error() + } + + if err := c.sendTo(DecodeNode(instr.Coordinator), complete); err != nil { + c.logger().Printf("sending resizeInstructionComplete error: err=%s", err) + } + }() + return nil +} + +func (c *Cluster) MarkResizeInstructionComplete(complete *internal.ResizeInstructionComplete) error { + + j := c.Job(complete.JobID) + + // Abort the job if an error exists in the complete object. + if complete.Error != "" { + j.result <- ResizeJobStateAborted + return errors.New(complete.Error) + } + + j.mu.Lock() + defer j.mu.Unlock() + + if j.isComplete() { + return fmt.Errorf("ResizeJob %d is no longer running", j.ID) + } + + // Mark host complete. + j.IDs[complete.Node.ID] = true + + if !j.nodesArePending() { + j.result <- ResizeJobStateDone + } + + return nil +} + +// Job returns a ResizeJob by id. +func (c *Cluster) Job(id int64) *ResizeJob { + c.mu.RLock() + defer c.mu.RUnlock() + return c.job(id) +} + +func (c *Cluster) job(id int64) *ResizeJob { return c.jobs[id] } + +type ResizeJob struct { + ID int64 + IDs map[string]bool + Instructions []*internal.ResizeInstruction + Broadcaster Broadcaster + + action string + result chan string + + mu sync.RWMutex + state string + + // The writer for any logging. + LogOutput io.Writer +} + +// logger returns a logger for the resize job. +func (j *ResizeJob) logger() *log.Logger { + return log.New(j.LogOutput, "", log.LstdFlags) +} + +// NewResizeJob returns a new instance of ResizeJob. +func NewResizeJob(existingNodes []*Node, node *Node, action string) *ResizeJob { + + // Build a map of uris to track their resize status. + // The value for a node will be set to true after that node + // has indicated that it has completed all resize instructions. + ids := make(map[string]bool) + + if action == ResizeJobActionRemove { + for _, n := range existingNodes { + // Exclude the removed node from the map. + if n.ID == node.ID { + continue + } + ids[n.ID] = false + } + } else if action == ResizeJobActionAdd { + for _, n := range existingNodes { + ids[n.ID] = false + } + // Include the added node in the map for tracking. + ids[node.ID] = false + } + + return &ResizeJob{ + ID: rand.Int63(), + IDs: ids, + action: action, + result: make(chan string), + LogOutput: os.Stderr, + } +} + +func (j *ResizeJob) State() string { + j.mu.RLock() + defer j.mu.RUnlock() + return j.state +} + +func (j *ResizeJob) SetState(state string) { + j.mu.Lock() + j.setState(state) + j.mu.Unlock() +} + +func (j *ResizeJob) setState(state string) { + if j.state == "" || j.state == ResizeJobStateRunning { + j.state = state + } +} + +// Run distributes ResizeInstructions. +func (j *ResizeJob) Run() error { + j.logger().Printf("run ResizeJob") + // Set job state to RUNNING. + j.SetState(ResizeJobStateRunning) + + // Job can be considered done in the case where it doesn't require any action. + if !j.nodesArePending() { + j.logger().Printf("ResizeJob contains no pending tasks; mark as done") + j.result <- ResizeJobStateDone + return nil + } + + j.logger().Printf("distribute tasks for ResizeJob") + err := j.distributeResizeInstructions() + if err != nil { + j.result <- ResizeJobStateAborted + return err + } + return nil +} + +// isComplete return true if the job is any one of several completion states. +func (j *ResizeJob) isComplete() bool { + switch j.state { + case ResizeJobStateDone, ResizeJobStateAborted: + return true + default: + return false + } +} + +// nodesArePending returns true if any node is still working on the resize. +func (j *ResizeJob) nodesArePending() bool { + for _, complete := range j.IDs { + if !complete { + return true + } + } + return false +} + +func (j *ResizeJob) distributeResizeInstructions() error { + j.logger().Printf("distributeResizeInstructions for job %d", j.ID) + // Loop through the ResizeInstructions in ResizeJob and send to each host. + for _, instr := range j.Instructions { + // Because the node may not be in the cluster yet, create + // a dummy node object to use in the SendTo() method. + node := &Node{ + ID: instr.Node.ID, + URI: decodeURI(instr.Node.URI), + } + j.logger().Printf("send resize instructions: %v", instr) + if err := j.Broadcaster.SendTo(node, instr); err != nil { + return err + } + } + return nil +} + +type NodeIDs []string + +func (n NodeIDs) Len() int { return len(n) } +func (n NodeIDs) Swap(i, j int) { n[i], n[j] = n[j], n[i] } +func (n NodeIDs) Less(i, j int) bool { return n[i] < n[j] } + +// ContainsID returns true if idi matches one of the nodesets's IDs. +func (n NodeIDs) ContainsID(id string) bool { + for _, nid := range n { + if nid == id { + return true + } + } + return false +} + +// Topology represents the list of hosts in the cluster. +type Topology struct { + mu sync.RWMutex + NodeIDs []string + + ClusterID string + + // nodeStates holds the state of each node according to + // the coordinator. Used during startup and data load. + nodeStates map[string]string +} + +func NewTopology() *Topology { + return &Topology{ + nodeStates: make(map[string]string), + } +} + +// ContainsID returns true if id matches one of the topology's IDs. +func (t *Topology) ContainsID(id string) bool { + t.mu.RLock() + defer t.mu.RUnlock() + return t.containsID(id) +} + +func (t *Topology) containsID(id string) bool { + return NodeIDs(t.NodeIDs).ContainsID(id) +} + +func (t *Topology) positionByID(nodeID string) int { + for i, tid := range t.NodeIDs { + if tid == nodeID { + return i + } + } + return -1 +} + +// AddID adds the node ID to the topology and returns true if added. +func (t *Topology) AddID(nodeID string) bool { + t.mu.Lock() + defer t.mu.Unlock() + if t.containsID(nodeID) { + return false + } + t.NodeIDs = append(t.NodeIDs, nodeID) + + sort.Slice(t.NodeIDs, + func(i, j int) bool { + return t.NodeIDs[i] < t.NodeIDs[j] + }) + + return true +} + +// RemoveID removes the node ID from the topology and returns true if removed. +func (t *Topology) RemoveID(nodeID string) bool { + t.mu.Lock() + defer t.mu.Unlock() + + i := t.positionByID(nodeID) + if i < 0 { + return false + } + + copy(t.NodeIDs[i:], t.NodeIDs[i+1:]) + t.NodeIDs[len(t.NodeIDs)-1] = "" + t.NodeIDs = t.NodeIDs[:len(t.NodeIDs)-1] + + return true +} + +// Encode converts t into its internal representation. +func (t *Topology) Encode() *internal.Topology { + return encodeTopology(t) +} + +// loadTopology reads the topology for the node. +func (c *Cluster) loadTopology() error { + buf, err := ioutil.ReadFile(filepath.Join(c.Path, ".topology")) + if os.IsNotExist(err) { + c.Topology = NewTopology() + return nil + } else if err != nil { + return err + } + + var pb internal.Topology + if err := proto.Unmarshal(buf, &pb); err != nil { + return err + } + top, err := decodeTopology(&pb) + if err != nil { + return err + } + c.Topology = top + + return nil +} + +// saveTopology writes the current topology to disk. +func (c *Cluster) saveTopology() error { + + if err := os.MkdirAll(c.Path, 0777); err != nil { + return err + } + + if buf, err := proto.Marshal(encodeTopology(c.Topology)); err != nil { + return err + } else if err := ioutil.WriteFile(filepath.Join(c.Path, ".topology"), buf, 0666); err != nil { + return err + } + return nil +} + +func encodeTopology(topology *Topology) *internal.Topology { + if topology == nil { + return nil + } + return &internal.Topology{ + ClusterID: topology.ClusterID, + NodeIDs: topology.NodeIDs, + } +} + +func decodeTopology(topology *internal.Topology) (*Topology, error) { + if topology == nil { + return nil, nil + } + + t := NewTopology() + t.ClusterID = topology.ClusterID + t.NodeIDs = topology.NodeIDs + sort.Slice(t.NodeIDs, + func(i, j int) bool { + return t.NodeIDs[i] < t.NodeIDs[j] + }) + + return t, nil +} + +func (c *Cluster) considerTopology() error { + // Create ClusterID if one does not already exist. + if c.ID == "" { + u := uuid.NewV4() + c.ID = u.String() + c.Topology.ClusterID = c.ID + } + + if c.Static { + return nil + } + + // If there is no .topology file, it's safe to proceed. + if len(c.Topology.NodeIDs) == 0 { + return nil + } + + // The local node (coordinator) must be in the .topology. + if !c.Topology.ContainsID(c.Node.ID) { + return fmt.Errorf("coordinator %s is not in topology: %v", c.Node.ID, c.Topology.NodeIDs) + } + + // If local node is the only thing in .topology, continue. + //if len(c.Topology.NodeIDs) == 1 { + // return nil + //} + + // Keep the cluster in state "STARTING" until hearing from all nodes. + // Topology contains 2+ hosts. + return nil +} + +// ReceiveEvent represents an implementation of EventHandler. +func (c *Cluster) ReceiveEvent(e *NodeEvent) error { + // Ignore events sent from this node. + if e.Node.ID == c.Node.ID { + return nil + } + + switch e.Event { + case NodeJoin: + c.logger().Printf("received NodeJoin event: %v", e) + // Ignore the event if this is not the coordinator. + if !c.IsCoordinator() { + return nil + } + return c.nodeJoin(e.Node) + case NodeLeave: + // Automatic nodeLeave is intentionally not implemented. + case NodeUpdate: + // NodeUpdate is intentionally not implemented. + } + + return nil +} + +func (c *Cluster) nodeJoin(node *Node) error { + if c.needTopologyAgreement() { + // A host that is not part of the topology can't be added to the STARTING cluster. + if !c.Topology.ContainsID(node.ID) { + err := fmt.Sprintf("host is not in topology: %s", node.ID) + c.logger().Print(err) + return errors.New(err) + } + + if err := c.AddNode(node); err != nil { + return err + } + + // Only change to normal if there is no existing data. Otherwise, + // the coordinator needs to wait to receive READY messages (nodeStates) + // from remote nodes before setting the cluster to state NORMAL. + if !c.Holder.HasData() { + // If the result of the previous AddNode completed the joining of nodes + // in the topology, then change the state to NORMAL. + if c.haveTopologyAgreement() { + return c.setStateAndBroadcast(ClusterStateNormal) + } + return nil + } + + if c.haveTopologyAgreement() && c.allNodesReady() { + return c.setStateAndBroadcast(ClusterStateNormal) + } else { + // Send the status to the remote node. This lets the remote node + // know that it can proceed with opening its Holder. + return c.sendTo(node, c.Status()) + } + + return nil + } + + // If the cluster already contains the node, just send it the cluster status. + // This is useful in the case where a node is restarted or temporarily leaves + // the cluster. + if node := c.nodeByID(node.ID); node != nil { + return c.sendTo(node, c.Status()) + } + + // If the holder does not yet contain data, go ahead and add the node. + if !c.Holder.HasData() { + if err := c.AddNode(node); err != nil { + return err + } + return c.setStateAndBroadcast(ClusterStateNormal) + } + + // If the cluster has data, we need to change to RESIZING and + // kick off the resizing process. + if err := c.setStateAndBroadcast(ClusterStateResizing); err != nil { + return err + } + c.joiningLeavingNodes <- nodeAction{node, ResizeJobActionAdd} + + return nil +} + +// NodeLeave initiates the removal of a node from the cluster. +func (c *Cluster) NodeLeave(node *Node) error { + // Refuse the request if this is not the coordinator. + if !c.IsCoordinator() { + return fmt.Errorf("Node removal requests are only valid on the Coordinator node: %s", c.CoordinatorNode().ID) + } + + if c.State() != ClusterStateNormal { + return fmt.Errorf("Cluster must be in state %s to remove a node. Current state: %s", ClusterStateNormal, c.State()) + } + + // Ensure that node is in the cluster. + if c.nodeByID(node.ID) == nil { + return fmt.Errorf("Node is not a member of the cluster: %s", node.ID) + } + + // Prevent removing the coordinator node (this node). + if node.ID == c.Node.ID { + return fmt.Errorf("The coordinator node cannot be removed. First, make a different node the new coordinator.") + } + + // See if resize job can be generated + _, err := c.generateResizeJobByAction(nodeAction{c.nodeByID(node.ID), ResizeJobActionRemove}) + + if err != nil { + return err + } + + return c.nodeLeave(node) +} + +func (c *Cluster) nodeLeave(node *Node) error { + // Get the actual node in the local cluster. + n := c.nodeByID(node.ID) + + // Don't do anything else if the cluster doesn't contain the node. + if n == nil { + return nil + } + + // If the holder does not yet contain data, go ahead and remove the node. + if !c.Holder.HasData() { + if err := c.RemoveNode(n); err != nil { + return err + } + return c.setStateAndBroadcast(ClusterStateNormal) + } + + // If the cluster has data then change state to RESIZING and + // kick off the resizing process. + if err := c.setStateAndBroadcast(ClusterStateResizing); err != nil { + return err + } + c.joiningLeavingNodes <- nodeAction{n, ResizeJobActionRemove} + + return nil +} + +func (c *Cluster) MergeClusterStatus(cs *internal.ClusterStatus) error { + c.logger().Printf("merge cluster status: %v", cs) + // Ignore status updates from self (coordinator). + if c.IsCoordinator() { + return nil + } + + // Set ClusterID. + c.setID(cs.ClusterID) + + officialNodes := DecodeNodes(cs.Nodes) + + // Add all nodes from the coordinator. + for _, node := range officialNodes { + if err := c.AddNode(node); err != nil { + return err + } + } + + // Remove any nodes not specified by the coordinator + // except for self. Generate a list to remove first + // so that nodes aren't removed mid-loop. + nodeIDsToRemove := []string{} + for _, node := range c.Nodes { + // Don't remove this node. + if node.ID == c.Node.ID { + continue + } + if Nodes(officialNodes).ContainsID(node.ID) { + continue + } + nodeIDsToRemove = append(nodeIDsToRemove, node.ID) + } + + for _, nodeID := range nodeIDsToRemove { + if err := c.RemoveNode(c.nodeByID(nodeID)); err != nil { + return err + } + } + + c.SetState(cs.State) + + c.markAsJoined() + + return nil +} diff --git a/cluster_internal_test.go b/cluster_internal_test.go new file mode 100644 index 000000000..da9989760 --- /dev/null +++ b/cluster_internal_test.go @@ -0,0 +1,309 @@ +// Copyright 2017 Pilosa Corp. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pilosa + +import ( + "io/ioutil" + "reflect" + "testing" + + "github.com/pilosa/pilosa/internal" +) + +// Ensure that fragCombos creates the correct fragment mapping. +func TestFragCombos(t *testing.T) { + + uri0, err := NewURIFromAddress("host0") + if err != nil { + t.Fatal(err) + } + uri1, err := NewURIFromAddress("host1") + if err != nil { + t.Fatal(err) + } + + node0 := &Node{ID: "node0", URI: *uri0} + node1 := &Node{ID: "node1", URI: *uri1} + + c := NewCluster() + c.addNodeBasicSorted(node0) + c.addNodeBasicSorted(node1) + + tests := []struct { + idx string + maxSlice uint64 + frameViews viewsByFrame + expected fragsByHost + }{ + { + idx: "i", + maxSlice: uint64(2), + frameViews: viewsByFrame{"f": []string{"v1", "v2"}}, + expected: fragsByHost{ + "node0": []frag{{"f", "v1", uint64(0)}, {"f", "v2", uint64(0)}}, + "node1": []frag{{"f", "v1", uint64(1)}, {"f", "v2", uint64(1)}, {"f", "v1", uint64(2)}, {"f", "v2", uint64(2)}}, + }, + }, + { + idx: "foo", + maxSlice: uint64(3), + frameViews: viewsByFrame{"f": []string{"v0"}}, + expected: fragsByHost{ + "node0": []frag{{"f", "v0", uint64(1)}, {"f", "v0", uint64(2)}}, + "node1": []frag{{"f", "v0", uint64(0)}, {"f", "v0", uint64(3)}}, + }, + }, + } + for _, test := range tests { + + actual := c.fragCombos(test.idx, test.maxSlice, test.frameViews) + if !reflect.DeepEqual(actual, test.expected) { + t.Errorf("expected: %v, but got: %v", test.expected, actual) + } + + } +} + +// newIndexWithTempPath returns a new instance of Index. +func newIndexWithTempPath(name string) *Index { + path, err := ioutil.TempDir("", "pilosa-index-") + if err != nil { + panic(err) + } + index, err := NewIndex(path, name) + if err != nil { + panic(err) + } + return index +} + +// Ensure that fragSources creates the correct fragment mapping. +func TestFragSources(t *testing.T) { + + uri0, err := NewURIFromAddress("host0") + if err != nil { + t.Fatal(err) + } + uri1, err := NewURIFromAddress("host1") + if err != nil { + t.Fatal(err) + } + uri2, err := NewURIFromAddress("host2") + if err != nil { + t.Fatal(err) + } + uri3, err := NewURIFromAddress("host3") + if err != nil { + t.Fatal(err) + } + + node0 := &Node{ID: "node0", URI: *uri0} + node1 := &Node{ID: "node1", URI: *uri1} + node2 := &Node{ID: "node2", URI: *uri2} + node3 := &Node{ID: "node3", URI: *uri3} + + c1 := NewCluster() + c1.ReplicaN = 1 + c1.addNodeBasicSorted(node0) + c1.addNodeBasicSorted(node1) + + c2 := NewCluster() + c2.ReplicaN = 1 + c2.addNodeBasicSorted(node0) + c2.addNodeBasicSorted(node1) + c2.addNodeBasicSorted(node2) + + c3 := NewCluster() + c3.ReplicaN = 2 + c3.addNodeBasicSorted(node0) + c3.addNodeBasicSorted(node1) + + c4 := NewCluster() + c4.ReplicaN = 2 + c4.addNodeBasicSorted(node0) + c4.addNodeBasicSorted(node1) + c4.addNodeBasicSorted(node2) + + c5 := NewCluster() + c5.ReplicaN = 2 + c5.addNodeBasicSorted(node0) + c5.addNodeBasicSorted(node1) + c5.addNodeBasicSorted(node2) + c5.addNodeBasicSorted(node3) + + idx := newIndexWithTempPath("i") + frame, err := idx.CreateFrameIfNotExists("f", FrameOptions{}) + if err != nil { + t.Fatal(err) + } + _, err = frame.SetBit("standard", 1, 101, nil) + if err != nil { + t.Fatal(err) + } + _, err = frame.SetBit("standard", 1, 1300000, nil) + if err != nil { + t.Fatal(err) + } + _, err = frame.SetBit("standard", 1, 2600000, nil) + if err != nil { + t.Fatal(err) + } + _, err = frame.SetBit("standard", 1, 3900000, nil) + if err != nil { + t.Fatal(err) + } + + tests := []struct { + from *Cluster + to *Cluster + idx *Index + expected map[string][]*internal.ResizeSource + err string + }{ + { + from: c1, + to: c2, + idx: idx, + expected: map[string][]*internal.ResizeSource{ + "node0": []*internal.ResizeSource{}, + "node1": []*internal.ResizeSource{}, + "node2": []*internal.ResizeSource{ + {&internal.Node{"node0", &internal.URI{"http", "host0", 10101}, false}, "i", "f", "standard", uint64(0)}, + {&internal.Node{"node1", &internal.URI{"http", "host1", 10101}, false}, "i", "f", "standard", uint64(2)}, + }, + }, + err: "", + }, + { + from: c4, + to: c3, + idx: idx, + expected: map[string][]*internal.ResizeSource{ + "node0": []*internal.ResizeSource{ + {&internal.Node{"node1", &internal.URI{"http", "host1", 10101}, false}, "i", "f", "standard", uint64(1)}, + }, + "node1": []*internal.ResizeSource{ + {&internal.Node{"node0", &internal.URI{"http", "host0", 10101}, false}, "i", "f", "standard", uint64(0)}, + {&internal.Node{"node0", &internal.URI{"http", "host0", 10101}, false}, "i", "f", "standard", uint64(2)}, + }, + }, + err: "", + }, + { + from: c5, + to: c4, + idx: idx, + expected: map[string][]*internal.ResizeSource{ + "node0": []*internal.ResizeSource{ + {&internal.Node{"node2", &internal.URI{"http", "host2", 10101}, false}, "i", "f", "standard", uint64(0)}, + {&internal.Node{"node2", &internal.URI{"http", "host2", 10101}, false}, "i", "f", "standard", uint64(2)}, + }, + "node1": []*internal.ResizeSource{ + {&internal.Node{"node0", &internal.URI{"http", "host0", 10101}, false}, "i", "f", "standard", uint64(3)}, + }, + "node2": []*internal.ResizeSource{}, + }, + err: "", + }, + { + from: c2, + to: c4, + idx: idx, + expected: nil, + err: "clusters are the same size", + }, + { + from: c1, + to: c5, + idx: idx, + expected: nil, + err: "adding more than one node at a time is not supported", + }, + { + from: c5, + to: c1, + idx: idx, + expected: nil, + err: "removing more than one node at a time is not supported", + }, + } + for _, test := range tests { + + actual, err := (test.from).fragSources(test.to, test.idx) + if test.err != "" { + if err.Error() != test.err { + t.Fatalf("expected error: %s", test.err) + } + } else { + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(actual, test.expected) { + t.Errorf("expected: %v, but got: %v", test.expected, actual) + } + } + } +} + +// Ensure that fragSources creates the correct fragment mapping. +func TestResizeJob(t *testing.T) { + + uri0, err := NewURIFromAddress("host0") + if err != nil { + t.Fatal(err) + } + uri1, err := NewURIFromAddress("host1") + if err != nil { + t.Fatal(err) + } + uri2, err := NewURIFromAddress("host2") + if err != nil { + t.Fatal(err) + } + + node0 := &Node{ID: "node0", URI: *uri0} + node1 := &Node{ID: "node1", URI: *uri1} + node2 := &Node{ID: "node2", URI: *uri2} + + tests := []struct { + existingNodes []*Node + node *Node + action string + expectedIDs map[string]bool + }{ + { + existingNodes: []*Node{node0, node1}, + node: node2, + action: ResizeJobActionAdd, + expectedIDs: map[string]bool{node0.ID: false, node1.ID: false, node2.ID: false}, + }, + { + existingNodes: []*Node{node0, node1, node2}, + node: node2, + action: ResizeJobActionRemove, + expectedIDs: map[string]bool{node0.ID: false, node1.ID: false}, + }, + } + for _, test := range tests { + + actual := NewResizeJob(test.existingNodes, test.node, test.action) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(actual.IDs, test.expectedIDs) { + t.Errorf("expected: %v, but got: %v", test.expectedIDs, actual.IDs) + } + } +} diff --git a/cluster_test.go b/cluster_test.go index 8ab1ab461..c5c01d58d 100644 --- a/cluster_test.go +++ b/cluster_test.go @@ -15,6 +15,7 @@ package pilosa_test import ( + "bytes" "math/rand" "reflect" "testing" @@ -29,9 +30,9 @@ import ( func TestCluster_Owners(t *testing.T) { c := pilosa.Cluster{ Nodes: []*pilosa.Node{ - {Host: "serverA:1000"}, - {Host: "serverB:1000"}, - {Host: "serverC:1000"}, + {URI: test.NewURIFromHostPort("serverA", 1000)}, + {URI: test.NewURIFromHostPort("serverB", 1000)}, + {URI: test.NewURIFromHostPort("serverC", 1000)}, }, Hasher: test.NewModHasher(), ReplicaN: 2, @@ -91,52 +92,442 @@ func TestHasher(t *testing.T) { } } -// Ensure that an empty cluster returns a valid (empty) NodeSet -func TestCluster_NodeSetHosts(t *testing.T) { - - c := pilosa.Cluster{} - - if h := c.NodeSetHosts(); !reflect.DeepEqual(h, []string{}) { - t.Fatalf("unexpected slice of hosts: %s", h) - } -} - -// Ensure cluster can compare its Nodes and Members -func TestCluster_NodeStates(t *testing.T) { - c := pilosa.Cluster{ - Nodes: []*pilosa.Node{ - {Host: "serverA:1000"}, - {Host: "serverB:1000"}, - {Host: "serverC:1000"}, - }, - NodeSet: &pilosa.StaticNodeSet{}, - } - - err := c.NodeSet.(*pilosa.StaticNodeSet).Join([]*pilosa.Node{ - &pilosa.Node{Host: "serverA:1000"}, - &pilosa.Node{Host: "serverC:1000"}, - &pilosa.Node{Host: "serverD:1000"}, - }) - if err != nil { - t.Fatalf("unexpected gossiper nodes: %s", err) - } - - // Verify a DOWN node is reported, and extraneous nodes are ignored - if a := c.NodeStates(); !reflect.DeepEqual(a, map[string]string{ - "serverA:1000": pilosa.NodeStateUp, - "serverB:1000": pilosa.NodeStateDown, - "serverC:1000": pilosa.NodeStateUp, - }) { - t.Fatalf("unexpected node state: %s", spew.Sdump(a)) - } -} - -// Ensure OwnsSlices can find the actual slice list for node and index +// Ensure OwnsSlices can find the actual slice list for node and index. func TestCluster_OwnsSlices(t *testing.T) { c := test.NewCluster(5) - slices := c.OwnsSlices("test", 10, "host2") + slices := c.OwnsSlices("test", 10, test.NewURIFromHostPort("host2", 0)) if !reflect.DeepEqual(slices, []uint64{0, 3, 6, 10}) { t.Fatalf("unexpected slices for node's index: %v", slices) } } + +// Ensure ContainsSlices can find the actual slice list for node and index. +func TestCluster_ContainsSlices(t *testing.T) { + c := test.NewCluster(5) + c.ReplicaN = 3 + slices := c.ContainsSlices("test", 10, c.Nodes[2]) + + if !reflect.DeepEqual(slices, []uint64{0, 2, 3, 5, 6, 9, 10}) { + t.Fatalf("unexpected slices for node's index: %v", slices) + } +} + +func TestCluster_Nodes(t *testing.T) { + uri0 := test.NewURIFromHostPort("node0", 0) + uri1 := test.NewURIFromHostPort("node1", 0) + uri2 := test.NewURIFromHostPort("node2", 0) + uri3 := test.NewURIFromHostPort("node3", 0) + + node0 := &pilosa.Node{ID: "node0", URI: uri0} + node1 := &pilosa.Node{ID: "node1", URI: uri1} + node2 := &pilosa.Node{ID: "node2", URI: uri2} + node3 := &pilosa.Node{ID: "node3", URI: uri3} + + nodes := []*pilosa.Node{node0, node1, node2} + + t.Run("NodeIDs", func(t *testing.T) { + actual := pilosa.Nodes(nodes).IDs() + expected := []string{node0.ID, node1.ID, node2.ID} + if !reflect.DeepEqual(actual, expected) { + t.Errorf("expected: %v, but got: %v", expected, actual) + } + }) + + t.Run("Filter", func(t *testing.T) { + actual := pilosa.Nodes(pilosa.Nodes(nodes).Filter(nodes[1])).URIs() + expected := []pilosa.URI{uri0, uri2} + if !reflect.DeepEqual(actual, expected) { + t.Errorf("expected: %v, but got: %v", expected, actual) + } + }) + + t.Run("FilterURI", func(t *testing.T) { + actual := pilosa.Nodes(pilosa.Nodes(nodes).FilterURI(uri1)).URIs() + expected := []pilosa.URI{uri0, uri2} + if !reflect.DeepEqual(actual, expected) { + t.Errorf("expected: %v, but got: %v", expected, actual) + } + }) + + t.Run("Contains", func(t *testing.T) { + actualTrue := pilosa.Nodes(nodes).Contains(node1) + actualFalse := pilosa.Nodes(nodes).Contains(node3) + if !reflect.DeepEqual(actualTrue, true) { + t.Errorf("expected: %v, but got: %v", true, actualTrue) + } + if !reflect.DeepEqual(actualFalse, false) { + t.Errorf("expected: %v, but got: %v", false, actualTrue) + } + }) + + t.Run("Clone", func(t *testing.T) { + clone := pilosa.Nodes(nodes).Clone() + actual := pilosa.Nodes(clone).URIs() + expected := []pilosa.URI{uri0, uri1, uri2} + if !reflect.DeepEqual(actual, expected) { + t.Errorf("expected: %v, but got: %v", expected, actual) + } + }) +} + +func TestCluster_Coordinator(t *testing.T) { + uri1 := test.NewURIFromHostPort("node1", 0) + uri2 := test.NewURIFromHostPort("node2", 0) + + node1 := &pilosa.Node{ID: "node1", URI: uri1} + node2 := &pilosa.Node{ID: "node2", URI: uri2} + + c1 := *pilosa.NewCluster() + c1.Node = node1 + c1.Coordinator = node1.ID + c2 := *pilosa.NewCluster() + c2.Node = node2 + c2.Coordinator = node1.ID + + t.Run("IsCoordinator", func(t *testing.T) { + if !c1.IsCoordinator() { + t.Errorf("!IsCoordinator error: %v", c1.Node) + } else if c2.IsCoordinator() { + t.Errorf("IsCoordinator error: %v", c2.Node) + } + }) +} + +func TestCluster_Topology(t *testing.T) { + c1 := test.NewCluster(1) // automatically creates Node{ID: "node0"} + + uri0 := test.NewURIFromHostPort("host0", 0) + uri1 := test.NewURIFromHostPort("host1", 0) + uri2 := test.NewURIFromHostPort("host2", 0) + invalid := test.NewURIFromHostPort("invalid", 0) + + node0 := &pilosa.Node{ID: "node0", URI: uri0} + node1 := &pilosa.Node{ID: "node1", URI: uri1} + node2 := &pilosa.Node{ID: "node2", URI: uri2} + nodeinvalid := &pilosa.Node{ID: "nodeinvalid", URI: invalid} + + t.Run("AddNode", func(t *testing.T) { + err := c1.AddNode(node1) + if err != nil { + t.Fatal(err) + } + // add the same host. + err = c1.AddNode(node1) + if err != nil { + t.Fatal(err) + } + err = c1.AddNode(node2) + if err != nil { + t.Fatal(err) + } + + actual := c1.NodeIDs() + expected := []string{node0.ID, node1.ID, node2.ID} + + if !reflect.DeepEqual(actual, expected) { + t.Errorf("expected: %v, but got: %v", expected, actual) + } + }) + + t.Run("ContainsID", func(t *testing.T) { + if !c1.Topology.ContainsID(node1.ID) { + t.Errorf("!ContainsHost error: %v", node1.ID) + } else if c1.Topology.ContainsID(nodeinvalid.ID) { + t.Errorf("ContainsHost error: %v", nodeinvalid.ID) + } + }) +} + +// Ensure that general cluster functionality works as expected. +func TestCluster_ResizeStates(t *testing.T) { + + t.Run("Single node, no data", func(t *testing.T) { + tc := test.NewTestCluster(1) + + // Open TestCluster. + if err := tc.Open(); err != nil { + t.Fatal(err) + } + + node := tc.Clusters[0] + + // Ensure that node comes up in state NORMAL. + if node.State() != pilosa.ClusterStateNormal { + t.Errorf("expected state: %v, but got: %v", pilosa.ClusterStateNormal, node.State()) + } + + expectedTop := &pilosa.Topology{ + NodeIDs: []string{node.Node.ID}, + } + + // Verify topology file. + if !reflect.DeepEqual(node.Topology.NodeIDs, expectedTop.NodeIDs) { + t.Errorf("expected topology: %v, but got: %v", expectedTop.NodeIDs, node.Topology.NodeIDs) + } + + // Close TestCluster. + if err := tc.Close(); err != nil { + t.Fatal(err) + } + }) + + t.Run("Single node, in topology", func(t *testing.T) { + tc := test.NewTestCluster(0) + tc.AddNode(false) + + node := tc.Clusters[0] + + // write topology to data file + top := &pilosa.Topology{ + NodeIDs: []string{node.Node.ID}, + } + tc.WriteTopology(node.Path, top) + + // Open TestCluster. + if err := tc.Open(); err != nil { + t.Fatal(err) + } + + // Ensure that node comes up in state NORMAL. + if node.State() != pilosa.ClusterStateNormal { + t.Errorf("expected state: %v, but got: %v", pilosa.ClusterStateNormal, node.State()) + } + + // Close TestCluster. + if err := tc.Close(); err != nil { + t.Fatal(err) + } + }) + + t.Run("Single node, not in topology", func(t *testing.T) { + tc := test.NewTestCluster(0) + tc.AddNode(false) + + node := tc.Clusters[0] + + // write topology to data file + top := &pilosa.Topology{ + NodeIDs: []string{"some-other-host"}, + } + tc.WriteTopology(node.Path, top) + + // Open TestCluster. + expected := "considerTopology: coordinator node0 is not in topology: [some-other-host]" + err := tc.Open() + if err == nil || err.Error() != expected { + t.Errorf("did not receive expected error: %s", expected) + } + + // Close TestCluster. + if err := tc.Close(); err != nil { + t.Fatal(err) + } + }) + + t.Run("Multiple nodes, no data", func(t *testing.T) { + tc := test.NewTestCluster(0) + tc.AddNode(false) + + // Open TestCluster. + if err := tc.Open(); err != nil { + t.Fatal(err) + } + + tc.AddNode(false) + + node0 := tc.Clusters[0] + node1 := tc.Clusters[1] + + // Ensure that nodes comes up in state NORMAL. + if node0.State() != pilosa.ClusterStateNormal { + t.Errorf("expected node0 state: %v, but got: %v", pilosa.ClusterStateNormal, node0.State()) + } else if node1.State() != pilosa.ClusterStateNormal { + t.Errorf("expected node1 state: %v, but got: %v", pilosa.ClusterStateNormal, node1.State()) + } + + expectedTop := &pilosa.Topology{ + NodeIDs: []string{node0.Node.ID, node1.Node.ID}, + } + + // Verify topology file. + if !reflect.DeepEqual(node0.Topology.NodeIDs, expectedTop.NodeIDs) { + t.Errorf("expected node0 topology: %v, but got: %v", expectedTop.NodeIDs, node0.Topology.NodeIDs) + } else if !reflect.DeepEqual(node1.Topology.NodeIDs, expectedTop.NodeIDs) { + t.Errorf("expected node1 topology: %v, but got: %v", expectedTop.NodeIDs, node1.Topology.NodeIDs) + } + + // Close TestCluster. + if err := tc.Close(); err != nil { + t.Fatal(err) + } + }) + + t.Run("Multiple nodes, in/not in topology", func(t *testing.T) { + tc := test.NewTestCluster(0) + tc.AddNode(false) + node0 := tc.Clusters[0] + + // write topology to data file + top := &pilosa.Topology{ + NodeIDs: []string{"node0", "node2"}, + } + tc.WriteTopology(node0.Path, top) + + // Open TestCluster. + if err := tc.Open(); err != nil { + t.Fatal(err) + } + + // Ensure that node is in state STARTING before the other node joins. + if node0.State() != pilosa.ClusterStateStarting { + t.Errorf("expected node0 state: %v, but got: %v", pilosa.ClusterStateStarting, node0.State()) + } + + // Expect an error by adding a node not in the topology. + expectedError := "host is not in topology: node1" + err := tc.AddNode(false) + if err == nil || err.Error() != expectedError { + t.Errorf("did not receive expected error: %s", expectedError) + } + + tc.AddNode(false) + node2 := tc.Clusters[2] + + // Ensure that node comes up in state NORMAL. + if node0.State() != pilosa.ClusterStateNormal { + t.Errorf("expected node0 state: %v, but got: %v", pilosa.ClusterStateNormal, node0.State()) + } else if node2.State() != pilosa.ClusterStateNormal { + t.Errorf("expected node1 state: %v, but got: %v", pilosa.ClusterStateNormal, node2.State()) + } + + // Close TestCluster. + if err := tc.Close(); err != nil { + t.Fatal(err) + } + }) + + t.Run("Multiple nodes, with data", func(t *testing.T) { + tc := test.NewTestCluster(0) + tc.AddNode(false) + node0 := tc.Clusters[0] + + // Open TestCluster. + if err := tc.Open(); err != nil { + t.Fatal(err) + } + + // Add Bit Data to node0. + if err := tc.CreateFrame("i", "f", pilosa.FrameOptions{}); err != nil { + t.Fatal(err) + } + tc.SetBit("i", "f", "standard", 1, 101, nil) + tc.SetBit("i", "f", "standard", 1, 1300000, nil) + + // Add Field Data to node0. + if err := tc.CreateFrame("i", "fields", pilosa.FrameOptions{ + InverseEnabled: false, + RangeEnabled: true, + //CacheType: pilosa.CacheTypeNone, + Fields: []*pilosa.Field{ + { + Name: "fld0", + Type: pilosa.FieldTypeInt, + Min: -100, + Max: 100, + }, + }, + }); err != nil { + t.Fatal(err) + } + tc.SetFieldValue("i", "fields", 1, "fld0", -10) + tc.SetFieldValue("i", "fields", 1, "fld0", 10) + tc.SetFieldValue("i", "fields", 1300000, "fld0", -99) + tc.SetFieldValue("i", "fields", 1300000, "fld0", 99) + + // Before starting the resize, get the CheckSum to use for + // comparison later. + node0Frame := node0.Holder.Frame("i", "f") + node0View := node0Frame.View("standard") + node0Fragment := node0View.Fragment(1) + node0Checksum := node0Fragment.Checksum() + + node0Frame = node0.Holder.Frame("i", "fields") + node0View = node0Frame.View("field_fld0") + node0Fragment = node0View.Fragment(1) + node0ChecksumFld := node0Fragment.Checksum() + + // AddNode needs to block until the resize process has completed. + tc.AddNode(false) + node1 := tc.Clusters[1] + + // Ensure that nodes come up in state NORMAL. + if node0.State() != pilosa.ClusterStateNormal { + t.Errorf("expected node0 state: %v, but got: %v", pilosa.ClusterStateNormal, node0.State()) + } else if node1.State() != pilosa.ClusterStateNormal { + t.Errorf("expected node1 state: %v, but got: %v", pilosa.ClusterStateNormal, node1.State()) + } + + expectedTop := &pilosa.Topology{ + NodeIDs: []string{node0.Node.ID, node1.Node.ID}, + } + + // Verify topology file. + if !reflect.DeepEqual(node0.Topology.NodeIDs, expectedTop.NodeIDs) { + t.Errorf("expected node0 topology: %v, but got: %v", expectedTop.NodeIDs, node0.Topology.NodeIDs) + } else if !reflect.DeepEqual(node1.Topology.NodeIDs, expectedTop.NodeIDs) { + t.Errorf("expected node1 topology: %v, but got: %v", expectedTop.NodeIDs, node1.Topology.NodeIDs) + } + + // Bits + // Verify that node-1 contains the fragment (i/f/standard/1) transferred from node-0. + node1Frame := node1.Holder.Frame("i", "f") + node1View := node1Frame.View("standard") + node1Fragment := node1View.Fragment(1) + + // Ensure checksums are the same. + if chksum := node1Fragment.Checksum(); !bytes.Equal(chksum, node0Checksum) { + t.Fatalf("expected standard view checksum to match: %x - %x", chksum, node0Checksum) + } + + // Values + // Verify that node-1 contains the fragment (i/fields/field_fld0/1) transferred from node-0. + node1Frame = node1.Holder.Frame("i", "fields") + node1View = node1Frame.View("field_fld0") + node1Fragment = node1View.Fragment(1) + + // Ensure checksums are the same. + if chksum := node1Fragment.Checksum(); !bytes.Equal(chksum, node0ChecksumFld) { + t.Fatalf("expected checksum to match: %x - %x", chksum, node0ChecksumFld) + } + + // Close TestCluster. + if err := tc.Close(); err != nil { + t.Fatal(err) + } + }) +} + +// Ensures that coordinator can be changed. +func TestCluster_UpdateCoordinator(t *testing.T) { + t.Run("UpdateCoordinator", func(t *testing.T) { + c := test.NewCluster(2) + + oldNode := c.Nodes[0] + newNode := c.Nodes[1] + + // Update coordinator to the same value. + if c.UpdateCoordinator(oldNode) { + t.Errorf("did not expect coordinator to change") + } else if c.Coordinator != oldNode.ID { + t.Errorf("expected coordinator: %s, but got: %s", c.Coordinator, oldNode.URI) + } + + // Update coordinator to a new value. + if !c.UpdateCoordinator(newNode) { + t.Errorf("expected coordinator to change") + } else if c.Coordinator != newNode.ID { + t.Errorf("expected coordinator: %s, but got: %s", c.Coordinator, newNode.URI) + } + }) +} diff --git a/cmd/root_test.go b/cmd/root_test.go index 93711a5a1..431d3a66f 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -181,7 +181,6 @@ func TestRootCommand_Config(t *testing.T) { bind = "127.0.0.1:10101" [cluster] - poll-interval = "2m0s" replicas = 2 partitions = 128 hosts = [ diff --git a/cmd/server_test.go b/cmd/server_test.go index 04436c1c4..b1f39e921 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -45,15 +45,14 @@ func TestServerConfig(t *testing.T) { // TEST 0 { args: []string{"server", "--data-dir", actualDataDir, "--cluster.hosts", "localhost:10111,localhost:10110", "--bind", "localhost:10111"}, - env: map[string]string{"PILOSA_DATA_DIR": "/tmp/myEnvDatadir", "PILOSA_CLUSTER_POLL_INTERVAL": "3m2s", "PILOSA_CLUSTER_LONG_QUERY_TIME": "1m30s", "PILOSA_MAX_WRITES_PER_REQUEST": "2000"}, + env: map[string]string{"PILOSA_DATA_DIR": "/tmp/myEnvDatadir", "PILOSA_CLUSTER_LONG_QUERY_TIME": "1m30s", "PILOSA_MAX_WRITES_PER_REQUEST": "2000"}, cfgFileContent: ` data-dir = "/tmp/myFileDatadir" bind = "localhost:0" max-writes-per-request = 3000 [cluster] - poll-interval = "45s" - type = "static" + disabled = true replicas = 2 hosts = [ "localhost:19444", @@ -66,7 +65,6 @@ func TestServerConfig(t *testing.T) { v.Check(cmd.Server.Config.Bind, "localhost:10111") v.Check(cmd.Server.Config.Cluster.ReplicaN, 2) v.Check(cmd.Server.Config.Cluster.Hosts, []string{"localhost:10111", "localhost:10110"}) - v.Check(cmd.Server.Config.Cluster.PollInterval, pilosa.Duration(time.Second*182)) v.Check(cmd.Server.Config.Cluster.LongQueryTime, pilosa.Duration(time.Second*90)) v.Check(cmd.Server.Config.MaxWritesPerRequest, 2000) return v.Error() @@ -80,7 +78,7 @@ func TestServerConfig(t *testing.T) { bind = "localhost:0" data-dir = "` + actualDataDir + `" [cluster] - type = "static" + disabled = true hosts = [ "localhost:19444", ] @@ -94,13 +92,12 @@ func TestServerConfig(t *testing.T) { }, // TEST 2 { - args: []string{"server", "--log-path", logFile.Name(), "--cluster.type", "static"}, + args: []string{"server", "--log-path", logFile.Name(), "--cluster.disabled", "true"}, env: map[string]string{"PILOSA_PROFILE_CPU_TIME": "1m"}, cfgFileContent: ` bind = "localhost:19444" data-dir = "` + actualDataDir + `" [cluster] - poll-interval = "2m0s" hosts = [ "localhost:19444", ] @@ -116,7 +113,6 @@ func TestServerConfig(t *testing.T) { validation: func() error { v := validator{} v.Check(cmd.Server.Config.Cluster.Hosts, []string{"localhost:19444"}) - v.Check(cmd.Server.Config.Cluster.PollInterval, pilosa.Duration(time.Minute*2)) v.Check(cmd.Server.Config.AntiEntropy.Interval, pilosa.Duration(time.Minute*11)) v.Check(cmd.Server.CPUProfile, profFile.Name()) v.Check(cmd.Server.CPUTime, time.Minute) diff --git a/config.go b/config.go index cd978c5ca..f2dd41793 100644 --- a/config.go +++ b/config.go @@ -26,27 +26,92 @@ const ( ) const ( + // DefaultDataDir is the default data directory. + DefaultDataDir = "~/.pilosa" + // DefaultHost is the default hostname to use. DefaultHost = "localhost" - // DefaultPort is the default port use with the hostname. + // DefaultPort is the default port to use with the hostname. DefaultPort = "10101" - // DefaultClusterType sets the node intercommunication method. - DefaultClusterType = ClusterGossip - - // DefaultGossipPort indicates the port to which pilosa should bind for internal state sharing. - DefaultGossipPort = "14000" + // DefaultClusterDisabled sets the node intercommunication method. + DefaultClusterDisabled = false // DefaultMetrics sets the internal metrics to no-op. DefaultMetrics = "nop" // DefaultMaxWritesPerRequest is the default number of writes per request. DefaultMaxWritesPerRequest = 5000 -) -// ClusterTypes set of cluster types. -var ClusterTypes = []string{ClusterNone, ClusterStatic, ClusterGossip} + // Gossip config based on memberlist.Config. + + // Port indicates the port to which pilosa should bind for internal state sharing. + DefaultGossipPort = "14000" + + // StreamTimeout is the timeout for establishing a stream connection with + // a remote node for a full state sync, and for stream read and write + // operations. Maps to memberlist TCPTimeout. + DefaultGossipStreamTimeout = 10 * time.Second + + // SuspicionMult is the multiplier for determining the time an + // inaccessible node is considered suspect before declaring it dead. + // The actual timeout is calculated using the formula: + // + // SuspicionTimeout = SuspicionMult * log(N+1) * ProbeInterval + // + // This allows the timeout to scale properly with expected propagation + // delay with a larger cluster size. The higher the multiplier, the longer + // an inaccessible node is considered part of the cluster before declaring + // it dead, giving that suspect node more time to refute if it is indeed + // still alive. + DefaultGossipSuspicionMult = 4 + + // PushPullInterval is the interval between complete state syncs. + // Complete state syncs are done with a single node over TCP and are + // quite expensive relative to standard gossiped messages. Setting this + // to zero will disable state push/pull syncs completely. + // + // Setting this interval lower (more frequent) will increase convergence + // speeds across larger clusters at the expense of increased bandwidth + // usage. + DefaultGossipPushPullInterval = 30 * time.Second + + // ProbeInterval and ProbeTimeout are used to configure probing behavior + // for memberlist. + // + // ProbeInterval is the interval between random node probes. Setting + // this lower (more frequent) will cause the memberlist cluster to detect + // failed nodes more quickly at the expense of increased bandwidth usage. + // + // ProbeTimeout is the timeout to wait for an ack from a probed node + // before assuming it is unhealthy. This should be set to 99-percentile + // of RTT (round-trip time) on your network. + DefaultGossipProbeInterval = 1 * time.Second + DefaultGossipProbeTimeout = 500 * time.Millisecond + + // Interval and Nodes are used to configure the gossip + // behavior of memberlist. + // + // Interval is the interval between sending messages that need + // to be gossiped that haven't been able to piggyback on probing messages. + // If this is set to zero, non-piggyback gossip is disabled. By lowering + // this value (more frequent) gossip messages are propagated across + // the cluster more quickly at the expense of increased bandwidth. + // + // Nodes is the number of random nodes to send gossip messages to + // per Interval. Increasing this number causes the gossip messages + // to propagate across the cluster more quickly at the expense of + // increased bandwidth. + // + // ToTheDeadTime is the interval after which a node has died that + // we will still try to gossip to it. This gives it a chance to refute. + DefaultGossipInterval = 200 * time.Millisecond + DefaultGossipNodes = 3 + DefaultGossipToTheDeadTime = 30 * time.Second + + DefaultMetricPollInterval = 0 * time.Minute +) // TLSConfig contains TLS configuration type TLSConfig struct { @@ -62,28 +127,6 @@ type TLSConfig struct { type Config struct { DataDir string `toml:"data-dir"` Bind string `toml:"bind"` - // GossipPort DEPRECATED - GossipPort string `toml:"gossip-port"` - // GossipSeed DEPRECATED - GossipSeed string `toml:"gossip-seed"` - - Gossip struct { - Port string `toml:"port"` - Seed string `toml:"seed"` - Key string `toml:"key"` - } `toml:"gossip"` - - Cluster struct { - ReplicaN int `toml:"replicas"` - Type string `toml:"type"` - Hosts []string `toml:"hosts"` - PollInterval Duration `toml:"poll-interval"` - LongQueryTime Duration `toml:"long-query-time"` - } `toml:"cluster"` - - AntiEntropy struct { - Interval Duration `toml:"interval"` - } `toml:"anti-entropy"` // Limits the number of mutating commands that can be in a single request to // the server. This includes SetBit, ClearBit, SetRowAttrs & SetColumnAttrs. @@ -91,67 +134,93 @@ type Config struct { LogPath string `toml:"log-path"` + // TLS + TLS TLSConfig + + Cluster struct { + Disabled bool `toml:"disabled"` + Coordinator bool `toml:"coordinator"` + ReplicaN int `toml:"replicas"` + Hosts []string `toml:"hosts"` + LongQueryTime Duration `toml:"long-query-time"` + } `toml:"cluster"` + + Gossip struct { + Port string `toml:"port"` + Seeds []string `toml:"seeds"` + Key string `toml:"key"` + StreamTimeout Duration `toml:"stream-timeout"` + SuspicionMult int `toml:"suspicion-mult"` + PushPullInterval Duration `toml:"push-pull-interval"` + ProbeTimeout Duration `toml:"probe-timeout"` + ProbeInterval Duration `toml:"probe-interval"` + Nodes int `toml:"nodes"` + Interval Duration `toml:"interval"` + ToTheDeadTime Duration `toml:"to-the-dead-time"` + } `toml:"gossip"` + + AntiEntropy struct { + Interval Duration `toml:"interval"` + } `toml:"anti-entropy"` + Metric struct { Service string `toml:"service"` Host string `toml:"host"` PollInterval Duration `toml:"poll-interval"` Diagnostics bool `toml:"diagnostics"` } `toml:"metric"` - - TLS TLSConfig } // NewConfig returns an instance of Config with default options. func NewConfig() *Config { c := &Config{ - Bind: DefaultHost + ":" + DefaultPort, + DataDir: DefaultDataDir, + Bind: ":" + DefaultPort, MaxWritesPerRequest: DefaultMaxWritesPerRequest, + // LogPath: "", + TLS: TLSConfig{}, } + + // Cluster config. + c.Cluster.Disabled = DefaultClusterDisabled + // c.Cluster.Coordinator = false c.Cluster.ReplicaN = DefaultReplicaN - c.Cluster.Type = DefaultClusterType - c.Cluster.PollInterval = Duration(DefaultPollingInterval) c.Cluster.Hosts = []string{} + c.Cluster.LongQueryTime = Duration(time.Minute) + + // Gossip config. + // c.Gossip.Port = "" + // c.Gossip.Seeds = []string{} + // c.Gossip.Key = "" + c.Gossip.StreamTimeout = Duration(DefaultGossipStreamTimeout) + c.Gossip.SuspicionMult = DefaultGossipSuspicionMult + c.Gossip.PushPullInterval = Duration(DefaultGossipPushPullInterval) + c.Gossip.ProbeTimeout = Duration(DefaultGossipProbeTimeout) + c.Gossip.ProbeInterval = Duration(DefaultGossipProbeInterval) + c.Gossip.Nodes = DefaultGossipNodes + c.Gossip.Interval = Duration(DefaultGossipInterval) + c.Gossip.ToTheDeadTime = Duration(DefaultGossipToTheDeadTime) + + // AntiEntropy config. c.AntiEntropy.Interval = Duration(DefaultAntiEntropyInterval) + + // Metric config. c.Metric.Service = DefaultMetrics + // c.Metric.Host = "" + c.Metric.PollInterval = Duration(DefaultMetricPollInterval) c.Metric.Diagnostics = true - c.TLS = TLSConfig{} + return c } // Validate that all configuration permutations are compatible with each other. func (c *Config) Validate() error { - if !StringInSlice(c.Cluster.Type, ClusterTypes) { - return ErrConfigClusterTypeInvalid + if !c.Cluster.Disabled && len(c.Cluster.Hosts) > 0 { + return ErrConfigClusterEnabledHosts } - - if c.Cluster.Type == ClusterGossip { - if len(c.Cluster.Hosts) > 0 { - bindWithDefaults, err := AddressWithDefaults(c.Bind) - if err != nil { - return err - } - if !c.foundHost(bindWithDefaults) { - return ErrConfigHostsMissing - } - } - } - return nil } -func (c *Config) foundHost(host *URI) bool { - for _, clusterHost := range c.Cluster.Hosts { - uri, err := NewURIFromAddress(clusterHost) - if err != nil { - continue - } - if host.Equals(uri) { - return true - } - } - return false -} - // Duration is a TOML wrapper type for time.Duration. type Duration time.Duration diff --git a/config_test.go b/config_test.go index dbeba89a0..1359e2e31 100644 --- a/config_test.go +++ b/config_test.go @@ -25,27 +25,15 @@ import ( func Test_NewConfig(t *testing.T) { c := pilosa.NewConfig() + if c.Cluster.Disabled != pilosa.DefaultClusterDisabled { + t.Fatalf("unexpected Cluster.Disabled: %v", c.Cluster.Disabled) + } + + // Ensure that hosts can't be specificed on a non-disabled cluster. c.Cluster.Hosts = []string{c.Bind, "localhost:10102"} // Change cluster type from the default (gossip) to an invalid string. - c.Cluster.Type = "invalid-type" - if err := c.Validate(); err != pilosa.ErrConfigClusterTypeInvalid { - t.Fatal(err) - } - - // Change cluster type back to gossip. - c.Cluster.Type = pilosa.ClusterGossip - - // Check for bind address in cluster hosts. - c.Bind = "localhost:1" - if err := c.Validate(); err != pilosa.ErrConfigHostsMissing { - t.Fatal(err) - } - - c.Bind = "localhost:10101" - c.Cluster.ReplicaN = 2 - c.GossipSeed = "localhost:14000" - if err := c.Validate(); err != nil { + if err := c.Validate(); err != pilosa.ErrConfigClusterEnabledHosts { t.Fatal(err) } } diff --git a/ctl/backup_test.go b/ctl/backup_test.go index 3db4b3b73..d23b1a917 100644 --- a/ctl/backup_test.go +++ b/ctl/backup_test.go @@ -50,9 +50,11 @@ func TestBackupCommand_Run(t *testing.T) { if err != nil { t.Fatal(err) } - s.Handler.URI = uri + node := &pilosa.Node{ID: "node", URI: *uri} + + s.Handler.Node = node s.Handler.Cluster = test.NewCluster(1) - s.Handler.Cluster.Nodes[0].Host = s.Host() + s.Handler.Cluster.Nodes[0].URI = *uri s.Handler.Holder = hldr.Holder cm := NewBackupCommand(stdin, stdout, stderr) file, err := ioutil.TempFile("", "import.csv") diff --git a/ctl/config_test.go b/ctl/config_test.go index a3bd035ac..b54446e34 100644 --- a/ctl/config_test.go +++ b/ctl/config_test.go @@ -17,11 +17,12 @@ package ctl import ( "bytes" "context" - "github.com/pilosa/pilosa" "io" "os" "strings" "testing" + + "github.com/pilosa/pilosa" ) func TestConfigCommand_Run(t *testing.T) { @@ -38,7 +39,7 @@ func TestConfigCommand_Run(t *testing.T) { if err != nil { t.Fatalf("Config Run doesn't work: %s", err) - } else if !strings.Contains(buf.String(), pilosa.DefaultHost) { - t.Fatalf("Unexpected config: %s", buf.String()) + } else if !strings.Contains(buf.String(), ":10101") { + t.Fatalf("Unexpected config: \n%s", buf.String()) } } diff --git a/ctl/export_test.go b/ctl/export_test.go index 401b902e9..5d1d5a4d7 100644 --- a/ctl/export_test.go +++ b/ctl/export_test.go @@ -63,9 +63,11 @@ func TestExportCommand_Run(t *testing.T) { if err != nil { t.Fatal(err) } - s.Handler.URI = uri + node := &pilosa.Node{ID: "node", URI: *uri} + + s.Handler.Node = node s.Handler.Cluster = test.NewCluster(1) - s.Handler.Cluster.Nodes[0].Host = s.Host() + s.Handler.Cluster.Nodes[0] = node s.Handler.Holder = hldr.Holder cm.Host = s.Host() diff --git a/ctl/generate_config.go b/ctl/generate_config.go index 5ddef99b1..a64678475 100644 --- a/ctl/generate_config.go +++ b/ctl/generate_config.go @@ -43,7 +43,6 @@ bind = "localhost:10101" max-writes-per-request = 5000 [cluster] - poll-interval = "2m0s" replicas = 1 hosts = [ "localhost:10101", diff --git a/ctl/import_test.go b/ctl/import_test.go index 254c21a05..9022efae9 100644 --- a/ctl/import_test.go +++ b/ctl/import_test.go @@ -69,9 +69,11 @@ func TestImportCommand_Run(t *testing.T) { if err != nil { t.Fatal(err) } - s.Handler.URI = uri + node := &pilosa.Node{ID: "node", URI: *uri} + + s.Handler.Node = node s.Handler.Cluster = test.NewCluster(1) - s.Handler.Cluster.Nodes[0].Host = s.Host() + s.Handler.Cluster.Nodes[0] = node s.Handler.Holder = hldr.Holder cm.Host = s.Host() @@ -109,9 +111,11 @@ func TestImportCommand_RunValue(t *testing.T) { if err != nil { t.Fatal(err) } - s.Handler.URI = uri + node := &pilosa.Node{ID: "node", URI: *uri} + + s.Handler.Node = node s.Handler.Cluster = test.NewCluster(1) - s.Handler.Cluster.Nodes[0].Host = s.Host() + s.Handler.Cluster.Nodes[0] = node s.Handler.Holder = hldr.Holder cm.Host = s.Host() diff --git a/ctl/restore_test.go b/ctl/restore_test.go index 9dd2d3661..bb8eb3b5f 100644 --- a/ctl/restore_test.go +++ b/ctl/restore_test.go @@ -52,9 +52,11 @@ func TestRestoreCommand_Run(t *testing.T) { if err != nil { t.Fatal(err) } - s.Handler.URI = uri + node := &pilosa.Node{ID: "node", URI: *uri} + + s.Handler.Node = node s.Handler.Cluster = test.NewCluster(1) - s.Handler.Cluster.Nodes[0].Host = s.Host() + s.Handler.Cluster.Nodes[0].URI = *uri s.Handler.Holder = hldr.Holder cm := NewRestoreCommand(stdin, stdout, stderr) diff --git a/ctl/server.go b/ctl/server.go index 78f72b732..87d6a8e15 100644 --- a/ctl/server.go +++ b/ctl/server.go @@ -24,26 +24,44 @@ import ( // BuildServerFlags attaches a set of flags to the command for a server instance. func BuildServerFlags(cmd *cobra.Command, srv *server.Command) { flags := cmd.Flags() - flags.StringVarP(&srv.Config.DataDir, "data-dir", "d", "~/.pilosa", "Directory to store pilosa data files.") - flags.StringVarP(&srv.Config.Bind, "bind", "b", ":10101", "Default URI on which pilosa should listen.") - flags.StringVarP(&srv.Config.GossipPort, "gossip-port", "", "", "(DEPRECATED) Port to which pilosa should bind for internal state sharing.") - flags.StringVarP(&srv.Config.GossipSeed, "gossip-seed", "", "", "(DEPRECATED) Host with which to seed the gossip membership.") - flags.StringVarP(&srv.Config.Gossip.Port, "gossip.port", "", "", "Port to which pilosa should bind for internal state sharing.") - flags.StringVarP(&srv.Config.Gossip.Seed, "gossip.seed", "", "", "Host with which to seed the gossip membership.") - flags.StringVarP(&srv.Config.Gossip.Key, "gossip.key", "", "", "The path to file of the encryption key for gossip. The contents of the file should be either 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256.") + flags.StringVarP(&srv.Config.DataDir, "data-dir", "d", srv.Config.DataDir, "Directory to store pilosa data files.") + flags.StringVarP(&srv.Config.Bind, "bind", "b", srv.Config.Bind, "Default URI on which pilosa should listen.") flags.IntVarP(&srv.Config.MaxWritesPerRequest, "max-writes-per-request", "", srv.Config.MaxWritesPerRequest, "Number of write commands per request.") + flags.StringVar(&srv.Config.LogPath, "log-path", srv.Config.LogPath, "Log path") + + // TLS + SetTLSConfig(flags, &srv.Config.TLS.CertificatePath, &srv.Config.TLS.CertificateKeyPath, &srv.Config.TLS.SkipVerify) + + // Cluster + flags.BoolVarP(&srv.Config.Cluster.Disabled, "cluster.disabled", "", srv.Config.Cluster.Disabled, "Disabled multi-node cluster communication (used for testing)") + flags.BoolVarP(&srv.Config.Cluster.Coordinator, "cluster.coordinator", "", srv.Config.Cluster.Coordinator, "Host that will act as cluster coordinator during startup and resizing.") flags.IntVarP(&srv.Config.Cluster.ReplicaN, "cluster.replicas", "", 1, "Number of hosts each piece of data should be stored on.") - flags.StringSliceVarP(&srv.Config.Cluster.Hosts, "cluster.hosts", "", []string{}, "Comma separated list of hosts in cluster.") - flags.DurationVarP((*time.Duration)(&srv.Config.Cluster.PollInterval), "cluster.poll-interval", "", time.Minute, "Polling interval for cluster.") // TODO what actually is this? + flags.StringSliceVarP(&srv.Config.Cluster.Hosts, "cluster.hosts", "", []string{}, "Comma separated list of hosts in cluster. Only used for testing.") flags.DurationVarP((*time.Duration)(&srv.Config.Cluster.LongQueryTime), "cluster.long-query-time", "", time.Minute, "Duration that will trigger log and stat messages for slow queries.") - flags.StringVar(&srv.Config.LogPath, "log-path", "", "Log path") - flags.DurationVarP((*time.Duration)(&srv.Config.AntiEntropy.Interval), "anti-entropy.interval", "", time.Minute*10, "Interval at which to run anti-entropy routine.") + + // Gossip + flags.StringVarP(&srv.Config.Gossip.Port, "gossip.port", "", srv.Config.Gossip.Port, "Port to which pilosa should bind for internal state sharing.") + flags.StringSliceVarP(&srv.Config.Gossip.Seeds, "gossip.seeds", "", srv.Config.Gossip.Seeds, "Host with which to seed the gossip membership.") + flags.StringVarP(&srv.Config.Gossip.Key, "gossip.key", "", srv.Config.Gossip.Key, "The path to file of the encryption key for gossip. The contents of the file should be either 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256.") + flags.DurationVarP((*time.Duration)(&srv.Config.Gossip.StreamTimeout), "gossip.stream-timeout", "", (time.Duration)(srv.Config.Gossip.StreamTimeout), "Timeout for establishing a stream connection with a remote node for a full state sync.") + flags.IntVarP(&srv.Config.Gossip.SuspicionMult, "gossip.suspicion-mult", "", srv.Config.Gossip.SuspicionMult, "Multiplier for determining the time an inaccessible node is considered suspect before declaring it dead.") + flags.DurationVarP((*time.Duration)(&srv.Config.Gossip.PushPullInterval), "gossip.push-pull-interval", "", (time.Duration)(srv.Config.Gossip.PushPullInterval), "Interval between complete state syncs.") + flags.DurationVarP((*time.Duration)(&srv.Config.Gossip.ProbeTimeout), "gossip.probe-timeout", "", (time.Duration)(srv.Config.Gossip.ProbeTimeout), "Timeout to wait for an ack from a probed node before assuming it is unhealthy.") + flags.DurationVarP((*time.Duration)(&srv.Config.Gossip.ProbeInterval), "gossip.probe-interval", "", (time.Duration)(srv.Config.Gossip.ProbeInterval), "Interval between random node probes.") + flags.IntVarP(&srv.Config.Gossip.Nodes, "gossip.nodes", "", srv.Config.Gossip.Nodes, "Number of random nodes to send gossip messages to per GossipInterval.") + flags.DurationVarP((*time.Duration)(&srv.Config.Gossip.Interval), "gossip.interval", "", (time.Duration)(srv.Config.Gossip.Interval), "Interval between sending messages that need to be gossiped that haven't piggybacked on probing messages.") + flags.DurationVarP((*time.Duration)(&srv.Config.Gossip.ToTheDeadTime), "gossip.to-the-dead-time", "", (time.Duration)(srv.Config.Gossip.ToTheDeadTime), "Interval after which a node has died that we will still try to gossip to it.") + + // AntiEntropy + flags.DurationVarP((*time.Duration)(&srv.Config.AntiEntropy.Interval), "anti-entropy.interval", "", (time.Duration)(srv.Config.AntiEntropy.Interval), "Interval at which to run anti-entropy routine.") + + // Metric + flags.StringVarP(&srv.Config.Metric.Service, "metric.service", "", srv.Config.Metric.Service, "Default URI on which pilosa should listen.") + flags.StringVarP(&srv.Config.Metric.Host, "metric.host", "", srv.Config.Metric.Host, "Default URI to send metrics.") + flags.DurationVarP((*time.Duration)(&srv.Config.Metric.PollInterval), "metric.poll-interval", "", (time.Duration)(srv.Config.Metric.PollInterval), "Polling interval metrics.") + flags.BoolVarP((&srv.Config.Metric.Diagnostics), "metric.diagnostics", "", srv.Config.Metric.Diagnostics, "Enabled diagnostics reporting.") + + // CPU Profiling flags.StringVarP(&srv.CPUProfile, "profile.cpu", "", "", "Where to store CPU profile.") flags.DurationVarP(&srv.CPUTime, "profile.cpu-time", "", 30*time.Second, "CPU profile duration.") - flags.StringVarP(&srv.Config.Cluster.Type, "cluster.type", "", "gossip", "Determine how the cluster handles membership and state sharing. Choose from [static, gossip]") - flags.StringVarP(&srv.Config.Metric.Service, "metric.service", "", "nop", "Default URI on which pilosa should listen.") - flags.StringVarP(&srv.Config.Metric.Host, "metric.host", "", "", "Default URI to send metrics.") - flags.BoolVarP((&srv.Config.Metric.Diagnostics), "metric.diagnostics", "", true, "Enabled diagnostics reporting.") - flags.DurationVarP((*time.Duration)(&srv.Config.Metric.PollInterval), "metric.poll-interval", "", time.Minute*0, "Polling interval metrics.") - SetTLSConfig(flags, &srv.Config.TLS.CertificatePath, &srv.Config.TLS.CertificateKeyPath, &srv.Config.TLS.SkipVerify) } diff --git a/ctl/server_test.go b/ctl/server_test.go index dc31426ac..9866b497d 100644 --- a/ctl/server_test.go +++ b/ctl/server_test.go @@ -28,9 +28,6 @@ func TestBuildServerFlags(t *testing.T) { stdin, stdout, stderr := GetIO(buf) Server := server.NewCommand(stdin, stdout, stderr) BuildServerFlags(cm, Server) - if cm.Flags().Lookup("gossip-port").Name == "" { - t.Fatal("gossip-port flag is required") - } if cm.Flags().Lookup("data-dir").Name == "" { t.Fatal("data-dir flag is required") } diff --git a/diagnostics.go b/diagnostics.go new file mode 100644 index 000000000..3ad07ed42 --- /dev/null +++ b/diagnostics.go @@ -0,0 +1,324 @@ +// Copyright 2017 Pilosa Corp. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pilosa + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "strconv" + "strings" + "sync" + "time" +) + +// Default version check URL. +const ( + defaultVersionCheckURL = "https://diagnostics.pilosa.com/v0/version" +) + +type versionResponse struct { + Version string `json:"version"` + Message string `json:"message"` +} + +// DiagnosticsCollector represents a collector/sender of diagnostics data. +type DiagnosticsCollector struct { + mu sync.Mutex + host string + VersionURL string + version string + lastVersion string + startTime int64 + start time.Time + + metrics map[string]interface{} + + client *http.Client + + logOutput io.Writer + + server *Server +} + +// NewDiagnosticsCollector returns a new DiagnosticsCollector given an addr in the format "hostname:port". +func NewDiagnosticsCollector(host string) *DiagnosticsCollector { + return &DiagnosticsCollector{ + host: host, + VersionURL: defaultVersionCheckURL, + startTime: time.Now().Unix(), + start: time.Now(), + client: &http.Client{Timeout: 10 * time.Second}, + metrics: make(map[string]interface{}), + logOutput: ioutil.Discard, + } +} + +// SetVersion of locally running Pilosa Cluster to check against master. +func (d *DiagnosticsCollector) SetVersion(v string) { + d.version = v + d.Set("Version", v) +} + +// Flush sends the current metrics. +func (d *DiagnosticsCollector) Flush() error { + d.mu.Lock() + defer d.mu.Unlock() + d.metrics["Uptime"] = (time.Now().Unix() - d.startTime) + buf, err := d.encode() + if err != nil { + return err + } + req, err := http.NewRequest("POST", d.host, bytes.NewReader(buf)) + req.Header.Set("Content-Type", "application/json") + resp, err := d.client.Do(req) + if err != nil { + return err + } + // Intentionally ignoring response body, as user does not need to be notified of error. + defer resp.Body.Close() + return nil +} + +// CheckVersion of the local build against Pilosa master. +func (d *DiagnosticsCollector) CheckVersion() error { + var rsp versionResponse + req, err := http.NewRequest("GET", d.VersionURL, nil) + resp, err := d.client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("http: status=%d", resp.StatusCode) + } else if err := json.NewDecoder(resp.Body).Decode(&rsp); err != nil { + return fmt.Errorf("json decode: %s", err) + } + + // If version has not changed since the last check, return + if rsp.Version == d.lastVersion { + return nil + } + + d.lastVersion = rsp.Version + if err := d.compareVersion(rsp.Version); err != nil { + d.logger().Printf("%s\n", err.Error()) + } + + return nil +} + +// compareVersion check version strings. +func (d *DiagnosticsCollector) compareVersion(value string) error { + currentVersion := versionSegments(value) + localVersion := versionSegments(d.version) + + if localVersion[0] < currentVersion[0] { //Major + return fmt.Errorf("Warning: You are running Pilosa %s. A newer version (%s) is available: https://github.com/pilosa/pilosa/releases", d.version, value) + } else if localVersion[1] < currentVersion[1] && localVersion[0] == currentVersion[0] { // Minor + return fmt.Errorf("Warning: You are running Pilosa %s. The latest Minor release is %s: https://github.com/pilosa/pilosa/releases", d.version, value) + } else if localVersion[2] < currentVersion[2] && localVersion[0] == currentVersion[0] && localVersion[1] == currentVersion[1] { // Patch + return fmt.Errorf("There is a new patch release of Pilosa available: %s: https://github.com/pilosa/pilosa/releases", value) + } + + return nil +} + +// Encode metrics maps into the json message format. +func (d *DiagnosticsCollector) encode() ([]byte, error) { + return json.Marshal(d.metrics) +} + +// Set adds a key value metric. +func (d *DiagnosticsCollector) Set(name string, value interface{}) { + switch v := value.(type) { + case string: + if v == "" { + // Do not set empty string + return + } + } + d.mu.Lock() + defer d.mu.Unlock() + d.metrics[name] = value +} + +// SetLogger Set the logger output type. +func (d *DiagnosticsCollector) SetLogger(logger io.Writer) { + d.logOutput = logger +} + +// logger returns a logger that writes to LogOutput. +func (d *DiagnosticsCollector) logger() *log.Logger { + return log.New(d.logOutput, "", log.LstdFlags) +} + +// logErr logs the error and returns true if an error exists +func (d *DiagnosticsCollector) logErr(err error) bool { + if err != nil { + d.logOutput.Write([]byte(err.Error())) + return true + } + return false +} + +// EnrichWithOSInfo adds OS information to the diagnostics payload. +func (d *DiagnosticsCollector) EnrichWithOSInfo() { + uptime, err := d.server.SystemInfo.Uptime() + if !d.logErr(err) { + d.Set("HostUptime", uptime) + } + platform, err := d.server.SystemInfo.Platform() + if !d.logErr(err) { + d.Set("OSPlatform", platform) + } + family, err := d.server.SystemInfo.Family() + if !d.logErr(err) { + d.Set("OSFamily", family) + } + version, err := d.server.SystemInfo.OSVersion() + if !d.logErr(err) { + d.Set("OSVersion", version) + } + kernelVersion, err := d.server.SystemInfo.KernelVersion() + if !d.logErr(err) { + d.Set("OSKernelVersion", kernelVersion) + } +} + +// EnrichWithMemoryInfo adds memory information to the diagnostics payload. +func (d *DiagnosticsCollector) EnrichWithMemoryInfo() { + memFree, err := d.server.SystemInfo.MemFree() + if !d.logErr(err) { + d.Set("MemFree", memFree) + } + memTotal, err := d.server.SystemInfo.MemTotal() + if !d.logErr(err) { + d.Set("MemTotal", memTotal) + } + memUsed, err := d.server.SystemInfo.MemUsed() + if !d.logErr(err) { + d.Set("MemUsed", memUsed) + } +} + +// EnrichWithSchemaProperties adds schema info to the diagnostics payload. +func (d *DiagnosticsCollector) EnrichWithSchemaProperties() { + var numSlices uint64 + numFrames := 0 + numIndexes := 0 + bsiFieldCount := 0 + timeQuantumEnabled := false + + for _, index := range d.server.Holder.Indexes() { + numSlices += index.MaxSlice() + 1 + numIndexes += 1 + for _, frame := range index.Frames() { + numFrames += 1 + if frame.rangeEnabled { + if fields, err := frame.GetFields(); err == nil { + bsiFieldCount += len(fields) + } + } + if frame.TimeQuantum() != "" { + timeQuantumEnabled = true + } + } + } + + d.Set("NumIndexes", numIndexes) + d.Set("NumFrames", numFrames) + d.Set("NumSlices", numSlices) + d.Set("BSIFieldCount", bsiFieldCount) + d.Set("TimeQuantumEnabled", timeQuantumEnabled) +} + +// versionSegments returns the numeric segments of the version as a slice of ints. +func versionSegments(segments string) []int { + segments = strings.Trim(segments, "v") + segments = strings.Split(segments, "-")[0] + s := strings.Split(segments, ".") + segmentSlice := make([]int, len(s)) + for i, v := range s { + segmentSlice[i], _ = strconv.Atoi(v) + } + return segmentSlice +} + +// SystemInfo collects information about the host OS. +type SystemInfo interface { + Uptime() (uint64, error) + Platform() (string, error) + Family() (string, error) + OSVersion() (string, error) + KernelVersion() (string, error) + MemFree() (uint64, error) + MemTotal() (uint64, error) + MemUsed() (uint64, error) +} + +// NewNopSystemInfo creates a no-op implementation of SystemInfo. +func NewNopSystemInfo() *NopSystemInfo { + return &NopSystemInfo{} +} + +// NopSystemInfo is a no-op implementation of SystemInfo. +type NopSystemInfo struct { +} + +// Uptime is a no-op implementation of SystemInfo.Uptime. +func (n *NopSystemInfo) Uptime() (uint64, error) { + return 0, nil +} + +// Platform is a no-op implementation of SystemInfo.Platform. +func (n *NopSystemInfo) Platform() (string, error) { + return "", nil +} + +// Family is a no-op implementation of SystemInfo.Family. +func (n *NopSystemInfo) Family() (string, error) { + return "", nil +} + +// OSVersion is a no-op implementation of SystemInfo.OSVersion. +func (n *NopSystemInfo) OSVersion() (string, error) { + return "", nil +} + +// KernelVersion is a no-op implementation of SystemInfo.KernelVersion. +func (n *NopSystemInfo) KernelVersion() (string, error) { + return "", nil +} + +// MemFree is a no-op implementation of SystemInfo.MemFree. +func (n *NopSystemInfo) MemFree() (uint64, error) { + return 0, nil +} + +// MemTotal is a no-op implementation of SystemInfo.MemTotal. +func (n *NopSystemInfo) MemTotal() (uint64, error) { + return 0, nil +} + +// MemUsed is a no-op implementation of SystemInfo.MemUsed. +func (n *NopSystemInfo) MemUsed() (uint64, error) { + return 0, nil +} diff --git a/diagnostics/diagnostics.go b/diagnostics/diagnostics.go deleted file mode 100644 index 60248d6bb..000000000 --- a/diagnostics/diagnostics.go +++ /dev/null @@ -1,267 +0,0 @@ -// Copyright 2017 Pilosa Corp. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package diagnostics - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "io/ioutil" - "log" - "net/http" - "strconv" - "strings" - "sync" - "time" - - "github.com/shirou/gopsutil/host" - "github.com/shirou/gopsutil/mem" - "github.com/sony/gobreaker" -) - -// TODO: unique Cluster ID - -// Default version check URL. -const ( - DefaultVersionCheckURL = "https://diagnostics.pilosa.com/v0/version" -) - -type versionResponse struct { - Version string `json:"version"` - Message string `json:"message"` -} - -// Diagnostics represents a client to the Pilosa cluster. -type Diagnostics struct { - mu sync.Mutex - wg sync.WaitGroup - closing chan struct{} - host string - VersionURL string - version string - lastVersion string - startTime int64 - start time.Time - - metrics map[string]interface{} - - client *http.Client - interval time.Duration - - cb *gobreaker.CircuitBreaker - logOutput io.Writer -} - -// New returns a pointer to a new Diagnostics Client given an addr in the format "hostname:port". -func New(host string) *Diagnostics { - - return &Diagnostics{ - closing: make(chan struct{}), - host: host, - VersionURL: DefaultVersionCheckURL, - startTime: time.Now().Unix(), - start: time.Now(), - client: http.DefaultClient, - metrics: make(map[string]interface{}), - logOutput: ioutil.Discard, - } -} - -// SetVersion of locally running Pilosa Cluster to check against master. -func (d *Diagnostics) SetVersion(v string) { - d.version = v - d.Set("Version", v) -} - -// SetInterval of the diagnostic go routine and match with the circuit breaker timeout. -func (d *Diagnostics) SetInterval(i time.Duration) { - d.interval = i -} - -// schedule start the diagnostics service ticker. -func (d *Diagnostics) schedule() { - ticker := time.NewTicker(d.interval) - defer ticker.Stop() - - for { - select { - case <-d.closing: - return - case <-ticker.C: - d.CheckVersion() - d.Flush() - } - } -} - -// Flush sends the current metrics. -func (d *Diagnostics) Flush() error { - d.mu.Lock() - d.metrics["Uptime"] = (time.Now().Unix() - d.startTime) - buf, _ := d.Encode() - d.mu.Unlock() - - _, err := d.cb.Execute(func() (interface{}, error) { - req, err := http.NewRequest("POST", d.host, bytes.NewReader(buf)) - req.Header.Set("Content-Type", "application/json") - resp, err := d.client.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - // TODO verify response - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, err - } - return body, nil - }) - - return err -} - -// Open configures the circuit breaker used by the HTTP client. -func (d *Diagnostics) Open() { - var st gobreaker.Settings - if d.interval > 0 { - st.Timeout = d.interval * 2 - } - d.cb = gobreaker.NewCircuitBreaker(st) - - d.logger().Printf("Pilosa is currently configured to send small diagnostics reports to our team every hour. More information here: https://www.pilosa.com/docs/latest/administration/#diagnostics") -} - -// Close notify goroutine to stop. -func (d *Diagnostics) Close() error { - close(d.closing) - d.wg.Wait() - return nil -} - -// CheckVersion of the local build against Pilosa master. -func (d *Diagnostics) CheckVersion() error { - var rsp versionResponse - req, err := http.NewRequest("GET", d.VersionURL, nil) - resp, err := d.client.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("http: status=%d", resp.StatusCode) - } else if err := json.NewDecoder(resp.Body).Decode(&rsp); err != nil { - return fmt.Errorf("json decode: %s", err) - } - - // Same a version as last test - if rsp.Version == d.lastVersion { - return nil - } - - d.lastVersion = rsp.Version - if err := d.CompareVersion(rsp.Version); err != nil { - d.logger().Printf("%s\n", err.Error()) - } - - return nil -} - -// CompareVersion check version strings. -func (d *Diagnostics) CompareVersion(value string) error { - currentVersion := VersionSegments(value) - localVersion := VersionSegments(d.version) - - if localVersion[0] < currentVersion[0] { //Major - return fmt.Errorf("Warning: You are running Pilosa %s. A newer version (%s) is available: https://github.com/pilosa/pilosa/releases", d.version, value) - } else if localVersion[1] < currentVersion[1] && localVersion[0] == currentVersion[0] { // Minor - return fmt.Errorf("Warning: You are running Pilosa %s. The latest Minor release is %s: https://github.com/pilosa/pilosa/releases", d.version, value) - } else if localVersion[2] < currentVersion[2] && localVersion[0] == currentVersion[0] && localVersion[1] == currentVersion[1] { // Patch - return fmt.Errorf("There is a new patch release of Pilosa available: %s: https://github.com/pilosa/pilosa/releases", value) - } - - return nil -} - -// Encode metrics maps into the json message format. -func (d *Diagnostics) Encode() ([]byte, error) { - return json.Marshal(d.metrics) -} - -// Set adds a key value metric. -func (d *Diagnostics) Set(name string, value interface{}) { - d.mu.Lock() - defer d.mu.Unlock() - d.metrics[name] = value -} - -// SetLogger Set the logger output type. -func (d *Diagnostics) SetLogger(logger io.Writer) { - d.logOutput = logger -} - -// logger returns a logger that writes to LogOutput. -func (d *Diagnostics) logger() *log.Logger { - return log.New(d.logOutput, "", log.LstdFlags) -} - -// EnrichWithOSInfo adds OS information to the diagnostics payload. -func (d *Diagnostics) EnrichWithOSInfo() { - osInfo, err := host.Info() - if err != nil { - d.logOutput.Write([]byte(err.Error())) - } - d.Set("HostUptime", osInfo.Uptime) - - platform, family, version, err := host.PlatformInformation() - if err != nil { - d.logOutput.Write([]byte(err.Error())) - } - d.Set("OSPlatform", platform) - d.Set("OSFamily", family) - d.Set("OSVersion", version) - - kernelVersion, err := host.KernelVersion() - if err != nil { - d.logOutput.Write([]byte(err.Error())) - } - d.Set("OSKernelVersion", kernelVersion) -} - -// EnrichWithMemoryInfo adds memory information to the diagnostics payload. -func (d *Diagnostics) EnrichWithMemoryInfo() { - memory, err := mem.VirtualMemory() - if err != nil { - d.logOutput.Write([]byte(err.Error())) - } - d.Set("MemFree", memory.Free) - d.Set("MemTotal", memory.Total) - d.Set("MemUsed", memory.Used) - -} - -// VersionSegments returns the numeric segments of the version as a slice of ints. -func VersionSegments(segments string) []int { - segments = strings.Trim(segments, "v") - segments = strings.Split(segments, "-")[0] - s := strings.Split(segments, ".") - segmentSlice := make([]int, len(s)) - for i, v := range s { - segmentSlice[i], _ = strconv.Atoi(v) - } - return segmentSlice -} diff --git a/diagnostics/diagnostics_test.go b/diagnostics_internal_test.go similarity index 82% rename from diagnostics/diagnostics_test.go rename to diagnostics_internal_test.go index 8f85a57db..eb2498297 100644 --- a/diagnostics/diagnostics_test.go +++ b/diagnostics_internal_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package diagnostics_test +package pilosa import ( "encoding/json" @@ -23,25 +23,20 @@ import ( "runtime" "strings" "testing" - - "github.com/pilosa/pilosa/diagnostics" ) func TestDiagnosticsClient(t *testing.T) { // Mock server. server := httptest.NewServer(nil) - defer server.Close() // Create a new client. - d := diagnostics.New(server.URL) + d := NewDiagnosticsCollector(server.URL) d.SetLogger(ioutil.Discard) - d.Open() - defer d.Close() d.Set("gg", 10) d.Set("ss", "ss") - data, err := d.Encode() + data, err := d.encode() if err != nil { t.Fatal(err) } @@ -58,7 +53,7 @@ func TestDiagnosticsClient(t *testing.T) { // Test the metrics after a flush. d.Flush() - data, err = d.Encode() + data, err = d.encode() if err != nil { t.Fatal(err) } @@ -74,7 +69,7 @@ func TestDiagnosticsClient(t *testing.T) { func TestDiagnosticsVersion_Parse(t *testing.T) { version := "0.1.1" - vs := diagnostics.VersionSegments(version) + vs := versionSegments(version) output := []int{0, 1, 1} if !reflect.DeepEqual(vs, output) { @@ -83,35 +78,33 @@ func TestDiagnosticsVersion_Parse(t *testing.T) { } func TestDiagnosticsVersion_Compare(t *testing.T) { - d := diagnostics.New("localhost:10101") - d.Open() - defer d.Close() + d := NewDiagnosticsCollector("localhost:10101") version := "v0.1.1" d.SetVersion(version) - err := d.CompareVersion("v1.7.0") + err := d.compareVersion("v1.7.0") if !strings.Contains(err.Error(), "A newer version") { t.Fatalf("Expected a newer version is available, actual error: %s", err) } - err = d.CompareVersion("1.7.0") + err = d.compareVersion("1.7.0") if !strings.Contains(err.Error(), "A newer version") { t.Fatalf("Expected a newer version is available, actual error: %s", err) } - err = d.CompareVersion("0.7.0") + err = d.compareVersion("0.7.0") if !strings.Contains(err.Error(), "The latest Minor release is") { t.Fatalf("Expected Minor Version Missmatch, actual error: %s", err) } - err = d.CompareVersion("0.1.2") + err = d.compareVersion("0.1.2") if !strings.Contains(err.Error(), "There is a new patch release of Pilosa") { t.Fatalf("Expected Patch Version Missmatch, actual error: %s", err) } - err = d.CompareVersion("0.1.1") + err = d.compareVersion("0.1.1") if err != nil { t.Fatalf("Versions should match") } d.SetVersion("v1.7.0") - err = d.CompareVersion("0.7.2") + err = d.compareVersion("0.7.2") if err != nil { t.Fatalf("Local version is greater") } @@ -125,11 +118,9 @@ func TestDiagnosticsVersion_Check(t *testing.T) { Version: "1.1.1", }) })) - defer server.Close() // Create a new client. - d := diagnostics.New("localhost:10101") - defer d.Close() + d := NewDiagnosticsCollector("localhost:10101") version := "0.1.1" d.SetVersion(version) @@ -138,10 +129,6 @@ func TestDiagnosticsVersion_Check(t *testing.T) { d.CheckVersion() } -type versionResponse struct { - Version string `json:"version"` -} - func compareJSON(a, b []byte) (bool, error) { var j1, j2 interface{} if err := json.Unmarshal(a, &j1); err != nil { @@ -156,12 +143,10 @@ func compareJSON(a, b []byte) (bool, error) { func BenchmarkDiagnostics(b *testing.B) { // Mock server. server := httptest.NewServer(nil) - defer server.Close() // Create a new client. - d := diagnostics.New(server.URL) + d := NewDiagnosticsCollector(server.URL) d.SetLogger(ioutil.Discard) - defer d.Close() prev := runtime.GOMAXPROCS(4) defer runtime.GOMAXPROCS(prev) diff --git a/docs/administration.md b/docs/administration.md index 44d3fcbb6..9d8c5b70c 100644 --- a/docs/administration.md +++ b/docs/administration.md @@ -5,6 +5,7 @@ nav = [ "Installing in production", "Importing and Exporting Data", "Versioning", + "Resizing the Cluster", "Backup/restore", ] +++ @@ -93,6 +94,71 @@ The Pilosa server should support PQL versioning using HTTP headers. On each requ When upgrading, upgrade clients first, followed by server for all Minor and Patch level changes. +### Resizing the Cluster + +If you need to increase (or decrease) the capacity of a Pilosa server, you can add or remove nodes to a running cluster at any time. Note that you can only add or remove one node at a time; if you attempt to add multiple nodes at once, those requests will be enqueued and processed serially. Also note that during any resize process, the cluster goes into state `RESIZING` during which all read/write requests are denied. When the cluster returns to state `NORMAL` then read/write operations can resume. The amount of time that the cluster stays in state `RESIZING` depends on the amount of data that needs to be moved during the resize process. + +#### Adding a Node + +You can add a new, empty node to an existing cluster by starting `pilosa server` on the new node with the correct configuration options. Specifically, you must specify the [cluster coordinator](../configuration/#cluster-coordinator) to be the same as the coordinator on the existing nodes. You must also specify at least one valid [gossip seed](../configuration/#gossip-seeds) (preferably multiple for redundancy). When the new node starts, the coordinator node will receive a `nodeJoin` event indicating that a new node is joining the cluster. At this point, the coordinator will put the cluster into state `RESIZING` and kick off a resize job that instructs all of the nodes in the cluster how to rebalance data to accomodate the additional capacity of the new node. Once the resize job is complete, the coordinator will put the cluster back to state `NORMAL` and ensure that the new node is included in future queries. + +If the node is being added to a cluster which contains no data (for example, during startup of a new cluster), the coordinator will bypass the `RESIZING` state and allow the node to join the cluster immediately. + +#### Removing a Node + +In order to remove a node from a cluster, your cluster must be configured to have a [cluster replicas](../configuration/#cluster-replicas) value of at least 2; if you're removing a node that no longer exists (for example a node that has died), there must be at least one additional replica of the data owned by the dead node in order for the cluster to correctly rebalance itself. + +To remove node `localhost:10102` from a cluster having coordinator `localhost:10101`, first determine the ID of the node to be removed. If the node to be removed is still available, you can find the ID by issuing an `/id` request to the node: +``` request +curl localhost:10102/id +``` +``` response +40a891fa-243b-4d71-ae24-4f5c78a0f4b1 +``` + +If the node to be removed is no longer available, you can get the IDs of the nodes in the cluster by issuing a `/status` request to any available node: +``` request +curl localhost:10101/status +``` +``` response +{ + "state":"NORMAL", + "nodes":[ + {"id":"24824777-62ec-4151-9fbd-67e4676e317d","uri":{"scheme":"http","host":"localhost","port":10101}} + {"id":"40a891fa-243b-4d71-ae24-4f5c78a0f4b1","uri":{"scheme":"http","host":"localhost","port":10102}} + {"id":"9fab09cc-3c26-4202-9622-d167c84684d9","uri":{"scheme":"http","host":"localhost","port":10103}} + ] +} +``` + +Once you have the ID of the node that you want to remove from the cluster, issue the following request: +``` +curl localhost:10101/cluster/resize/remove-node \ + -X POST \ + -d '{"id": "40a891fa-243b-4d71-ae24-4f5c78a0f4b1"}' +``` +At this point, the coordinator will put the cluster into state `RESIZING` and kick off a resize job that instructs all of the nodes in the cluster how to rebalance data to accomodate the reduced capacity of the cluster. Once the resize job is complete, the coordinator will put the cluster back to state `NORMAL` and ensure that the removed node is no longer included in future queries. + +Note that you can't directly remove the coordinator node. If you need to remove the coordinator node from the cluster, you must first [make one of the other nodes the coordinator](#changing-the-coordinator). + +#### Aborting a Resize Job + +If at any point you need to abort an active resize job, you can issue a `POST` request to the `/cluster/resize/abort` endpoint on the coordinator node. +For example, if your coordinator node is `localhost:10101`, then you can run: +``` +curl localhost:10101/cluster/resize/abort -X POST +``` +This will immediately abort the resize job and return the cluster to state `NORMAL`. Because data is never removed from a node during a resize job (only once a resize job has successfully completed), aborting a resize job will return the cluster back to the state it was in before the resize began. + +#### Changing the Coordinator + +In order to assign a different node to be the coordinator, you can issue a `/cluster/resize/set-coordinator` request to any node in the cluster. The payload should indicate the ID of the node to be made coordinator. +``` +curl localhost:10101/cluster/resize/set-coordinator \ + -X POST \ + -d '{"id": "9fab09cc-3c26-4202-9622-d167c84684d9"}' +``` + ### Backup/restore Pilosa continuously writes out the in-memory bitmap data to disk. This data is organized by Index->Frame->Views->Fragment->numbered slice files. These data files can be routinely backed up to restore nodes in a cluster. diff --git a/docs/configuration.md b/docs/configuration.md index bc9bd75b0..633e83f3c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -27,24 +27,18 @@ Every command line flag has a corresponding environment variable. The environmen ### Config file -The config file is in the [toml format](https://github.com/toml-lang/toml) and has exactly the same options available as the flags and environment variables. Any flag which contains a dot (".") denotes nesting within the config file, so the two flags `--cluster.poll-interval=2m0s` and `--cluster.replicas=1` look like this in the config file: +The config file is in the [toml format](https://github.com/toml-lang/toml) and has exactly the same options available as the flags and environment variables. Any flag which contains a dot (".") denotes nesting within the config file, so the two flags `--cluster.coordinator` and `--cluster.replicas=1` look like this in the config file: ```toml [cluster] - poll-interval = "2m0s" + coordinator = true replicas = 1 ``` -Any flag that has a value that is a comma separated list on the command line becomes an array in toml. For example `--cluster.hosts=one.pilosa.com:10101,two.pilosa.com:10101` becomes: -```toml -[cluster] - hosts = ["one.pilosa.com:10101", "two.pilosa.com:10101"] -``` - ### All Options #### Anti Entropy Interval -* Description: Interval at which the cluster will run its anti-entropy routine which makes sure that all replicas of each fragment are in sync. +* Description: Interval at which the cluster will run its anti-entropy routine which ensures that all replicas of each fragment are in sync. * Flag: `--anti-entropy.interval="10m0s"` * Env: `PILOSA_ANTI_ENTROPY_INTERVAL="10m0s"` * Config: @@ -84,7 +78,7 @@ Any flag that has a value that is a comma separated list on the command line bec * Config: ```toml - log_path = "/path/to/logfile" + log-path = "/path/to/logfile" ``` #### Max Writes Per Request @@ -110,16 +104,16 @@ Any flag that has a value that is a comma separated list on the command line bec port = 11101 ``` -#### Gossip Seed +#### Gossip Seeds -* Description: When using the gossip [Cluster Type](#cluster-type), this specifies which internal host should be used to initialize membership in the cluster. Typcially this can be the address of any available host in the cluster. For example, when starting a three-node cluster made up of `node0`, `node1`, and `node2`, the `gossip-seed` for all three nodes can be configured to be the address of `node0`. -* Flag: `--gossip.seed="localhost:11101"` -* Env: `PILOSA_GOSSIP_SEED="localhost:11101"` +* Description: This specifies which internal host(s) should be used to initialize membership in the cluster. Typcially this can be the address of any available host in the cluster. For example, when starting a three-node cluster made up of `node0`, `node1`, and `node2`, the `gossip.seeds` for all three nodes can be configured to be the address of `node0`. Multiple seeds should be comma-separated in the flag and env forms. +* Flag: `--gossip.seeds="localhost:11101"` +* Env: `PILOSA_GOSSIP_SEEDS="localhost:11101"` * Config: ```toml [gossip] - seed = "localhost:11101" + seeds = ["localhost:11101"] ``` #### Gossip Key @@ -133,28 +127,16 @@ Any flag that has a value that is a comma separated list on the command line bec key = "/var/secret/gossip.key32" ``` -#### Cluster Hosts +#### Cluster Coordinator -* Description: List of hosts in the cluster. Multiple hosts should be comma separated in the flag and env forms. -* Flag: `--cluster.hosts="localhost:10101"` -* Env: `PILOSA_CLUSTER_HOSTS="localhost:10101"` +* Description: Indicates whether the node should act as the coordinator for the cluster. Only one node per cluster should be the coordinator. +* Flag: `cluster.coordinator` +* Env: `PILOSA_CLUSTER_COORDINATOR` * Config: ```toml [cluster] - hosts = ["localhost:10101"] - ``` - -#### Cluster Poll Interval - -* Description: Polling interval for cluster. -* Flag: `cluster.poll-interval="1m0s"` -* Env: `PILOSA_CLUSTER_POLL_INTERVAL="1m0s"` -* Config: - - ```toml - [cluster] - poll-interval = "1m0s" + coordinator = true ``` #### Cluster Long Query Time @@ -218,7 +200,7 @@ Any flag that has a value that is a comma separated list on the command line bec [profile] cpu-time = "30s" ``` - + #### Metric Service * Description: Which stats service to use. Choose from [statsd, expvar]. * Flag: `--metric.service=statsd` @@ -317,8 +299,7 @@ A three node cluster running on different hosts could be minimally configured as [cluster] replicas = 1 - type = "gossip" - hosts = ["node0.pilosa.com:10101","node1.pilosa.com:10101","node2.pilosa.com:10101"] + coordinator = true #### Node 1 @@ -331,8 +312,7 @@ A three node cluster running on different hosts could be minimally configured as [cluster] replicas = 1 - type = "gossip" - hosts = ["node0.pilosa.com:10101","node1.pilosa.com:10101","node2.pilosa.com:10101"] + coordinator = false #### Node 2 @@ -345,8 +325,7 @@ A three node cluster running on different hosts could be minimally configured as [cluster] replicas = 1 - type = "gossip" - hosts = ["node0.pilosa.com:10101","node1.pilosa.com:10101","node2.pilosa.com:10101"] + coordinator = false ### Example Cluster Configuration (HTTPS) @@ -365,8 +344,7 @@ The same cluster which uses HTTPS instead of HTTP can be configured as follows. [cluster] replicas = 1 - type = "gossip" - hosts = ["https://node0.pilosa.com:10101","https://node1.pilosa.com:10101","https://node2.pilosa.com:10101"] + coordinator = true [tls] certificate = "/home/pilosa/private/server.crt" @@ -384,8 +362,7 @@ The same cluster which uses HTTPS instead of HTTP can be configured as follows. [cluster] replicas = 1 - type = "gossip" - hosts = ["https://node0.pilosa.com:10101","https://node1.pilosa.com:10101","https://node2.pilosa.com:10101"] + coordinator = false [tls] certificate = "/home/pilosa/private/server.crt" @@ -403,8 +380,7 @@ The same cluster which uses HTTPS instead of HTTP can be configured as follows. [cluster] replicas = 1 - type = "gossip" - hosts = ["https://node0.pilosa.com:10101","https://node1.pilosa.com:10101","https://node2.pilosa.com:10101"] + coordinator = false [tls] certificate = "/home/pilosa/private/server.crt" @@ -426,8 +402,7 @@ You can run a cluster on the same host using the configuration above with a few [cluster] replicas = 1 - type = "gossip" - hosts = ["https://localhost:10100","https://localhost:10101","https://localhost:10102"] + coordinator = true [tls] certificate = "/home/pilosa/private/server.crt" @@ -445,8 +420,7 @@ You can run a cluster on the same host using the configuration above with a few [cluster] replicas = 1 - type = "gossip" - hosts = ["https://localhost:10100","https://localhost:10101","https://localhost:10102"] + coordinator = false [tls] certificate = "/home/pilosa/private/server.crt" @@ -464,8 +438,7 @@ You can run a cluster on the same host using the configuration above with a few [cluster] replicas = 1 - type = "gossip" - hosts = ["https://localhost:10100","https://localhost:10101","https://localhost:10102"] + coordinator = false [tls] certificate = "/home/pilosa/private/server.crt" diff --git a/docs/getting-started.md b/docs/getting-started.md index bd227d154..dc1e07ceb 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -35,7 +35,7 @@ Let's make sure Pilosa is running: curl localhost:10101/status ``` ``` response -{"status":{"Nodes":[{"Host":":10101","State":"UP"}]}} +{"state":"NORMAL","nodes":[{"id":"18eb5546-5a1a-4ba4-9c52-b53fbe22317e","uri":{"scheme":"http","host":"localhost","port":10101}}]} ``` ### Sample Project @@ -46,7 +46,9 @@ Although Pilosa doesn't keep the data in a tabular format, we still use the term #### Create the Schema -The queries in this section which are used to set up the indexes in Pilosa just the empty object on success: `{}` - if you would like to verify that a query worked as you expected, you can request the schema as follows: +Note: +The queries in this section which are used to set up the indexes in Pilosa just return the empty object on success: `{}` - if you would like to verify that a query worked as you expected, you can request the schema as follows: + ``` request curl localhost:10101/schema ``` @@ -107,7 +109,7 @@ docker cp language.csv pilosa:/language.csv docker exec -it pilosa /pilosa import -i repository -f language /language.csv ``` -Note that, both the user IDs and the repository IDs were remapped to sequential integers in the data files, they don't correspond to actual Github IDs anymore. You can check out `languages.txt` to see the mapping for languages. +Note that both the user IDs and the repository IDs were remapped to sequential integers in the data files, they don't correspond to actual Github IDs anymore. You can check out [languages.txt](https://github.com/pilosa/getting-started/blob/master/languages.txt) to see the mapping for languages. ### Input Definition diff --git a/event.go b/event.go new file mode 100644 index 000000000..5df69361b --- /dev/null +++ b/event.go @@ -0,0 +1,55 @@ +// Copyright 2017 Pilosa Corp. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pilosa + +// NodeEventType are the types of events that can be sent from the +// ChannelEventDelegate. +type NodeEventType int + +const ( + NodeJoin NodeEventType = iota + NodeLeave + NodeUpdate +) + +// NodeEvent is a single event related to node activity in the cluster. +type NodeEvent struct { + Event NodeEventType + Node *Node +} + +// EventHandler is the interface for the pilosa object which knows how to +// handle broadcast messages. (Hint: this is implemented by pilosa.Server) +type EventHandler interface { + ReceiveEvent(e *NodeEvent) error +} + +// EventReceiver is the interface for the object which will listen for and +// decode broadcast messages before passing them to pilosa to handle. The +// implementation of this could be an http server which listens for messages, +// gets the protobuf payload, and then passes it to +// EventHandler.ReceiveMessage. +type EventReceiver interface { + // Start starts listening for broadcast messages - it should return + // immediately, spawning a goroutine if necessary. + Start(EventHandler) error +} + +type nopEventReceiver struct{} + +func (n *nopEventReceiver) Start(e EventHandler) error { return nil } + +// NopEventReceiver is a no-op implementation of the EventReceiver. +var NopEventReceiver = &nopEventReceiver{} diff --git a/executor.go b/executor.go index 9e831c9f0..854a9d8b9 100644 --- a/executor.go +++ b/executor.go @@ -40,8 +40,7 @@ type Executor struct { Holder *Holder // Local hostname & cluster configuration. - Scheme string - Host string + Node *Node Cluster *Cluster // Client used for remote requests. @@ -957,7 +956,7 @@ func (e *Executor) executeClearBitView(ctx context.Context, index string, c *pql ret := false for _, node := range e.Cluster.FragmentNodes(index, slice) { // Update locally if host matches. - if node.Host == e.Host { + if node.ID == e.Node.ID { val, err := f.ClearBit(view, rowID, colID, nil) if err != nil { return false, err @@ -1062,7 +1061,7 @@ func (e *Executor) executeSetBitView(ctx context.Context, index string, c *pql.C for _, node := range e.Cluster.FragmentNodes(index, slice) { // Update locally if host matches. - if node.Host == e.Host { + if node.ID == e.Node.ID { val, err := f.SetBit(view, rowID, colID, timestamp) if err != nil { return false, err @@ -1141,7 +1140,7 @@ func (e *Executor) executeSetFieldValue(ctx context.Context, index string, c *pq } // Execute on remote nodes in parallel. - nodes := Nodes(e.Cluster.Nodes).FilterHost(e.Host) + nodes := Nodes(e.Cluster.Nodes).FilterID(e.Node.ID) resp := make(chan error, len(nodes)) for _, node := range nodes { go func(node *Node) { @@ -1199,7 +1198,7 @@ func (e *Executor) executeSetRowAttrs(ctx context.Context, index string, c *pql. } // Execute on remote nodes in parallel. - nodes := Nodes(e.Cluster.Nodes).FilterHost(e.Host) + nodes := Nodes(e.Cluster.Nodes).FilterID(e.Node.ID) resp := make(chan error, len(nodes)) for _, node := range nodes { go func(node *Node) { @@ -1286,7 +1285,7 @@ func (e *Executor) executeBulkSetRowAttrs(ctx context.Context, index string, cal } // Execute on remote nodes in parallel. - nodes := Nodes(e.Cluster.Nodes).FilterHost(e.Host) + nodes := Nodes(e.Cluster.Nodes).FilterID(e.Node.ID) resp := make(chan error, len(nodes)) for _, node := range nodes { go func(node *Node) { @@ -1345,7 +1344,7 @@ func (e *Executor) executeSetColumnAttrs(ctx context.Context, index string, c *p } // Execute on remote nodes in parallel. - nodes := Nodes(e.Cluster.Nodes).FilterHost(e.Host) + nodes := Nodes(e.Cluster.Nodes).FilterID(e.Node.ID) resp := make(chan error, len(nodes)) for _, node := range nodes { go func(node *Node) { @@ -1372,12 +1371,8 @@ func (e *Executor) remoteExec(ctx context.Context, node *Node, index string, q * Slices: slices, Remote: true, } - uri, err := NewURIFromAddress(node.Host) - if err != nil { - return nil, err - } - uri.SetScheme(node.Scheme) - ctx = context.WithValue(ctx, "uri", uri) + + ctx = context.WithValue(ctx, "uri", node.URI) pb, err := e.client.ExecuteQuery(ctx, index, pbreq) if err != nil { return nil, err @@ -1456,7 +1451,7 @@ func (e *Executor) mapReduce(ctx context.Context, index string, slices []uint64, if !opt.Remote { nodes = Nodes(e.Cluster.Nodes).Clone() } else { - nodes = []*Node{e.Cluster.NodeByHost(e.Host)} + nodes = []*Node{e.Cluster.nodeByID(e.Node.ID)} } // Start mapping across all primary owners. @@ -1512,7 +1507,7 @@ func (e *Executor) mapper(ctx context.Context, ch chan mapResponse, nodes []*Nod resp := mapResponse{node: n, slices: nodeSlices} // Send local slices to mapper, otherwise remote exec. - if n.Host == e.Host { + if n.ID == e.Node.ID { resp.result, resp.err = e.mapperLocal(ctx, nodeSlices, mapFn, reduceFn) } else if !opt.Remote { results, err := e.remoteExec(ctx, n, index, &pql.Query{Calls: []*pql.Call{c}}, nodeSlices, opt) diff --git a/executor_test.go b/executor_test.go index dd8725298..c98239137 100644 --- a/executor_test.go +++ b/executor_test.go @@ -897,8 +897,12 @@ func TestExecutor_Execute_Remote_Bitmap(t *testing.T) { // Create secondary server and update second cluster node. s := test.NewServer() defer s.Close() - c.Nodes[1].Scheme = "http" - c.Nodes[1].Host = s.Host() + + uri, err := pilosa.NewURIFromAddress(s.Host()) + if err != nil { + t.Fatal(err) + } + c.Nodes[1].URI = *uri // Mock secondary server's executor to verify arguments and return a bitmap. s.Handler.Executor.ExecuteFn = func(ctx context.Context, index string, query *pql.Query, slices []uint64, opt *pilosa.ExecOptions) ([]interface{}, error) { @@ -941,7 +945,13 @@ func TestExecutor_Execute_Remote_Count(t *testing.T) { // Create secondary server and update second cluster node. s := test.NewServer() defer s.Close() - c.Nodes[1].Host = s.Host() + + uri, err := pilosa.NewURIFromAddress(s.Host()) + if err != nil { + t.Fatal(err) + } + + c.Nodes[1].URI = *uri // Mock secondary server's executor to return a count. s.Handler.Executor.ExecuteFn = func(ctx context.Context, index string, query *pql.Query, slices []uint64, opt *pilosa.ExecOptions) ([]interface{}, error) { @@ -971,7 +981,13 @@ func TestExecutor_Execute_Remote_SetBit(t *testing.T) { // Create secondary server and update second cluster node. s := test.NewServer() defer s.Close() - c.Nodes[1].Host = s.Host() + + uri, err := pilosa.NewURIFromAddress(s.Host()) + if err != nil { + t.Fatal(err) + } + + c.Nodes[1].URI = *uri // Mock secondary server's executor to verify arguments. var remoteCalled bool @@ -1017,7 +1033,13 @@ func TestExecutor_Execute_Remote_SetBit_With_Timestamp(t *testing.T) { // Create secondary server and update second cluster node. s := test.NewServer() defer s.Close() - c.Nodes[1].Host = s.Host() + + uri, err := pilosa.NewURIFromAddress(s.Host()) + if err != nil { + t.Fatal(err) + } + + c.Nodes[1].URI = *uri // Mock secondary server's executor to verify arguments. var remoteCalled bool @@ -1064,7 +1086,13 @@ func TestExecutor_Execute_Remote_TopN(t *testing.T) { // Create secondary server and update second cluster node. s := test.NewServer() defer s.Close() - c.Nodes[1].Host = s.Host() + + uri, err := pilosa.NewURIFromAddress(s.Host()) + if err != nil { + t.Fatal(err) + } + + c.Nodes[1].URI = *uri // Mock secondary server's executor to verify arguments and return a bitmap. var remoteExecN int diff --git a/filesystem.go b/filesystem.go new file mode 100644 index 000000000..5664f0987 --- /dev/null +++ b/filesystem.go @@ -0,0 +1,42 @@ +// Copyright 2017 Pilosa Corp. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pilosa + +import ( + "fmt" + "net/http" +) + +// Ensure nopFileSystem implements interface. +var _ FileSystem = &nopFileSystem{} + +// FileSystem represents an interface for a WebUI file system. +type FileSystem interface { + New() (http.FileSystem, error) +} + +func init() { + NopFileSystem = &nopFileSystem{} +} + +// NopFileSystem represents a FileSystem that returns an error if called. +var NopFileSystem FileSystem + +type nopFileSystem struct{} + +// New is a no-op implementation of FileSystem New method. +func (n *nopFileSystem) New() (http.FileSystem, error) { + return nil, fmt.Errorf("file system not implemented") +} diff --git a/fragment.go b/fragment.go index 95bcb7f2d..ab397af47 100644 --- a/fragment.go +++ b/fragment.go @@ -108,7 +108,7 @@ type Fragment struct { // Row attribute storage. // This is set by the parent frame unless overridden for testing. - RowAttrStore *AttrStore + RowAttrStore AttrStore stats StatsClient } @@ -256,6 +256,7 @@ func (f *Fragment) openCache() error { f.cache = NewLRUCache(f.CacheSize) case CacheTypeNone: f.cache = NewNopCache() + return nil default: return ErrInvalidCacheType } @@ -1455,6 +1456,10 @@ func (f *Fragment) flushCache() error { return nil } + if f.CacheType == CacheTypeNone { + return nil + } + // Retrieve a list of row ids from the cache. ids := f.cache.IDs() @@ -1681,7 +1686,7 @@ func (h *blockHasher) WriteValue(v uint64) { type FragmentSyncer struct { Fragment *Fragment - Host string + Node *Node Cluster *Cluster RemoteClient *http.Client @@ -1711,17 +1716,14 @@ func (s *FragmentSyncer) SyncFragment() error { blockSets := make([][]FragmentBlock, 0, len(nodes)) for _, node := range nodes { // Read local blocks. - if node.Host == s.Host { + if node.ID == s.Node.ID { b := s.Fragment.Blocks() blockSets = append(blockSets, b) continue } // Retrieve remote blocks. - client, err := NewInternalHTTPClient(node.Host, s.RemoteClient) - if err != nil { - return err - } + client := NewInternalHTTPClientFromURI(&node.URI, s.RemoteClient) blocks, err := client.FragmentBlocks(context.Background(), s.Fragment.Index(), s.Fragment.Frame(), s.Fragment.View(), s.Fragment.Slice()) if err != nil && err != ErrFragmentNotFound { return err @@ -1788,7 +1790,7 @@ func (s *FragmentSyncer) syncBlock(id int) error { var pairSets []PairSet var clients []InternalClient for _, node := range s.Cluster.FragmentNodes(f.Index(), f.Slice()) { - if s.Host == node.Host { + if s.Node.ID == node.ID { continue } @@ -1797,10 +1799,7 @@ func (s *FragmentSyncer) syncBlock(id int) error { return nil } - client, err := NewInternalHTTPClient(node.Host, s.RemoteClient) - if err != nil { - return err - } + client := NewInternalHTTPClientFromURI(&node.URI, s.RemoteClient) clients = append(clients, client) // Only sync the standard block. diff --git a/fragment_test.go b/fragment_test.go index c08b20a21..6a4a2f377 100644 --- a/fragment_test.go +++ b/fragment_test.go @@ -702,7 +702,7 @@ func TestFragment_TopN_CacheSize(t *testing.T) { Fragment: frag, RowAttrStore: test.MustOpenAttrStore(), } - f.Fragment.RowAttrStore = f.RowAttrStore.AttrStore + f.Fragment.RowAttrStore = f.RowAttrStore if err := f.Open(); err != nil { panic(err) } diff --git a/frame.go b/frame.go index 857c1b94c..466d7ae96 100644 --- a/frame.go +++ b/frame.go @@ -43,29 +43,27 @@ const ( // Frame represents a container for views. type Frame struct { - mu sync.RWMutex - path string - index string - name string - timeQuantum TimeQuantum - schema *FrameSchema + mu sync.RWMutex + path string + index string + name string views map[string]*View // Row attribute storage and cache - rowAttrStore *AttrStore + rowAttrStore AttrStore broadcaster Broadcaster Stats StatsClient - // Frame settings. + // Frame options. rowLabel string - cacheType string inverseEnabled bool + cacheType string + cacheSize uint32 + timeQuantum TimeQuantum rangeEnabled bool - - // Cache size for ranked frames - cacheSize uint32 + fields []*Field LogOutput io.Writer } @@ -78,22 +76,24 @@ func NewFrame(path, index, name string) (*Frame, error) { } return &Frame{ - path: path, - index: index, - name: name, - schema: &FrameSchema{}, + path: path, + index: index, + name: name, - views: make(map[string]*View), - rowAttrStore: NewAttrStore(filepath.Join(path, ".data")), + views: make(map[string]*View), + + rowAttrStore: NopAttrStore, broadcaster: NopBroadcaster, Stats: NopStatsClient, rowLabel: DefaultRowLabel, inverseEnabled: DefaultInverseEnabled, - rangeEnabled: DefaultRangeEnabled, cacheType: DefaultCacheType, cacheSize: DefaultCacheSize, + //timeQuantum + rangeEnabled: DefaultRangeEnabled, + //fields LogOutput: ioutil.Discard, }, nil @@ -109,7 +109,7 @@ func (f *Frame) Index() string { return f.index } func (f *Frame) Path() string { return f.path } // RowAttrStore returns the attribute storage. -func (f *Frame) RowAttrStore() *AttrStore { return f.rowAttrStore } +func (f *Frame) RowAttrStore() AttrStore { return f.rowAttrStore } // MaxSlice returns the max slice in the frame. func (f *Frame) MaxSlice() uint64 { @@ -230,7 +230,7 @@ func (f *Frame) options() FrameOptions { CacheType: f.cacheType, CacheSize: f.cacheSize, TimeQuantum: f.timeQuantum, - Fields: f.schema.Fields, + Fields: f.fields, } } @@ -244,8 +244,6 @@ func (f *Frame) Open() error { if err := f.loadMeta(); err != nil { return err - } else if err := f.loadSchema(); err != nil { - return err } if err := f.openViews(); err != nil { @@ -304,12 +302,13 @@ func (f *Frame) loadMeta() error { // Read data from meta file. buf, err := ioutil.ReadFile(filepath.Join(f.path, ".meta")) if os.IsNotExist(err) { - f.timeQuantum = "" f.rowLabel = DefaultRowLabel - f.cacheType = DefaultCacheType f.inverseEnabled = DefaultInverseEnabled - f.rangeEnabled = DefaultRangeEnabled + f.cacheType = DefaultCacheType f.cacheSize = DefaultCacheSize + f.timeQuantum = "" + f.rangeEnabled = DefaultRangeEnabled + //f.fields return nil } else if err != nil { return err @@ -320,17 +319,16 @@ func (f *Frame) loadMeta() error { } // Copy metadata fields. - f.timeQuantum = TimeQuantum(pb.TimeQuantum) f.rowLabel = pb.RowLabel f.inverseEnabled = pb.InverseEnabled - f.rangeEnabled = pb.RangeEnabled - f.cacheSize = pb.CacheSize - - // Copy cache type. f.cacheType = pb.CacheType if f.cacheType == "" { f.cacheType = DefaultCacheType } + f.cacheSize = pb.CacheSize + f.timeQuantum = TimeQuantum(pb.TimeQuantum) + f.rangeEnabled = pb.RangeEnabled + f.fields = decodeFields(pb.Fields) return nil } @@ -352,35 +350,6 @@ func (f *Frame) saveMeta() error { return nil } -// loadSchema reads the schema for the frame. -func (f *Frame) loadSchema() error { - buf, err := ioutil.ReadFile(filepath.Join(f.path, ".schema")) - if os.IsNotExist(err) { - f.schema = &FrameSchema{} - return nil - } else if err != nil { - return err - } - - var pb internal.FrameSchema - if err := proto.Unmarshal(buf, &pb); err != nil { - return err - } - f.schema = decodeFrameSchema(&pb) - - return nil -} - -// saveSchema writes the current schema to disk. -func (f *Frame) saveSchema() error { - if buf, err := proto.Marshal(encodeFrameSchema(f.schema)); err != nil { - return err - } else if err := ioutil.WriteFile(filepath.Join(f.path, ".schema"), buf, 0666); err != nil { - return err - } - return nil -} - // Close closes the frame and its views. func (f *Frame) Close() error { f.mu.Lock() @@ -402,16 +371,11 @@ func (f *Frame) Close() error { return nil } -// Schema returns the frame's current schema. -func (f *Frame) Schema() *FrameSchema { +// Field returns a field by name. +func (f *Frame) Field(name string) *Field { f.mu.RLock() defer f.mu.RUnlock() - return f.schema -} - -// Field returns a field from the schema by name. -func (f *Frame) Field(name string) *Field { - for _, field := range f.Schema().Fields { + for _, field := range f.fields { if field.Name == name { return field } @@ -419,7 +383,24 @@ func (f *Frame) Field(name string) *Field { return nil } -// CreateField creates a new field on the schema. +// Fields returns the fields on the frame. +func (f *Frame) Fields() []*Field { + f.mu.RLock() + defer f.mu.RUnlock() + return f.fields +} + +// HasField returns true if a field exists on the frame. +func (f *Frame) HasField(name string) bool { + for _, fld := range f.fields { + if fld.Name == name { + return true + } + } + return false +} + +// CreateField creates a new field on the frame. func (f *Frame) CreateField(field *Field) error { f.mu.Lock() defer f.mu.Unlock() @@ -429,18 +410,35 @@ func (f *Frame) CreateField(field *Field) error { return ErrFrameFieldsNotAllowed } - // Copy schema and append field. - schema := f.schema.Clone() - if err := schema.AddField(field); err != nil { + // Append field. + if err := f.addField(field); err != nil { return err } - f.schema = schema - f.saveSchema() + f.saveMeta() + return nil +} + +// addField adds a single field to fields. +func (f *Frame) addField(field *Field) error { + if err := ValidateField(field); err != nil { + return err + } else if f.HasField(field.Name) { + return ErrFieldExists + } + + // Add field to list. + f.fields = append(f.fields, field) + + // Sort fields by name. + sort.Slice(f.fields, func(i, j int) bool { + return f.fields[i].Name < f.fields[j].Name + }) + return nil } // GetFields returns a list of all the fields in the frame. -func (f *Frame) GetFields() (*FrameSchema, error) { +func (f *Frame) GetFields() ([]*Field, error) { f.mu.RLock() defer f.mu.RUnlock() @@ -449,12 +447,12 @@ func (f *Frame) GetFields() (*FrameSchema, error) { return nil, ErrFrameFieldsNotAllowed } - err := f.loadSchema() + err := f.loadMeta() if err != nil { return nil, err } - return f.schema, nil + return f.fields, nil } // DeleteField deletes an existing field on the schema. @@ -467,12 +465,10 @@ func (f *Frame) DeleteField(name string) error { return ErrFrameFieldsNotAllowed } - // Copy schema and remove field. - schema := f.schema.Clone() - if err := schema.DeleteField(name); err != nil { + // Remove field. + if err := f.deleteField(name); err != nil { return err } - f.schema = schema // Remove views. viewName := ViewFieldPrefix + name @@ -489,6 +485,18 @@ func (f *Frame) DeleteField(name string) error { return nil } +// deleteField removes a single field from fields. +func (f *Frame) deleteField(name string) error { + for i, field := range f.fields { + if field.Name == name { + copy(f.fields[i:], f.fields[i+1:]) + f.fields, f.fields[len(f.fields)-1] = f.fields[:len(f.fields)-1], nil + return nil + } + } + return ErrFieldNotFound +} + // TimeQuantum returns the time quantum for the frame. func (f *Frame) TimeQuantum() TimeQuantum { f.mu.Lock() @@ -543,6 +551,18 @@ func (f *Frame) Views() []*View { return other } +// viewNames returns a list of all views (as a string) in the frame. +func (f *Frame) viewNames() []string { + f.mu.Lock() + defer f.mu.Unlock() + + other := make([]string, 0, len(f.views)) + for viewName, _ := range f.views { + other = append(other, viewName) + } + return other +} + // RecalculateCaches recalculates caches on every view in the frame. func (f *Frame) RecalculateCaches() { for _, view := range f.Views() { @@ -551,27 +571,54 @@ func (f *Frame) RecalculateCaches() { } // CreateViewIfNotExists returns the named view, creating it if necessary. +// Additionally, a CreateViewMessage is sent to the cluster. func (f *Frame) CreateViewIfNotExists(name string) (*View, error) { + + view, created, err := f.createViewIfNotExistsBase(name) + if err != nil { + return nil, err + } + + if created { + // Broadcast view creation to the cluster. + err = f.broadcaster.SendSync( + &internal.CreateViewMessage{ + Index: f.index, + Frame: f.name, + View: name, + }) + if err != nil { + return nil, err + } + } + + return view, nil +} + +// createViewIfNotExistsBase returns the named view, creating it if necessary. +// The returned bool indicates whether the view was created or not. +func (f *Frame) createViewIfNotExistsBase(name string) (*View, bool, error) { // Don't create inverse views if they are not enabled. if !f.InverseEnabled() && IsInverseView(name) { - return nil, ErrFrameInverseDisabled + return nil, false, ErrFrameInverseDisabled } f.mu.Lock() defer f.mu.Unlock() if view := f.views[name]; view != nil { - return view, nil + return view, false, nil } view := f.newView(f.ViewPath(name), name) + if err := view.Open(); err != nil { - return nil, err + return nil, false, err } view.RowAttrStore = f.rowAttrStore f.views[view.Name()] = view - return view, nil + return view, true, nil } func (f *Frame) newView(path, name string) *View { @@ -956,8 +1003,9 @@ func encodeFrames(a []*Frame) []*internal.Frame { func encodeFrame(f *Frame) *internal.Frame { fo := f.options() return &internal.Frame{ - Name: f.name, - Meta: fo.Encode(), + Name: f.name, + Meta: fo.Encode(), + Views: f.viewNames(), } } @@ -969,8 +1017,9 @@ func (p frameSlice) Less(i, j int) bool { return p[i].Name() < p[j].Name() } // FrameInfo represents schema information for a frame. type FrameInfo struct { - Name string `json:"name"` - Views []*ViewInfo `json:"views,omitempty"` + Name string `json:"name"` + Options FrameOptions `json:"options"` + Views []*ViewInfo `json:"views,omitempty"` } type frameInfoSlice []*FrameInfo @@ -992,6 +1041,13 @@ type FrameOptions struct { // Encode converts o into its internal representation. func (o *FrameOptions) Encode() *internal.FrameMeta { + return encodeFrameOptions(o) +} + +func encodeFrameOptions(o *FrameOptions) *internal.FrameMeta { + if o == nil { + return nil + } return &internal.FrameMeta{ RowLabel: o.RowLabel, InverseEnabled: o.InverseEnabled, @@ -1003,74 +1059,18 @@ func (o *FrameOptions) Encode() *internal.FrameMeta { } } -// FrameSchema represents the list of fields on a frame. -type FrameSchema struct { - Fields []*Field -} - -// Clone returns a copy of s. -func (s *FrameSchema) Clone() *FrameSchema { - other := &FrameSchema{Fields: make([]*Field, len(s.Fields))} - copy(other.Fields, s.Fields) - return other -} - -// HasField returns true if a field exists on the schema. -func (s *FrameSchema) HasField(name string) bool { - for _, f := range s.Fields { - if f.Name == name { - return true - } - } - return false -} - -// AddField adds a single field to the schema. -func (s *FrameSchema) AddField(field *Field) error { - if err := ValidateField(field); err != nil { - return err - } else if s.HasField(field.Name) { - return ErrFieldExists - } - - // Add field to list. - s.Fields = append(s.Fields, field) - - // Sort fields by name. - sort.Slice(s.Fields, func(i, j int) bool { - return s.Fields[i].Name < s.Fields[j].Name - }) - - return nil -} - -// DeleteField removes a single field from the schema. -func (s *FrameSchema) DeleteField(name string) error { - for i, field := range s.Fields { - if field.Name == name { - copy(s.Fields[i:], s.Fields[i+1:]) - s.Fields, s.Fields[len(s.Fields)-1] = s.Fields[:len(s.Fields)-1], nil - return nil - } - } - return ErrFieldNotFound -} - -func encodeFrameSchema(schema *FrameSchema) *internal.FrameSchema { - if schema == nil { +func decodeFrameOptions(options *internal.FrameMeta) *FrameOptions { + if options == nil { return nil } - return &internal.FrameSchema{ - Fields: encodeFields(schema.Fields), - } -} - -func decodeFrameSchema(schema *internal.FrameSchema) *FrameSchema { - if schema == nil { - return nil - } - return &FrameSchema{ - Fields: decodeFields(schema.Fields), + return &FrameOptions{ + RowLabel: options.RowLabel, + InverseEnabled: options.InverseEnabled, + RangeEnabled: options.RangeEnabled, + CacheType: options.CacheType, + CacheSize: options.CacheSize, + TimeQuantum: TimeQuantum(options.TimeQuantum), + Fields: decodeFields(options.Fields), } } diff --git a/gc.go b/gc.go new file mode 100644 index 000000000..23dd0f0d0 --- /dev/null +++ b/gc.go @@ -0,0 +1,41 @@ +// Copyright 2017 Pilosa Corp. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pilosa + +// Ensure nopGCNotifier implements interface. +var _ GCNotifier = &nopGCNotifier{} + +// GCNotifier represents an interface for garbage collection notificationss. +type GCNotifier interface { + Close() + AfterGC() <-chan struct{} +} + +func init() { + NopGCNotifier = &nopGCNotifier{} +} + +// NopGCNotifier represents a GCNotifier that doesn't do anything. +var NopGCNotifier GCNotifier + +type nopGCNotifier struct{} + +// Close is a no-op implementation of GCNotifier Close method. +func (n *nopGCNotifier) Close() {} + +// AfterGC is a no-op implementation of GCNotifier AfterGC method. +func (c *nopGCNotifier) AfterGC() <-chan struct{} { + return nil +} diff --git a/gcnotify/gcnotify.go b/gcnotify/gcnotify.go new file mode 100644 index 000000000..76953a378 --- /dev/null +++ b/gcnotify/gcnotify.go @@ -0,0 +1,44 @@ +// Copyright 2017 Pilosa Corp. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gcnotify + +import ( + "github.com/CAFxX/gcnotifier" + "github.com/pilosa/pilosa" +) + +// Ensure ActiveGCNotifier implements interface. +var _ pilosa.GCNotifier = &ActiveGCNotifier{} + +type ActiveGCNotifier struct { + gcn *gcnotifier.GCNotifier +} + +// NewActiveGCNotifier creates an active GCNotifier. +func NewActiveGCNotifier() *ActiveGCNotifier { + return &ActiveGCNotifier{ + gcn: gcnotifier.New(), + } +} + +// Close implements the GCNotifier interface. +func (n *ActiveGCNotifier) Close() { + n.gcn.Close() +} + +// AfterGC implements the GCNotifier interface. +func (n *ActiveGCNotifier) AfterGC() <-chan struct{} { + return n.gcn.AfterGC() +} diff --git a/gopsutil/systeminfo.go b/gopsutil/systeminfo.go new file mode 100644 index 000000000..3310aeae1 --- /dev/null +++ b/gopsutil/systeminfo.go @@ -0,0 +1,115 @@ +// Copyright 2017 Pilosa Corp. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gopsutil + +import ( + "github.com/pilosa/pilosa" + "github.com/shirou/gopsutil/host" + "github.com/shirou/gopsutil/mem" +) + +var _ pilosa.SystemInfo = NewSystemInfo() + +// SystemInfo is an implementation of pilosa.SystemInfo that uses gopsutil to collect information about the host OS. +type SystemInfo struct { + platform string + family string + osVersion string +} + +// Uptime returns the system uptime in seconds. +func (s *SystemInfo) Uptime() (uptime uint64, err error) { + hostInfo, err := host.Info() + if err != nil { + return 0, err + } + return hostInfo.Uptime, nil +} + +// collectPlatformInfo fetches and caches system platform information. +func (s *SystemInfo) collectPlatformInfo() error { + var err error + if s.platform == "" { + s.platform, s.family, s.osVersion, err = host.PlatformInformation() + if err != nil { + return err + } + } + return nil +} + +// Platform returns the system platform. +func (s *SystemInfo) Platform() (string, error) { + err := s.collectPlatformInfo() + if err != nil { + return "", err + } + return s.platform, nil +} + +// Family returns the system family. +func (s *SystemInfo) Family() (string, error) { + err := s.collectPlatformInfo() + if err != nil { + return "", err + } + return s.family, err +} + +// OSVersion returns the OS Version. +func (s *SystemInfo) OSVersion() (string, error) { + err := s.collectPlatformInfo() + if err != nil { + return "", err + } + return s.osVersion, err +} + +// MemFree returns the amount of free memory in bytes. +func (s *SystemInfo) MemFree() (uint64, error) { + memInfo, err := mem.VirtualMemory() + if err != nil { + return 0, err + } + return memInfo.Free, err +} + +// MemTotal returns the amount of total memory in bytes. +func (s *SystemInfo) MemTotal() (uint64, error) { + memInfo, err := mem.VirtualMemory() + if err != nil { + return 0, err + } + return memInfo.Total, err +} + +// MemUsed returns the amount of used memory in bytes. +func (s *SystemInfo) MemUsed() (uint64, error) { + memInfo, err := mem.VirtualMemory() + if err != nil { + return 0, err + } + return memInfo.Used, err +} + +// KernelVersion returns the kernel version as a string. +func (s *SystemInfo) KernelVersion() (string, error) { + return host.KernelVersion() +} + +// NewSystemInfo is a constructor for the gopsutil implementation of SystemInfo. +func NewSystemInfo() *SystemInfo { + return &SystemInfo{} +} diff --git a/gopsutil/systeminfo_test.go b/gopsutil/systeminfo_test.go new file mode 100644 index 000000000..0f76b62da --- /dev/null +++ b/gopsutil/systeminfo_test.go @@ -0,0 +1,77 @@ +// Copyright 2017 Pilosa Corp. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gopsutil_test + +import ( + "log" + "testing" + + "github.com/pilosa/pilosa" + "github.com/pilosa/pilosa/gopsutil" +) + +func TestSystemInfo(t *testing.T) { + var systemInfo pilosa.SystemInfo = gopsutil.NewSystemInfo() + + // Uptime()(uint64, error) + // Platform()(string, error) + // Family()(string, error) + // OSVersion()(string, error) + // KernelVersion()(string, error) + // MemFree()(uint64, error) + // MemTotal()(uint64, error) + // MemUsed()(uint64, error) + // + uptime, err := systemInfo.Uptime() + if err != nil || uptime == 0 { + t.Fatalf("Error collecting uptime (error: %v)", err) + } + + platform, err := systemInfo.Platform() + if err != nil { + t.Fatalf("Error getting platform. (platform: %v, error: %v)", platform, err) + } + + family, err := systemInfo.Family() + if err != nil { + t.Fatalf("Error getting OS family. (family: %v, error: %v)", family, err) + } + + osversion, err := systemInfo.OSVersion() + if err != nil { + t.Fatalf("Error getting OS version. (osversion: %v, error: %v)", osversion, err) + } + + kernelversion, err := systemInfo.KernelVersion() + if err != nil { + t.Fatalf("Error getting kernel version. (kernelversion: %v, error: %v)", kernelversion, err) + } + + memfree, err := systemInfo.MemFree() + if err != nil { + t.Fatalf("Error getting memfree. (memfree: %v, error: %v)", memfree, err) + } + + memused, err := systemInfo.MemUsed() + if err != nil { + t.Fatalf("Error getting memused. (memused: %v, error: %v)", memused, err) + } + + memtotal, err := systemInfo.MemTotal() + log.Println(memtotal) + if err != nil { + t.Fatalf("Error getting memtotal. (memtotal: %v, error: %v)", memtotal, err) + } +} diff --git a/gossip/gossip.go b/gossip/gossip.go index 5a6233a94..62c794560 100644 --- a/gossip/gossip.go +++ b/gossip/gossip.go @@ -17,11 +17,16 @@ package gossip import ( "fmt" "io" + "io/ioutil" "log" "os" + "strconv" "strings" + "sync" "time" + "golang.org/x/sync/errgroup" + "github.com/gogo/protobuf/proto" "github.com/hashicorp/memberlist" "github.com/pilosa/pilosa" @@ -29,15 +34,15 @@ import ( "github.com/pkg/errors" ) -// Ensure GossipNodeSet implements interfaces. -var _ pilosa.BroadcastReceiver = &GossipNodeSet{} -var _ pilosa.Gossiper = &GossipNodeSet{} -var _ memberlist.Delegate = &GossipNodeSet{} +// Ensure GossipMemberSet implements interfaces. +var _ pilosa.BroadcastReceiver = &GossipMemberSet{} +var _ pilosa.Gossiper = &GossipMemberSet{} +var _ memberlist.Delegate = &GossipMemberSet{} -// GossipNodeSet represents a gossip implementation of NodeSet using memberlist -// GossipNodeSet also represents a gossip implementation of pilosa.Broadcaster -// GossipNodeSet also represents an implementation of memberlist.Delegate -type GossipNodeSet struct { +// GossipMemberSet represents a gossip implementation of MemberSet using memberlist. +type GossipMemberSet struct { + mu sync.RWMutex + node *pilosa.Node memberlist *memberlist.Memberlist handler pilosa.BroadcastHandler @@ -50,46 +55,60 @@ type GossipNodeSet struct { LogOutput io.Writer } -// Nodes implements the NodeSet interface and returns a list of nodes in the cluster. -func (g *GossipNodeSet) Nodes() []*pilosa.Node { - a := make([]*pilosa.Node, 0, g.memberlist.NumMembers()) - for _, n := range g.memberlist.Members() { - a = append(a, &pilosa.Node{Scheme: "gossip", Host: n.Name}) - } - return a -} - -// Start implements the BroadcastReceiver interface and sets the BroadcastHandler -func (g *GossipNodeSet) Start(h pilosa.BroadcastHandler) error { +// Start implements the BroadcastReceiver interface and sets the BroadcastHandler. +func (g *GossipMemberSet) Start(h pilosa.BroadcastHandler) error { g.handler = h return nil } -// Seed returns the gossipSeed determined by the config. -func (g *GossipNodeSet) Seed() string { - return g.config.gossipSeed +// GetBindAddr returns the gossip bind address based on config and auto bind port. +// This method is currently only used in a test scenario where a second node needs +// the auto-bind address of the first node to use as its gossip seed. +func (g *GossipMemberSet) GetBindAddr() string { + return fmt.Sprintf("%s:%d", g.config.memberlistConfig.BindAddr, g.config.memberlistConfig.BindPort) } -// Open implements the NodeSet interface to start network activity. -func (g *GossipNodeSet) Open() error { +// Open implements the MemberSet interface to start network activity. +func (g *GossipMemberSet) Open(n *pilosa.Node) error { if g.handler == nil { - return fmt.Errorf("opening GossipNodeSet: you must call Start(pilosa.BroadcastHandler) before calling Open()") + return fmt.Errorf("must call Start(pilosa.BroadcastHandler) before calling Open()") } - ml, err := memberlist.Create(g.config.memberlistConfig) + + g.node = n + + err := error(nil) + g.mu.Lock() + g.memberlist, err = memberlist.Create(g.config.memberlistConfig) + g.mu.Unlock() if err != nil { return errors.Wrap(err, "creating memberlist") } - g.memberlist = ml + g.broadcasts = &memberlist.TransmitLimitedQueue{ NumNodes: func() int { - return ml.NumMembers() + g.mu.RLock() + defer g.mu.RUnlock() + return g.memberlist.NumMembers() }, RetransmitMult: 3, } - // attach to gossip seed node - nodes := []*pilosa.Node{&pilosa.Node{Scheme: "gossip", Host: g.config.gossipSeed}} //TODO: support a list of seeds - err = g.joinWithRetry(pilosa.Nodes(nodes).Hosts()) + var uris = make([]*pilosa.URI, len(g.config.gossipSeeds)) + for i, addr := range g.config.gossipSeeds { + uris[i], err = pilosa.NewURIFromAddress(addr) + if err != nil { + return fmt.Errorf("new uri from address: %s", err) + } + } + + var nodes = make([]*pilosa.Node, len(uris)) + for i, uri := range uris { + nodes[i] = &pilosa.Node{URI: *uri} + } + + g.mu.RLock() + err = g.joinWithRetry(pilosa.URIs(pilosa.Nodes(nodes).URIs()).HostPortStrings()) + g.mu.RUnlock() if err != nil { return errors.Wrap(err, "joinWithRetry") } @@ -97,7 +116,7 @@ func (g *GossipNodeSet) Open() error { } // joinWithRetry wraps the standard memberlist Join function in a retry. -func (g *GossipNodeSet) joinWithRetry(hosts []string) error { +func (g *GossipMemberSet) joinWithRetry(hosts []string) error { err := retry(60, 2*time.Second, func() error { _, err := g.memberlist.Join(hosts) return err @@ -121,21 +140,334 @@ func retry(attempts int, sleep time.Duration, fn func() error) (err error) { return fmt.Errorf("after %d attempts, last error: %s", attempts, err) } -// logger returns a logger for the GossipNodeSet. -func (g *GossipNodeSet) logger() *log.Logger { +// logger returns a logger for the GossipMemberSet. +func (g *GossipMemberSet) logger() *log.Logger { return log.New(g.LogOutput, "", log.LstdFlags) } //////////////////////////////////////////////////////////////// type gossipConfig struct { - gossipSeed string + gossipSeeds []string memberlistConfig *memberlist.Config } +// NewGossipMemberSetWithTransport returns a new instance of GossipMemberSet given a Transport. +func NewGossipMemberSetWithTransport(name string, cfg *pilosa.Config, transport *Transport, server *pilosa.Server) (*GossipMemberSet, error) { + + g := &GossipMemberSet{ + LogOutput: server.LogOutput, + } + + port := transport.Net.GetAutoBindPort() + + bindURI, err := pilosa.NewURIFromAddress(cfg.Bind) + if err != nil { + return nil, fmt.Errorf("getting uri from bind address (with transport): %s", err) + } + host := bindURI.Host() + + var gossipKey []byte + if cfg.Gossip.Key != "" { + gossipKey, err = ioutil.ReadFile(cfg.Gossip.Key) + if err != nil { + return nil, fmt.Errorf("reading gossip key: %s", err) + } + } + + // memberlist config + conf := memberlist.DefaultWANConfig() + conf.Transport = transport.Net + conf.Name = name + conf.BindAddr = host + conf.BindPort = port + conf.AdvertisePort = port + conf.AdvertiseAddr = pilosa.HostToIP(host) + // + conf.TCPTimeout = time.Duration(cfg.Gossip.StreamTimeout) + conf.SuspicionMult = cfg.Gossip.SuspicionMult + conf.PushPullInterval = time.Duration(cfg.Gossip.PushPullInterval) + conf.ProbeTimeout = time.Duration(cfg.Gossip.ProbeTimeout) + conf.ProbeInterval = time.Duration(cfg.Gossip.ProbeInterval) + conf.GossipNodes = cfg.Gossip.Nodes + conf.GossipInterval = time.Duration(cfg.Gossip.Interval) + conf.GossipToTheDeadTime = time.Duration(cfg.Gossip.ToTheDeadTime) + // + conf.Delegate = g + conf.SecretKey = gossipKey + conf.Events = server.Cluster.EventReceiver.(memberlist.EventDelegate) + + g.config = &gossipConfig{ + memberlistConfig: conf, + gossipSeeds: cfg.Gossip.Seeds, + } + + g.statusHandler = server + + return g, nil +} + +// NewGossipMemberSet returns a new instance of GossipMemberSet given a gossip port. +func NewGossipMemberSet(name string, cfg *pilosa.Config, server *pilosa.Server) (*GossipMemberSet, error) { + port, err := strconv.Atoi(cfg.Gossip.Port) + if err != nil { + return nil, fmt.Errorf("convert port: %s", err) + } + + bindURI, err := pilosa.NewURIFromAddress(cfg.Bind) + if err != nil { + return nil, fmt.Errorf("getting uri from bind address: %s", err) + } + host := bindURI.Host() + + // Set up the transport. + transport, err := NewTransport(host, port) + if err != nil { + return nil, fmt.Errorf("new tranport: %s", err) + } + + return NewGossipMemberSetWithTransport(name, cfg, transport, server) +} + +// SendSync implementation of the Broadcaster interface. +func (g *GossipMemberSet) SendSync(pb proto.Message) error { + msg, err := pilosa.MarshalMessage(pb) + if err != nil { + return fmt.Errorf("marshal message: %s", err) + } + + mlist := g.memberlist + + // Direct sends the message directly to every node. + // An error from any node raises an error on the entire operation. + // + // Gossip uses the gossip protocol to eventually deliver the message + // to every node. + var eg errgroup.Group + for _, n := range mlist.Members() { + // Don't send the message to the local node. + if n == mlist.LocalNode() { + continue + } + node := n + eg.Go(func() error { + return mlist.SendToTCP(node, msg) + }) + } + return eg.Wait() +} + +// SendAsync implementation of the Gossiper interface. +func (g *GossipMemberSet) SendAsync(pb proto.Message) error { + msg, err := pilosa.MarshalMessage(pb) + if err != nil { + return fmt.Errorf("marshal message: %s", err) + } + + b := &broadcast{ + msg: msg, + notify: nil, + } + g.broadcasts.QueueBroadcast(b) + return nil +} + +// NodeMeta implementation of the memberlist.Delegate interface. +func (g *GossipMemberSet) NodeMeta(limit int) []byte { + buf, err := proto.Marshal(pilosa.EncodeNode(g.node)) + if err != nil { + g.logger().Printf("marshal message error: %s", err) + return []byte{} + } + return buf +} + +// NotifyMsg implementation of the memberlist.Delegate interface +// called when a user-data message is received. +func (g *GossipMemberSet) NotifyMsg(b []byte) { + m, err := pilosa.UnmarshalMessage(b) + if err != nil { + g.logger().Printf("unmarshal message error: %s", err) + return + } + if err := g.handler.ReceiveMessage(m); err != nil { + g.logger().Printf("receive message error: %s", err) + return + } +} + +// GetBroadcasts implementation of the memberlist.Delegate interface +// called when user data messages can be broadcast. +func (g *GossipMemberSet) GetBroadcasts(overhead, limit int) [][]byte { + return g.broadcasts.GetBroadcasts(overhead, limit) +} + +// 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() + if err != nil { + g.logger().Printf("error getting local state, err=%s", err) + return []byte{} + } + + // Marshal nodestate data to bytes. + buf, err := proto.Marshal(pb) + if err != nil { + g.logger().Printf("error marshalling nodestate data, err=%s", err) + return []byte{} + } + return buf +} + +// MergeRemoteState implementation of the memberlist.Delegate interface +// receive and process the remote side's LocalState. +func (g *GossipMemberSet) MergeRemoteState(buf []byte, join bool) { + // Unmarshal nodestate data. + var pb internal.NodeStatus + if err := proto.Unmarshal(buf, &pb); err != nil { + g.logger().Printf("error unmarshalling nodestate data, err=%s", err) + return + } + err := g.statusHandler.HandleRemoteStatus(&pb) + if err != nil { + g.logger().Printf("merge state error: %s", err) + } +} + +// GossipEventReceiver is used to enable an application to receive +// events about joins and leaves over a channel. +// +// Care must be taken that events are processed in a timely manner from +// the channel, since this delegate will block until an event can be sent. +type GossipEventReceiver struct { + ch chan memberlist.NodeEvent + eventHandler pilosa.EventHandler + + // The writer for any logging. + LogOutput io.Writer +} + +// NewGossipEventReceiver returns a new instance of GossipEventReceiver. +func NewGossipEventReceiver(logOutput io.Writer) *GossipEventReceiver { + return &GossipEventReceiver{ + ch: make(chan memberlist.NodeEvent, 1), + LogOutput: logOutput, + } +} + +func (g *GossipEventReceiver) NotifyJoin(n *memberlist.Node) { + g.ch <- memberlist.NodeEvent{memberlist.NodeJoin, n} +} + +func (g *GossipEventReceiver) NotifyLeave(n *memberlist.Node) { + g.ch <- memberlist.NodeEvent{memberlist.NodeLeave, n} +} + +func (g *GossipEventReceiver) NotifyUpdate(n *memberlist.Node) { + g.ch <- memberlist.NodeEvent{memberlist.NodeUpdate, n} +} + +// Start implements the pilosa.EventReceiver interface and sets the EventHandler. +func (g *GossipEventReceiver) Start(h pilosa.EventHandler) error { + g.eventHandler = h + go g.listen() + return nil +} + +// logger returns a logger for the GossipEventReceiver. +func (g *GossipEventReceiver) logger() *log.Logger { + return log.New(g.LogOutput, "", log.LstdFlags) +} + +func (g *GossipEventReceiver) listen() { + var nodeEventType pilosa.NodeEventType + for { + e := <-g.ch + switch e.Event { + case memberlist.NodeJoin: + nodeEventType = pilosa.NodeJoin + case memberlist.NodeLeave: + nodeEventType = pilosa.NodeLeave + case memberlist.NodeUpdate: + nodeEventType = pilosa.NodeUpdate + default: + continue + } + + // Get the node from the event.Node meta data. + var n internal.Node + if err := proto.Unmarshal(e.Node.Meta, &n); err != nil { + panic("failed to unmarshal event node meta data") + } + node := pilosa.DecodeNode(&n) + + ne := &pilosa.NodeEvent{ + Event: nodeEventType, + Node: node, + } + if err := g.eventHandler.ReceiveEvent(ne); err != nil { + g.logger().Printf("receive event error: %s", err) + } + } +} + +// broadcast represents an implementation of memberlist.Broadcast +type broadcast struct { + msg []byte + notify chan<- struct{} +} + +func (b *broadcast) Invalidates(other memberlist.Broadcast) bool { + return false +} + +func (b *broadcast) Message() []byte { + return b.msg +} + +func (b *broadcast) Finished() { + if b.notify != nil { + close(b.notify) + } +} + +// Transport is a gossip transport for binding to a port. +type Transport struct { + //memberlist.Transport + Net *memberlist.NetTransport + URI *pilosa.URI +} + +// NewTransport returns a NetTransport based on the given host and port. +// It will dynamically bind to a port if port is 0. +// This is useful for test cases where specifiying a port is not reasonable. +//func NewTransport(host string, port int) (*memberlist.NetTransport, error) { +func NewTransport(host string, port int) (*Transport, error) { + // memberlist config + conf := memberlist.DefaultWANConfig() + conf.BindAddr = host + conf.BindPort = port + conf.AdvertisePort = port + + net, err := newTransport(conf) + if err != nil { + return nil, fmt.Errorf("new transport: %s", err) + } + + uri, err := pilosa.NewURIFromHostPort(host, uint16(net.GetAutoBindPort())) + if err != nil { + return nil, fmt.Errorf("new uri from host port: %s", err) + } + + return &Transport{ + Net: net, + URI: uri, + }, nil +} + // newTransport returns a NetTransport based on the memberlist configuration. // It will dynamically bind to a port if conf.BindPort is 0. -// This is useful for test cases where specifying a port is not reasonable. func newTransport(conf *memberlist.Config) (*memberlist.NetTransport, error) { if conf.LogOutput != nil && conf.Logger != nil { return nil, fmt.Errorf("Cannot specify both LogOutput and Logger. Please choose a single log configuration setting.") @@ -189,144 +521,6 @@ func newTransport(conf *memberlist.Config) (*memberlist.NetTransport, error) { if err != nil { return nil, fmt.Errorf("Could not set up network transport: %v", err) } - if conf.BindPort == 0 { - port := nt.GetAutoBindPort() - conf.BindPort = port - conf.AdvertisePort = port - logger.Printf("[DEBUG] Using dynamic bind port %d", port) - } return nt, nil } - -// NewGossipNodeSet returns a new instance of GossipNodeSet. -func NewGossipNodeSet(name string, gossipHost string, gossipPort int, gossipSeed string, server *pilosa.Server, secretKey []byte) (*GossipNodeSet, error) { - g := &GossipNodeSet{ - LogOutput: server.LogOutput, - } - - conf := memberlist.DefaultWANConfig() - conf.BindPort = gossipPort - conf.AdvertisePort = gossipPort - - //TODO: pull memberlist config from pilosa.cfg file - g.config = &gossipConfig{ - memberlistConfig: conf, - gossipSeed: gossipSeed, - } - - g.config.memberlistConfig.Name = name - g.config.memberlistConfig.BindAddr = gossipHost - g.config.memberlistConfig.AdvertiseAddr = pilosa.HostToIP(gossipHost) - g.config.memberlistConfig.Delegate = g - g.config.memberlistConfig.SecretKey = secretKey - - g.statusHandler = server - - // set up the transport - transport, err := newTransport(g.config.memberlistConfig) - if err != nil { - return nil, err - } - g.config.memberlistConfig.Transport = transport - - // If no gossipSeed is provided, use local host:port. - if gossipSeed == "" { - g.config.gossipSeed = fmt.Sprintf("%s:%d", gossipHost, g.config.memberlistConfig.BindPort) - } - - return g, nil -} - -// SendAsync implementation of the Gossiper interface. -func (g *GossipNodeSet) SendAsync(pb proto.Message) error { - msg, err := pilosa.MarshalMessage(pb) - if err != nil { - return err - } - - b := &broadcast{ - msg: msg, - notify: nil, - } - g.broadcasts.QueueBroadcast(b) - return nil -} - -// NodeMeta implementation of the memberlist.Delegate interface. -func (g *GossipNodeSet) NodeMeta(limit int) []byte { - return []byte{} -} - -// NotifyMsg implementation of the memberlist.Delegate interface -// called when a user-data message is received. -func (g *GossipNodeSet) NotifyMsg(b []byte) { - m, err := pilosa.UnmarshalMessage(b) - if err != nil { - g.logger().Printf("unmarshal message error: %s", err) - return - } - if err := g.handler.ReceiveMessage(m); err != nil { - g.logger().Printf("receive message error: %s", err) - return - } -} - -// GetBroadcasts implementation of the memberlist.Delegate interface -// called when user data messages can be broadcast. -func (g *GossipNodeSet) GetBroadcasts(overhead, limit int) [][]byte { - return g.broadcasts.GetBroadcasts(overhead, limit) -} - -// LocalState implementation of the memberlist.Delegate interface -// sends this Node's state data. -func (g *GossipNodeSet) LocalState(join bool) []byte { - pb, err := g.statusHandler.LocalStatus() - if err != nil { - g.logger().Printf("error getting local state, err=%s", err) - return []byte{} - } - - // Marshal nodestate data to bytes. - buf, err := proto.Marshal(pb) - if err != nil { - g.logger().Printf("error marshalling nodestate data, err=%s", err) - return []byte{} - } - return buf -} - -// MergeRemoteState implementation of the memberlist.Delegate interface -// receive and process the remote side side's LocalState. -func (g *GossipNodeSet) MergeRemoteState(buf []byte, join bool) { - // Unmarshal nodestate data. - var pb internal.NodeStatus - if err := proto.Unmarshal(buf, &pb); err != nil { - g.logger().Printf("error unmarshalling nodestate data, err=%s", err) - return - } - err := g.statusHandler.HandleRemoteStatus(&pb) - if err != nil { - g.logger().Printf("merge state error: %s", err) - } -} - -// broadcast represents an implementation of memberlist.Broadcast -type broadcast struct { - msg []byte - notify chan<- struct{} -} - -func (b *broadcast) Invalidates(other memberlist.Broadcast) bool { - return false -} - -func (b *broadcast) Message() []byte { - return b.msg -} - -func (b *broadcast) Finished() { - if b.notify != nil { - close(b.notify) - } -} diff --git a/handler.go b/handler.go index ca2e6a4ef..a23b2e5ac 100644 --- a/handler.go +++ b/handler.go @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -//go:generate statik -src=./webui - package pilosa import ( @@ -44,10 +42,6 @@ import ( "github.com/pilosa/pilosa/pql" "unicode" - - // Allow building Pilosa without the web UI. - _ "github.com/pilosa/pilosa/statik" - "github.com/rakyll/statik/fs" ) // Handler represents an HTTP handler. @@ -57,12 +51,16 @@ type Handler struct { BroadcastHandler BroadcastHandler StatusHandler StatusHandler + FileSystem FileSystem + // Local hostname & cluster configuration. - URI *URI + Node *Node Cluster *Cluster RemoteClient *http.Client - Router *mux.Router + Router *mux.Router + NormalRouter *mux.Router + RestrictedRouter *mux.Router // The execution engine for running queries. Executor interface { @@ -96,13 +94,36 @@ type errorResponse struct { // NewHandler returns a new instance of Handler with a default logger. func NewHandler() *Handler { handler := &Handler{ + Broadcaster: NopBroadcaster, + //BroadcastHandler: NopBroadcastHandler, // TODO: implement the nop + //StatusHandler: NopStatusHandler, // TODO: implement the nop + FileSystem: NopFileSystem, + LogOutput: os.Stderr, } - handler.Router = NewRouter(handler) + BuildRouters(handler) handler.populateValidators() return handler } +// BuildRouters creates Gorilla Mux http routers for both normal and restricted endpoints. +func BuildRouters(handler *Handler) { + router := mux.NewRouter() + loadCommon(router, handler) + loadNormal(router, handler) + handler.NormalRouter = router + router.Use(handler.queryArgValidator) + + // Restricted router. + router = mux.NewRouter() + loadCommon(router, handler) + loadRestricted(router, handler) + handler.RestrictedRouter = router + router.Use(handler.queryArgValidator) + + handler.SetRestricted() +} + func (h *Handler) populateValidators() { h.validators = map[string]*queryValidationSpec{} h.validators["GetFragmentNodes"] = QueryValidationSpecRequired("slice").Optional("index") @@ -135,17 +156,46 @@ func (h *Handler) queryArgValidator(next http.Handler) http.Handler { }) } -// NewRouter creates a Gorilla Mux http router. -func NewRouter(handler *Handler) *mux.Router { - router := mux.NewRouter() +// SetNormal is a method of the SecurityManager interface which provides normal URI routing. +func (h *Handler) SetNormal() { + h.Router = h.NormalRouter +} + +// SetRestricted is a method of the SecurityManager interface which provides restricted URI routing. +func (h *Handler) SetRestricted() { + h.Router = h.RestrictedRouter +} + +func loadCommon(router *mux.Router, handler *Handler) { router.HandleFunc("/", handler.handleWebUI).Methods("GET") router.HandleFunc("/assets/{file}", handler.handleWebUI).Methods("GET") + router.HandleFunc("/cluster/message", handler.handlePostClusterMessage).Methods("POST") + router.HandleFunc("/cluster/resize/set-coordinator", handler.handlePostClusterResizeSetCoordinator).Methods("POST") + router.PathPrefix("/debug/pprof/").Handler(http.DefaultServeMux).Methods("GET") + router.HandleFunc("/debug/vars", handler.handleExpvar).Methods("GET") + router.HandleFunc("/fragment/data", handler.handleGetFragmentData).Methods("GET").Name("GetFragmentData") + router.HandleFunc("/hosts", handler.handleGetHosts).Methods("GET") + router.HandleFunc("/id", handler.handleGetID).Methods("GET") + router.HandleFunc("/schema", handler.handleGetSchema).Methods("GET") + router.HandleFunc("/slices/max", handler.handleGetSlicesMax).Methods("GET") // TODO: deprecate, but it's being used by the client (for backups) + router.HandleFunc("/status", handler.handleGetStatus).Methods("GET") + router.HandleFunc("/version", handler.handleGetVersion).Methods("GET") + router.Use(handler.queryArgValidator) +} + +func loadRestricted(router *mux.Router, handler *Handler) { + router.HandleFunc("/cluster/resize/abort", handler.handlePostClusterResizeAbort).Methods("POST") + router.NotFoundHandler = http.HandlerFunc(handler.reportRestricted) + router.Use(handler.queryArgValidator) +} + +func loadNormal(router *mux.Router, handler *Handler) { + router.HandleFunc("/cluster/resize/remove-node", handler.handlePostClusterResizeRemoveNode).Methods("POST") router.PathPrefix("/debug/pprof/").Handler(http.DefaultServeMux).Methods("GET") router.HandleFunc("/debug/vars", handler.handleExpvar).Methods("GET") router.HandleFunc("/export", handler.handleGetExport).Methods("GET").Name("GetExport") router.HandleFunc("/fragment/block/data", handler.handleGetFragmentBlockData).Methods("GET") router.HandleFunc("/fragment/blocks", handler.handleGetFragmentBlocks).Methods("GET").Name("GetFragmentBlocks") - router.HandleFunc("/fragment/data", handler.handleGetFragmentData).Methods("GET").Name("GetFragmentData") router.HandleFunc("/fragment/data", handler.handlePostFragmentData).Methods("POST").Name("PostFragmentData") router.HandleFunc("/fragment/nodes", handler.handleGetFragmentNodes).Methods("GET").Name("GetFragmentNodes") router.HandleFunc("/import", handler.handlePostImport).Methods("POST") @@ -172,14 +222,7 @@ func NewRouter(handler *Handler) *mux.Router { router.HandleFunc("/index/{index}/input-definition/{input-definition}", handler.handleDeleteInputDefinition).Methods("DELETE") router.HandleFunc("/index/{index}/query", handler.handlePostQuery).Methods("POST").Name("PostQuery") router.HandleFunc("/index/{index}/time-quantum", handler.handlePatchIndexTimeQuantum).Methods("PATCH") - router.HandleFunc("/hosts", handler.handleGetHosts).Methods("GET") - router.HandleFunc("/schema", handler.handleGetSchema).Methods("GET") - router.HandleFunc("/slices/max", handler.handleGetSliceMax).Methods("GET").Name("GetSliceMax") - router.HandleFunc("/status", handler.handleGetStatus).Methods("GET") - router.HandleFunc("/version", handler.handleGetVersion).Methods("GET") router.HandleFunc("/recalculate-caches", handler.handleRecalculateCaches).Methods("POST") - router.HandleFunc("/cluster/message", handler.handlePostClusterMessage).Methods("POST") - router.HandleFunc("/id", handler.handleGetID).Methods("GET") // TODO: Apply MethodNotAllowed statuses to all endpoints. // Ideally this would be automatic, as described in this (wontfix) ticket: @@ -187,9 +230,10 @@ func NewRouter(handler *Handler) *mux.Router { // For now we just do it for the most commonly used handler, /query router.HandleFunc("/index/{index}/query", handler.methodNotAllowedHandler).Methods("GET") - router.Use(handler.queryArgValidator) +} - return router +func (h *Handler) reportRestricted(w http.ResponseWriter, r *http.Request) { + http.Error(w, fmt.Sprintf("not allowed in cluster state %s", h.Cluster.State()), http.StatusMethodNotAllowed) } func (h *Handler) methodNotAllowedHandler(w http.ResponseWriter, r *http.Request) { @@ -242,13 +286,13 @@ func (h *Handler) handleWebUI(w http.ResponseWriter, r *http.Request) { http.Error(w, "Welcome. Pilosa is running. Visit https://www.pilosa.com/docs/ for more information or try the WebUI by visiting this URL in your browser.", http.StatusNotFound) return } - statikFS, err := fs.New() + filesystem, err := h.FileSystem.New() if err != nil { h.writeQueryResponse(w, r, &QueryResponse{Err: err}) h.logger().Println("Pilosa WebUI is not available. Please run `make generate-statik` before building Pilosa with `make install`.") return } - http.FileServer(statikFS).ServeHTTP(w, r) + http.FileServer(filesystem).ServeHTTP(w, r) } // handleGetSchema handles GET /schema requests. @@ -262,13 +306,16 @@ func (h *Handler) handleGetSchema(w http.ResponseWriter, r *http.Request) { // handleGetStatus handles GET /status requests. func (h *Handler) handleGetStatus(w http.ResponseWriter, r *http.Request) { - status, err := h.StatusHandler.ClusterStatus() + pb, err := h.StatusHandler.ClusterStatus() if err != nil { h.logger().Printf("cluster status error: %s", err) return } + + cs := pb.(*internal.ClusterStatus) if err := json.NewEncoder(w).Encode(getStatusResponse{ - Status: status, + State: cs.State, + Nodes: DecodeNodes(cs.Nodes), }); err != nil { h.logger().Printf("write status response error: %s", err) } @@ -279,7 +326,8 @@ type getSchemaResponse struct { } type getStatusResponse struct { - Status proto.Message `json:"status"` + State string `json:"state"` + Nodes []*Node `json:"nodes"` } // handlePostQuery handles /query requests. @@ -351,31 +399,19 @@ func (h *Handler) handlePostQuery(w http.ResponseWriter, r *http.Request) { } } -func (h *Handler) handleGetSliceMax(w http.ResponseWriter, r *http.Request) { - var ms map[string]uint64 - if inverse, _ := strconv.ParseBool(r.URL.Query().Get("inverse")); inverse { - ms = h.Holder.MaxInverseSlices() - } else { - ms = h.Holder.MaxSlices() +// handleGetSlicesMax handles GET /schema requests. +func (h *Handler) handleGetSlicesMax(w http.ResponseWriter, r *http.Request) { + if err := json.NewEncoder(w).Encode(getSlicesMaxResponse{ + Standard: h.Holder.MaxSlices(), + Inverse: h.Holder.MaxInverseSlices(), + }); err != nil { + h.logger().Printf("write slices-max response error: %s", err) } - if strings.Contains(r.Header.Get("Accept"), "application/x-protobuf") { - pb := &internal.MaxSlicesResponse{ - MaxSlices: ms, - } - if buf, err := proto.Marshal(pb); err != nil { - h.logger().Printf("protobuf marshal error: %s", err) - } else if _, err := w.Write(buf); err != nil { - h.logger().Printf("stream write error: %s", err) - } - return - } - json.NewEncoder(w).Encode(sliceMaxResponse{ - MaxSlices: ms, - }) } -type sliceMaxResponse struct { - MaxSlices map[string]uint64 `json:"maxSlices"` +type getSlicesMaxResponse struct { + Standard map[string]uint64 `json:"standard"` + Inverse map[string]uint64 `json:"inverse"` } // handleGetIndexes handles GET /index request. @@ -932,7 +968,7 @@ func (h *Handler) handleGetFrameFields(w http.ResponseWriter, r *http.Request) { return } - schema, err := frame.GetFields() + fields, err := frame.GetFields() if err == ErrFrameFieldsNotAllowed { http.Error(w, err.Error(), http.StatusBadRequest) return @@ -942,7 +978,7 @@ func (h *Handler) handleGetFrameFields(w http.ResponseWriter, r *http.Request) { } // Encode response. - if err := json.NewEncoder(w).Encode(getFrameFieldsResponse{Fields: schema.Fields}); err != nil { + if err := json.NewEncoder(w).Encode(getFrameFieldsResponse{Fields: fields}); err != nil { h.logger().Printf("response encoding error: %s", err) } } @@ -1233,9 +1269,9 @@ func (h *Handler) handlePostImport(w http.ResponseWriter, r *http.Request) { } // Validate that this handler owns the slice. - if !h.Cluster.OwnsFragment(h.URI.HostPort(), req.Index, req.Slice) { - mesg := fmt.Sprintf("host does not own slice %s-%s slice:%d", h.URI, req.Index, req.Slice) - http.Error(w, mesg, http.StatusPreconditionFailed) + if !h.Cluster.OwnsFragment(h.Node.ID, req.Index, req.Slice) { + msg := fmt.Sprintf("host does not own slice %s-%s slice:%d", h.Node.ID, req.Index, req.Slice) + http.Error(w, msg, http.StatusPreconditionFailed) return } @@ -1303,9 +1339,9 @@ func (h *Handler) handlePostImportValue(w http.ResponseWriter, r *http.Request) } // Validate that this handler owns the slice. - if !h.Cluster.OwnsFragment(h.URI.HostPort(), req.Index, req.Slice) { - mesg := fmt.Sprintf("host does not own slice %s-%s slice:%d", h.URI, req.Index, req.Slice) - http.Error(w, mesg, http.StatusPreconditionFailed) + if !h.Cluster.OwnsFragment(h.Node.ID, req.Index, req.Slice) { + msg := fmt.Sprintf("host does not own slice %s-%s slice:%d", h.Node.ID, req.Index, req.Slice) + http.Error(w, msg, http.StatusPreconditionFailed) return } @@ -1369,9 +1405,9 @@ func (h *Handler) handleGetExportCSV(w http.ResponseWriter, r *http.Request) { } // Validate that this handler owns the slice. - if !h.Cluster.OwnsFragment(h.URI.HostPort(), index, slice) { - mesg := fmt.Sprintf("host does not own slice %s-%s slice:%d", h.URI, index, slice) - http.Error(w, mesg, http.StatusPreconditionFailed) + if !h.Cluster.OwnsFragment(h.Node.ID, index, slice) { + msg := fmt.Sprintf("host does not own slice %s-%s slice:%d", h.Node.ID, index, slice) + http.Error(w, msg, http.StatusPreconditionFailed) return } @@ -1420,7 +1456,7 @@ func (h *Handler) handleGetFragmentNodes(w http.ResponseWriter, r *http.Request) } } -// handleGetFragmentBackup handles GET /fragment/data requests. +// handleGetFragmentData handles GET /fragment/data requests. func (h *Handler) handleGetFragmentData(w http.ResponseWriter, r *http.Request) { // Read slice parameter. q := r.URL.Query() @@ -1443,7 +1479,7 @@ func (h *Handler) handleGetFragmentData(w http.ResponseWriter, r *http.Request) } } -// handlePostFragmentRestore handles POST /fragment/data requests. +// handlePostFragmentData handles POST /fragment/data requests. func (h *Handler) handlePostFragmentData(w http.ResponseWriter, r *http.Request) { // Read slice parameter. q := r.URL.Query() @@ -1481,7 +1517,7 @@ func (h *Handler) handlePostFragmentData(w http.ResponseWriter, r *http.Request) } } -// handleGetFragmentData handles GET /fragment/block/data requests. +// handleGetFragmentBlockData handles GET /fragment/block/data requests. func (h *Handler) handleGetFragmentBlockData(w http.ResponseWriter, r *http.Request) { // Read request object. var req internal.BlockDataRequest @@ -1597,7 +1633,7 @@ func (h *Handler) handlePostFrameRestore(w http.ResponseWriter, r *http.Request) // Loop over each slice and import it if this node owns it. for slice := uint64(0); slice <= maxSlices[indexName]; slice++ { // Ignore this slice if we don't own it. - if !h.Cluster.OwnsFragment(h.URI.HostPort(), indexName, slice) { + if !h.Cluster.OwnsFragment(h.Node.ID, indexName, slice) { continue } @@ -1981,6 +2017,131 @@ func (h *Handler) handlePostInput(w http.ResponseWriter, r *http.Request) { } } +// handlePostClusterResizeSetCoordinator handles POST /cluster/resize/set-coordinator request. +func (h *Handler) handlePostClusterResizeSetCoordinator(w http.ResponseWriter, r *http.Request) { + // Decode request. + var req setCoordinatorRequest + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + oldNode := h.Cluster.nodeByID(h.Cluster.Coordinator) + newNode := h.Cluster.nodeByID(req.ID) + if newNode == nil { + http.Error(w, "Node with provided ID does not exist", http.StatusBadRequest) + return + } + + if err := func() error { + // If the new coordinator is this node, do the SetCoordinator directly. + if newNode.ID == h.Node.ID { + return h.Cluster.SetCoordinator(newNode) + } + + // Send the set-coordinator message to new node. + err := h.Broadcaster.SendTo( + newNode, + &internal.SetCoordinatorMessage{ + New: EncodeNode(newNode), + }) + if err != nil { + return fmt.Errorf("problem sending SetCoordinator message: %s", err) + } + + return nil + }(); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Encode response. + if err := json.NewEncoder(w).Encode(setCoordinatorResponse{ + Old: oldNode, + New: newNode, + }); err != nil { + h.logger().Printf("response encoding error: %s", err) + } +} + +type setCoordinatorRequest struct { + ID string `json:"id"` +} + +type setCoordinatorResponse struct { + Old *Node `json:"old"` + New *Node `json:"new"` +} + +// handlePostClusterResizeRemoveNode handles POST /cluster/resize/remove-node request. +func (h *Handler) handlePostClusterResizeRemoveNode(w http.ResponseWriter, r *http.Request) { + // Decode request. + var req removeNodeRequest + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + removeNode := h.Cluster.nodeByID(req.ID) + if removeNode == nil { + http.Error(w, fmt.Sprintf("Node is not a member of the cluster: %s", req.ID), http.StatusBadRequest) + return + } + + // Start the resize process (similar to NodeJoin) + err = h.Cluster.NodeLeave(removeNode) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Encode response. + if err := json.NewEncoder(w).Encode(removeNodeResponse{ + Remove: removeNode, + }); err != nil { + h.logger().Printf("response encoding error: %s", err) + } +} + +type removeNodeRequest struct { + ID string `json:"id"` +} + +type removeNodeResponse struct { + Remove *Node `json:"remove"` +} + +// handlePostClusterResizeAbort handles POST /cluster/resize/abort request. +func (h *Handler) handlePostClusterResizeAbort(w http.ResponseWriter, r *http.Request) { + var msg string + + if err := func() error { + if !h.Cluster.IsCoordinator() { + return fmt.Errorf("abort requests must be made on the coordinator node") + } + err := h.Cluster.CompleteCurrentJob(ResizeJobStateAborted) + if err != nil { + return err + } + return nil + }(); err != nil { + msg = err.Error() + } + + // Encode response. + if err := json.NewEncoder(w).Encode(clusterResizeAbortResponse{ + Info: msg, + }); err != nil { + h.logger().Printf("response encoding error: %s", err) + } +} + +type clusterResizeAbortResponse struct { + Info string `json:"info"` +} + // InputJSONDataParser validates input json file and executes SetBit. func (h *Handler) InputJSONDataParser(req map[string]interface{}, index *Index, name string) (map[string][]*Bit, error) { inputDef, err := index.InputDefinition(name) @@ -2051,6 +2212,12 @@ func (h *Handler) InputJSONDataParser(req map[string]interface{}, index *Index, } func (h *Handler) handleRecalculateCaches(w http.ResponseWriter, r *http.Request) { + err := h.Broadcaster.SendSync(&internal.RecalculateCaches{}) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + h.writeQueryResponse(w, r, &QueryResponse{Err: err}) + return + } h.Holder.RecalculateCaches() w.WriteHeader(http.StatusNoContent) } @@ -2078,7 +2245,6 @@ func GetTimeStamp(data map[string]interface{}, timeField string) (int64, error) func (h *Handler) handlePostClusterMessage(w http.ResponseWriter, r *http.Request) { // Verify that request is only communicating over protobufs. if r.Header.Get("Content-Type") != "application/x-protobuf" { - fmt.Println("**unsupported media type**") http.Error(w, "Unsupported media type", http.StatusUnsupportedMediaType) return } @@ -2110,7 +2276,7 @@ func (h *Handler) handlePostClusterMessage(w http.ResponseWriter, r *http.Reques } func (h *Handler) handleGetID(w http.ResponseWriter, r *http.Request) { - _, err := w.Write([]byte(h.Holder.LocalID)) + _, err := w.Write([]byte(h.Cluster.Node.ID)) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } diff --git a/handler_test.go b/handler_test.go index f1c533691..551c81d66 100644 --- a/handler_test.go +++ b/handler_test.go @@ -31,6 +31,7 @@ import ( "github.com/pilosa/pilosa" "github.com/pilosa/pilosa/internal" "github.com/pilosa/pilosa/pql" + "github.com/pilosa/pilosa/statik" "github.com/pilosa/pilosa/test" ) @@ -106,6 +107,7 @@ func TestHandler_Schema(t *testing.T) { if w.Code != http.StatusOK { t.Fatalf("unexpected status code: %d", w.Code) } else if body := w.Body.String(); body != `{"indexes":[{"name":"i0","frames":[{"name":"f0"},{"name":"f1","views":[{"name":"inverse"},{"name":"standard"}]}]},{"name":"i1","frames":[{"name":"f0","views":[{"name":"standard"}]}]}]}`+"\n" { + } else if body := w.Body.String(); body != `{"indexes":[{"name":"i0","frames":[{"name":"f0","options":{"rowLabel":"rowID","cacheType":"ranked","cacheSize":50000}},{"name":"f1","options":{"rowLabel":"rowID","inverseEnabled":true,"cacheType":"ranked","cacheSize":50000},"views":[{"name":"inverse"},{"name":"standard"}]}]},{"name":"i1","frames":[{"name":"f0","options":{"rowLabel":"rowID","cacheType":"ranked","cacheSize":50000},"views":[{"name":"standard"}]}]}]}`+"\n" { t.Fatalf("unexpected body: %s", body) } } @@ -146,11 +148,30 @@ func TestHandler_Status(t *testing.T) { h.ServeHTTP(w, test.MustNewHTTPRequest("GET", "/status", nil)) if w.Code != http.StatusOK { t.Fatalf("unexpected status code: %d", w.Code) - } else if body := w.Body.String(); body != `{"status":{"State":"UP","Indexes":[{"Name":"i0","Meta":{"ColumnLabel":"columnID"},"Frames":[{"Name":"f0","Meta":{"RowLabel":"rowID","CacheType":"ranked","CacheSize":50000}},{"Name":"f1","Meta":{"RowLabel":"rowID","InverseEnabled":true,"CacheType":"ranked","CacheSize":50000}}]},{"Name":"i1","Meta":{"ColumnLabel":"columnID"},"Frames":[{"Name":"f0","Meta":{"RowLabel":"rowID","CacheType":"ranked","CacheSize":50000}}]}]}}`+"\n" { + } else if body := w.Body.String(); body != `{"state":"NORMAL","nodes":[{"id":"test-node","uri":{"scheme":"http","host":"localhost","port":10101},"isCoordinator":false}]}`+"\n" { t.Fatalf("unexpected body: %s", body) } } +// Ensure the handler can abort a cluster resize. +func TestHandler_ClusterResizeAbort(t *testing.T) { + + t.Run("No resize job", func(t *testing.T) { + h := test.NewHandler() + h.Cluster = test.NewCluster(1) + h.SetRestricted() + + w := httptest.NewRecorder() + h.ServeHTTP(w, test.MustNewHTTPRequest("POST", "/cluster/resize/abort", nil)) + if w.Code != http.StatusOK { + t.Fatalf("unexpected status code: %d", w.Code) + } else if body := w.Body.String(); body != `{"info":"no resize job currently running"}`+"\n" { + t.Fatalf("unexpected body: %s", body) + } + }) + +} + // Ensure the handler can return the maxslice map. func TestHandler_MaxSlices(t *testing.T) { hldr := test.MustOpenHolder() @@ -171,7 +192,7 @@ func TestHandler_MaxSlices(t *testing.T) { h.ServeHTTP(w, test.MustNewHTTPRequest("GET", "/slices/max", nil)) if w.Code != http.StatusOK { t.Fatalf("unexpected status code: %d", w.Code) - } else if body := w.Body.String(); body != `{"maxSlices":{"i0":3,"i1":0}}`+"\n" { + } else if body := w.Body.String(); body != `{"standard":{"i0":3,"i1":0},"inverse":{"i0":0,"i1":0}}`+"\n" { t.Fatalf("unexpected body: %s", body) } } @@ -212,7 +233,7 @@ func TestHandler_MaxSlices_Inverse(t *testing.T) { h.ServeHTTP(w, test.MustNewHTTPRequest("GET", "/slices/max?inverse=true", nil)) if w.Code != http.StatusOK { t.Fatalf("unexpected status code: %d", w.Code) - } else if body := w.Body.String(); body != `{"maxSlices":{"i0":3,"i1":0}}`+"\n" { + } else if body := w.Body.String(); body != `{"standard":{"i0":0,"i1":0},"inverse":{"i0":3,"i1":0}}`+"\n" { t.Fatalf("unexpected body: %s", body) } } @@ -1055,9 +1076,6 @@ func TestHandler_Frame_GetFields(t *testing.T) { t.Fatal(err) } resp, err := http.Get(s.URL + "/index/i/frame/f/fields") - if err != nil { - t.Fatal(err) - } if err != nil { t.Fatal(err) } else if resp.StatusCode != http.StatusOK { @@ -1195,8 +1213,8 @@ func TestHandler_Fragment_Nodes(t *testing.T) { h.ServeHTTP(w, r) if w.Code != http.StatusOK { t.Fatalf("unexpected status code: %d", w.Code) - } else if w.Body.String() != `[{"scheme":"http","host":"host2"},{"scheme":"http","host":"host0"}]`+"\n" { - t.Fatalf("unexpected body: %q", w.Body.String()) + } else if body := w.Body.String(); body != `[{"id":"node2","uri":{"scheme":"http","host":"host2"},"isCoordinator":false},{"id":"node0","uri":{"scheme":"http","host":"host0"},"isCoordinator":false}]`+"\n" { + t.Fatalf("unexpected body: %q", body) } // invalid argument should return BadRequest @@ -1828,3 +1846,31 @@ func TestHandler_RecalculateCaches(t *testing.T) { } } + +func TestHandler_WebUI(t *testing.T) { + hldr := test.MustOpenHolder() + defer hldr.Close() + + h := test.NewHandler() + h.Holder = hldr.Holder + h.Cluster = test.NewCluster(1) + h.FileSystem = &statik.FileSystem{} + + w := httptest.NewRecorder() + h.ServeHTTP(w, test.MustNewHTTPRequest("GET", "/", nil)) + if w.Code != http.StatusOK { + t.Fatalf("unexpected status code: %d", w.Code) + } + if !strings.Contains(w.Body.String(), "Pilosa WebUI") { + t.Fatalf("WebUI is not being served correctly.") + } + + // If curl is the client, the response should be different + w = httptest.NewRecorder() + req := test.MustNewHTTPRequest("GET", "/", nil) + req.Header.Add("User-Agent", "curl/7.54.0") + h.ServeHTTP(w, req) + if !strings.Contains(w.Body.String(), "try the WebUI") { + t.Fatalf("WebUI is not being served correctly.") + } +} diff --git a/holder.go b/holder.go index c91c3d07d..63bbbbc86 100644 --- a/holder.go +++ b/holder.go @@ -31,6 +31,7 @@ import ( "syscall" "time" + "github.com/pilosa/pilosa/internal" uuid "github.com/satori/go.uuid" ) @@ -48,8 +49,15 @@ type Holder struct { // Indexes by name. indexes map[string]*Index + hasData bool + + // opened channel is closed once Open() completes. + opened chan struct{} Broadcaster Broadcaster + + NewAttrStore func(string) AttrStore + // Close management wg sync.WaitGroup closing chan struct{} @@ -64,8 +72,6 @@ type Holder struct { CacheFlushInterval time.Duration LogOutput io.Writer - - LocalID string } // NewHolder returns a new instance of Holder. @@ -74,19 +80,54 @@ func NewHolder() *Holder { indexes: make(map[string]*Index), closing: make(chan struct{}, 0), + opened: make(chan struct{}), + Broadcaster: NopBroadcaster, Stats: NopStatsClient, + NewAttrStore: NewNopAttrStore, + CacheFlushInterval: DefaultCacheFlushInterval, LogOutput: os.Stderr, } } +// Peek reads the root data directory for the holder +// without actually loading any data into memory. +// HasData is returned, and h.hasData is set. +func (h *Holder) Peek() bool { + h.logger().Printf("peek at holder path: %s", h.Path) + h.hasData = false + + // Open path to read all index directories. + f, err := os.Open(h.Path) + if err != nil { + return false + } + defer f.Close() + + fis, err := f.Readdir(0) + if err != nil { + return false + } + + for _, fi := range fis { + if !fi.IsDir() { + continue + } + h.hasData = true + break + } + + return h.hasData +} + // Open initializes the root data directory for the holder. func (h *Holder) Open() error { h.setFileLimit() + h.logger().Printf("open holder path: %s", h.Path) if err := os.MkdirAll(h.Path, 0777); err != nil { return err } @@ -124,14 +165,19 @@ func (h *Holder) Open() error { } return fmt.Errorf("open index: name=%s, err=%s", index.Name(), err) } + h.mu.Lock() h.indexes[index.Name()] = index + h.mu.Unlock() } + h.logger().Printf("open holder: complete") // Periodically flush cache. h.wg.Add(1) go func() { defer h.wg.Done(); h.monitorCacheFlush() }() h.Stats.Open() + + close(h.opened) return nil } @@ -151,6 +197,15 @@ func (h *Holder) Close() error { return nil } +// HasData returns true if Holder contains at least one index. +// This is used to determine if the rebalancing of data is necessary +// when a node joins the cluster. +func (h *Holder) HasData() bool { + h.mu.RLock() + defer h.mu.RUnlock() + return h.hasData || len(h.indexes) > 0 +} + // MaxSlices returns MaxSlice map for all indexes. func (h *Holder) MaxSlices() map[string]uint64 { a := make(map[string]uint64) @@ -169,13 +224,13 @@ func (h *Holder) MaxInverseSlices() map[string]uint64 { return a } -// Schema returns schema data for all indexes and frames. +// Schema returns schema information for all indexes, frames, and views. func (h *Holder) Schema() []*IndexInfo { var a []*IndexInfo for _, index := range h.Indexes() { di := &IndexInfo{Name: index.Name()} for _, frame := range index.Frames() { - fi := &FrameInfo{Name: frame.Name()} + fi := &FrameInfo{Name: frame.Name(), Options: frame.Options()} for _, view := range frame.Views() { fi.Views = append(fi.Views, &ViewInfo{Name: view.Name()}) } @@ -189,6 +244,50 @@ func (h *Holder) Schema() []*IndexInfo { return a } +// ApplySchema applies an internal Schema to Holder. +func (h *Holder) ApplySchema(schema *internal.Schema) error { + // Create indexes that don't exist. + for _, index := range schema.Indexes { + opt := IndexOptions{} + idx, err := h.CreateIndexIfNotExists(index.Name, opt) + if err != nil { + return err + } + // Create frames that don't exist. + for _, f := range index.Frames { + opt := decodeFrameOptions(f.Meta) + frame, err := idx.CreateFrameIfNotExists(f.Name, *opt) + if err != nil { + return err + } + // Create views that don't exist. + for _, v := range f.Views { + _, err := frame.CreateViewIfNotExists(v) + if err != nil { + return err + } + } + } + // TODO: Create inputDefinitions that don't exist. + } + return nil +} + +// EncodeMaxSlices creates and internal representation of max slices. +func (h *Holder) EncodeMaxSlices() *internal.MaxSlices { + return &internal.MaxSlices{ + Standard: h.MaxSlices(), + Inverse: h.MaxInverseSlices(), + } +} + +// EncodeSchema creates an internal representation of schema. +func (h *Holder) EncodeSchema() *internal.Schema { + return &internal.Schema{ + Indexes: EncodeIndexes(h.Indexes()), + } +} + // IndexPath returns the path where a given index is stored. func (h *Holder) IndexPath(name string) string { return filepath.Join(h.Path, name) } @@ -278,6 +377,8 @@ func (h *Holder) newIndex(path, name string) (*Index, error) { index.LogOutput = h.LogOutput index.Stats = h.Stats.WithTags(fmt.Sprintf("index:%s", index.Name())) index.broadcaster = h.Broadcaster + index.NewAttrStore = h.NewAttrStore + index.columnAttrStore = h.NewAttrStore(filepath.Join(index.path, ".data")) return index, nil } @@ -432,22 +533,29 @@ func (h *Holder) setFileLimit() { func (h *Holder) logger() *log.Logger { return log.New(h.LogOutput, "", log.LstdFlags) } -func (h *Holder) loadLocalID() error { +func (h *Holder) loadNodeID() (string, error) { idPath := path.Join(h.Path, "ID") - localID := "" - localIDBytes, err := ioutil.ReadFile(idPath) - if err == nil { - localID = strings.TrimSpace(string(localIDBytes)) - } else { - u := uuid.NewV4() - localID = u.String() - err = ioutil.WriteFile(idPath, []byte(localID), 0600) - if err != nil { - return err - } + nodeID := "" + + h.logger().Printf("load NodeID: %s", idPath) + if err := os.MkdirAll(h.Path, 0777); err != nil { + return "", err } - h.LocalID = localID - return nil + + nodeIDBytes, err := ioutil.ReadFile(idPath) + if err == nil { + nodeID = strings.TrimSpace(string(nodeIDBytes)) + } else if os.IsNotExist(err) { + nodeID = uuid.NewV4().String() + err = ioutil.WriteFile(idPath, []byte(nodeID), 0600) + if err != nil { + return "", err + } + } else if err != nil { + return "", err + } + + return nodeID, nil } // HolderSyncer is an active anti-entropy tool that compares the local holder @@ -455,7 +563,7 @@ func (h *Holder) loadLocalID() error { type HolderSyncer struct { Holder *Holder - URI *URI + Node *Node Cluster *Cluster RemoteClient *http.Client @@ -511,7 +619,7 @@ func (s *HolderSyncer) SyncHolder() error { for slice := uint64(0); slice <= s.Holder.Index(di.Name).MaxSlice(); slice++ { // Ignore slices that this host doesn't own. - if !s.Cluster.OwnsFragment(s.URI.HostPort(), di.Name, slice) { + if !s.Cluster.OwnsFragment(s.Node.ID, di.Name, slice) { continue } @@ -553,11 +661,8 @@ func (s *HolderSyncer) syncIndex(index string) error { s.Stats.CountWithCustomTags("ColumnAttrStoreBlocks", int64(len(blks)), 1.0, []string{indexTag}) // Sync with every other host. - for _, node := range Nodes(s.Cluster.Nodes).FilterHost(s.URI.HostPort()) { - client, err := NewInternalHTTPClient(node.Host, s.RemoteClient) - if err != nil { - return err - } + for _, node := range Nodes(s.Cluster.Nodes).FilterID(s.Node.ID) { + client := NewInternalHTTPClientFromURI(&node.URI, s.RemoteClient) // Retrieve attributes from differing blocks. // Skip update and recomputation if no attributes have changed. @@ -567,7 +672,7 @@ func (s *HolderSyncer) syncIndex(index string) error { } else if len(m) == 0 { continue } - s.Stats.CountWithCustomTags("ColumnAttrDiff", int64(len(m)), 1.0, []string{indexTag, node.Host}) + s.Stats.CountWithCustomTags("ColumnAttrDiff", int64(len(m)), 1.0, []string{indexTag, node.ID}) // Update local copy. if err := idx.ColumnAttrStore().SetBulkAttrs(m); err != nil { @@ -586,7 +691,7 @@ func (s *HolderSyncer) syncIndex(index string) error { // syncFrame synchronizes frame attributes with the rest of the cluster. func (s *HolderSyncer) syncFrame(index, name string) error { - // Retrieve index reference. + // Retrieve frame reference. f := s.Holder.Frame(index, name) if f == nil { return nil @@ -602,11 +707,8 @@ func (s *HolderSyncer) syncFrame(index, name string) error { s.Stats.CountWithCustomTags("RowAttrStoreBlocks", int64(len(blks)), 1.0, []string{indexTag, frameTag}) // Sync with every other host. - for _, node := range Nodes(s.Cluster.Nodes).FilterHost(s.URI.HostPort()) { - client, err := NewInternalHTTPClient(node.Host, s.RemoteClient) - if err != nil { - return err - } + for _, node := range Nodes(s.Cluster.Nodes).FilterID(s.Node.ID) { + client := NewInternalHTTPClientFromURI(&node.URI, s.RemoteClient) // Retrieve attributes from differing blocks. // Skip update and recomputation if no attributes have changed. @@ -618,7 +720,7 @@ func (s *HolderSyncer) syncFrame(index, name string) error { } else if len(m) == 0 { continue } - s.Stats.CountWithCustomTags("RowAttrDiff", int64(len(m)), 1.0, []string{indexTag, frameTag, node.Host}) + s.Stats.CountWithCustomTags("RowAttrDiff", int64(len(m)), 1.0, []string{indexTag, frameTag, node.ID}) // Update local copy. if err := f.RowAttrStore().SetBulkAttrs(m); err != nil { @@ -658,7 +760,7 @@ func (s *HolderSyncer) syncFragment(index, frame, view string, slice uint64) err // Sync fragments together. fs := FragmentSyncer{ Fragment: frag, - Host: s.URI.HostPort(), + Node: s.Node, Cluster: s.Cluster, Closing: s.Closing, RemoteClient: s.RemoteClient, @@ -669,3 +771,65 @@ func (s *HolderSyncer) syncFragment(index, frame, view string, slice uint64) err return nil } + +// HolderCleaner removes fragments and data files that are no longer used. +type HolderCleaner struct { + Node *Node + + Holder *Holder + Cluster *Cluster + + // Signals that the sync should stop. + Closing <-chan struct{} +} + +// IsClosing returns true if the cleaner has been marked to close. +func (c *HolderCleaner) IsClosing() bool { + select { + case <-c.Closing: + return true + default: + return false + } +} + +// CleanHolder compares the holder with the cluster state and removes +// any unnecessary fragments and files. +func (c *HolderCleaner) CleanHolder() error { + for _, index := range c.Holder.Indexes() { + // Verify cleaner has not closed. + if c.IsClosing() { + return nil + } + + // Get the fragments that node is responsible for (based on hash(index, node)). + containedSlices := c.Cluster.ContainsSlices(index.Name(), index.MaxSlice(), c.Node) + + // Get the fragments registered in memory. + for _, frame := range index.Frames() { + for _, view := range frame.Views() { + for _, fragment := range view.Fragments() { + fragSlice := fragment.Slice() + // Ignore fragments that should be present. + if uint64InSlice(fragSlice, containedSlices) { + continue + } + // Delete fragment. + if err := view.DeleteFragment(fragSlice); err != nil { + return err + } + } + } + } + } + return nil +} + +func uint64InSlice(i uint64, s []uint64) bool { + for _, o := range s { + if i == o { + return true + } + } + return false +} diff --git a/holder_test.go b/holder_test.go index 2bf656e10..5a06a9272 100644 --- a/holder_test.go +++ b/holder_test.go @@ -284,6 +284,60 @@ func TestHolder_Open(t *testing.T) { }) } +func TestHolder_HasData(t *testing.T) { + t.Run("IndexDirectory", func(t *testing.T) { + h := test.MustOpenHolder() + defer h.Close() + + if h.HasData() { + t.Fatal("expected HasData to return false") + } + + if _, err := h.CreateIndex("test", pilosa.IndexOptions{}); err != nil { + t.Fatal(err) + } + + if !h.HasData() { + t.Fatal("expected HasData to return true") + } + }) + + t.Run("Peek", func(t *testing.T) { + h := test.NewHolder() + + if hasData := h.Peek(); hasData != false { + t.Fatal("expected Peek to return false") + } else if h.HasData() { + t.Fatal("expected HasData to return false") + } + + // Create an index directory to indicate data exists. + if err := os.Mkdir(h.IndexPath("test"), 0777); err != nil { + t.Fatal(err) + } + + if hasData := h.Peek(); hasData != true { + t.Fatal("expected Peek to return true") + } else if !h.HasData() { + t.Fatal("expected HasData to return true") + } + }) + + t.Run("Peek at missing directory", func(t *testing.T) { + h := test.NewHolder() + + // Ensure that hasData is false when trying to peek into + // a directory that doesn't exist. + h.Path = "bad-path" + + if hasData := h.Peek(); hasData != false { + t.Fatal("expected Peek to return false") + } else if h.HasData() { + t.Fatal("expected HasData to return false") + } + }) +} + // Ensure holder can delete an index and its underlying files. func TestHolder_DeleteIndex(t *testing.T) { hldr := test.MustOpenHolder() @@ -334,16 +388,21 @@ func TestHolderSyncer_SyncHolder(t *testing.T) { s.Handler.Executor.ExecuteFn = func(ctx context.Context, index string, query *pql.Query, slices []uint64, opt *pilosa.ExecOptions) ([]interface{}, error) { e := pilosa.NewExecutor(client) e.Holder = hldr1.Holder - e.Scheme = cluster.Nodes[1].Scheme - e.Host = cluster.Nodes[1].Host + e.Node = cluster.Nodes[1] e.Cluster = cluster return e.Execute(ctx, index, query, slices, opt) } // Mock 2-node, fully replicated cluster. cluster.ReplicaN = 2 - cluster.Nodes[0].Host = "localhost:0" - cluster.Nodes[1].Host = test.MustParseURLHost(s.URL) + + uri, err := pilosa.NewURIFromAddress(s.URL) + if err != nil { + t.Fatal(err) + } + + cluster.Nodes[0].URI = test.NewURIFromHostPort("localhost", 0) + cluster.Nodes[1].URI = *uri // Create frames on nodes. for _, hldr := range []*test.Holder{hldr0, hldr1} { @@ -395,13 +454,9 @@ func TestHolderSyncer_SyncHolder(t *testing.T) { hldr0.Index("y").SetRemoteMaxSlice(3) // Set up syncer. - uri, err := cluster.Nodes[0].URI() - if err != nil { - t.Fatal(err) - } syncer := pilosa.HolderSyncer{ Holder: hldr0.Holder, - URI: uri, + Node: cluster.Nodes[0], Cluster: cluster, RemoteClient: pilosa.GetHTTPClient(nil), Stats: pilosa.NopStatsClient, @@ -440,3 +495,141 @@ func TestHolderSyncer_SyncHolder(t *testing.T) { } } } + +// Ensure holder can clean up orphaned fragments. +func TestHolderCleaner_CleanHolder(t *testing.T) { + cluster := test.NewCluster(2) + + // Create a local holder. + hldr0 := test.MustOpenHolder() + defer hldr0.Close() + + // Mock 2-node, fully replicated cluster. + cluster.ReplicaN = 2 + + cluster.Nodes[0].URI = test.NewURIFromHostPort("localhost", 0) + + // Create frames on nodes. + for _, hldr := range []*test.Holder{hldr0} { + hldr.MustCreateFrameIfNotExists("i", "f") + hldr.MustCreateFrameIfNotExists("i", "f0") + hldr.MustCreateFrameIfNotExists("y", "z") + } + + // Set data on the local holder. + f := hldr0.MustCreateFragmentIfNotExists("i", "f", pilosa.ViewStandard, 0) + if _, err := f.SetBit(0, 10); err != nil { + t.Fatal(err) + } else if _, err := f.SetBit(0, 4000); err != nil { + t.Fatal(err) + } else if _, err := f.SetBit(2, 20); err != nil { + t.Fatal(err) + } else if _, err := f.SetBit(3, 10); err != nil { + t.Fatal(err) + } else if _, err := f.SetBit(120, 10); err != nil { + t.Fatal(err) + } else if _, err := f.SetBit(200, 4); err != nil { + t.Fatal(err) + } + + f = hldr0.MustCreateFragmentIfNotExists("i", "f0", pilosa.ViewStandard, 1) + if _, err := f.SetBit(9, SliceWidth+5); err != nil { + t.Fatal(err) + } + + f = hldr0.MustCreateFragmentIfNotExists("y", "z", pilosa.ViewStandard, 2) + if _, err := f.SetBit(10, (2*SliceWidth)+4); err != nil { + t.Fatal(err) + } else if _, err := f.SetBit(10, (2*SliceWidth)+5); err != nil { + t.Fatal(err) + } else if _, err := f.SetBit(10, (2*SliceWidth)+7); err != nil { + t.Fatal(err) + } + + // Set highest slice. + hldr0.Index("i").SetRemoteMaxSlice(1) + hldr0.Index("y").SetRemoteMaxSlice(2) + + // Keep replication the same and ensure we get the expected results. + cluster.ReplicaN = 2 + + // Set up cleaner for replication 2. + cleaner2 := pilosa.HolderCleaner{ + Node: cluster.Nodes[0], + Holder: hldr0.Holder, + Cluster: cluster, + } + + if err := cleaner2.CleanHolder(); err != nil { + t.Fatal(err) + } + + // Verify data is the same on both nodes. + for i, hldr := range []*test.Holder{hldr0} { + f := hldr.Fragment("i", "f", pilosa.ViewStandard, 0) + if a := f.Row(0).Bits(); !reflect.DeepEqual(a, []uint64{10, 4000}) { + t.Fatalf("unexpected bits(%d/0): %+v", i, a) + } else if a := f.Row(2).Bits(); !reflect.DeepEqual(a, []uint64{20}) { + t.Fatalf("unexpected bits(%d/2): %+v", i, a) + } else if a := f.Row(3).Bits(); !reflect.DeepEqual(a, []uint64{10}) { + t.Fatalf("unexpected bits(%d/3): %+v", i, a) + } else if a := f.Row(120).Bits(); !reflect.DeepEqual(a, []uint64{10}) { + t.Fatalf("unexpected bits(%d/120): %+v", i, a) + } else if a := f.Row(200).Bits(); !reflect.DeepEqual(a, []uint64{4}) { + t.Fatalf("unexpected bits(%d/200): %+v", i, a) + } + + f = hldr.Fragment("i", "f0", pilosa.ViewStandard, 1) + a := f.Row(9).Bits() + if !reflect.DeepEqual(a, []uint64{SliceWidth + 5}) { + t.Fatalf("unexpected bits(%d/i/f0): %+v", i, a) + } + if a := f.Row(9).Bits(); !reflect.DeepEqual(a, []uint64{SliceWidth + 5}) { + t.Fatalf("unexpected bits(%d/d/f0): %+v", i, a) + } + f = hldr.Fragment("y", "z", pilosa.ViewStandard, 2) + if a := f.Row(10).Bits(); !reflect.DeepEqual(a, []uint64{(2 * SliceWidth) + 4, (2 * SliceWidth) + 5, (2 * SliceWidth) + 7}) { + t.Fatalf("unexpected bits(%d/y/z): %+v", i, a) + } + } + + // Change replication factor to ensure we have fragments to remove. + cluster.ReplicaN = 1 + + // Set up cleaner for replication 1. + cleaner1 := pilosa.HolderCleaner{ + Node: cluster.Nodes[0], + Holder: hldr0.Holder, + Cluster: cluster, + } + + if err := cleaner1.CleanHolder(); err != nil { + t.Fatal(err) + } + + // Verify data is the same on both nodes. + for i, hldr := range []*test.Holder{hldr0} { + f := hldr.Fragment("i", "f", pilosa.ViewStandard, 0) + if a := f.Row(0).Bits(); !reflect.DeepEqual(a, []uint64{10, 4000}) { + t.Fatalf("unexpected bits(%d/0): %+v", i, a) + } else if a := f.Row(2).Bits(); !reflect.DeepEqual(a, []uint64{20}) { + t.Fatalf("unexpected bits(%d/2): %+v", i, a) + } else if a := f.Row(3).Bits(); !reflect.DeepEqual(a, []uint64{10}) { + t.Fatalf("unexpected bits(%d/3): %+v", i, a) + } else if a := f.Row(120).Bits(); !reflect.DeepEqual(a, []uint64{10}) { + t.Fatalf("unexpected bits(%d/120): %+v", i, a) + } else if a := f.Row(200).Bits(); !reflect.DeepEqual(a, []uint64{4}) { + t.Fatalf("unexpected bits(%d/200): %+v", i, a) + } + + f = hldr.Fragment("i", "f0", pilosa.ViewStandard, 1) + if f != nil { + t.Fatalf("expected fragment to be deleted: (%d/i/f0): %+v", i, f) + } + + f = hldr.Fragment("y", "z", pilosa.ViewStandard, 2) + if a := f.Row(10).Bits(); !reflect.DeepEqual(a, []uint64{(2 * SliceWidth) + 4, (2 * SliceWidth) + 5, (2 * SliceWidth) + 7}) { + t.Fatalf("unexpected bits(%d/y/z): %+v", i, a) + } + } +} diff --git a/index.go b/index.go index bdf2832b0..707b87213 100644 --- a/index.go +++ b/index.go @@ -55,8 +55,10 @@ type Index struct { remoteMaxSlice uint64 remoteMaxInverseSlice uint64 + NewAttrStore func(string) AttrStore + // Column attribute storage and cache. - columnAttrStore *AttrStore + columnAttrStore AttrStore // InputDefinitions by name. inputDefinitions map[string]*InputDefinition @@ -83,7 +85,8 @@ func NewIndex(path, name string) (*Index, error) { remoteMaxSlice: 0, remoteMaxInverseSlice: 0, - columnAttrStore: NewAttrStore(filepath.Join(path, ".data")), + NewAttrStore: NewNopAttrStore, + columnAttrStore: NopAttrStore, columnLabel: DefaultColumnLabel, @@ -100,7 +103,7 @@ func (i *Index) Name() string { return i.name } func (i *Index) Path() string { return i.path } // ColumnAttrStore returns the storage for column attributes. -func (i *Index) ColumnAttrStore() *AttrStore { return i.columnAttrStore } +func (i *Index) ColumnAttrStore() AttrStore { return i.columnAttrStore } // SetColumnLabel sets the column label. Persists to meta file on update. func (i *Index) SetColumnLabel(v string) error { @@ -256,9 +259,7 @@ func (i *Index) Close() error { defer i.mu.Unlock() // Close the attribute store. - if i.columnAttrStore != nil { - i.columnAttrStore.Close() - } + i.columnAttrStore.Close() // Close all frames. for _, f := range i.frames { @@ -392,6 +393,20 @@ func (i *Index) Frames() []*Frame { return a } +// InputDefinitions returns a list of all inputDefinitions in the index. +func (i *Index) InputDefinitions() []*InputDefinition { + i.mu.RLock() + defer i.mu.RUnlock() + + a := make([]*InputDefinition, 0, len(i.inputDefinitions)) + for _, d := range i.inputDefinitions { + a = append(a, d) + } + //sort.Sort(inputDefintionSlice(a)) // TODO + + return a +} + // RecalculateCaches recalculates caches on every frame in the index. func (i *Index) RecalculateCaches() { for _, frame := range i.Frames() { @@ -440,8 +455,6 @@ func (i *Index) createFrame(name string, opt FrameOptions) (*Frame, error) { if opt.RangeEnabled { if opt.InverseEnabled { return nil, ErrInverseRangeNotAllowed - } else if opt.CacheType != "" && opt.CacheType != CacheTypeNone { - return nil, ErrRangeCacheNotAllowed } } else { if len(opt.Fields) > 0 { @@ -494,18 +507,12 @@ func (i *Index) createFrame(name string, opt FrameOptions) (*Frame, error) { f.inverseEnabled = opt.InverseEnabled f.rangeEnabled = opt.RangeEnabled - if err := f.saveMeta(); err != nil { - f.Close() - return nil, err - } - f.rangeEnabled = opt.RangeEnabled - // Set schema & save. - f.schema = &FrameSchema{ - Fields: opt.Fields, - } - if err := f.saveSchema(); err != nil { + // Set fields. + f.fields = opt.Fields + + if err := f.saveMeta(); err != nil { f.Close() return nil, err } @@ -524,6 +531,7 @@ func (i *Index) newFrame(path, name string) (*Frame, error) { f.LogOutput = i.LogOutput f.Stats = i.Stats.WithTags(fmt.Sprintf("frame:%s", name)) f.broadcaster = i.broadcaster + f.rowAttrStore = i.NewAttrStore(filepath.Join(f.path, ".data")) return f, nil } @@ -623,12 +631,10 @@ func EncodeIndexes(a []*Index) []*internal.Index { // encodeIndex converts d into its internal representation. func encodeIndex(d *Index) *internal.Index { - io := d.options() return &internal.Index{ - Name: d.name, - Meta: io.Encode(), - MaxSlice: d.MaxSlice(), - Frames: encodeFrames(d.Frames()), + Name: d.name, + Frames: encodeFrames(d.Frames()), + InputDefinitions: encodeInputDefinitions(d.InputDefinitions()), } } @@ -723,7 +729,6 @@ func (i *Index) newInputDefinition(name string) (*InputDefinition, error) { if err != nil { return nil, err } - inputDef.broadcaster = i.broadcaster return inputDef, nil } @@ -776,7 +781,6 @@ func (i *Index) openInputDefinitions() error { return nil } } - } return nil } diff --git a/index_test.go b/index_test.go index 5e44a53a4..23a820f2d 100644 --- a/index_test.go +++ b/index_test.go @@ -106,25 +106,21 @@ func TestIndex_CreateFrame(t *testing.T) { }, }); err != nil { t.Fatal(err) - } else if !reflect.DeepEqual(f.Schema(), &pilosa.FrameSchema{ - Fields: []*pilosa.Field{ - {Name: "field0", Type: pilosa.FieldTypeInt, Min: 10, Max: 20}, - {Name: "field1", Type: pilosa.FieldTypeInt, Min: 11, Max: 21}, - }, + } else if !reflect.DeepEqual(f.Fields(), []*pilosa.Field{ + {Name: "field0", Type: pilosa.FieldTypeInt, Min: 10, Max: 20}, + {Name: "field1", Type: pilosa.FieldTypeInt, Min: 11, Max: 21}, }) { - t.Fatalf("unexpected schema: %#v", f.Schema()) + t.Fatalf("unexpected fields: %#v", f.Fields()) } // Reopen the index & verify the fields are loaded. if err := index.Reopen(); err != nil { t.Fatal(err) - } else if f := index.Frame("f"); !reflect.DeepEqual(f.Schema(), &pilosa.FrameSchema{ - Fields: []*pilosa.Field{ - {Name: "field0", Type: pilosa.FieldTypeInt, Min: 10, Max: 20}, - {Name: "field1", Type: pilosa.FieldTypeInt, Min: 11, Max: 21}, - }, + } else if f := index.Frame("f"); !reflect.DeepEqual(f.Fields(), []*pilosa.Field{ + {Name: "field0", Type: pilosa.FieldTypeInt, Min: 10, Max: 20}, + {Name: "field1", Type: pilosa.FieldTypeInt, Min: 11, Max: 21}, }) { - t.Fatalf("unexpected schema after reopen: %#v", f.Schema()) + t.Fatalf("unexpected fields after reopen: %#v", f.Fields()) } }) @@ -140,14 +136,14 @@ func TestIndex_CreateFrame(t *testing.T) { } }) - t.Run("ErrRangeCacheNotAllowed", func(t *testing.T) { + t.Run("ErrRangeCacheAllowed", func(t *testing.T) { index := test.MustOpenIndex() defer index.Close() if _, err := index.CreateFrame("f", pilosa.FrameOptions{ RangeEnabled: true, CacheType: pilosa.CacheTypeRanked, - }); err != pilosa.ErrRangeCacheNotAllowed { + }); err != nil { t.Fatal(err) } }) diff --git a/input_definition.go b/input_definition.go index 84d22d4ac..f4571d0bb 100644 --- a/input_definition.go +++ b/input_definition.go @@ -36,12 +36,11 @@ var validValueDestination = []string{InputMapping, InputValueToRow, InputSingleR // InputDefinition represents a container for the data input definition. type InputDefinition struct { - name string - path string - index string - broadcaster Broadcaster - frames []InputFrame - fields []InputDefinitionField + name string + path string + index string + frames []InputFrame + fields []InputDefinitionField } // NewInputDefinition returns a new instance of InputDefinition. @@ -86,17 +85,9 @@ func (i *InputDefinition) LoadDefinition(pb *internal.InputDefinition) error { // Copy metadata fields. i.name = pb.Name for _, fr := range pb.Frames { - frameMeta := fr.Meta inputFrame := InputFrame{ - Name: fr.Name, - Options: FrameOptions{ - // Deprecating row labels per #810. So, setting the default row label here. - RowLabel: DefaultRowLabel, - InverseEnabled: frameMeta.InverseEnabled, - CacheSize: frameMeta.CacheSize, - CacheType: frameMeta.CacheType, - TimeQuantum: TimeQuantum(frameMeta.TimeQuantum), - }, + Name: fr.Name, + Options: *decodeFrameOptions(fr.Meta), } i.frames = append(i.frames, inputFrame) } @@ -338,6 +329,43 @@ func (i *InputDefinitionInfo) Encode() *internal.InputDefinition { return &def } +// encodeInputDefinitions converts a into its internal representation. +func encodeInputDefinitions(a []*InputDefinition) []*internal.InputDefinition { + other := make([]*internal.InputDefinition, len(a)) + for i := range a { + other[i] = encodeInputDefinition(a[i]) + } + return other +} + +// encodeInputDefinition converts i into its internal representation. +func encodeInputDefinition(i *InputDefinition) *internal.InputDefinition { + //fo := f.options() + return &internal.InputDefinition{ + Name: i.name, + Frames: encodeInputFrames(i.frames), + Fields: encodeInputDefinitionFields(i.fields), + } +} + +// encodeInputFrames converts a into its internal representation. +func encodeInputFrames(a []InputFrame) []*internal.Frame { + other := make([]*internal.Frame, len(a)) + for i := range a { + other[i] = a[i].Encode() + } + return other +} + +// encodeInputDefinitionFields converts a into its internal representation. +func encodeInputDefinitionFields(a []InputDefinitionField) []*internal.InputDefinitionField { + other := make([]*internal.InputDefinitionField, len(a)) + for i := range a { + other[i] = a[i].Encode() + } + return other +} + // AddFrame manually add frame to input definition. func (i *InputDefinition) AddFrame(frame InputFrame) error { i.frames = append(i.frames, frame) diff --git a/internal/private.pb.go b/internal/private.pb.go index 63f2ca581..1b7f71ac4 100644 --- a/internal/private.pb.go +++ b/internal/private.pb.go @@ -15,7 +15,7 @@ BlockDataRequest BlockDataResponse Cache - MaxSlicesResponse + MaxSlices CreateSliceMessage DeleteIndexMessage CreateIndexMessage @@ -24,17 +24,29 @@ CreateFieldMessage DeleteFieldMessage Frame + Schema Index InputDefinition InputDefinitionField InputDefinitionAction CreateInputDefinitionMessage DeleteInputDefinitionMessage + URI + Node + NodeStateMessage + NodeEventMessage NodeStatus ClusterStatus - FrameSchema Field + CreateViewMessage DeleteViewMessage + ResizeInstruction + ResizeSource + ResizeInstructionComplete + SetCoordinatorMessage + UpdateCoordinatorMessage + Topology + RecalculateCaches */ package internal @@ -247,18 +259,26 @@ func (m *Cache) GetIDs() []uint64 { return nil } -type MaxSlicesResponse struct { - MaxSlices map[string]uint64 `protobuf:"bytes,1,rep,name=MaxSlices" json:"MaxSlices,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"` +type MaxSlices struct { + Standard map[string]uint64 `protobuf:"bytes,1,rep,name=Standard" json:"Standard,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"` + Inverse map[string]uint64 `protobuf:"bytes,2,rep,name=Inverse" json:"Inverse,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"` } -func (m *MaxSlicesResponse) Reset() { *m = MaxSlicesResponse{} } -func (m *MaxSlicesResponse) String() string { return proto.CompactTextString(m) } -func (*MaxSlicesResponse) ProtoMessage() {} -func (*MaxSlicesResponse) Descriptor() ([]byte, []int) { return fileDescriptorPrivate, []int{6} } +func (m *MaxSlices) Reset() { *m = MaxSlices{} } +func (m *MaxSlices) String() string { return proto.CompactTextString(m) } +func (*MaxSlices) ProtoMessage() {} +func (*MaxSlices) Descriptor() ([]byte, []int) { return fileDescriptorPrivate, []int{6} } -func (m *MaxSlicesResponse) GetMaxSlices() map[string]uint64 { +func (m *MaxSlices) GetStandard() map[string]uint64 { if m != nil { - return m.MaxSlices + return m.Standard + } + return nil +} + +func (m *MaxSlices) GetInverse() map[string]uint64 { + if m != nil { + return m.Inverse } return nil } @@ -456,8 +476,9 @@ func (m *DeleteFieldMessage) GetField() string { } type Frame struct { - Name string `protobuf:"bytes,1,opt,name=Name,proto3" json:"Name,omitempty"` - Meta *FrameMeta `protobuf:"bytes,2,opt,name=Meta" json:"Meta,omitempty"` + Name string `protobuf:"bytes,1,opt,name=Name,proto3" json:"Name,omitempty"` + Meta *FrameMeta `protobuf:"bytes,2,opt,name=Meta" json:"Meta,omitempty"` + Views []string `protobuf:"bytes,3,rep,name=Views" json:"Views,omitempty"` } func (m *Frame) Reset() { *m = Frame{} } @@ -479,19 +500,39 @@ func (m *Frame) GetMeta() *FrameMeta { return nil } +func (m *Frame) GetViews() []string { + if m != nil { + return m.Views + } + return nil +} + +type Schema struct { + Indexes []*Index `protobuf:"bytes,1,rep,name=Indexes" json:"Indexes,omitempty"` +} + +func (m *Schema) Reset() { *m = Schema{} } +func (m *Schema) String() string { return proto.CompactTextString(m) } +func (*Schema) ProtoMessage() {} +func (*Schema) Descriptor() ([]byte, []int) { return fileDescriptorPrivate, []int{15} } + +func (m *Schema) GetIndexes() []*Index { + if m != nil { + return m.Indexes + } + return nil +} + type Index struct { Name string `protobuf:"bytes,1,opt,name=Name,proto3" json:"Name,omitempty"` - Meta *IndexMeta `protobuf:"bytes,2,opt,name=Meta" json:"Meta,omitempty"` - MaxSlice uint64 `protobuf:"varint,3,opt,name=MaxSlice,proto3" json:"MaxSlice,omitempty"` Frames []*Frame `protobuf:"bytes,4,rep,name=Frames" json:"Frames,omitempty"` - Slices []uint64 `protobuf:"varint,5,rep,packed,name=Slices" json:"Slices,omitempty"` InputDefinitions []*InputDefinition `protobuf:"bytes,6,rep,name=InputDefinitions" json:"InputDefinitions,omitempty"` } func (m *Index) Reset() { *m = Index{} } func (m *Index) String() string { return proto.CompactTextString(m) } func (*Index) ProtoMessage() {} -func (*Index) Descriptor() ([]byte, []int) { return fileDescriptorPrivate, []int{15} } +func (*Index) Descriptor() ([]byte, []int) { return fileDescriptorPrivate, []int{16} } func (m *Index) GetName() string { if m != nil { @@ -500,20 +541,6 @@ func (m *Index) GetName() string { return "" } -func (m *Index) GetMeta() *IndexMeta { - if m != nil { - return m.Meta - } - return nil -} - -func (m *Index) GetMaxSlice() uint64 { - if m != nil { - return m.MaxSlice - } - return 0 -} - func (m *Index) GetFrames() []*Frame { if m != nil { return m.Frames @@ -521,13 +548,6 @@ func (m *Index) GetFrames() []*Frame { return nil } -func (m *Index) GetSlices() []uint64 { - if m != nil { - return m.Slices - } - return nil -} - func (m *Index) GetInputDefinitions() []*InputDefinition { if m != nil { return m.InputDefinitions @@ -544,7 +564,7 @@ type InputDefinition struct { func (m *InputDefinition) Reset() { *m = InputDefinition{} } func (m *InputDefinition) String() string { return proto.CompactTextString(m) } func (*InputDefinition) ProtoMessage() {} -func (*InputDefinition) Descriptor() ([]byte, []int) { return fileDescriptorPrivate, []int{16} } +func (*InputDefinition) Descriptor() ([]byte, []int) { return fileDescriptorPrivate, []int{17} } func (m *InputDefinition) GetName() string { if m != nil { @@ -576,7 +596,7 @@ type InputDefinitionField struct { func (m *InputDefinitionField) Reset() { *m = InputDefinitionField{} } func (m *InputDefinitionField) String() string { return proto.CompactTextString(m) } func (*InputDefinitionField) ProtoMessage() {} -func (*InputDefinitionField) Descriptor() ([]byte, []int) { return fileDescriptorPrivate, []int{17} } +func (*InputDefinitionField) Descriptor() ([]byte, []int) { return fileDescriptorPrivate, []int{18} } func (m *InputDefinitionField) GetName() string { if m != nil { @@ -609,7 +629,7 @@ type InputDefinitionAction struct { func (m *InputDefinitionAction) Reset() { *m = InputDefinitionAction{} } func (m *InputDefinitionAction) String() string { return proto.CompactTextString(m) } func (*InputDefinitionAction) ProtoMessage() {} -func (*InputDefinitionAction) Descriptor() ([]byte, []int) { return fileDescriptorPrivate, []int{18} } +func (*InputDefinitionAction) Descriptor() ([]byte, []int) { return fileDescriptorPrivate, []int{19} } func (m *InputDefinitionAction) GetFrame() string { if m != nil { @@ -648,7 +668,7 @@ func (m *CreateInputDefinitionMessage) Reset() { *m = CreateInputDefinit func (m *CreateInputDefinitionMessage) String() string { return proto.CompactTextString(m) } func (*CreateInputDefinitionMessage) ProtoMessage() {} func (*CreateInputDefinitionMessage) Descriptor() ([]byte, []int) { - return fileDescriptorPrivate, []int{19} + return fileDescriptorPrivate, []int{20} } func (m *CreateInputDefinitionMessage) GetIndex() string { @@ -674,7 +694,7 @@ func (m *DeleteInputDefinitionMessage) Reset() { *m = DeleteInputDefinit func (m *DeleteInputDefinitionMessage) String() string { return proto.CompactTextString(m) } func (*DeleteInputDefinitionMessage) ProtoMessage() {} func (*DeleteInputDefinitionMessage) Descriptor() ([]byte, []int) { - return fileDescriptorPrivate, []int{20} + return fileDescriptorPrivate, []int{21} } func (m *DeleteInputDefinitionMessage) GetIndex() string { @@ -691,78 +711,182 @@ func (m *DeleteInputDefinitionMessage) GetName() string { return "" } -type NodeStatus struct { - Host string `protobuf:"bytes,1,opt,name=Host,proto3" json:"Host,omitempty"` - State string `protobuf:"bytes,2,opt,name=State,proto3" json:"State,omitempty"` - Indexes []*Index `protobuf:"bytes,3,rep,name=Indexes" json:"Indexes,omitempty"` - Scheme string `protobuf:"bytes,4,opt,name=Scheme,proto3" json:"Scheme,omitempty"` +type URI struct { + Scheme string `protobuf:"bytes,1,opt,name=Scheme,proto3" json:"Scheme,omitempty"` + Host string `protobuf:"bytes,2,opt,name=Host,proto3" json:"Host,omitempty"` + Port uint32 `protobuf:"varint,3,opt,name=Port,proto3" json:"Port,omitempty"` } -func (m *NodeStatus) Reset() { *m = NodeStatus{} } -func (m *NodeStatus) String() string { return proto.CompactTextString(m) } -func (*NodeStatus) ProtoMessage() {} -func (*NodeStatus) Descriptor() ([]byte, []int) { return fileDescriptorPrivate, []int{21} } +func (m *URI) Reset() { *m = URI{} } +func (m *URI) String() string { return proto.CompactTextString(m) } +func (*URI) ProtoMessage() {} +func (*URI) Descriptor() ([]byte, []int) { return fileDescriptorPrivate, []int{22} } -func (m *NodeStatus) GetHost() string { - if m != nil { - return m.Host - } - return "" -} - -func (m *NodeStatus) GetState() string { - if m != nil { - return m.State - } - return "" -} - -func (m *NodeStatus) GetIndexes() []*Index { - if m != nil { - return m.Indexes - } - return nil -} - -func (m *NodeStatus) GetScheme() string { +func (m *URI) GetScheme() string { if m != nil { return m.Scheme } return "" } +func (m *URI) GetHost() string { + if m != nil { + return m.Host + } + return "" +} + +func (m *URI) GetPort() uint32 { + if m != nil { + return m.Port + } + return 0 +} + +type Node struct { + ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"` + URI *URI `protobuf:"bytes,2,opt,name=URI" json:"URI,omitempty"` + IsCoordinator bool `protobuf:"varint,3,opt,name=IsCoordinator,proto3" json:"IsCoordinator,omitempty"` +} + +func (m *Node) Reset() { *m = Node{} } +func (m *Node) String() string { return proto.CompactTextString(m) } +func (*Node) ProtoMessage() {} +func (*Node) Descriptor() ([]byte, []int) { return fileDescriptorPrivate, []int{23} } + +func (m *Node) GetID() string { + if m != nil { + return m.ID + } + return "" +} + +func (m *Node) GetURI() *URI { + if m != nil { + return m.URI + } + return nil +} + +func (m *Node) GetIsCoordinator() bool { + if m != nil { + return m.IsCoordinator + } + return false +} + +type NodeStateMessage struct { + NodeID string `protobuf:"bytes,1,opt,name=NodeID,proto3" json:"NodeID,omitempty"` + State string `protobuf:"bytes,2,opt,name=State,proto3" json:"State,omitempty"` +} + +func (m *NodeStateMessage) Reset() { *m = NodeStateMessage{} } +func (m *NodeStateMessage) String() string { return proto.CompactTextString(m) } +func (*NodeStateMessage) ProtoMessage() {} +func (*NodeStateMessage) Descriptor() ([]byte, []int) { return fileDescriptorPrivate, []int{24} } + +func (m *NodeStateMessage) GetNodeID() string { + if m != nil { + return m.NodeID + } + return "" +} + +func (m *NodeStateMessage) GetState() string { + if m != nil { + return m.State + } + return "" +} + +type NodeEventMessage struct { + Event uint32 `protobuf:"varint,1,opt,name=Event,proto3" json:"Event,omitempty"` + Node *Node `protobuf:"bytes,2,opt,name=Node" json:"Node,omitempty"` +} + +func (m *NodeEventMessage) Reset() { *m = NodeEventMessage{} } +func (m *NodeEventMessage) String() string { return proto.CompactTextString(m) } +func (*NodeEventMessage) ProtoMessage() {} +func (*NodeEventMessage) Descriptor() ([]byte, []int) { return fileDescriptorPrivate, []int{25} } + +func (m *NodeEventMessage) GetEvent() uint32 { + if m != nil { + return m.Event + } + return 0 +} + +func (m *NodeEventMessage) GetNode() *Node { + if m != nil { + return m.Node + } + return nil +} + +type NodeStatus struct { + Node *Node `protobuf:"bytes,1,opt,name=Node" json:"Node,omitempty"` + MaxSlices *MaxSlices `protobuf:"bytes,2,opt,name=MaxSlices" json:"MaxSlices,omitempty"` + Schema *Schema `protobuf:"bytes,3,opt,name=Schema" json:"Schema,omitempty"` +} + +func (m *NodeStatus) Reset() { *m = NodeStatus{} } +func (m *NodeStatus) String() string { return proto.CompactTextString(m) } +func (*NodeStatus) ProtoMessage() {} +func (*NodeStatus) Descriptor() ([]byte, []int) { return fileDescriptorPrivate, []int{26} } + +func (m *NodeStatus) GetNode() *Node { + if m != nil { + return m.Node + } + return nil +} + +func (m *NodeStatus) GetMaxSlices() *MaxSlices { + if m != nil { + return m.MaxSlices + } + return nil +} + +func (m *NodeStatus) GetSchema() *Schema { + if m != nil { + return m.Schema + } + return nil +} + type ClusterStatus struct { - Nodes []*NodeStatus `protobuf:"bytes,1,rep,name=Nodes" json:"Nodes,omitempty"` + ClusterID string `protobuf:"bytes,1,opt,name=ClusterID,proto3" json:"ClusterID,omitempty"` + State string `protobuf:"bytes,2,opt,name=State,proto3" json:"State,omitempty"` + Nodes []*Node `protobuf:"bytes,3,rep,name=Nodes" json:"Nodes,omitempty"` } func (m *ClusterStatus) Reset() { *m = ClusterStatus{} } func (m *ClusterStatus) String() string { return proto.CompactTextString(m) } func (*ClusterStatus) ProtoMessage() {} -func (*ClusterStatus) Descriptor() ([]byte, []int) { return fileDescriptorPrivate, []int{22} } +func (*ClusterStatus) Descriptor() ([]byte, []int) { return fileDescriptorPrivate, []int{27} } -func (m *ClusterStatus) GetNodes() []*NodeStatus { +func (m *ClusterStatus) GetClusterID() string { + if m != nil { + return m.ClusterID + } + return "" +} + +func (m *ClusterStatus) GetState() string { + if m != nil { + return m.State + } + return "" +} + +func (m *ClusterStatus) GetNodes() []*Node { if m != nil { return m.Nodes } return nil } -type FrameSchema struct { - Fields []*Field `protobuf:"bytes,1,rep,name=Fields" json:"Fields,omitempty"` -} - -func (m *FrameSchema) Reset() { *m = FrameSchema{} } -func (m *FrameSchema) String() string { return proto.CompactTextString(m) } -func (*FrameSchema) ProtoMessage() {} -func (*FrameSchema) Descriptor() ([]byte, []int) { return fileDescriptorPrivate, []int{23} } - -func (m *FrameSchema) GetFields() []*Field { - if m != nil { - return m.Fields - } - return nil -} - type Field struct { Name string `protobuf:"bytes,1,opt,name=Name,proto3" json:"Name,omitempty"` Type string `protobuf:"bytes,2,opt,name=Type,proto3" json:"Type,omitempty"` @@ -773,7 +897,7 @@ type Field struct { func (m *Field) Reset() { *m = Field{} } func (m *Field) String() string { return proto.CompactTextString(m) } func (*Field) ProtoMessage() {} -func (*Field) Descriptor() ([]byte, []int) { return fileDescriptorPrivate, []int{24} } +func (*Field) Descriptor() ([]byte, []int) { return fileDescriptorPrivate, []int{28} } func (m *Field) GetName() string { if m != nil { @@ -803,6 +927,38 @@ func (m *Field) GetMax() int64 { return 0 } +type CreateViewMessage struct { + Index string `protobuf:"bytes,1,opt,name=Index,proto3" json:"Index,omitempty"` + Frame string `protobuf:"bytes,2,opt,name=Frame,proto3" json:"Frame,omitempty"` + View string `protobuf:"bytes,3,opt,name=View,proto3" json:"View,omitempty"` +} + +func (m *CreateViewMessage) Reset() { *m = CreateViewMessage{} } +func (m *CreateViewMessage) String() string { return proto.CompactTextString(m) } +func (*CreateViewMessage) ProtoMessage() {} +func (*CreateViewMessage) Descriptor() ([]byte, []int) { return fileDescriptorPrivate, []int{29} } + +func (m *CreateViewMessage) GetIndex() string { + if m != nil { + return m.Index + } + return "" +} + +func (m *CreateViewMessage) GetFrame() string { + if m != nil { + return m.Frame + } + return "" +} + +func (m *CreateViewMessage) GetView() string { + if m != nil { + return m.View + } + return "" +} + type DeleteViewMessage struct { Index string `protobuf:"bytes,1,opt,name=Index,proto3" json:"Index,omitempty"` Frame string `protobuf:"bytes,2,opt,name=Frame,proto3" json:"Frame,omitempty"` @@ -812,7 +968,7 @@ type DeleteViewMessage struct { func (m *DeleteViewMessage) Reset() { *m = DeleteViewMessage{} } func (m *DeleteViewMessage) String() string { return proto.CompactTextString(m) } func (*DeleteViewMessage) ProtoMessage() {} -func (*DeleteViewMessage) Descriptor() ([]byte, []int) { return fileDescriptorPrivate, []int{25} } +func (*DeleteViewMessage) Descriptor() ([]byte, []int) { return fileDescriptorPrivate, []int{30} } func (m *DeleteViewMessage) GetIndex() string { if m != nil { @@ -835,6 +991,208 @@ func (m *DeleteViewMessage) GetView() string { return "" } +type ResizeInstruction struct { + JobID int64 `protobuf:"varint,1,opt,name=JobID,proto3" json:"JobID,omitempty"` + Node *Node `protobuf:"bytes,2,opt,name=Node" json:"Node,omitempty"` + Coordinator *Node `protobuf:"bytes,3,opt,name=Coordinator" json:"Coordinator,omitempty"` + Sources []*ResizeSource `protobuf:"bytes,4,rep,name=Sources" json:"Sources,omitempty"` + Schema *Schema `protobuf:"bytes,5,opt,name=Schema" json:"Schema,omitempty"` + ClusterStatus *ClusterStatus `protobuf:"bytes,6,opt,name=ClusterStatus" json:"ClusterStatus,omitempty"` +} + +func (m *ResizeInstruction) Reset() { *m = ResizeInstruction{} } +func (m *ResizeInstruction) String() string { return proto.CompactTextString(m) } +func (*ResizeInstruction) ProtoMessage() {} +func (*ResizeInstruction) Descriptor() ([]byte, []int) { return fileDescriptorPrivate, []int{31} } + +func (m *ResizeInstruction) GetJobID() int64 { + if m != nil { + return m.JobID + } + return 0 +} + +func (m *ResizeInstruction) GetNode() *Node { + if m != nil { + return m.Node + } + return nil +} + +func (m *ResizeInstruction) GetCoordinator() *Node { + if m != nil { + return m.Coordinator + } + return nil +} + +func (m *ResizeInstruction) GetSources() []*ResizeSource { + if m != nil { + return m.Sources + } + return nil +} + +func (m *ResizeInstruction) GetSchema() *Schema { + if m != nil { + return m.Schema + } + return nil +} + +func (m *ResizeInstruction) GetClusterStatus() *ClusterStatus { + if m != nil { + return m.ClusterStatus + } + return nil +} + +type ResizeSource struct { + Node *Node `protobuf:"bytes,1,opt,name=Node" json:"Node,omitempty"` + Index string `protobuf:"bytes,2,opt,name=Index,proto3" json:"Index,omitempty"` + Frame string `protobuf:"bytes,3,opt,name=Frame,proto3" json:"Frame,omitempty"` + View string `protobuf:"bytes,4,opt,name=View,proto3" json:"View,omitempty"` + Slice uint64 `protobuf:"varint,5,opt,name=Slice,proto3" json:"Slice,omitempty"` +} + +func (m *ResizeSource) Reset() { *m = ResizeSource{} } +func (m *ResizeSource) String() string { return proto.CompactTextString(m) } +func (*ResizeSource) ProtoMessage() {} +func (*ResizeSource) Descriptor() ([]byte, []int) { return fileDescriptorPrivate, []int{32} } + +func (m *ResizeSource) GetNode() *Node { + if m != nil { + return m.Node + } + return nil +} + +func (m *ResizeSource) GetIndex() string { + if m != nil { + return m.Index + } + return "" +} + +func (m *ResizeSource) GetFrame() string { + if m != nil { + return m.Frame + } + return "" +} + +func (m *ResizeSource) GetView() string { + if m != nil { + return m.View + } + return "" +} + +func (m *ResizeSource) GetSlice() uint64 { + if m != nil { + return m.Slice + } + return 0 +} + +type ResizeInstructionComplete struct { + JobID int64 `protobuf:"varint,1,opt,name=JobID,proto3" json:"JobID,omitempty"` + Node *Node `protobuf:"bytes,2,opt,name=Node" json:"Node,omitempty"` + Error string `protobuf:"bytes,3,opt,name=Error,proto3" json:"Error,omitempty"` +} + +func (m *ResizeInstructionComplete) Reset() { *m = ResizeInstructionComplete{} } +func (m *ResizeInstructionComplete) String() string { return proto.CompactTextString(m) } +func (*ResizeInstructionComplete) ProtoMessage() {} +func (*ResizeInstructionComplete) Descriptor() ([]byte, []int) { + return fileDescriptorPrivate, []int{33} +} + +func (m *ResizeInstructionComplete) GetJobID() int64 { + if m != nil { + return m.JobID + } + return 0 +} + +func (m *ResizeInstructionComplete) GetNode() *Node { + if m != nil { + return m.Node + } + return nil +} + +func (m *ResizeInstructionComplete) GetError() string { + if m != nil { + return m.Error + } + return "" +} + +type SetCoordinatorMessage struct { + New *Node `protobuf:"bytes,1,opt,name=New" json:"New,omitempty"` +} + +func (m *SetCoordinatorMessage) Reset() { *m = SetCoordinatorMessage{} } +func (m *SetCoordinatorMessage) String() string { return proto.CompactTextString(m) } +func (*SetCoordinatorMessage) ProtoMessage() {} +func (*SetCoordinatorMessage) Descriptor() ([]byte, []int) { return fileDescriptorPrivate, []int{34} } + +func (m *SetCoordinatorMessage) GetNew() *Node { + if m != nil { + return m.New + } + return nil +} + +type UpdateCoordinatorMessage struct { + New *Node `protobuf:"bytes,1,opt,name=New" json:"New,omitempty"` +} + +func (m *UpdateCoordinatorMessage) Reset() { *m = UpdateCoordinatorMessage{} } +func (m *UpdateCoordinatorMessage) String() string { return proto.CompactTextString(m) } +func (*UpdateCoordinatorMessage) ProtoMessage() {} +func (*UpdateCoordinatorMessage) Descriptor() ([]byte, []int) { return fileDescriptorPrivate, []int{35} } + +func (m *UpdateCoordinatorMessage) GetNew() *Node { + if m != nil { + return m.New + } + return nil +} + +type Topology struct { + ClusterID string `protobuf:"bytes,1,opt,name=ClusterID,proto3" json:"ClusterID,omitempty"` + NodeIDs []string `protobuf:"bytes,2,rep,name=NodeIDs" json:"NodeIDs,omitempty"` +} + +func (m *Topology) Reset() { *m = Topology{} } +func (m *Topology) String() string { return proto.CompactTextString(m) } +func (*Topology) ProtoMessage() {} +func (*Topology) Descriptor() ([]byte, []int) { return fileDescriptorPrivate, []int{36} } + +func (m *Topology) GetClusterID() string { + if m != nil { + return m.ClusterID + } + return "" +} + +func (m *Topology) GetNodeIDs() []string { + if m != nil { + return m.NodeIDs + } + return nil +} + +type RecalculateCaches struct { +} + +func (m *RecalculateCaches) Reset() { *m = RecalculateCaches{} } +func (m *RecalculateCaches) String() string { return proto.CompactTextString(m) } +func (*RecalculateCaches) ProtoMessage() {} +func (*RecalculateCaches) Descriptor() ([]byte, []int) { return fileDescriptorPrivate, []int{37} } + func init() { proto.RegisterType((*IndexMeta)(nil), "internal.IndexMeta") proto.RegisterType((*FrameMeta)(nil), "internal.FrameMeta") @@ -842,7 +1200,7 @@ func init() { proto.RegisterType((*BlockDataRequest)(nil), "internal.BlockDataRequest") proto.RegisterType((*BlockDataResponse)(nil), "internal.BlockDataResponse") proto.RegisterType((*Cache)(nil), "internal.Cache") - proto.RegisterType((*MaxSlicesResponse)(nil), "internal.MaxSlicesResponse") + proto.RegisterType((*MaxSlices)(nil), "internal.MaxSlices") proto.RegisterType((*CreateSliceMessage)(nil), "internal.CreateSliceMessage") proto.RegisterType((*DeleteIndexMessage)(nil), "internal.DeleteIndexMessage") proto.RegisterType((*CreateIndexMessage)(nil), "internal.CreateIndexMessage") @@ -851,17 +1209,29 @@ func init() { proto.RegisterType((*CreateFieldMessage)(nil), "internal.CreateFieldMessage") proto.RegisterType((*DeleteFieldMessage)(nil), "internal.DeleteFieldMessage") proto.RegisterType((*Frame)(nil), "internal.Frame") + proto.RegisterType((*Schema)(nil), "internal.Schema") proto.RegisterType((*Index)(nil), "internal.Index") proto.RegisterType((*InputDefinition)(nil), "internal.InputDefinition") proto.RegisterType((*InputDefinitionField)(nil), "internal.InputDefinitionField") proto.RegisterType((*InputDefinitionAction)(nil), "internal.InputDefinitionAction") proto.RegisterType((*CreateInputDefinitionMessage)(nil), "internal.CreateInputDefinitionMessage") proto.RegisterType((*DeleteInputDefinitionMessage)(nil), "internal.DeleteInputDefinitionMessage") + proto.RegisterType((*URI)(nil), "internal.URI") + proto.RegisterType((*Node)(nil), "internal.Node") + proto.RegisterType((*NodeStateMessage)(nil), "internal.NodeStateMessage") + proto.RegisterType((*NodeEventMessage)(nil), "internal.NodeEventMessage") proto.RegisterType((*NodeStatus)(nil), "internal.NodeStatus") proto.RegisterType((*ClusterStatus)(nil), "internal.ClusterStatus") - proto.RegisterType((*FrameSchema)(nil), "internal.FrameSchema") proto.RegisterType((*Field)(nil), "internal.Field") + proto.RegisterType((*CreateViewMessage)(nil), "internal.CreateViewMessage") proto.RegisterType((*DeleteViewMessage)(nil), "internal.DeleteViewMessage") + proto.RegisterType((*ResizeInstruction)(nil), "internal.ResizeInstruction") + proto.RegisterType((*ResizeSource)(nil), "internal.ResizeSource") + proto.RegisterType((*ResizeInstructionComplete)(nil), "internal.ResizeInstructionComplete") + proto.RegisterType((*SetCoordinatorMessage)(nil), "internal.SetCoordinatorMessage") + proto.RegisterType((*UpdateCoordinatorMessage)(nil), "internal.UpdateCoordinatorMessage") + proto.RegisterType((*Topology)(nil), "internal.Topology") + proto.RegisterType((*RecalculateCaches)(nil), "internal.RecalculateCaches") } func (m *IndexMeta) Marshal() (dAtA []byte, err error) { size := m.Size() @@ -1123,7 +1493,7 @@ func (m *Cache) MarshalTo(dAtA []byte) (int, error) { return i, nil } -func (m *MaxSlicesResponse) Marshal() (dAtA []byte, err error) { +func (m *MaxSlices) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalTo(dAtA) @@ -1133,16 +1503,32 @@ func (m *MaxSlicesResponse) Marshal() (dAtA []byte, err error) { return dAtA[:n], nil } -func (m *MaxSlicesResponse) MarshalTo(dAtA []byte) (int, error) { +func (m *MaxSlices) MarshalTo(dAtA []byte) (int, error) { var i int _ = i var l int _ = l - if len(m.MaxSlices) > 0 { - for k, _ := range m.MaxSlices { + if len(m.Standard) > 0 { + for k, _ := range m.Standard { dAtA[i] = 0xa i++ - v := m.MaxSlices[k] + v := m.Standard[k] + mapSize := 1 + len(k) + sovPrivate(uint64(len(k))) + 1 + sovPrivate(uint64(v)) + i = encodeVarintPrivate(dAtA, i, uint64(mapSize)) + dAtA[i] = 0xa + i++ + i = encodeVarintPrivate(dAtA, i, uint64(len(k))) + i += copy(dAtA[i:], k) + dAtA[i] = 0x10 + i++ + i = encodeVarintPrivate(dAtA, i, uint64(v)) + } + } + if len(m.Inverse) > 0 { + for k, _ := range m.Inverse { + dAtA[i] = 0x12 + i++ + v := m.Inverse[k] mapSize := 1 + len(k) + sovPrivate(uint64(len(k))) + 1 + sovPrivate(uint64(v)) i = encodeVarintPrivate(dAtA, i, uint64(mapSize)) dAtA[i] = 0xa @@ -1431,6 +1817,51 @@ func (m *Frame) MarshalTo(dAtA []byte) (int, error) { } i += n10 } + if len(m.Views) > 0 { + for _, s := range m.Views { + dAtA[i] = 0x1a + i++ + l = len(s) + for l >= 1<<7 { + dAtA[i] = uint8(uint64(l)&0x7f | 0x80) + l >>= 7 + i++ + } + dAtA[i] = uint8(l) + i++ + i += copy(dAtA[i:], s) + } + } + return i, nil +} + +func (m *Schema) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalTo(dAtA) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *Schema) MarshalTo(dAtA []byte) (int, error) { + var i int + _ = i + var l int + _ = l + if len(m.Indexes) > 0 { + for _, msg := range m.Indexes { + dAtA[i] = 0xa + i++ + i = encodeVarintPrivate(dAtA, i, uint64(msg.Size())) + n, err := msg.MarshalTo(dAtA[i:]) + if err != nil { + return 0, err + } + i += n + } + } return i, nil } @@ -1455,21 +1886,6 @@ func (m *Index) MarshalTo(dAtA []byte) (int, error) { i = encodeVarintPrivate(dAtA, i, uint64(len(m.Name))) i += copy(dAtA[i:], m.Name) } - if m.Meta != nil { - dAtA[i] = 0x12 - i++ - i = encodeVarintPrivate(dAtA, i, uint64(m.Meta.Size())) - n11, err := m.Meta.MarshalTo(dAtA[i:]) - if err != nil { - return 0, err - } - i += n11 - } - if m.MaxSlice != 0 { - dAtA[i] = 0x18 - i++ - i = encodeVarintPrivate(dAtA, i, uint64(m.MaxSlice)) - } if len(m.Frames) > 0 { for _, msg := range m.Frames { dAtA[i] = 0x22 @@ -1482,23 +1898,6 @@ func (m *Index) MarshalTo(dAtA []byte) (int, error) { i += n } } - if len(m.Slices) > 0 { - dAtA13 := make([]byte, len(m.Slices)*10) - var j12 int - for _, num := range m.Slices { - for num >= 1<<7 { - dAtA13[j12] = uint8(uint64(num)&0x7f | 0x80) - num >>= 7 - j12++ - } - dAtA13[j12] = uint8(num) - j12++ - } - dAtA[i] = 0x2a - i++ - i = encodeVarintPrivate(dAtA, i, uint64(j12)) - i += copy(dAtA[i:], dAtA13[:j12]) - } if len(m.InputDefinitions) > 0 { for _, msg := range m.InputDefinitions { dAtA[i] = 0x32 @@ -1684,11 +2083,11 @@ func (m *CreateInputDefinitionMessage) MarshalTo(dAtA []byte) (int, error) { dAtA[i] = 0x1a i++ i = encodeVarintPrivate(dAtA, i, uint64(m.Definition.Size())) - n14, err := m.Definition.MarshalTo(dAtA[i:]) + n11, err := m.Definition.MarshalTo(dAtA[i:]) if err != nil { return 0, err } - i += n14 + i += n11 } return i, nil } @@ -1723,6 +2122,148 @@ func (m *DeleteInputDefinitionMessage) MarshalTo(dAtA []byte) (int, error) { return i, nil } +func (m *URI) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalTo(dAtA) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *URI) MarshalTo(dAtA []byte) (int, error) { + var i int + _ = i + var l int + _ = l + if len(m.Scheme) > 0 { + dAtA[i] = 0xa + i++ + i = encodeVarintPrivate(dAtA, i, uint64(len(m.Scheme))) + i += copy(dAtA[i:], m.Scheme) + } + if len(m.Host) > 0 { + dAtA[i] = 0x12 + i++ + i = encodeVarintPrivate(dAtA, i, uint64(len(m.Host))) + i += copy(dAtA[i:], m.Host) + } + if m.Port != 0 { + dAtA[i] = 0x18 + i++ + i = encodeVarintPrivate(dAtA, i, uint64(m.Port)) + } + return i, nil +} + +func (m *Node) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalTo(dAtA) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *Node) MarshalTo(dAtA []byte) (int, error) { + var i int + _ = i + var l int + _ = l + if len(m.ID) > 0 { + dAtA[i] = 0xa + i++ + i = encodeVarintPrivate(dAtA, i, uint64(len(m.ID))) + i += copy(dAtA[i:], m.ID) + } + if m.URI != nil { + dAtA[i] = 0x12 + i++ + i = encodeVarintPrivate(dAtA, i, uint64(m.URI.Size())) + n12, err := m.URI.MarshalTo(dAtA[i:]) + if err != nil { + return 0, err + } + i += n12 + } + if m.IsCoordinator { + dAtA[i] = 0x18 + i++ + if m.IsCoordinator { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i++ + } + return i, nil +} + +func (m *NodeStateMessage) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalTo(dAtA) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *NodeStateMessage) MarshalTo(dAtA []byte) (int, error) { + var i int + _ = i + var l int + _ = l + if len(m.NodeID) > 0 { + dAtA[i] = 0xa + i++ + i = encodeVarintPrivate(dAtA, i, uint64(len(m.NodeID))) + i += copy(dAtA[i:], m.NodeID) + } + if len(m.State) > 0 { + dAtA[i] = 0x12 + i++ + i = encodeVarintPrivate(dAtA, i, uint64(len(m.State))) + i += copy(dAtA[i:], m.State) + } + return i, nil +} + +func (m *NodeEventMessage) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalTo(dAtA) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *NodeEventMessage) MarshalTo(dAtA []byte) (int, error) { + var i int + _ = i + var l int + _ = l + if m.Event != 0 { + dAtA[i] = 0x8 + i++ + i = encodeVarintPrivate(dAtA, i, uint64(m.Event)) + } + if m.Node != nil { + dAtA[i] = 0x12 + i++ + i = encodeVarintPrivate(dAtA, i, uint64(m.Node.Size())) + n13, err := m.Node.MarshalTo(dAtA[i:]) + if err != nil { + return 0, err + } + i += n13 + } + return i, nil +} + func (m *NodeStatus) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) @@ -1738,35 +2279,35 @@ func (m *NodeStatus) MarshalTo(dAtA []byte) (int, error) { _ = i var l int _ = l - if len(m.Host) > 0 { + if m.Node != nil { dAtA[i] = 0xa i++ - i = encodeVarintPrivate(dAtA, i, uint64(len(m.Host))) - i += copy(dAtA[i:], m.Host) + i = encodeVarintPrivate(dAtA, i, uint64(m.Node.Size())) + n14, err := m.Node.MarshalTo(dAtA[i:]) + if err != nil { + return 0, err + } + i += n14 } - if len(m.State) > 0 { + if m.MaxSlices != nil { dAtA[i] = 0x12 i++ - i = encodeVarintPrivate(dAtA, i, uint64(len(m.State))) - i += copy(dAtA[i:], m.State) - } - if len(m.Indexes) > 0 { - for _, msg := range m.Indexes { - dAtA[i] = 0x1a - i++ - i = encodeVarintPrivate(dAtA, i, uint64(msg.Size())) - n, err := msg.MarshalTo(dAtA[i:]) - if err != nil { - return 0, err - } - i += n + i = encodeVarintPrivate(dAtA, i, uint64(m.MaxSlices.Size())) + n15, err := m.MaxSlices.MarshalTo(dAtA[i:]) + if err != nil { + return 0, err } + i += n15 } - if len(m.Scheme) > 0 { - dAtA[i] = 0x22 + if m.Schema != nil { + dAtA[i] = 0x1a i++ - i = encodeVarintPrivate(dAtA, i, uint64(len(m.Scheme))) - i += copy(dAtA[i:], m.Scheme) + i = encodeVarintPrivate(dAtA, i, uint64(m.Schema.Size())) + n16, err := m.Schema.MarshalTo(dAtA[i:]) + if err != nil { + return 0, err + } + i += n16 } return i, nil } @@ -1786,39 +2327,21 @@ func (m *ClusterStatus) MarshalTo(dAtA []byte) (int, error) { _ = i var l int _ = l + if len(m.ClusterID) > 0 { + dAtA[i] = 0xa + i++ + i = encodeVarintPrivate(dAtA, i, uint64(len(m.ClusterID))) + i += copy(dAtA[i:], m.ClusterID) + } + if len(m.State) > 0 { + dAtA[i] = 0x12 + i++ + i = encodeVarintPrivate(dAtA, i, uint64(len(m.State))) + i += copy(dAtA[i:], m.State) + } if len(m.Nodes) > 0 { for _, msg := range m.Nodes { - dAtA[i] = 0xa - i++ - i = encodeVarintPrivate(dAtA, i, uint64(msg.Size())) - n, err := msg.MarshalTo(dAtA[i:]) - if err != nil { - return 0, err - } - i += n - } - } - return i, nil -} - -func (m *FrameSchema) Marshal() (dAtA []byte, err error) { - size := m.Size() - dAtA = make([]byte, size) - n, err := m.MarshalTo(dAtA) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *FrameSchema) MarshalTo(dAtA []byte) (int, error) { - var i int - _ = i - var l int - _ = l - if len(m.Fields) > 0 { - for _, msg := range m.Fields { - dAtA[i] = 0xa + dAtA[i] = 0x1a i++ i = encodeVarintPrivate(dAtA, i, uint64(msg.Size())) n, err := msg.MarshalTo(dAtA[i:]) @@ -1871,6 +2394,42 @@ func (m *Field) MarshalTo(dAtA []byte) (int, error) { return i, nil } +func (m *CreateViewMessage) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalTo(dAtA) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *CreateViewMessage) MarshalTo(dAtA []byte) (int, error) { + var i int + _ = i + var l int + _ = l + if len(m.Index) > 0 { + dAtA[i] = 0xa + i++ + i = encodeVarintPrivate(dAtA, i, uint64(len(m.Index))) + i += copy(dAtA[i:], m.Index) + } + if len(m.Frame) > 0 { + dAtA[i] = 0x12 + i++ + i = encodeVarintPrivate(dAtA, i, uint64(len(m.Frame))) + i += copy(dAtA[i:], m.Frame) + } + if len(m.View) > 0 { + dAtA[i] = 0x1a + i++ + i = encodeVarintPrivate(dAtA, i, uint64(len(m.View))) + i += copy(dAtA[i:], m.View) + } + return i, nil +} + func (m *DeleteViewMessage) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) @@ -1907,6 +2466,284 @@ func (m *DeleteViewMessage) MarshalTo(dAtA []byte) (int, error) { return i, nil } +func (m *ResizeInstruction) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalTo(dAtA) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *ResizeInstruction) MarshalTo(dAtA []byte) (int, error) { + var i int + _ = i + var l int + _ = l + if m.JobID != 0 { + dAtA[i] = 0x8 + i++ + i = encodeVarintPrivate(dAtA, i, uint64(m.JobID)) + } + if m.Node != nil { + dAtA[i] = 0x12 + i++ + i = encodeVarintPrivate(dAtA, i, uint64(m.Node.Size())) + n17, err := m.Node.MarshalTo(dAtA[i:]) + if err != nil { + return 0, err + } + i += n17 + } + if m.Coordinator != nil { + dAtA[i] = 0x1a + i++ + i = encodeVarintPrivate(dAtA, i, uint64(m.Coordinator.Size())) + n18, err := m.Coordinator.MarshalTo(dAtA[i:]) + if err != nil { + return 0, err + } + i += n18 + } + if len(m.Sources) > 0 { + for _, msg := range m.Sources { + dAtA[i] = 0x22 + i++ + i = encodeVarintPrivate(dAtA, i, uint64(msg.Size())) + n, err := msg.MarshalTo(dAtA[i:]) + if err != nil { + return 0, err + } + i += n + } + } + if m.Schema != nil { + dAtA[i] = 0x2a + i++ + i = encodeVarintPrivate(dAtA, i, uint64(m.Schema.Size())) + n19, err := m.Schema.MarshalTo(dAtA[i:]) + if err != nil { + return 0, err + } + i += n19 + } + if m.ClusterStatus != nil { + dAtA[i] = 0x32 + i++ + i = encodeVarintPrivate(dAtA, i, uint64(m.ClusterStatus.Size())) + n20, err := m.ClusterStatus.MarshalTo(dAtA[i:]) + if err != nil { + return 0, err + } + i += n20 + } + return i, nil +} + +func (m *ResizeSource) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalTo(dAtA) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *ResizeSource) MarshalTo(dAtA []byte) (int, error) { + var i int + _ = i + var l int + _ = l + if m.Node != nil { + dAtA[i] = 0xa + i++ + i = encodeVarintPrivate(dAtA, i, uint64(m.Node.Size())) + n21, err := m.Node.MarshalTo(dAtA[i:]) + if err != nil { + return 0, err + } + i += n21 + } + if len(m.Index) > 0 { + dAtA[i] = 0x12 + i++ + i = encodeVarintPrivate(dAtA, i, uint64(len(m.Index))) + i += copy(dAtA[i:], m.Index) + } + if len(m.Frame) > 0 { + dAtA[i] = 0x1a + i++ + i = encodeVarintPrivate(dAtA, i, uint64(len(m.Frame))) + i += copy(dAtA[i:], m.Frame) + } + if len(m.View) > 0 { + dAtA[i] = 0x22 + i++ + i = encodeVarintPrivate(dAtA, i, uint64(len(m.View))) + i += copy(dAtA[i:], m.View) + } + if m.Slice != 0 { + dAtA[i] = 0x28 + i++ + i = encodeVarintPrivate(dAtA, i, uint64(m.Slice)) + } + return i, nil +} + +func (m *ResizeInstructionComplete) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalTo(dAtA) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *ResizeInstructionComplete) MarshalTo(dAtA []byte) (int, error) { + var i int + _ = i + var l int + _ = l + if m.JobID != 0 { + dAtA[i] = 0x8 + i++ + i = encodeVarintPrivate(dAtA, i, uint64(m.JobID)) + } + if m.Node != nil { + dAtA[i] = 0x12 + i++ + i = encodeVarintPrivate(dAtA, i, uint64(m.Node.Size())) + n22, err := m.Node.MarshalTo(dAtA[i:]) + if err != nil { + return 0, err + } + i += n22 + } + if len(m.Error) > 0 { + dAtA[i] = 0x1a + i++ + i = encodeVarintPrivate(dAtA, i, uint64(len(m.Error))) + i += copy(dAtA[i:], m.Error) + } + return i, nil +} + +func (m *SetCoordinatorMessage) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalTo(dAtA) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *SetCoordinatorMessage) MarshalTo(dAtA []byte) (int, error) { + var i int + _ = i + var l int + _ = l + if m.New != nil { + dAtA[i] = 0xa + i++ + i = encodeVarintPrivate(dAtA, i, uint64(m.New.Size())) + n23, err := m.New.MarshalTo(dAtA[i:]) + if err != nil { + return 0, err + } + i += n23 + } + return i, nil +} + +func (m *UpdateCoordinatorMessage) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalTo(dAtA) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *UpdateCoordinatorMessage) MarshalTo(dAtA []byte) (int, error) { + var i int + _ = i + var l int + _ = l + if m.New != nil { + dAtA[i] = 0xa + i++ + i = encodeVarintPrivate(dAtA, i, uint64(m.New.Size())) + n24, err := m.New.MarshalTo(dAtA[i:]) + if err != nil { + return 0, err + } + i += n24 + } + return i, nil +} + +func (m *Topology) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalTo(dAtA) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *Topology) MarshalTo(dAtA []byte) (int, error) { + var i int + _ = i + var l int + _ = l + if len(m.ClusterID) > 0 { + dAtA[i] = 0xa + i++ + i = encodeVarintPrivate(dAtA, i, uint64(len(m.ClusterID))) + i += copy(dAtA[i:], m.ClusterID) + } + if len(m.NodeIDs) > 0 { + for _, s := range m.NodeIDs { + dAtA[i] = 0x12 + i++ + l = len(s) + for l >= 1<<7 { + dAtA[i] = uint8(uint64(l)&0x7f | 0x80) + l >>= 7 + i++ + } + dAtA[i] = uint8(l) + i++ + i += copy(dAtA[i:], s) + } + } + return i, nil +} + +func (m *RecalculateCaches) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalTo(dAtA) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *RecalculateCaches) MarshalTo(dAtA []byte) (int, error) { + var i int + _ = i + var l int + _ = l + return i, nil +} + func encodeFixed64Private(dAtA []byte, offset int, v uint64) int { dAtA[offset] = uint8(v) dAtA[offset+1] = uint8(v >> 8) @@ -2048,11 +2885,19 @@ func (m *Cache) Size() (n int) { return n } -func (m *MaxSlicesResponse) Size() (n int) { +func (m *MaxSlices) Size() (n int) { var l int _ = l - if len(m.MaxSlices) > 0 { - for k, v := range m.MaxSlices { + if len(m.Standard) > 0 { + for k, v := range m.Standard { + _ = k + _ = v + mapEntrySize := 1 + len(k) + sovPrivate(uint64(len(k))) + 1 + sovPrivate(uint64(v)) + n += mapEntrySize + 1 + sovPrivate(uint64(mapEntrySize)) + } + } + if len(m.Inverse) > 0 { + for k, v := range m.Inverse { _ = k _ = v mapEntrySize := 1 + len(k) + sovPrivate(uint64(len(k))) + 1 + sovPrivate(uint64(v)) @@ -2181,6 +3026,24 @@ func (m *Frame) Size() (n int) { l = m.Meta.Size() n += 1 + l + sovPrivate(uint64(l)) } + if len(m.Views) > 0 { + for _, s := range m.Views { + l = len(s) + n += 1 + l + sovPrivate(uint64(l)) + } + } + return n +} + +func (m *Schema) Size() (n int) { + var l int + _ = l + if len(m.Indexes) > 0 { + for _, e := range m.Indexes { + l = e.Size() + n += 1 + l + sovPrivate(uint64(l)) + } + } return n } @@ -2191,26 +3054,12 @@ func (m *Index) Size() (n int) { if l > 0 { n += 1 + l + sovPrivate(uint64(l)) } - if m.Meta != nil { - l = m.Meta.Size() - n += 1 + l + sovPrivate(uint64(l)) - } - if m.MaxSlice != 0 { - n += 1 + sovPrivate(uint64(m.MaxSlice)) - } if len(m.Frames) > 0 { for _, e := range m.Frames { l = e.Size() n += 1 + l + sovPrivate(uint64(l)) } } - if len(m.Slices) > 0 { - l = 0 - for _, e := range m.Slices { - l += sovPrivate(uint64(e)) - } - n += 1 + sovPrivate(uint64(l)) + l - } if len(m.InputDefinitions) > 0 { for _, e := range m.InputDefinitions { l = e.Size() @@ -2314,25 +3163,80 @@ func (m *DeleteInputDefinitionMessage) Size() (n int) { return n } -func (m *NodeStatus) Size() (n int) { +func (m *URI) Size() (n int) { var l int _ = l + l = len(m.Scheme) + if l > 0 { + n += 1 + l + sovPrivate(uint64(l)) + } l = len(m.Host) if l > 0 { n += 1 + l + sovPrivate(uint64(l)) } + if m.Port != 0 { + n += 1 + sovPrivate(uint64(m.Port)) + } + return n +} + +func (m *Node) Size() (n int) { + var l int + _ = l + l = len(m.ID) + if l > 0 { + n += 1 + l + sovPrivate(uint64(l)) + } + if m.URI != nil { + l = m.URI.Size() + n += 1 + l + sovPrivate(uint64(l)) + } + if m.IsCoordinator { + n += 2 + } + return n +} + +func (m *NodeStateMessage) Size() (n int) { + var l int + _ = l + l = len(m.NodeID) + if l > 0 { + n += 1 + l + sovPrivate(uint64(l)) + } l = len(m.State) if l > 0 { n += 1 + l + sovPrivate(uint64(l)) } - if len(m.Indexes) > 0 { - for _, e := range m.Indexes { - l = e.Size() - n += 1 + l + sovPrivate(uint64(l)) - } + return n +} + +func (m *NodeEventMessage) Size() (n int) { + var l int + _ = l + if m.Event != 0 { + n += 1 + sovPrivate(uint64(m.Event)) } - l = len(m.Scheme) - if l > 0 { + if m.Node != nil { + l = m.Node.Size() + n += 1 + l + sovPrivate(uint64(l)) + } + return n +} + +func (m *NodeStatus) Size() (n int) { + var l int + _ = l + if m.Node != nil { + l = m.Node.Size() + n += 1 + l + sovPrivate(uint64(l)) + } + if m.MaxSlices != nil { + l = m.MaxSlices.Size() + n += 1 + l + sovPrivate(uint64(l)) + } + if m.Schema != nil { + l = m.Schema.Size() n += 1 + l + sovPrivate(uint64(l)) } return n @@ -2341,6 +3245,14 @@ func (m *NodeStatus) Size() (n int) { func (m *ClusterStatus) Size() (n int) { var l int _ = l + l = len(m.ClusterID) + if l > 0 { + n += 1 + l + sovPrivate(uint64(l)) + } + l = len(m.State) + if l > 0 { + n += 1 + l + sovPrivate(uint64(l)) + } if len(m.Nodes) > 0 { for _, e := range m.Nodes { l = e.Size() @@ -2350,18 +3262,6 @@ func (m *ClusterStatus) Size() (n int) { return n } -func (m *FrameSchema) Size() (n int) { - var l int - _ = l - if len(m.Fields) > 0 { - for _, e := range m.Fields { - l = e.Size() - n += 1 + l + sovPrivate(uint64(l)) - } - } - return n -} - func (m *Field) Size() (n int) { var l int _ = l @@ -2382,6 +3282,24 @@ func (m *Field) Size() (n int) { return n } +func (m *CreateViewMessage) Size() (n int) { + var l int + _ = l + l = len(m.Index) + if l > 0 { + n += 1 + l + sovPrivate(uint64(l)) + } + l = len(m.Frame) + if l > 0 { + n += 1 + l + sovPrivate(uint64(l)) + } + l = len(m.View) + if l > 0 { + n += 1 + l + sovPrivate(uint64(l)) + } + return n +} + func (m *DeleteViewMessage) Size() (n int) { var l int _ = l @@ -2400,6 +3318,121 @@ func (m *DeleteViewMessage) Size() (n int) { return n } +func (m *ResizeInstruction) Size() (n int) { + var l int + _ = l + if m.JobID != 0 { + n += 1 + sovPrivate(uint64(m.JobID)) + } + if m.Node != nil { + l = m.Node.Size() + n += 1 + l + sovPrivate(uint64(l)) + } + if m.Coordinator != nil { + l = m.Coordinator.Size() + n += 1 + l + sovPrivate(uint64(l)) + } + if len(m.Sources) > 0 { + for _, e := range m.Sources { + l = e.Size() + n += 1 + l + sovPrivate(uint64(l)) + } + } + if m.Schema != nil { + l = m.Schema.Size() + n += 1 + l + sovPrivate(uint64(l)) + } + if m.ClusterStatus != nil { + l = m.ClusterStatus.Size() + n += 1 + l + sovPrivate(uint64(l)) + } + return n +} + +func (m *ResizeSource) Size() (n int) { + var l int + _ = l + if m.Node != nil { + l = m.Node.Size() + n += 1 + l + sovPrivate(uint64(l)) + } + l = len(m.Index) + if l > 0 { + n += 1 + l + sovPrivate(uint64(l)) + } + l = len(m.Frame) + if l > 0 { + n += 1 + l + sovPrivate(uint64(l)) + } + l = len(m.View) + if l > 0 { + n += 1 + l + sovPrivate(uint64(l)) + } + if m.Slice != 0 { + n += 1 + sovPrivate(uint64(m.Slice)) + } + return n +} + +func (m *ResizeInstructionComplete) Size() (n int) { + var l int + _ = l + if m.JobID != 0 { + n += 1 + sovPrivate(uint64(m.JobID)) + } + if m.Node != nil { + l = m.Node.Size() + n += 1 + l + sovPrivate(uint64(l)) + } + l = len(m.Error) + if l > 0 { + n += 1 + l + sovPrivate(uint64(l)) + } + return n +} + +func (m *SetCoordinatorMessage) Size() (n int) { + var l int + _ = l + if m.New != nil { + l = m.New.Size() + n += 1 + l + sovPrivate(uint64(l)) + } + return n +} + +func (m *UpdateCoordinatorMessage) Size() (n int) { + var l int + _ = l + if m.New != nil { + l = m.New.Size() + n += 1 + l + sovPrivate(uint64(l)) + } + return n +} + +func (m *Topology) Size() (n int) { + var l int + _ = l + l = len(m.ClusterID) + if l > 0 { + n += 1 + l + sovPrivate(uint64(l)) + } + if len(m.NodeIDs) > 0 { + for _, s := range m.NodeIDs { + l = len(s) + n += 1 + l + sovPrivate(uint64(l)) + } + } + return n +} + +func (m *RecalculateCaches) Size() (n int) { + var l int + _ = l + return n +} + func sovPrivate(x uint64) (n int) { for { n++ @@ -3288,7 +4321,7 @@ func (m *Cache) Unmarshal(dAtA []byte) error { } return nil } -func (m *MaxSlicesResponse) Unmarshal(dAtA []byte) error { +func (m *MaxSlices) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { @@ -3311,15 +4344,15 @@ func (m *MaxSlicesResponse) Unmarshal(dAtA []byte) error { fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { - return fmt.Errorf("proto: MaxSlicesResponse: wiretype end group for non-group") + return fmt.Errorf("proto: MaxSlices: wiretype end group for non-group") } if fieldNum <= 0 { - return fmt.Errorf("proto: MaxSlicesResponse: illegal tag %d (wire type %d)", fieldNum, wire) + return fmt.Errorf("proto: MaxSlices: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field MaxSlices", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field Standard", wireType) } var msglen int for shift := uint(0); ; shift += 7 { @@ -3383,8 +4416,8 @@ func (m *MaxSlicesResponse) Unmarshal(dAtA []byte) error { } mapkey := string(dAtA[iNdEx:postStringIndexmapkey]) iNdEx = postStringIndexmapkey - if m.MaxSlices == nil { - m.MaxSlices = make(map[string]uint64) + if m.Standard == nil { + m.Standard = make(map[string]uint64) } if iNdEx < postIndex { var valuekey uint64 @@ -3417,10 +4450,116 @@ func (m *MaxSlicesResponse) Unmarshal(dAtA []byte) error { break } } - m.MaxSlices[mapkey] = mapvalue + m.Standard[mapkey] = mapvalue } else { var mapvalue uint64 - m.MaxSlices[mapkey] = mapvalue + m.Standard[mapkey] = mapvalue + } + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Inverse", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthPrivate + } + postIndex := iNdEx + msglen + if postIndex > l { + return io.ErrUnexpectedEOF + } + var keykey uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + keykey |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + var stringLenmapkey uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLenmapkey |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLenmapkey := int(stringLenmapkey) + if intStringLenmapkey < 0 { + return ErrInvalidLengthPrivate + } + postStringIndexmapkey := iNdEx + intStringLenmapkey + if postStringIndexmapkey > l { + return io.ErrUnexpectedEOF + } + mapkey := string(dAtA[iNdEx:postStringIndexmapkey]) + iNdEx = postStringIndexmapkey + if m.Inverse == nil { + m.Inverse = make(map[string]uint64) + } + if iNdEx < postIndex { + var valuekey uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + valuekey |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + var mapvalue uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + mapvalue |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + m.Inverse[mapkey] = mapvalue + } else { + var mapvalue uint64 + m.Inverse[mapkey] = mapvalue } iNdEx = postIndex default: @@ -4371,6 +5510,116 @@ func (m *Frame) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Views", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthPrivate + } + postIndex := iNdEx + intStringLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Views = append(m.Views, string(dAtA[iNdEx:postIndex])) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipPrivate(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthPrivate + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *Schema) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: Schema: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: Schema: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Indexes", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthPrivate + } + postIndex := iNdEx + msglen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Indexes = append(m.Indexes, &Index{}) + if err := m.Indexes[len(m.Indexes)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipPrivate(dAtA[iNdEx:]) @@ -4450,58 +5699,6 @@ func (m *Index) Unmarshal(dAtA []byte) error { } m.Name = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Meta", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowPrivate - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= (int(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return ErrInvalidLengthPrivate - } - postIndex := iNdEx + msglen - if postIndex > l { - return io.ErrUnexpectedEOF - } - if m.Meta == nil { - m.Meta = &IndexMeta{} - } - if err := m.Meta.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { - return err - } - iNdEx = postIndex - case 3: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field MaxSlice", wireType) - } - m.MaxSlice = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowPrivate - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.MaxSlice |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } case 4: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Frames", wireType) @@ -4533,68 +5730,6 @@ func (m *Index) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex - case 5: - if wireType == 0 { - var v uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowPrivate - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - v |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - m.Slices = append(m.Slices, v) - } else if wireType == 2 { - var packedLen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowPrivate - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - packedLen |= (int(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - if packedLen < 0 { - return ErrInvalidLengthPrivate - } - postIndex := iNdEx + packedLen - if postIndex > l { - return io.ErrUnexpectedEOF - } - for iNdEx < postIndex { - var v uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowPrivate - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - v |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - m.Slices = append(m.Slices, v) - } - } else { - return fmt.Errorf("proto: wrong wireType = %d for field Slices", wireType) - } case 6: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field InputDefinitions", wireType) @@ -5371,6 +6506,475 @@ func (m *DeleteInputDefinitionMessage) Unmarshal(dAtA []byte) error { } return nil } +func (m *URI) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: URI: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: URI: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Scheme", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthPrivate + } + postIndex := iNdEx + intStringLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Scheme = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Host", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthPrivate + } + postIndex := iNdEx + intStringLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Host = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 3: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Port", wireType) + } + m.Port = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Port |= (uint32(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipPrivate(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthPrivate + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *Node) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: Node: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: Node: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ID", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthPrivate + } + postIndex := iNdEx + intStringLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ID = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field URI", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthPrivate + } + postIndex := iNdEx + msglen + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.URI == nil { + m.URI = &URI{} + } + if err := m.URI.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 3: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field IsCoordinator", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + m.IsCoordinator = bool(v != 0) + default: + iNdEx = preIndex + skippy, err := skipPrivate(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthPrivate + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *NodeStateMessage) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: NodeStateMessage: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: NodeStateMessage: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field NodeID", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthPrivate + } + postIndex := iNdEx + intStringLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.NodeID = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field State", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthPrivate + } + postIndex := iNdEx + intStringLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.State = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipPrivate(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthPrivate + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *NodeEventMessage) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: NodeEventMessage: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: NodeEventMessage: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Event", wireType) + } + m.Event = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Event |= (uint32(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Node", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthPrivate + } + postIndex := iNdEx + msglen + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.Node == nil { + m.Node = &Node{} + } + if err := m.Node.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipPrivate(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthPrivate + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func (m *NodeStatus) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 @@ -5402,65 +7006,7 @@ func (m *NodeStatus) Unmarshal(dAtA []byte) error { switch fieldNum { case 1: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Host", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowPrivate - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthPrivate - } - postIndex := iNdEx + intStringLen - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Host = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field State", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowPrivate - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthPrivate - } - postIndex := iNdEx + intStringLen - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.State = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 3: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Indexes", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field Node", wireType) } var msglen int for shift := uint(0); ; shift += 7 { @@ -5484,16 +7030,18 @@ func (m *NodeStatus) Unmarshal(dAtA []byte) error { if postIndex > l { return io.ErrUnexpectedEOF } - m.Indexes = append(m.Indexes, &Index{}) - if err := m.Indexes[len(m.Indexes)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + if m.Node == nil { + m.Node = &Node{} + } + if err := m.Node.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex - case 4: + case 2: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Scheme", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field MaxSlices", wireType) } - var stringLen uint64 + var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowPrivate @@ -5503,20 +7051,57 @@ func (m *NodeStatus) Unmarshal(dAtA []byte) error { } b := dAtA[iNdEx] iNdEx++ - stringLen |= (uint64(b) & 0x7F) << shift + msglen |= (int(b) & 0x7F) << shift if b < 0x80 { break } } - intStringLen := int(stringLen) - if intStringLen < 0 { + if msglen < 0 { return ErrInvalidLengthPrivate } - postIndex := iNdEx + intStringLen + postIndex := iNdEx + msglen if postIndex > l { return io.ErrUnexpectedEOF } - m.Scheme = string(dAtA[iNdEx:postIndex]) + if m.MaxSlices == nil { + m.MaxSlices = &MaxSlices{} + } + if err := m.MaxSlices.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Schema", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthPrivate + } + postIndex := iNdEx + msglen + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.Schema == nil { + m.Schema = &Schema{} + } + if err := m.Schema.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } iNdEx = postIndex default: iNdEx = preIndex @@ -5569,6 +7154,64 @@ func (m *ClusterStatus) Unmarshal(dAtA []byte) error { } switch fieldNum { case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ClusterID", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthPrivate + } + postIndex := iNdEx + intStringLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ClusterID = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field State", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthPrivate + } + postIndex := iNdEx + intStringLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.State = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 3: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field Nodes", wireType) } @@ -5594,7 +7237,7 @@ func (m *ClusterStatus) Unmarshal(dAtA []byte) error { if postIndex > l { return io.ErrUnexpectedEOF } - m.Nodes = append(m.Nodes, &NodeStatus{}) + m.Nodes = append(m.Nodes, &Node{}) if err := m.Nodes[len(m.Nodes)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } @@ -5620,87 +7263,6 @@ func (m *ClusterStatus) Unmarshal(dAtA []byte) error { } return nil } -func (m *FrameSchema) Unmarshal(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowPrivate - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: FrameSchema: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: FrameSchema: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Fields", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowPrivate - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= (int(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return ErrInvalidLengthPrivate - } - postIndex := iNdEx + msglen - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Fields = append(m.Fields, &Field{}) - if err := m.Fields[len(m.Fields)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { - return err - } - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skipPrivate(dAtA[iNdEx:]) - if err != nil { - return err - } - if skippy < 0 { - return ErrInvalidLengthPrivate - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} func (m *Field) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 @@ -5847,6 +7409,143 @@ func (m *Field) Unmarshal(dAtA []byte) error { } return nil } +func (m *CreateViewMessage) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: CreateViewMessage: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: CreateViewMessage: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Index", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthPrivate + } + postIndex := iNdEx + intStringLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Index = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Frame", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthPrivate + } + postIndex := iNdEx + intStringLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Frame = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field View", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthPrivate + } + postIndex := iNdEx + intStringLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.View = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipPrivate(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthPrivate + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func (m *DeleteViewMessage) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 @@ -5984,6 +7683,882 @@ func (m *DeleteViewMessage) Unmarshal(dAtA []byte) error { } return nil } +func (m *ResizeInstruction) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: ResizeInstruction: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: ResizeInstruction: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field JobID", wireType) + } + m.JobID = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.JobID |= (int64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Node", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthPrivate + } + postIndex := iNdEx + msglen + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.Node == nil { + m.Node = &Node{} + } + if err := m.Node.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Coordinator", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthPrivate + } + postIndex := iNdEx + msglen + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.Coordinator == nil { + m.Coordinator = &Node{} + } + if err := m.Coordinator.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Sources", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthPrivate + } + postIndex := iNdEx + msglen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Sources = append(m.Sources, &ResizeSource{}) + if err := m.Sources[len(m.Sources)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 5: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Schema", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthPrivate + } + postIndex := iNdEx + msglen + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.Schema == nil { + m.Schema = &Schema{} + } + if err := m.Schema.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 6: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ClusterStatus", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthPrivate + } + postIndex := iNdEx + msglen + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.ClusterStatus == nil { + m.ClusterStatus = &ClusterStatus{} + } + if err := m.ClusterStatus.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipPrivate(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthPrivate + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *ResizeSource) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: ResizeSource: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: ResizeSource: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Node", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthPrivate + } + postIndex := iNdEx + msglen + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.Node == nil { + m.Node = &Node{} + } + if err := m.Node.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Index", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthPrivate + } + postIndex := iNdEx + intStringLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Index = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Frame", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthPrivate + } + postIndex := iNdEx + intStringLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Frame = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field View", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthPrivate + } + postIndex := iNdEx + intStringLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.View = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 5: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Slice", wireType) + } + m.Slice = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Slice |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipPrivate(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthPrivate + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *ResizeInstructionComplete) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: ResizeInstructionComplete: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: ResizeInstructionComplete: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field JobID", wireType) + } + m.JobID = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.JobID |= (int64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Node", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthPrivate + } + postIndex := iNdEx + msglen + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.Node == nil { + m.Node = &Node{} + } + if err := m.Node.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Error", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthPrivate + } + postIndex := iNdEx + intStringLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Error = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipPrivate(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthPrivate + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *SetCoordinatorMessage) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: SetCoordinatorMessage: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: SetCoordinatorMessage: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field New", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthPrivate + } + postIndex := iNdEx + msglen + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.New == nil { + m.New = &Node{} + } + if err := m.New.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipPrivate(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthPrivate + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *UpdateCoordinatorMessage) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: UpdateCoordinatorMessage: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: UpdateCoordinatorMessage: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field New", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthPrivate + } + postIndex := iNdEx + msglen + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.New == nil { + m.New = &Node{} + } + if err := m.New.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipPrivate(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthPrivate + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *Topology) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: Topology: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: Topology: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ClusterID", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthPrivate + } + postIndex := iNdEx + intStringLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ClusterID = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field NodeIDs", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthPrivate + } + postIndex := iNdEx + intStringLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.NodeIDs = append(m.NodeIDs, string(dAtA[iNdEx:postIndex])) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipPrivate(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthPrivate + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *RecalculateCaches) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPrivate + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: RecalculateCaches: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: RecalculateCaches: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + default: + iNdEx = preIndex + skippy, err := skipPrivate(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthPrivate + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func skipPrivate(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 @@ -6092,67 +8667,89 @@ var ( func init() { proto.RegisterFile("private.proto", fileDescriptorPrivate) } var fileDescriptorPrivate = []byte{ - // 978 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x56, 0xcd, 0x6e, 0x23, 0x45, - 0x10, 0x66, 0xfc, 0xb7, 0x76, 0x99, 0x24, 0x4e, 0x13, 0x56, 0xde, 0x28, 0x32, 0x56, 0x4b, 0xb0, - 0x21, 0x12, 0x39, 0x04, 0x69, 0x05, 0x2c, 0x07, 0xd8, 0x38, 0xab, 0x58, 0xe0, 0x05, 0xda, 0xab, - 0x70, 0x43, 0xea, 0x38, 0x45, 0x32, 0xca, 0x78, 0xc6, 0xcc, 0xb4, 0x93, 0x98, 0x03, 0x47, 0x9e, - 0x01, 0x89, 0x23, 0x2f, 0xc3, 0x91, 0x47, 0x40, 0xe1, 0xc2, 0x1b, 0x20, 0x71, 0x5a, 0x75, 0x75, - 0xf7, 0xcc, 0xf8, 0x37, 0x4a, 0x6e, 0x5d, 0x5f, 0xd7, 0xcf, 0xd7, 0xd5, 0x55, 0xd5, 0x0d, 0x6b, - 0xa3, 0xd8, 0xbf, 0x92, 0x0a, 0xf7, 0x47, 0x71, 0xa4, 0x22, 0x56, 0xf5, 0x43, 0x85, 0x71, 0x28, - 0x03, 0xfe, 0x0d, 0xd4, 0xba, 0xe1, 0x19, 0xde, 0xf4, 0x50, 0x49, 0xd6, 0x86, 0xfa, 0x61, 0x14, - 0x8c, 0x87, 0xe1, 0xd7, 0xf2, 0x14, 0x83, 0xa6, 0xd7, 0xf6, 0x76, 0x6b, 0x22, 0x0f, 0x69, 0x8d, - 0xd7, 0xfe, 0x10, 0xbf, 0x1b, 0xcb, 0x50, 0x8d, 0x87, 0xcd, 0x82, 0xd1, 0xc8, 0x41, 0xfc, 0x7f, - 0x0f, 0x6a, 0x2f, 0x63, 0x39, 0x44, 0xf2, 0xb8, 0x0d, 0x55, 0x11, 0x5d, 0xe7, 0xdd, 0xa5, 0x32, - 0xfb, 0x00, 0xd6, 0xbb, 0xe1, 0x15, 0xc6, 0x09, 0x1e, 0x85, 0xf2, 0x34, 0xc0, 0x33, 0x72, 0x57, - 0x15, 0x33, 0x28, 0xdb, 0x81, 0xda, 0xa1, 0x1c, 0x5c, 0xe0, 0xeb, 0xc9, 0x08, 0x9b, 0x45, 0x72, - 0x92, 0x01, 0xe9, 0x6e, 0xdf, 0xff, 0x19, 0x9b, 0xa5, 0xb6, 0xb7, 0xbb, 0x26, 0x32, 0x60, 0x96, - 0x6f, 0x79, 0x8e, 0x2f, 0xe3, 0xf0, 0xb6, 0x90, 0xe1, 0x79, 0xca, 0xa1, 0x42, 0x1c, 0xa6, 0x30, - 0xf6, 0x14, 0x2a, 0x2f, 0x7d, 0x0c, 0xce, 0x92, 0xe6, 0xa3, 0x76, 0x71, 0xb7, 0x7e, 0xb0, 0xb1, - 0xef, 0xf2, 0xb7, 0x4f, 0xb8, 0xb0, 0xdb, 0x9c, 0xc3, 0x7a, 0x77, 0x38, 0x8a, 0x62, 0x25, 0x30, - 0x19, 0x45, 0x61, 0x82, 0xac, 0x01, 0xc5, 0xa3, 0x38, 0xb6, 0x67, 0xd7, 0x4b, 0xfe, 0x0b, 0x34, - 0x5e, 0x04, 0xd1, 0xe0, 0xb2, 0x23, 0x95, 0x14, 0xf8, 0xd3, 0x18, 0x13, 0xc5, 0xb6, 0xa0, 0x4c, - 0xb7, 0x60, 0xf5, 0x8c, 0xa0, 0x51, 0xca, 0xa4, 0x4d, 0xb3, 0x11, 0x34, 0x4a, 0xf6, 0x94, 0x8a, - 0x92, 0x30, 0x82, 0x46, 0xfb, 0x81, 0x3f, 0x30, 0x29, 0x28, 0x09, 0x23, 0x30, 0x06, 0xa5, 0x13, - 0x1f, 0xaf, 0xed, 0xb9, 0x69, 0xcd, 0xbb, 0xb0, 0x99, 0x8b, 0x6f, 0x69, 0x3e, 0x86, 0x8a, 0x88, - 0xae, 0xbb, 0x9d, 0xa4, 0xe9, 0xb5, 0x8b, 0xbb, 0x25, 0x61, 0x25, 0xca, 0x2e, 0x5d, 0xbf, 0xde, - 0x2a, 0xd0, 0x56, 0x06, 0xf0, 0x27, 0x50, 0xa6, 0x54, 0xeb, 0x53, 0x66, 0xb6, 0x7a, 0xc9, 0x7f, - 0xf7, 0x60, 0xb3, 0x27, 0x6f, 0x88, 0x46, 0x92, 0x86, 0x39, 0x86, 0x5a, 0x0a, 0x92, 0x76, 0xfd, - 0x60, 0x2f, 0xcb, 0xe5, 0x9c, 0x7e, 0x86, 0x1c, 0x85, 0x2a, 0x9e, 0x88, 0xcc, 0x78, 0xfb, 0x73, - 0x58, 0x9f, 0xde, 0xd4, 0x1c, 0x2e, 0x71, 0xe2, 0x32, 0x7d, 0x89, 0x13, 0x9d, 0x93, 0x2b, 0x19, - 0x8c, 0x4d, 0xfe, 0x4a, 0xc2, 0x08, 0x9f, 0x15, 0x3e, 0xf1, 0xf8, 0x0f, 0xc0, 0x0e, 0x63, 0x94, - 0x0a, 0xc9, 0x41, 0x0f, 0x93, 0x44, 0x9e, 0xe3, 0xf2, 0x5b, 0x30, 0x99, 0x2d, 0xe4, 0x33, 0xbb, - 0x03, 0xb5, 0x6e, 0x62, 0x0b, 0x95, 0x6e, 0xa2, 0x2a, 0x32, 0x80, 0xef, 0x01, 0xeb, 0x60, 0x80, - 0x0a, 0x6d, 0x6f, 0xad, 0xf0, 0xcf, 0xfb, 0x8e, 0xcb, 0xdd, 0xba, 0xec, 0x29, 0x94, 0x74, 0x5b, - 0x11, 0x95, 0xfa, 0xc1, 0x3b, 0x59, 0xea, 0xd2, 0x1e, 0x16, 0xa4, 0xc0, 0x7d, 0xe7, 0xd4, 0xb6, - 0xe2, 0x1d, 0x07, 0x5c, 0x50, 0x66, 0x2e, 0x54, 0x71, 0x36, 0x54, 0xda, 0xdc, 0x36, 0xd4, 0x17, - 0xee, 0xac, 0x0f, 0x0d, 0xc5, 0xcf, 0x53, 0xb2, 0xba, 0x8b, 0x1e, 0x42, 0xf6, 0x7d, 0x28, 0x93, - 0xad, 0x65, 0x3b, 0xd7, 0x9f, 0x66, 0x97, 0x9f, 0xa4, 0x54, 0x1f, 0x1a, 0x68, 0x2b, 0x1f, 0xa8, - 0xe6, 0xfc, 0x76, 0xac, 0xae, 0xee, 0xb7, 0x57, 0xda, 0xc6, 0x78, 0xa2, 0xf5, 0xf2, 0x3b, 0x9b, - 0x4d, 0xe4, 0xbf, 0x9e, 0x25, 0x72, 0x3f, 0x37, 0x33, 0x57, 0xaf, 0x47, 0xae, 0xeb, 0x0c, 0x3b, - 0x22, 0x52, 0x99, 0x06, 0x99, 0x8e, 0x9a, 0x34, 0x4b, 0x73, 0x83, 0x4c, 0xe3, 0xc2, 0x6e, 0xeb, - 0x79, 0x60, 0xbb, 0xb4, 0x6c, 0xe6, 0x81, 0x91, 0xd8, 0x11, 0x34, 0xba, 0xe1, 0x68, 0xac, 0x3a, - 0xf8, 0xa3, 0x1f, 0xfa, 0xca, 0x8f, 0xc2, 0xa4, 0x59, 0x21, 0x57, 0x4f, 0xf2, 0x8c, 0xa6, 0x34, - 0xc4, 0x9c, 0x09, 0xff, 0xd5, 0x83, 0x8d, 0x19, 0x70, 0xc9, 0xa1, 0x1d, 0xdf, 0xc2, 0x6a, 0xbe, - 0xcf, 0xd2, 0x09, 0x5d, 0x24, 0xc5, 0xd6, 0x52, 0x36, 0xd3, 0x03, 0xfb, 0x0f, 0x0f, 0xb6, 0x16, - 0x29, 0x2c, 0x64, 0xd3, 0x02, 0xf8, 0x36, 0xf6, 0x87, 0x32, 0x9e, 0x7c, 0x85, 0x13, 0xfb, 0x58, - 0xe5, 0x10, 0xf6, 0x3d, 0x3c, 0x9e, 0xf1, 0xf5, 0xe5, 0xc0, 0xa4, 0xc8, 0x90, 0x7a, 0x6f, 0x29, - 0x29, 0xa3, 0x27, 0x96, 0x98, 0xf3, 0xff, 0x3c, 0x78, 0x77, 0xe1, 0x56, 0x56, 0xa5, 0x5e, 0xbe, - 0x4a, 0xf7, 0xa0, 0x71, 0xa2, 0x67, 0x5d, 0x07, 0x13, 0xe5, 0x87, 0x52, 0x6b, 0xda, 0x32, 0x9e, - 0xc3, 0x59, 0x17, 0xaa, 0x84, 0xf5, 0xe4, 0xc8, 0xd2, 0xfc, 0xe8, 0x0e, 0x9a, 0xfb, 0x4e, 0xdf, - 0x0c, 0xe5, 0xd4, 0x5c, 0x93, 0xa1, 0x67, 0xc3, 0xbd, 0x41, 0x24, 0x6c, 0x3f, 0x87, 0xb5, 0x29, - 0x83, 0x7b, 0x0d, 0xea, 0x08, 0x76, 0xdc, 0x70, 0x9c, 0x62, 0xb2, 0xba, 0x77, 0x3f, 0x05, 0xc8, - 0x54, 0xed, 0x4c, 0x58, 0x51, 0x9f, 0x39, 0x65, 0x7e, 0x0c, 0x3b, 0x6e, 0x72, 0xdf, 0x23, 0xa0, - 0xab, 0x96, 0x42, 0x56, 0x2d, 0x7c, 0x02, 0xf0, 0x2a, 0x3a, 0xc3, 0xbe, 0x92, 0x6a, 0x9c, 0x68, - 0x8d, 0xe3, 0x28, 0x51, 0xae, 0x9e, 0xf4, 0x9a, 0x5e, 0x16, 0x25, 0x55, 0x3a, 0x62, 0x48, 0x60, - 0x1f, 0xc2, 0x23, 0x72, 0x8a, 0xae, 0x6c, 0x36, 0x66, 0x7a, 0x5d, 0xb8, 0x7d, 0xea, 0xd2, 0xc1, - 0x05, 0x0e, 0xcd, 0xab, 0x5f, 0x13, 0x56, 0xe2, 0xcf, 0x61, 0xed, 0x30, 0x18, 0x27, 0x0a, 0x63, - 0x1b, 0x7d, 0x0f, 0xca, 0x9a, 0x8b, 0x7b, 0x73, 0xb7, 0x32, 0x8f, 0x19, 0x45, 0x61, 0x54, 0xf8, - 0x33, 0xa8, 0x53, 0x15, 0x91, 0x2f, 0x99, 0xfb, 0xfb, 0x78, 0xab, 0xff, 0x3e, 0x7d, 0x3b, 0x1a, - 0x17, 0xb6, 0x0e, 0x83, 0x12, 0x7d, 0xdf, 0x6c, 0x82, 0xe8, 0xe7, 0xd6, 0x80, 0x62, 0xcf, 0x37, - 0xd7, 0x53, 0x14, 0x7a, 0x49, 0x88, 0xbc, 0xa1, 0xc3, 0x68, 0x44, 0xea, 0xc7, 0x71, 0xd3, 0x5c, - 0x87, 0xfe, 0xba, 0x3c, 0x64, 0x60, 0xbb, 0x1f, 0x50, 0x31, 0xfb, 0x01, 0xbd, 0x68, 0xfc, 0x79, - 0xdb, 0xf2, 0xfe, 0xba, 0x6d, 0x79, 0x7f, 0xdf, 0xb6, 0xbc, 0xdf, 0xfe, 0x69, 0xbd, 0x75, 0x5a, - 0xa1, 0x6f, 0xf1, 0xc7, 0x6f, 0x02, 0x00, 0x00, 0xff, 0xff, 0x93, 0xbf, 0xbc, 0x1f, 0x27, 0x0b, - 0x00, 0x00, + // 1334 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x57, 0x5d, 0x6f, 0x1b, 0x45, + 0x17, 0x7e, 0xd7, 0x6b, 0x3b, 0xf6, 0x71, 0x9c, 0x38, 0xd3, 0x34, 0xaf, 0x13, 0x45, 0xae, 0x19, + 0x15, 0x1a, 0x2a, 0x11, 0x95, 0x54, 0x42, 0x34, 0x50, 0xa9, 0xc4, 0x76, 0xd5, 0x85, 0x26, 0x94, + 0x71, 0x12, 0x24, 0x24, 0x90, 0x26, 0xf6, 0x90, 0xae, 0xb2, 0xde, 0x35, 0xbb, 0xe3, 0x24, 0xee, + 0x05, 0x97, 0x08, 0x09, 0x71, 0x8f, 0xb8, 0xe5, 0xcf, 0x70, 0xc9, 0x4f, 0x40, 0xe1, 0x47, 0x20, + 0x71, 0x03, 0x9a, 0xaf, 0xdd, 0xf5, 0x57, 0xd2, 0x04, 0xee, 0xf6, 0x3c, 0x73, 0xce, 0x99, 0x67, + 0xce, 0xd7, 0xcc, 0x42, 0xb9, 0x1f, 0xba, 0xa7, 0x94, 0xb3, 0xcd, 0x7e, 0x18, 0xf0, 0x00, 0x15, + 0x5c, 0x9f, 0xb3, 0xd0, 0xa7, 0x1e, 0xfe, 0x14, 0x8a, 0x8e, 0xdf, 0x65, 0xe7, 0xbb, 0x8c, 0x53, + 0x54, 0x87, 0x52, 0x23, 0xf0, 0x06, 0x3d, 0xff, 0x39, 0x3d, 0x62, 0x5e, 0xd5, 0xaa, 0x5b, 0x1b, + 0x45, 0x92, 0x86, 0x84, 0xc6, 0xbe, 0xdb, 0x63, 0x9f, 0x0d, 0xa8, 0xcf, 0x07, 0xbd, 0x6a, 0x46, + 0x69, 0xa4, 0x20, 0xfc, 0x97, 0x05, 0xc5, 0xa7, 0x21, 0xed, 0x31, 0xe9, 0x71, 0x0d, 0x0a, 0x24, + 0x38, 0x4b, 0xbb, 0x8b, 0x65, 0xf4, 0x16, 0x2c, 0x38, 0xfe, 0x29, 0x0b, 0x23, 0xd6, 0xf2, 0xe9, + 0x91, 0xc7, 0xba, 0xd2, 0x5d, 0x81, 0x8c, 0xa1, 0x68, 0x1d, 0x8a, 0x0d, 0xda, 0x79, 0xc9, 0xf6, + 0x87, 0x7d, 0x56, 0xb5, 0xa5, 0x93, 0x04, 0x88, 0x57, 0xdb, 0xee, 0x2b, 0x56, 0xcd, 0xd6, 0xad, + 0x8d, 0x32, 0x49, 0x80, 0x71, 0xbe, 0xb9, 0x09, 0xbe, 0x08, 0xc3, 0x3c, 0xa1, 0xfe, 0x71, 0xcc, + 0x21, 0x2f, 0x39, 0x8c, 0x60, 0xe8, 0x1e, 0xe4, 0x9f, 0xba, 0xcc, 0xeb, 0x46, 0xd5, 0xb9, 0xba, + 0xbd, 0x51, 0xda, 0x5a, 0xdc, 0x34, 0xf1, 0xdb, 0x94, 0x38, 0xd1, 0xcb, 0x18, 0xc3, 0x82, 0xd3, + 0xeb, 0x07, 0x21, 0x27, 0x2c, 0xea, 0x07, 0x7e, 0xc4, 0x50, 0x05, 0xec, 0x56, 0x18, 0xea, 0xb3, + 0x8b, 0x4f, 0xfc, 0x2d, 0x54, 0x76, 0xbc, 0xa0, 0x73, 0xd2, 0xa4, 0x9c, 0x12, 0xf6, 0xcd, 0x80, + 0x45, 0x1c, 0x2d, 0x43, 0x4e, 0x66, 0x41, 0xeb, 0x29, 0x41, 0xa0, 0x32, 0x92, 0x3a, 0xcc, 0x4a, + 0x10, 0xa8, 0xb4, 0x97, 0xa1, 0xc8, 0x12, 0x25, 0x08, 0xb4, 0xed, 0xb9, 0x1d, 0x15, 0x82, 0x2c, + 0x51, 0x02, 0x42, 0x90, 0x3d, 0x74, 0xd9, 0x99, 0x3e, 0xb7, 0xfc, 0xc6, 0x0e, 0x2c, 0xa5, 0xf6, + 0xd7, 0x34, 0x57, 0x20, 0x4f, 0x82, 0x33, 0xa7, 0x19, 0x55, 0xad, 0xba, 0xbd, 0x91, 0x25, 0x5a, + 0x92, 0xd1, 0x95, 0xe9, 0x17, 0x4b, 0x19, 0xb9, 0x94, 0x00, 0x78, 0x15, 0x72, 0x32, 0xd4, 0xe2, + 0x94, 0x89, 0xad, 0xf8, 0xc4, 0x7f, 0x5b, 0x50, 0xdc, 0xa5, 0xe7, 0x92, 0x46, 0x84, 0x1e, 0x43, + 0xa1, 0xcd, 0xa9, 0xdf, 0xa5, 0x61, 0x57, 0x2a, 0x95, 0xb6, 0xde, 0x48, 0x42, 0x18, 0xab, 0x6d, + 0x1a, 0x9d, 0x96, 0xcf, 0xc3, 0x21, 0x89, 0x4d, 0xd0, 0x36, 0xcc, 0xe9, 0x9a, 0x90, 0x1c, 0x4a, + 0x5b, 0xf5, 0x69, 0xd6, 0x71, 0xd9, 0x08, 0x63, 0x63, 0xb0, 0xf6, 0x01, 0x94, 0x47, 0xdc, 0x0a, + 0xae, 0x27, 0x6c, 0x68, 0x32, 0x72, 0xc2, 0x86, 0x22, 0x76, 0xa7, 0xd4, 0x1b, 0xa8, 0x38, 0x67, + 0x89, 0x12, 0xb6, 0x33, 0xef, 0x5b, 0x6b, 0xdb, 0x30, 0x9f, 0xf6, 0x7a, 0x1d, 0x5b, 0xfc, 0x15, + 0xa0, 0x46, 0xc8, 0x28, 0x67, 0x92, 0xde, 0x2e, 0x8b, 0x22, 0x7a, 0xcc, 0x66, 0x67, 0x5a, 0x65, + 0x2f, 0x93, 0xce, 0xde, 0x3a, 0x14, 0x9d, 0xc8, 0x1c, 0xdc, 0x96, 0x75, 0x99, 0x00, 0xf8, 0x3e, + 0xa0, 0x26, 0xf3, 0x18, 0x67, 0xba, 0x7f, 0x2f, 0xf1, 0x8f, 0xdb, 0x86, 0xcb, 0xd5, 0xba, 0xe8, + 0x1e, 0x64, 0x45, 0xeb, 0x4a, 0x2a, 0xa5, 0xad, 0x5b, 0x49, 0xa4, 0xe3, 0x39, 0x41, 0xa4, 0x02, + 0x76, 0x8d, 0x53, 0xdd, 0xee, 0x57, 0x1c, 0x70, 0x4a, 0x29, 0x9b, 0xad, 0xec, 0xf1, 0xad, 0xe2, + 0x01, 0xa2, 0xb7, 0x7a, 0x62, 0xce, 0x7a, 0xd3, 0xad, 0xf0, 0x71, 0x4c, 0x56, 0x74, 0xea, 0x4d, + 0xc8, 0xbe, 0x09, 0x39, 0x69, 0xab, 0xd9, 0x4e, 0xcc, 0x00, 0xb5, 0x8a, 0x0f, 0x63, 0xaa, 0x37, + 0xdd, 0x68, 0x39, 0xbd, 0x51, 0xd1, 0xf8, 0xfd, 0x42, 0xeb, 0x8a, 0x9e, 0xde, 0x13, 0x36, 0xca, + 0x93, 0xfc, 0x9e, 0x9d, 0xb3, 0xb1, 0x40, 0x0a, 0xdf, 0x62, 0x08, 0x44, 0x55, 0xbb, 0x6e, 0x0b, + 0xdf, 0x52, 0xc0, 0x0f, 0x21, 0xdf, 0xee, 0xbc, 0x64, 0x3d, 0x8a, 0xde, 0x16, 0x9d, 0xd6, 0x65, + 0xe7, 0x2c, 0xd2, 0x7d, 0xba, 0x38, 0x96, 0x7f, 0x62, 0xd6, 0xf1, 0x0f, 0x96, 0x3e, 0xd3, 0x0c, + 0x46, 0x79, 0xb9, 0x77, 0x54, 0xcd, 0x4e, 0x8c, 0x4c, 0x81, 0x13, 0xbd, 0x8c, 0x5a, 0x50, 0x71, + 0xfc, 0xfe, 0x80, 0x37, 0xd9, 0xd7, 0xae, 0xef, 0x72, 0x37, 0xf0, 0xa3, 0x6a, 0x5e, 0x9a, 0xac, + 0xa6, 0xb7, 0x1e, 0xd1, 0x20, 0x13, 0x26, 0xf8, 0x3b, 0x0b, 0x16, 0xc7, 0xc0, 0x2b, 0x78, 0x65, + 0x2e, 0xe7, 0xf5, 0x5e, 0x3c, 0xf3, 0x6d, 0xa9, 0x58, 0x9b, 0xc9, 0x66, 0xf4, 0x0a, 0xf8, 0xc5, + 0x82, 0xe5, 0x69, 0x0a, 0x53, 0xd9, 0xd4, 0x00, 0x5e, 0x84, 0x6e, 0x8f, 0x86, 0xc3, 0x4f, 0xd8, + 0x50, 0x5f, 0x7f, 0x29, 0x04, 0x7d, 0x0e, 0x2b, 0x63, 0xbe, 0x3e, 0xea, 0xa8, 0x10, 0x29, 0x52, + 0x77, 0x66, 0x92, 0x52, 0x7a, 0x64, 0x86, 0x39, 0xfe, 0xd3, 0x82, 0xdb, 0x53, 0x97, 0x92, 0x9a, + 0xb4, 0xd2, 0x35, 0x79, 0x1f, 0x2a, 0x87, 0x62, 0xb2, 0x35, 0x59, 0xc4, 0x5d, 0x9f, 0x0a, 0x4d, + 0x5d, 0xb4, 0x13, 0x38, 0x72, 0xa0, 0x20, 0xb1, 0x5d, 0xda, 0xd7, 0x34, 0xdf, 0xb9, 0x82, 0xe6, + 0xa6, 0xd1, 0xd7, 0x83, 0xdf, 0x88, 0x82, 0x8c, 0xbc, 0x88, 0xcc, 0xad, 0x26, 0x05, 0x31, 0xd2, + 0x47, 0x0c, 0xae, 0x35, 0x96, 0x03, 0x58, 0x37, 0xa3, 0x70, 0x84, 0xc9, 0xe5, 0x9d, 0xfa, 0x08, + 0x20, 0x51, 0xd5, 0x13, 0xe0, 0x92, 0xfa, 0x4c, 0x29, 0xe3, 0x67, 0xb0, 0x6e, 0xe6, 0xf4, 0x35, + 0x36, 0x34, 0xd5, 0x92, 0x49, 0xaa, 0x05, 0xb7, 0xc0, 0x3e, 0x20, 0x8e, 0xb8, 0xab, 0x65, 0xb7, + 0x9a, 0x14, 0x69, 0x49, 0x98, 0x3c, 0x0b, 0x22, 0x6e, 0x4c, 0xc4, 0xb7, 0xc0, 0x5e, 0x04, 0x21, + 0x97, 0x8c, 0xcb, 0x44, 0x7e, 0xe3, 0x2f, 0x21, 0xbb, 0x17, 0x74, 0x19, 0x5a, 0x80, 0x8c, 0xd3, + 0xd4, 0x3e, 0x32, 0x4e, 0x13, 0xdd, 0x91, 0xee, 0xf5, 0x0c, 0x29, 0x27, 0x87, 0x3b, 0x20, 0x0e, + 0x91, 0x1b, 0xdf, 0x85, 0xb2, 0x13, 0x35, 0x82, 0x20, 0xec, 0x8a, 0x54, 0x07, 0xa1, 0xbe, 0x93, + 0x46, 0x41, 0xfc, 0x04, 0x2a, 0xc2, 0x7d, 0x9b, 0x53, 0x1e, 0x4f, 0xea, 0x15, 0xc8, 0x0b, 0x2c, + 0xde, 0x4e, 0x4b, 0xf2, 0xde, 0x13, 0x7a, 0x66, 0x00, 0x4a, 0x01, 0x3f, 0x57, 0x1e, 0x5a, 0xa7, + 0xcc, 0xe7, 0xa9, 0x28, 0x49, 0x59, 0x3a, 0x28, 0x13, 0x25, 0x20, 0xac, 0x8e, 0xa2, 0x39, 0x2f, + 0x24, 0x9c, 0x05, 0x4a, 0xe4, 0x1a, 0xfe, 0xd1, 0x02, 0x30, 0x84, 0x06, 0x51, 0x6c, 0x62, 0xcd, + 0x36, 0x41, 0xef, 0xa6, 0xde, 0x2e, 0x93, 0x33, 0x35, 0x5e, 0x22, 0xa9, 0x17, 0xce, 0x86, 0x19, + 0xa1, 0xba, 0x38, 0x2a, 0x89, 0xbe, 0xc2, 0x75, 0x9a, 0xc4, 0xb5, 0x59, 0x6e, 0x78, 0x83, 0x88, + 0xb3, 0x50, 0x33, 0x12, 0x6f, 0x2c, 0x05, 0xc4, 0xf1, 0x49, 0x80, 0xe9, 0x21, 0x42, 0x77, 0x21, + 0x27, 0x98, 0x9a, 0x39, 0x30, 0x7e, 0x0c, 0xb5, 0x88, 0xdb, 0xfa, 0x26, 0x99, 0x3a, 0x7b, 0x10, + 0x64, 0xe5, 0x8b, 0x5a, 0x97, 0x8b, 0x7c, 0x4c, 0x57, 0xc0, 0xde, 0x75, 0x55, 0x7d, 0xdb, 0x44, + 0x7c, 0x4a, 0x84, 0x9e, 0xcb, 0xfe, 0x13, 0x08, 0x15, 0x6f, 0x89, 0x25, 0xd5, 0x40, 0xe2, 0xee, + 0xb8, 0xc9, 0xfd, 0x66, 0x1e, 0xa5, 0x76, 0xea, 0x51, 0xda, 0x86, 0x25, 0xd5, 0x24, 0xff, 0xa5, + 0xd3, 0x9f, 0x33, 0xb0, 0x44, 0x58, 0xe4, 0xbe, 0x62, 0x8e, 0x1f, 0xf1, 0x70, 0x10, 0x0f, 0xb8, + 0x8f, 0x83, 0x23, 0x1d, 0x6a, 0x9b, 0x28, 0xe1, 0x75, 0x2a, 0x09, 0x3d, 0x10, 0xbf, 0x47, 0xa3, + 0xd5, 0x3f, 0xa9, 0x9a, 0x56, 0x41, 0x0f, 0x60, 0xae, 0x1d, 0x0c, 0xc2, 0x4e, 0x7c, 0x0d, 0xae, + 0x24, 0xda, 0x8a, 0x99, 0x5a, 0x26, 0x46, 0x2d, 0x55, 0x47, 0xb9, 0xcb, 0xeb, 0x08, 0x3d, 0x1e, + 0xab, 0x23, 0xf9, 0xe7, 0x52, 0xda, 0xfa, 0x7f, 0x62, 0x30, 0xb2, 0x4c, 0x46, 0xb5, 0xf1, 0xf7, + 0x16, 0xcc, 0xa7, 0x29, 0xbc, 0x56, 0x63, 0xc4, 0x19, 0xc9, 0x4c, 0xcd, 0x88, 0x3d, 0x2d, 0x23, + 0xd9, 0x24, 0x23, 0xc9, 0x3b, 0x37, 0x97, 0x7a, 0xe7, 0xe2, 0x13, 0x58, 0x9d, 0x48, 0x53, 0x23, + 0xe8, 0xf5, 0x45, 0x3d, 0xfc, 0x8b, 0x74, 0x89, 0x91, 0x11, 0x86, 0x3a, 0x51, 0x45, 0xa2, 0x04, + 0xfc, 0x08, 0x6e, 0xb7, 0x19, 0x4f, 0x25, 0xc9, 0x54, 0x5b, 0x1d, 0xec, 0x3d, 0x76, 0x36, 0xe3, + 0xf8, 0x62, 0x09, 0x7f, 0x08, 0xd5, 0x83, 0x7e, 0x97, 0x72, 0x76, 0x23, 0xeb, 0x1d, 0x28, 0xec, + 0x07, 0xfd, 0xc0, 0x0b, 0x8e, 0x87, 0x57, 0xb4, 0x7c, 0x15, 0xe6, 0xd4, 0x7c, 0x54, 0x8f, 0x94, + 0x22, 0x31, 0x22, 0xbe, 0x25, 0x0a, 0xba, 0x43, 0xbd, 0xce, 0xc0, 0x13, 0x34, 0xc4, 0xbf, 0x57, + 0xb4, 0x53, 0xf9, 0xf5, 0xa2, 0x66, 0xfd, 0x76, 0x51, 0xb3, 0x7e, 0xbf, 0xa8, 0x59, 0x3f, 0xfd, + 0x51, 0xfb, 0xdf, 0x51, 0x5e, 0xfe, 0xe5, 0x3f, 0xfc, 0x27, 0x00, 0x00, 0xff, 0xff, 0x66, 0x19, + 0x3d, 0xd2, 0xf6, 0x0f, 0x00, 0x00, } diff --git a/internal/private.proto b/internal/private.proto index ed664115a..8642d9a13 100644 --- a/internal/private.proto +++ b/internal/private.proto @@ -38,8 +38,9 @@ message Cache { repeated uint64 IDs = 1; } -message MaxSlicesResponse { - map MaxSlices = 1; +message MaxSlices { + map Standard = 1; + map Inverse = 2; } message CreateSliceMessage { @@ -83,14 +84,16 @@ message DeleteFieldMessage { message Frame { string Name = 1; FrameMeta Meta = 2; + repeated string Views = 3; +} + +message Schema { + repeated Index Indexes = 1; } message Index { string Name = 1; - IndexMeta Meta = 2; - uint64 MaxSlice = 3; repeated Frame Frames = 4; - repeated uint64 Slices = 5; repeated InputDefinition InputDefinitions = 6; } @@ -124,19 +127,38 @@ message DeleteInputDefinitionMessage { string Name = 2; } -message NodeStatus { - string Host = 1; +message URI { + string Scheme = 1; + string Host = 2; + uint32 Port = 3; +} + +message Node { + string ID = 1; + URI URI = 2; + bool IsCoordinator = 3; +} + +message NodeStateMessage { + string NodeID = 1; string State = 2; - repeated Index Indexes = 3; - string Scheme = 4; +} + +message NodeEventMessage { + uint32 Event = 1; + Node Node = 2; +} + +message NodeStatus { + Node Node = 1; + MaxSlices MaxSlices = 2; + Schema Schema = 3; } message ClusterStatus { - repeated NodeStatus Nodes = 1; -} - -message FrameSchema { - repeated Field Fields = 1; + string ClusterID = 1; + string State = 2; + repeated Node Nodes = 3; } message Field { @@ -146,8 +168,52 @@ message Field { int64 Max = 4; } +message CreateViewMessage { + string Index = 1; + string Frame = 2; + string View = 3; +} + message DeleteViewMessage { string Index = 1; string Frame = 2; string View = 3; } + +message ResizeInstruction { + int64 JobID = 1; + Node Node = 2; + Node Coordinator = 3; + repeated ResizeSource Sources = 4; + Schema Schema = 5; + ClusterStatus ClusterStatus = 6; +} + +message ResizeSource { + Node Node = 1; + string Index = 2; + string Frame = 3; + string View = 4; + uint64 Slice = 5; +} + +message ResizeInstructionComplete { + int64 JobID = 1; + Node Node = 2; + string Error = 3; +} + +message SetCoordinatorMessage { + Node New = 1; +} + +message UpdateCoordinatorMessage { + Node New = 1; +} + +message Topology { + string ClusterID = 1; + repeated string NodeIDs = 2; +} + +message RecalculateCaches {} diff --git a/pilosa.go b/pilosa.go index aa1378a36..98c67203f 100644 --- a/pilosa.go +++ b/pilosa.go @@ -73,8 +73,7 @@ var ( ErrQueryRequired = errors.New("query required") ErrTooManyWrites = errors.New("too many write commands") - ErrConfigClusterTypeInvalid = errors.New("invalid cluster type") - ErrConfigHostsMissing = errors.New("missing bind address in cluster hosts") + ErrConfigClusterEnabledHosts = errors.New("providing hosts to a non-disabled cluster is not allowed") ) // Regular expression to validate index and frame names. @@ -162,6 +161,50 @@ func StringInSlice(a string, list []string) bool { return false } +// StringSlicesAreEqual determines if two string slices are equal. +func StringSlicesAreEqual(a, b []string) bool { + + if a == nil && b == nil { + return true + } + + if a == nil || b == nil { + return false + } + + if len(a) != len(b) { + return false + } + + for i := range a { + if a[i] != b[i] { + return false + } + } + + return true +} + +// SliceDiff returns the difference between two uint64 slices. +func SliceDiff(a, b []uint64) []uint64 { + m := make(map[uint64]uint64) + + for _, y := range b { + m[y]++ + } + + var ret []uint64 + for _, x := range a { + if m[x] > 0 { + m[x]-- + continue + } + ret = append(ret, x) + } + + return ret +} + // ContainsSubstring checks to see if substring a is contained in any string in the slice. func ContainsSubstring(a string, list []string) bool { for _, b := range list { diff --git a/security_manager.go b/security_manager.go new file mode 100644 index 000000000..80b696c38 --- /dev/null +++ b/security_manager.go @@ -0,0 +1,32 @@ +// Copyright 2017 Pilosa Corp. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pilosa + +// SecurityManager provides the ability to limit access to restricted endpoints +// during cluster configuration. +type SecurityManager interface { + SetRestricted() + SetNormal() +} + +// NopSecurityManager provides a no-op implementation of the SecurityManager interface. +type NopSecurityManager struct { +} + +// SetRestricted no-op. +func (sdm *NopSecurityManager) SetRestricted() {} + +// SetNormal no-op. +func (sdm *NopSecurityManager) SetNormal() {} diff --git a/server.go b/server.go index 95dff018d..7f6eeb6cb 100644 --- a/server.go +++ b/server.go @@ -23,7 +23,6 @@ import ( "log" "net" "net/http" - "net/url" "os" "os/exec" "runtime" @@ -32,17 +31,15 @@ import ( "sync" "time" - "github.com/CAFxX/gcnotifier" "github.com/gogo/protobuf/proto" - "github.com/pilosa/pilosa/diagnostics" "github.com/pilosa/pilosa/internal" + "golang.org/x/sync/errgroup" ) // Default server settings. const ( DefaultAntiEntropyInterval = 10 * time.Minute - DefaultPollingInterval = 60 * time.Second DefaultDiagnosticServer = "https://diagnostics.pilosa.com/v0/diagnostics" ) @@ -68,16 +65,19 @@ type Server struct { RemoteClient *http.Client // Cluster configuration. - // Host is replaced with actual host after opening if port is ":0". Network string - URI *URI + NodeID string + URI URI Cluster *Cluster - diagnostics *diagnostics.Diagnostics - ClusterID string + diagnostics *DiagnosticsCollector + SystemInfo SystemInfo + + GCNotifier GCNotifier + + NewAttrStore func(string) AttrStore // Background monitoring intervals. AntiEntropyInterval time.Duration - PollingInterval time.Duration MetricInterval time.Duration DiagnosticInterval time.Duration @@ -102,12 +102,16 @@ func NewServer() *Server { Handler: NewHandler(), Broadcaster: NopBroadcaster, BroadcastReceiver: NopBroadcastReceiver, - diagnostics: diagnostics.New(DefaultDiagnosticServer), + diagnostics: NewDiagnosticsCollector(DefaultDiagnosticServer), + SystemInfo: NewNopSystemInfo(), Network: "tcp", + GCNotifier: NopGCNotifier, + + NewAttrStore: NewNopAttrStore, + AntiEntropyInterval: DefaultAntiEntropyInterval, - PollingInterval: DefaultPollingInterval, MetricInterval: 0, DiagnosticInterval: 0, @@ -116,11 +120,117 @@ func NewServer() *Server { s.logger = log.New(s.LogOutput, "", log.LstdFlags) s.Handler.Holder = s.Holder + s.diagnostics.server = s return s } // Open opens and initializes the server. func (s *Server) Open() error { + s.Logger().Printf("open server") + // s.ln can be configured prior to Open() via s.OpenListener(). + if s.ln == nil { + if err := s.OpenListener(); err != nil { + return err + } + } + + // Get or create NodeID. + s.NodeID = s.LoadNodeID() + + // Set Cluster Node. + node := &Node{ + ID: s.NodeID, + URI: s.URI, + IsCoordinator: s.Cluster.Coordinator == s.NodeID, + } + s.Cluster.Node = node + + // Append the NodeID tag to stats. + s.Holder.Stats = s.Holder.Stats.WithTags(fmt.Sprintf("NodeID:%s", s.NodeID)) + + // Peek at the holder to determine if there is data on disk. + // Don't actually load the data until after the Cluster + // management starts. + s.Holder.LogOutput = s.LogOutput + s.Holder.Peek() + + // Create default HTTP client + s.createDefaultClient(s.RemoteClient) + + // Create executor for executing queries. + e := NewExecutor(s.RemoteClient) + e.Holder = s.Holder + e.Node = node + e.Cluster = s.Cluster + e.MaxWritesPerRequest = s.MaxWritesPerRequest + + // Cluster settings. + s.Cluster.Broadcaster = s.Broadcaster + s.Cluster.MaxWritesPerRequest = s.MaxWritesPerRequest + + // Initialize HTTP handler. + s.Handler.Broadcaster = s.Broadcaster + s.Handler.BroadcastHandler = s + s.Handler.StatusHandler = s + s.Handler.Node = node + s.Handler.Cluster = s.Cluster + s.Handler.Executor = e + s.Handler.LogOutput = s.LogOutput + + s.Cluster.prefect = s.Handler + + // Initialize Holder. + s.Holder.Broadcaster = s.Broadcaster + + // Serve HTTP. + go func() { + err := http.Serve(s.ln, s.Handler) + if err != nil { + s.Logger().Printf("HTTP handler terminated with error: %s\n", err) + } + }() + + // 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) + } + + // Open holder. + if err := s.Holder.Open(); err != nil { + return fmt.Errorf("opening Holder: %v", err) + } + if err := s.Cluster.SetNodeState(NodeStateReady); err != nil { + return fmt.Errorf("setting nodeState: %v", err) + } + + // Listen for joining nodes. + // This needs to start after the Holder has opened so that nodes can join + // the cluster without waiting for data to load on the coordinator. Before + // this starts, the joins are queued up in the Cluster.joiningLeavingNodes + // buffered channel. + s.Cluster.ListenForJoins() + + // Start background monitoring. + s.wg.Add(3) + go func() { defer s.wg.Done(); s.monitorAntiEntropy() }() + go func() { defer s.wg.Done(); s.monitorRuntime() }() + go func() { defer s.wg.Done(); s.monitorDiagnostics() }() + + return nil +} + +// OpenListener opens a listener for the Server. +func (s *Server) OpenListener() error { + s.Logger().Printf("open server listener: %s", s.URI) + if s.ln != nil { + return fmt.Errorf("a listener already exists for server: %s", s.URI) + } + var ln net.Listener var err error @@ -148,87 +258,6 @@ func (s *Server) Open() error { s.URI.SetPort(uint16(s.ln.Addr().(*net.TCPAddr).Port)) } - // Create local node if no cluster is specified. - if len(s.Cluster.Nodes) == 0 { - s.Cluster.Nodes = []*Node{ - {Scheme: s.URI.Scheme(), Host: s.URI.HostPort()}, - } - } - - for i, n := range s.Cluster.Nodes { - if s.Cluster.NodeByHost(n.Host) != nil { - s.Holder.Stats = s.Holder.Stats.WithTags(fmt.Sprintf("NodeID:%d", i)) - } - } - - // Open holder. - s.Holder.LogOutput = s.LogOutput - if err := s.Holder.Open(); err != nil { - return fmt.Errorf("opening Holder: %v", err) - } - - if err := s.BroadcastReceiver.Start(s); err != nil { - return fmt.Errorf("starting BroadcastReceiver: %v", err) - } - - // Open NodeSet communication - if err := s.Cluster.NodeSet.Open(); err != nil { - return fmt.Errorf("opening NodeSet: %v", err) - } - - // Create default HTTP client - s.createDefaultClient(s.RemoteClient) - - // Create executor for executing queries. - e := NewExecutor(s.RemoteClient) - e.Holder = s.Holder - e.Scheme = s.URI.Scheme() - e.Host = s.URI.HostPort() - e.Cluster = s.Cluster - e.MaxWritesPerRequest = s.MaxWritesPerRequest - s.Cluster.MaxWritesPerRequest = s.MaxWritesPerRequest - - // Initialize HTTP handler. - s.Handler.Broadcaster = s.Broadcaster - s.Handler.BroadcastHandler = s - s.Handler.StatusHandler = s - s.Handler.URI = s.URI - s.Handler.Cluster = s.Cluster - s.Handler.Executor = e - s.Handler.LogOutput = s.LogOutput - - // Initialize Holder. - s.Holder.Broadcaster = s.Broadcaster - - // Serve HTTP. - go func() { - server := &http.Server{Handler: s.Handler} - go func() { - <-s.closing - server.Close() - }() - err := server.Serve(ln) - if err != nil && err.Error() != "http: Server closed" { - s.Logger().Printf("HTTP handler terminated with error: %s\n", err) - } - }() - - // load local ID - if err := s.Holder.loadLocalID(); err != nil { - s.Logger().Println(err) - } - - if err := s.loadClusterID(); err != nil { - s.Logger().Println(err) - } - - // Start background monitoring. - s.wg.Add(4) - go func() { defer s.wg.Done(); s.monitorAntiEntropy() }() - go func() { defer s.wg.Done(); s.monitorMaxSlices() }() - go func() { defer s.wg.Done(); s.monitorRuntime() }() - go func() { defer s.wg.Done(); s.monitorDiagnostics() }() - return nil } @@ -241,6 +270,9 @@ func (s *Server) Close() error { if s.ln != nil { s.ln.Close() } + if s.Cluster != nil { + s.Cluster.Close() + } if s.Holder != nil { s.Holder.Close() } @@ -248,6 +280,20 @@ func (s *Server) Close() error { return nil } +// LoadNodeID gets NodeID from disk, or creates a new value. +// If server.NodeID is already set, a new ID is not created. +func (s *Server) LoadNodeID() string { + if s.NodeID != "" { + return s.NodeID + } + nodeID, err := s.Holder.loadNodeID() + if err != nil { + s.Logger().Printf("loading NodeID: %v", err) + return s.NodeID + } + return nodeID +} + // Addr returns the address of the listener. func (s *Server) Addr() net.Addr { if s.ln == nil { @@ -298,7 +344,7 @@ func (s *Server) monitorAntiEntropy() { // Initialize syncer with local holder and remote client. var syncer HolderSyncer syncer.Holder = s.Holder - syncer.URI = s.URI + syncer.Node = s.Cluster.Node syncer.Cluster = s.Cluster syncer.Closing = s.closing syncer.RemoteClient = s.RemoteClient @@ -317,44 +363,6 @@ func (s *Server) monitorAntiEntropy() { } } -// monitorMaxSlices periodically pulls the highest slice from each node in the cluster. -func (s *Server) monitorMaxSlices() { - // Ignore if only one node in the cluster. - if len(s.Cluster.Nodes) <= 1 { - return - } - - ticker := time.NewTicker(s.PollingInterval) - defer ticker.Stop() - - for { - select { - case <-s.closing: - return - case <-ticker.C: - } - - oldmaxslices := s.Holder.MaxSlices() - for _, node := range s.Cluster.Nodes { - if s.URI.HostPort() != node.Host { - maxSlices, _ := s.checkMaxSlices(node.Scheme, node.Host) - for index, newmax := range maxSlices { - // if we don't know about an index locally, log an error because - // indexes should be created and synced prior to slice creation - if localIndex := s.Holder.Index(index); localIndex != nil { - if newmax > oldmaxslices[index] { - oldmaxslices[index] = newmax - localIndex.SetRemoteMaxSlice(newmax) - } - } else { - s.Logger().Printf("Local Index not found: %s", index) - } - } - } - } - } -} - // ReceiveMessage represents an implementation of BroadcastHandler. func (s *Server) ReceiveMessage(pb proto.Message) error { switch obj := pb.(type) { @@ -386,16 +394,8 @@ func (s *Server) ReceiveMessage(pb proto.Message) error { if idx == nil { return fmt.Errorf("Local Index not found: %s", obj.Index) } - opt := FrameOptions{ - RowLabel: obj.Meta.RowLabel, - InverseEnabled: obj.Meta.InverseEnabled, - RangeEnabled: obj.Meta.RangeEnabled, - CacheType: obj.Meta.CacheType, - CacheSize: obj.Meta.CacheSize, - TimeQuantum: TimeQuantum(obj.Meta.TimeQuantum), - Fields: decodeFields(obj.Meta.Fields), - } - _, err := idx.CreateFrame(obj.Frame, opt) + opt := decodeFrameOptions(obj.Meta) + _, err := idx.CreateFrame(obj.Frame, *opt) if err != nil { return err } @@ -427,6 +427,15 @@ func (s *Server) ReceiveMessage(pb proto.Message) error { if err != nil { return err } + case *internal.CreateViewMessage: + f := s.Holder.Frame(obj.Index, obj.Frame) + if f == nil { + return fmt.Errorf("Local Frame not found: %s", obj.Frame) + } + _, _, err := f.createViewIfNotExistsBase(obj.View) + if err != nil { + return err + } case *internal.DeleteViewMessage: f := s.Holder.Frame(obj.Index, obj.Frame) if f == nil { @@ -436,7 +445,36 @@ func (s *Server) ReceiveMessage(pb proto.Message) error { if err != nil { return err } + case *internal.ClusterStatus: + err := s.Cluster.MergeClusterStatus(obj) + if err != nil { + return err + } + case *internal.ResizeInstruction: + err := s.Cluster.FollowResizeInstruction(obj) + if err != nil { + return err + } + case *internal.ResizeInstructionComplete: + err := s.Cluster.MarkResizeInstructionComplete(obj) + if err != nil { + return err + } + case *internal.SetCoordinatorMessage: + s.Cluster.SetCoordinator(DecodeNode(obj.New)) + case *internal.UpdateCoordinatorMessage: + s.Cluster.UpdateCoordinator(DecodeNode(obj.New)) + case *internal.NodeStateMessage: + err := s.Cluster.ReceiveNodeState(obj.NodeID, obj.State) + if err != nil { + return err + } + case *internal.RecalculateCaches: + s.Holder.RecalculateCaches() + case *internal.NodeEventMessage: + s.Cluster.ReceiveEvent(DecodeNodeEvent(obj)) } + return nil } @@ -444,17 +482,13 @@ func (s *Server) ReceiveMessage(pb proto.Message) error { func (s *Server) SendSync(pb proto.Message) error { var eg errgroup.Group for _, node := range s.Cluster.Nodes { - uri, err := node.URI() - if err != nil { - return err - } - + s.Logger().Printf("SendSync to: %s", node.URI) // Don't forward the message to ourselves. - if *s.URI == *uri { + if s.URI == node.URI { continue } - ctx := context.WithValue(context.Background(), "uri", uri) + ctx := context.WithValue(context.Background(), "uri", &node.URI) eg.Go(func() error { return s.defaultClient.SendMessage(ctx, pb) }) @@ -468,150 +502,145 @@ func (s *Server) SendAsync(pb proto.Message) error { return s.Gossiper.SendAsync(pb) } -// LocalStatus returns the state of the local node as well as the -// holder (indexes/frames) according to the local node. -// In a gossip implementation, memberlist.Delegate.LocalState() uses this. +// SendTo represents an implementation of Broadcaster. +func (s *Server) SendTo(to *Node, pb proto.Message) error { + s.Logger().Printf("SendTo: %s", to.URI) + ctx := context.WithValue(context.Background(), "uri", &to.URI) + return s.defaultClient.SendMessage(ctx, pb) +} + // Server implements StatusHandler. +// LocalStatus is used to periodically sync information +// between nodes. Under normal conditions, nodes should +// remain in sync through Broadcast messages. For cases +// where a node fails to receive a Broadcast message, or +// when a new (empty) node needs to get in sync with the +// rest of the cluster, two things are shared via gossip: +// - MaxSlice/MaxInverseSlice by Index +// - Schema +// In a gossip implementation, memberlist.Delegate.LocalState() uses this. func (s *Server) LocalStatus() (proto.Message, error) { + if s.Cluster == nil { + return nil, errors.New("Server.Cluster is nil") + } if s.Holder == nil { return nil, errors.New("Server.Holder is nil") } ns := internal.NodeStatus{ - Scheme: s.URI.Scheme(), - Host: s.URI.HostPort(), - State: NodeStateUp, - Indexes: EncodeIndexes(s.Holder.Indexes()), - } - - // Append Slice list per this Node's indexes - for _, index := range ns.Indexes { - index.Slices = s.Cluster.OwnsSlices(index.Name, index.MaxSlice, s.URI.HostPort()) + Node: EncodeNode(s.Cluster.Node), + MaxSlices: s.Holder.EncodeMaxSlices(), + Schema: s.Holder.EncodeSchema(), } return &ns, nil } -// ClusterStatus returns the NodeState for all nodes in the cluster. +// ClusterStatus returns the ClusterState and NodeSet for the cluster. func (s *Server) ClusterStatus() (proto.Message, error) { - // Update local Node.state. - ns, err := s.LocalStatus() - if err != nil { - return nil, err - } - node := s.Cluster.NodeByHost(s.URI.HostPort()) - node.SetStatus(ns.(*internal.NodeStatus)) - - // Update NodeState for all nodes. - for host, nodeState := range s.Cluster.NodeStates() { - // In a default configuration (or single-node) where a StaticNodeSet is used - // then all nodes are marked as DOWN. At the very least, we should consider - // the local node as UP. - // TODO: we should be able to remove this check if/when cluster.Nodes and - // cluster.NodeSet are unified. - if host == s.URI.HostPort() { - nodeState = NodeStateUp - } - node := s.Cluster.NodeByHost(host) - node.SetState(nodeState) - } - return s.Cluster.Status(), nil } -// HandleRemoteStatus receives incoming NodeState from remote nodes. +// HandleRemoteStatus receives incoming NodeStatus from remote nodes. func (s *Server) HandleRemoteStatus(pb proto.Message) error { - return s.mergeRemoteStatus(pb.(*internal.NodeStatus)) + // Ignore NodeStatus messages until the cluster is in a Normal state. + if s.Cluster.State() != ClusterStateNormal { + return nil + } + + go func() { + // Make sure the holder has opened. + <-s.Holder.opened + + err := s.mergeRemoteStatus(pb.(*internal.NodeStatus)) + if err != nil { + s.Logger().Printf("merge remote status: %s", err) + } + }() + + return nil } func (s *Server) mergeRemoteStatus(ns *internal.NodeStatus) error { - // Update Node.state. - node := s.Cluster.NodeByHost(ns.Host) - node.SetStatus(ns) + // Ignore status updates from self. + if s.NodeID == DecodeNode(ns.Node).ID { + return nil + } - // Create indexes that don't exist. - for _, index := range ns.Indexes { - opt := IndexOptions{ - ColumnLabel: index.Meta.ColumnLabel, - TimeQuantum: TimeQuantum(index.Meta.TimeQuantum), + // Sync schema. + if err := s.Holder.ApplySchema(ns.Schema); err != nil { + return err + } + + // Sync maxSlices (standard). + oldmaxslices := s.Holder.MaxSlices() + for index, newMax := range ns.MaxSlices.Standard { + localIndex := s.Holder.Index(index) + // if we don't know about an index locally, log an error because + // indexes should be created and synced prior to slice creation + if localIndex == nil { + s.Logger().Printf("Local Index not found: %s", index) + continue } - idx, err := s.Holder.CreateIndexIfNotExists(index.Name, opt) - if err != nil { - return err + if newMax > oldmaxslices[index] { + oldmaxslices[index] = newMax + localIndex.SetRemoteMaxSlice(newMax) } - // Create frames that don't exist. - for _, f := range index.Frames { - opt := FrameOptions{ - RowLabel: f.Meta.RowLabel, - TimeQuantum: TimeQuantum(f.Meta.TimeQuantum), - CacheSize: f.Meta.CacheSize, - } - _, err := idx.CreateFrameIfNotExists(f.Name, opt) - if err != nil { - return err - } + } + + // Sync maxSlices (inverse). + oldMaxInverseSlices := s.Holder.MaxInverseSlices() + for index, newMaxInverse := range ns.MaxSlices.Inverse { + localIndex := s.Holder.Index(index) + // if we don't know about an index locally, log an error because + // indexes should be created and synced prior to slice creation + if localIndex == nil { + s.Logger().Printf("Local Index not found: %s", index) + continue + } + if newMaxInverse > oldMaxInverseSlices[index] { + oldMaxInverseSlices[index] = newMaxInverse + localIndex.SetRemoteMaxInverseSlice(newMaxInverse) } } return nil } -func (s *Server) checkMaxSlices(scheme string, hostPort string) (map[string]uint64, error) { - // Create HTTP request. - req, err := http.NewRequest("GET", (&url.URL{ - Scheme: scheme, - Host: hostPort, - Path: "/slices/max", - }).String(), nil) - - if err != nil { - return nil, err - } - - // Require protobuf encoding. - req.Header.Set("Accept", "application/x-protobuf") - req.Header.Set("Content-Type", "application/x-protobuf") - req.Header.Set("User-Agent", "pilosa/"+Version) - - nodeURI, err := NewURIFromAddress(hostPort) - if err != nil { - return nil, err - } - nodeURI.SetScheme(scheme) - ctx := context.WithValue(context.Background(), "uri", nodeURI) - return s.defaultClient.MaxSliceByIndex(ctx) -} - // monitorDiagnostics periodically polls the Pilosa Indexes for cluster info. func (s *Server) monitorDiagnostics() { - if s.DiagnosticInterval <= 0 { + // Do not send more than once a minute + if s.DiagnosticInterval < time.Minute { s.Logger().Printf("diagnostics disabled") return + } else { + s.Logger().Printf("Pilosa is currently configured to send small diagnostics reports to our team every %v. More information here: https://www.pilosa.com/docs/latest/administration/#diagnostics", s.DiagnosticInterval) } s.diagnostics.SetLogger(s.LogOutput) s.diagnostics.SetVersion(Version) - s.diagnostics.SetInterval(s.DiagnosticInterval) - s.diagnostics.Open() s.diagnostics.Set("Host", s.URI.host) - s.diagnostics.Set("Cluster", strings.Join(s.Cluster.NodeSetHosts(), ",")) + s.diagnostics.Set("Cluster", strings.Join(s.Cluster.NodeIDs(), ",")) s.diagnostics.Set("NumNodes", len(s.Cluster.Nodes)) s.diagnostics.Set("NumCPU", runtime.NumCPU()) - s.diagnostics.Set("LocalID", s.Holder.LocalID) - s.diagnostics.Set("ClusterID", s.ClusterID) + s.diagnostics.Set("NodeID", s.NodeID) + s.diagnostics.Set("ClusterID", s.Cluster.ID) s.diagnostics.EnrichWithOSInfo() // Flush the diagnostics metrics at startup, then on each tick interval flush := func() { - enrichDiagnosticsWithSchemaProperties(s.diagnostics, s.Holder) openFiles, err := CountOpenFiles() if err == nil { s.diagnostics.Set("OpenFiles", openFiles) } s.diagnostics.Set("GoRoutines", runtime.NumGoroutine()) s.diagnostics.EnrichWithMemoryInfo() + s.diagnostics.EnrichWithSchemaProperties() s.diagnostics.CheckVersion() - s.diagnostics.Flush() + err = s.diagnostics.Flush() + if err != nil { + s.Logger().Printf("Diagnostics error: %s", err) + } } ticker := time.NewTicker(s.DiagnosticInterval) @@ -639,8 +668,7 @@ func (s *Server) monitorRuntime() { ticker := time.NewTicker(s.MetricInterval) defer ticker.Stop() - gcn := gcnotifier.New() - defer gcn.Close() + defer s.GCNotifier.Close() s.Logger().Printf("runtime stats initializing (%s interval)", s.MetricInterval) @@ -649,7 +677,7 @@ func (s *Server) monitorRuntime() { select { case <-s.closing: return - case <-gcn.AfterGC(): + case <-s.GCNotifier.AfterGC(): // GC just ran. s.Holder.Stats.Count("garbage_collection", 1, 1.0) case <-ticker.C: @@ -678,25 +706,6 @@ func (s *Server) createDefaultClient(remoteClient *http.Client) { s.defaultClient = NewInternalHTTPClientFromURI(nil, remoteClient) } -func (s *Server) loadClusterID() error { - // If this is the first node in the cluster, set the ClusterID to its ID - node0URI, err := s.Cluster.Nodes[0].URI() - if err == nil { - if s.URI.Equals(node0URI) { - s.ClusterID = s.Holder.LocalID - return nil - } - } else { - return err - } - if clusterID, err := s.defaultClient.NodeID(node0URI); err == nil { - s.ClusterID = clusterID - return nil - } else { - return err - } -} - // CountOpenFiles on operating systems that support lsof. func CountOpenFiles() (int, error) { switch runtime.GOOS { @@ -718,47 +727,11 @@ func CountOpenFiles() (int, error) { } } -// StatusHandler specifies two methods which an object must implement to share -// state in the cluster. These are used by the GossipNodeSet to implement the +// StatusHandler specifies the methods which an object must implement to share +// state in the cluster. These are used by the GossipMemberSet to implement the // LocalState and MergeRemoteState methods of memberlist.Delegate type StatusHandler interface { LocalStatus() (proto.Message, error) ClusterStatus() (proto.Message, error) HandleRemoteStatus(proto.Message) error } - -type diagnosticsFrameProperties struct { - BSIFieldCount int - TimeQuantumEnabled bool -} - -func enrichDiagnosticsWithSchemaProperties(d *diagnostics.Diagnostics, holder *Holder) { - // NOTE: this function is not in the diagnostics package, since circular imports are not allowed. - var numSlices uint64 - numFrames := 0 - numIndexes := 0 - bsiFieldCount := 0 - timeQuantumEnabled := false - - for _, index := range holder.Indexes() { - numSlices += index.MaxSlice() + 1 - numIndexes += 1 - for _, frame := range index.Frames() { - numFrames += 1 - if frame.rangeEnabled { - if fields, err := frame.GetFields(); err == nil { - bsiFieldCount += len(fields.Fields) - } - } - if frame.TimeQuantum() != "" { - timeQuantumEnabled = true - } - } - } - - d.Set("NumIndexes", numIndexes) - d.Set("NumFrames", numFrames) - d.Set("NumSlices", numSlices) - d.Set("BSIFieldCount", bsiFieldCount) - d.Set("TimeQuantumEnabled", timeQuantumEnabled) -} diff --git a/server/cluster_test.go b/server/cluster_test.go new file mode 100644 index 000000000..fbb784691 --- /dev/null +++ b/server/cluster_test.go @@ -0,0 +1,581 @@ +// Copyright 2017 Pilosa Corp. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package server_test + +import ( + "context" + "fmt" + "net/http" + "reflect" + "strings" + "testing" + "time" + + "golang.org/x/sync/errgroup" + + "github.com/pilosa/pilosa" + "github.com/pilosa/pilosa/gossip" + "github.com/pilosa/pilosa/test" +) + +// Ensure program can send/receive broadcast messages. +func TestMain_SendReceiveMessage(t *testing.T) { + + m0 := test.MustRunMain() + defer m0.Close() + + m1 := test.MustRunMain() + defer m1.Close() + + // Update cluster config + m0.Server.Cluster.Nodes = []*pilosa.Node{ + {ID: m0.Server.NodeID, URI: m0.Server.URI}, + {ID: m1.Server.NodeID, URI: m1.Server.URI}, + } + m1.Server.Cluster.Nodes = m0.Server.Cluster.Nodes + + // Configure node0 + + // get the host portion of addr to use for binding + m0.Config.Gossip.Port = "0" + m0.Config.Gossip.Seeds = []string{} + + m0.Server.Cluster.Coordinator = m0.Server.NodeID + m0.Server.Cluster.Topology = &pilosa.Topology{NodeIDs: []string{m0.Server.NodeID, m1.Server.NodeID}} + m0.Server.Cluster.EventReceiver = gossip.NewGossipEventReceiver(m0.Server.LogOutput) + gossipMemberSet0, err := gossip.NewGossipMemberSet(m0.Server.URI.HostPort(), m0.Config, m0.Server) + if err != nil { + t.Fatal(err) + } + m0.Server.Cluster.MemberSet = gossipMemberSet0 + m0.Server.Broadcaster = m0.Server + m0.Server.Gossiper = gossipMemberSet0 + m0.Server.Handler.Broadcaster = m0.Server.Broadcaster + m0.Server.Holder.Broadcaster = m0.Server.Broadcaster + m0.Server.BroadcastReceiver = gossipMemberSet0 + + if err := m0.Server.BroadcastReceiver.Start(m0.Server); err != nil { + t.Fatal(err) + } + // Open Cluster management. + if err := m0.Server.Cluster.Open(); err != nil { + t.Fatal(err) + } + + // Configure node1 + + // get the host portion of addr to use for binding + m1.Config.Gossip.Port = "0" + m1.Config.Gossip.Seeds = []string{gossipMemberSet0.GetBindAddr()} + + m1.Server.Cluster.Coordinator = m0.Server.NodeID + m1.Server.Cluster.EventReceiver = gossip.NewGossipEventReceiver(m1.Server.LogOutput) + gossipMemberSet1, err := gossip.NewGossipMemberSet(m1.Server.URI.HostPort(), m1.Config, m1.Server) + if err != nil { + t.Fatal(err) + } + m1.Server.Cluster.MemberSet = gossipMemberSet1 + m1.Server.Broadcaster = m1.Server + m1.Server.Gossiper = gossipMemberSet1 + m1.Server.Handler.Broadcaster = m1.Server.Broadcaster + m1.Server.Holder.Broadcaster = m1.Server.Broadcaster + m1.Server.BroadcastReceiver = gossipMemberSet1 + + if err := m1.Server.BroadcastReceiver.Start(m1.Server); err != nil { + t.Fatal(err) + } + // Open Cluster management. + if err := m1.Server.Cluster.Open(); err != nil { + t.Fatal(err) + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + // Expected indexes and Frames + expected := map[string][]string{ + "i": []string{"f"}, + } + + // Create a client for each node. + client0 := m0.Client() + client1 := m1.Client() + + // Create indexes and frames on one node. + if err := client0.CreateIndex(context.Background(), "i", pilosa.IndexOptions{}); err != nil && err != pilosa.ErrIndexExists { + t.Fatal(err) + } else if err := client0.CreateFrame(context.Background(), "i", "f", pilosa.FrameOptions{}); err != nil { + t.Fatal(err) + } + + // Make sure node0 knows about the index and frame created. + schema0, err := client0.Schema(context.Background()) + if err != nil { + t.Fatal(err) + } + received0 := map[string][]string{} + for _, idx := range schema0 { + received0[idx.Name] = []string{} + for _, frame := range idx.Frames { + received0[idx.Name] = append(received0[idx.Name], frame.Name) + } + } + if !reflect.DeepEqual(received0, expected) { + t.Fatalf("unexpected schema on node0: %s", received0) + } + + // Make sure node1 knows about the index and frame created. + schema1, err := client1.Schema(context.Background()) + if err != nil { + t.Fatal(err) + } + received1 := map[string][]string{} + for _, idx := range schema1 { + received1[idx.Name] = []string{} + for _, frame := range idx.Frames { + received1[idx.Name] = append(received1[idx.Name], frame.Name) + } + } + if !reflect.DeepEqual(received1, expected) { + t.Fatalf("unexpected schema on node1: %s", received1) + } + + // Write data on first node. + if _, err := m0.Query("i", "", ` + SetBit(rowID=1, frame="f", columnID=1) + SetBit(rowID=1, frame="f", columnID=2400000) + `); err != nil { + t.Fatal(err) + } + + // We have to wait for the broadcast message to be sent before checking state. + time.Sleep(1 * time.Second) + + // Make sure node0 knows about the latest MaxSlice. + maxSlices0, err := client0.MaxSliceByIndex(context.Background()) + if err != nil { + t.Fatal(err) + } + if maxSlices0["i"] != 2 { + t.Fatalf("unexpected maxSlice on node0: %d", maxSlices0["i"]) + } + + // Make sure node1 knows about the latest MaxSlice. + maxSlices1, err := client1.MaxSliceByIndex(context.Background()) + if err != nil { + t.Fatal(err) + } + if maxSlices1["i"] != 2 { + t.Fatalf("unexpected maxSlice on node1: %d", maxSlices1["i"]) + } + + // Write input definition to the first node. + if _, err := m0.CreateDefinition("i", "test", `{ + "frames": [{"name": "event-time", + "options": { + "cacheType": "ranked", + "timeQuantum": "YMD" + }}], + "fields": [{"name": "columnID", + "primaryKey": true + }]} + `); err != nil { + t.Fatal(err) + } + + // We have to wait for the broadcast message to be sent before checking state. + time.Sleep(1 * time.Second) + + frame0 := m0.Server.Holder.Frame("i", "event-time") + if frame0 == nil { + t.Fatal("frame not found") + } + frame1 := m1.Server.Holder.Frame("i", "event-time") + if frame1 == nil { + t.Fatal("frame not found") + } +} + +// Ensure that an empty node comes up in a NORMAL state. +func TestClusterResize_EmptyNode(t *testing.T) { + m0 := test.MustRunMain() + defer m0.Close() + + if m0.Server.Cluster.State() != pilosa.ClusterStateNormal { + t.Fatalf("unexpected cluster state: %s", m0.Server.Cluster.State()) + } +} + +// Ensure that a cluster of empty nodes comes up in a NORMAL state. +func TestClusterResize_EmptyNodes(t *testing.T) { + // Configure node0 + m0 := test.NewMainWithCluster(true) + defer m0.Close() + + gossipHost := "localhost" + gossipPort := 0 + seed, err := m0.RunWithTransport(gossipHost, gossipPort, []string{}) + if err != nil { + t.Fatal(err) + } + + // Configure node1 + m1 := test.NewMainWithCluster(false) + defer m1.Close() + + seed, err = m1.RunWithTransport(gossipHost, gossipPort, []string{seed}) + if err != nil { + t.Fatal(err) + } + + if m0.Server.Cluster.State() != pilosa.ClusterStateNormal { + t.Fatalf("unexpected node0 cluster state: %s", m0.Server.Cluster.State()) + } else if m1.Server.Cluster.State() != pilosa.ClusterStateNormal { + t.Fatalf("unexpected node1 cluster state: %s", m1.Server.Cluster.State()) + } +} + +// Ensure that adding a node correctly resizes the cluster. +func TestClusterResize_AddNode(t *testing.T) { + t.Run("NoData", func(t *testing.T) { + // Configure node0 + m0 := test.NewMainWithCluster(true) + defer m0.Close() + + seed, err := m0.RunWithTransport("localhost", 0, []string{}) + if err != nil { + t.Fatal(err) + } + + // Configure node1 + m1 := test.NewMainWithCluster(false) + defer m1.Close() + + var eg errgroup.Group + eg.Go(func() error { + _, err = m1.RunWithTransport("localhost", 0, []string{seed}) + if err != nil { + return err + } + return nil + }) + if err := eg.Wait(); err != nil { + t.Fatal(err) + } + + time.Sleep(1 * time.Second) + + if m0.Server.Cluster.State() != pilosa.ClusterStateNormal { + t.Fatalf("unexpected node0 cluster state: %s", m0.Server.Cluster.State()) + } else if m1.Server.Cluster.State() != pilosa.ClusterStateNormal { + t.Fatalf("unexpected node1 cluster state: %s", m1.Server.Cluster.State()) + } + }) + t.Run("WithIndex", func(t *testing.T) { + // Configure node0 + m0 := test.NewMainWithCluster(true) + defer m0.Close() + + seed, err := m0.RunWithTransport("localhost", 0, []string{}) + if err != nil { + t.Fatal(err) + } + + // Create a client for each node. + client0 := m0.Client() + + // Create indexes and frames on one node. + if err := client0.CreateIndex(context.Background(), "i", pilosa.IndexOptions{}); err != nil && err != pilosa.ErrIndexExists { + t.Fatal(err) + } else if err := client0.CreateFrame(context.Background(), "i", "f", pilosa.FrameOptions{}); err != nil { + t.Fatal(err) + } + + // Configure node1 + m1 := test.NewMainWithCluster(false) + defer m1.Close() + + var eg errgroup.Group + eg.Go(func() error { + _, err = m1.RunWithTransport("localhost", 0, []string{seed}) + if err != nil { + return err + } + return nil + }) + if err := eg.Wait(); err != nil { + t.Fatal(err) + } + + // Give the cluster time to settle. + time.Sleep(1 * time.Second) + + if m0.Server.Cluster.State() != pilosa.ClusterStateNormal { + t.Fatalf("unexpected node0 cluster state: %s", m0.Server.Cluster.State()) + } else if m1.Server.Cluster.State() != pilosa.ClusterStateNormal { + t.Fatalf("unexpected node1 cluster state: %s", m1.Server.Cluster.State()) + } + }) + t.Run("ContinuousSlices", func(t *testing.T) { + + // Configure node0 + m0 := test.NewMainWithCluster(true) + defer m0.Close() + + seed, err := m0.RunWithTransport("localhost", 0, []string{}) + if err != nil { + t.Fatal(err) + } + + // Create a client for each node. + client0 := m0.Client() + //client1 := m1.Client() + + // Create indexes and frames on one node. + if err := client0.CreateIndex(context.Background(), "i", pilosa.IndexOptions{}); err != nil && err != pilosa.ErrIndexExists { + t.Fatal(err) + } else if err := client0.CreateFrame(context.Background(), "i", "f", pilosa.FrameOptions{}); err != nil { + t.Fatal(err) + } + + // Write data on first node. + if _, err := m0.Query("i", "", ` + SetBit(rowID=1, frame="f", columnID=1) + SetBit(rowID=1, frame="f", columnID=1300000) + `); err != nil { + t.Fatal(err) + } + + // Configure node1 + m1 := test.NewMainWithCluster(false) + defer m1.Close() + + var eg errgroup.Group + eg.Go(func() error { + _, err = m1.RunWithTransport("localhost", 0, []string{seed}) + if err != nil { + return err + } + return nil + }) + if err := eg.Wait(); err != nil { + t.Fatal(err) + } + + // Give the cluster time to settle. + time.Sleep(1 * time.Second) + + if m0.Server.Cluster.State() != pilosa.ClusterStateNormal { + t.Fatalf("unexpected node0 cluster state: %s", m0.Server.Cluster.State()) + } else if m1.Server.Cluster.State() != pilosa.ClusterStateNormal { + t.Fatalf("unexpected node1 cluster state: %s", m1.Server.Cluster.State()) + } + }) + t.Run("SkippedSlice", func(t *testing.T) { + + // Configure node0 + m0 := test.NewMainWithCluster(true) + defer m0.Close() + + seed, err := m0.RunWithTransport("localhost", 0, []string{}) + if err != nil { + t.Fatal(err) + } + + // Create a client for each node. + client0 := m0.Client() + //client1 := m1.Client() + + // Create indexes and frames on one node. + if err := client0.CreateIndex(context.Background(), "i", pilosa.IndexOptions{}); err != nil && err != pilosa.ErrIndexExists { + t.Fatal(err) + } else if err := client0.CreateFrame(context.Background(), "i", "f", pilosa.FrameOptions{}); err != nil { + t.Fatal(err) + } + + // Write data on first node. Note that no data is placed on slice 1. + if _, err := m0.Query("i", "", ` + SetBit(rowID=1, frame="f", columnID=1) + SetBit(rowID=1, frame="f", columnID=2400000) + `); err != nil { + t.Fatal(err) + } + + // Configure node1 + m1 := test.NewMainWithCluster(false) + defer m1.Close() + + var eg errgroup.Group + eg.Go(func() error { + _, err = m1.RunWithTransport("localhost", 0, []string{seed}) + if err != nil { + return err + } + return nil + }) + if err := eg.Wait(); err != nil { + t.Fatal(err) + } + + // Give the cluster time to settle. + time.Sleep(1 * time.Second) + + if m0.Server.Cluster.State() != pilosa.ClusterStateNormal { + t.Fatalf("unexpected node0 cluster state: %s", m0.Server.Cluster.State()) + } else if m1.Server.Cluster.State() != pilosa.ClusterStateNormal { + t.Fatalf("unexpected node1 cluster state: %s", m1.Server.Cluster.State()) + } + }) +} + +// Ensure that redundant gossip seeds are used +func TestCluster_GossipMembership(t *testing.T) { + t.Run("Node0Down", func(t *testing.T) { + // Configure node0 + m0 := test.NewMainWithCluster(true) + defer m0.Close() + + seed, err := m0.RunWithTransport("localhost", 0, []string{}) + if err != nil { + t.Fatal(err) + } + + // Configure node1 + m1 := test.NewMainWithCluster(false) + defer m1.Close() + + var eg errgroup.Group + eg.Go(func() error { + // Pass invalid seed as first in list + _, err = m1.RunWithTransport("localhost", 0, []string{"http://localhost:8765", seed}) + if err != nil { + return err + } + return nil + }) + + // Configure node2 + m2 := test.NewMainWithCluster(false) + defer m2.Close() + + eg.Go(func() error { + // Pass invalid seed as last in list + _, err = m2.RunWithTransport("localhost", 0, []string{seed, "http://localhost:8765"}) + if err != nil { + return err + } + return nil + }) + + if err := eg.Wait(); err != nil { + t.Fatal(err) + } + + // Give the cluster time to settle. + time.Sleep(1 * time.Second) + + if m0.Server.Cluster.State() != pilosa.ClusterStateNormal { + t.Fatalf("unexpected node0 cluster state: %s", m0.Server.Cluster.State()) + } else if m1.Server.Cluster.State() != pilosa.ClusterStateNormal { + t.Fatalf("unexpected node1 cluster state: %s", m1.Server.Cluster.State()) + } else if m2.Server.Cluster.State() != pilosa.ClusterStateNormal { + t.Fatalf("unexpected node2 cluster state: %s", m2.Server.Cluster.State()) + } + + numNodes := len(m0.Server.Cluster.Status().Nodes) + if numNodes != 3 { + t.Fatalf("Expected 3 nodes, got %d", numNodes) + } + }) +} + +func TestClusterResize_RemoveNode(t *testing.T) { + cluster := test.MustRunMainWithCluster(t, 3) + m0 := cluster[0] + m1 := cluster[1] + + t.Run("ErrorRemoveInvalidNode", func(t *testing.T) { + resp := test.MustDo("POST", m0.URL()+fmt.Sprintf("/cluster/resize/remove-node"), `{"id": "invalid-node-id"}`) + expBody := "Node is not a member of the cluster: invalid-node-id" + if resp.StatusCode != http.StatusBadRequest { + t.Fatalf("expected StatusCode %d but got %d", http.StatusBadRequest, resp.StatusCode) + } else if strings.TrimSpace(resp.Body) != expBody { + t.Fatalf("expected Body '%s' but got '%s'", expBody, strings.TrimSpace(resp.Body)) + } + }) + + t.Run("ErrorRemoveCoordinator", func(t *testing.T) { + resp := test.MustDo("GET", m0.URL()+fmt.Sprintf("/id"), "") + nodeID := resp.Body + + resp = test.MustDo("POST", m0.URL()+fmt.Sprintf("/cluster/resize/remove-node"), fmt.Sprintf(`{"id": "%s"}`, nodeID)) + + expBody := "The coordinator node cannot be removed. First, make a different node the new coordinator." + if resp.StatusCode != http.StatusInternalServerError { + t.Fatalf("expected StatusCode %d but got %d", http.StatusInternalServerError, resp.StatusCode) + } else if strings.TrimSpace(resp.Body) != expBody { + t.Fatalf("expected Body '%s' but got '%s'", expBody, strings.TrimSpace(resp.Body)) + } + }) + + t.Run("ErrorRemoveOnNonCoordinator", func(t *testing.T) { + resp := test.MustDo("GET", m0.URL()+fmt.Sprintf("/id"), "") + coordinatorNodeID := resp.Body + + resp = test.MustDo("GET", m1.URL()+fmt.Sprintf("/id"), "") + nodeID := resp.Body + + resp = test.MustDo("POST", m1.URL()+fmt.Sprintf("/cluster/resize/remove-node"), fmt.Sprintf(`{"id": "%s"}`, nodeID)) + + expBody := fmt.Sprintf("Node removal requests are only valid on the Coordinator node: %s", coordinatorNodeID) + if resp.StatusCode != http.StatusInternalServerError { + t.Fatalf("expected StatusCode %d but got %d", http.StatusInternalServerError, resp.StatusCode) + } else if strings.TrimSpace(resp.Body) != expBody { + t.Fatalf("expected Body '%s' but got '%s'", expBody, strings.TrimSpace(resp.Body)) + } + }) + + t.Run("ErrorRemoveWithoutReplicas", func(t *testing.T) { + client0 := m0.Client() + + // Create indexes and frames on one node. + if err := client0.CreateIndex(context.Background(), "i", pilosa.IndexOptions{}); err != nil && err != pilosa.ErrIndexExists { + t.Fatal(err) + } else if err := client0.CreateFrame(context.Background(), "i", "f", pilosa.FrameOptions{}); err != nil { + t.Fatal(err) + } + + // This is an attempt to ensure there is data on both nodes, but is not guaranteed. + // TODO: Deterministic node IDs would ensure consistent results + setBits := "" + for i := 0; i < 20; i++ { + setBits += fmt.Sprintf("SetBit(rowID=1, frame=\"f\", columnID=%d) ", i*pilosa.SliceWidth) + } + + if _, err := m0.Query("i", "", setBits); err != nil { + t.Fatal(err) + } + + resp := test.MustDo("GET", m1.URL()+fmt.Sprintf("/id"), "") + nodeID := resp.Body + + resp = test.MustDo("POST", m0.URL()+fmt.Sprintf("/cluster/resize/remove-node"), fmt.Sprintf(`{"id": "%s"}`, nodeID)) + expBody := "not enough data to perform resize" + if resp.StatusCode != http.StatusInternalServerError { + t.Fatalf("expected StatusCode %d but got %d", http.StatusInternalServerError, resp.StatusCode) + } else if !strings.Contains(resp.Body, expBody) { + t.Fatalf("expected to contain '%s' but got '%s'", expBody, strings.TrimSpace(resp.Body)) + } + }) +} diff --git a/server/server.go b/server/server.go index d706d83f1..ce1eff629 100644 --- a/server/server.go +++ b/server/server.go @@ -32,10 +32,12 @@ import ( "crypto/tls" - "io/ioutil" - "github.com/pilosa/pilosa" + "github.com/pilosa/pilosa/boltdb" + "github.com/pilosa/pilosa/gcnotify" + "github.com/pilosa/pilosa/gopsutil" "github.com/pilosa/pilosa/gossip" + "github.com/pilosa/pilosa/statik" "github.com/pilosa/pilosa/statsd" ) @@ -59,10 +61,13 @@ type Command struct { CPUProfile string CPUTime time.Duration + // Gossip transport + GossipTransport *gossip.Transport + // Standard input/output *pilosa.CmdIO - // running will be closed once Command.Run is finished. + // Started will be closed once Command.Run is finished. Started chan struct{} // Done will be closed when Command.Close() is called Done chan struct{} @@ -99,12 +104,18 @@ func (m *Command) Run(args ...string) (err error) { return err } + // SetupNetworking + err = m.SetupNetworking() + if err != nil { + return err + } + // Initialize server. if err = m.Server.Open(); err != nil { return fmt.Errorf("server.Open: %v", err) } - m.Server.Logger().Printf("Listening as %s\n", m.Server.URI.Normalize()) + m.Server.Logger().Printf("Listening as %s\n", m.Server.URI) return nil } @@ -116,24 +127,16 @@ func (m *Command) SetupServer() error { } uri, err := pilosa.AddressWithDefaults(m.Config.Bind) + if err != nil { return err } - m.Server.URI = uri + m.Server.URI = *uri cluster := pilosa.NewCluster() cluster.ReplicaN = m.Config.Cluster.ReplicaN + cluster.Holder = m.Server.Holder - for _, address := range m.Config.Cluster.Hosts { - uri, err := pilosa.NewURIFromAddress(address) - if err != nil { - return err - } - cluster.Nodes = append(cluster.Nodes, &pilosa.Node{ - Scheme: uri.Scheme(), - Host: uri.HostPort(), - }) - } m.Server.Cluster = cluster // Setup logging output. @@ -142,6 +145,12 @@ func (m *Command) SetupServer() error { return err } + // Configure data directory (for Cluster .topology) + m.Server.Cluster.Path = m.Config.DataDir + + m.Server.NewAttrStore = boltdb.NewAttrStore + m.Server.Holder.NewAttrStore = boltdb.NewAttrStore + // Configure holder. m.Server.Logger().Printf("Using data from: %s\n", m.Config.DataDir) m.Server.Holder.Path = m.Config.DataDir @@ -149,6 +158,8 @@ func (m *Command) SetupServer() error { if m.Config.Metric.Diagnostics { m.Server.DiagnosticInterval = time.Duration(DefaultDiagnosticsInterval) } + m.Server.SystemInfo = gopsutil.NewSystemInfo() + m.Server.GCNotifier = gcnotify.NewActiveGCNotifier() m.Server.Holder.Stats, err = NewStatsClient(m.Config.Metric.Service, m.Config.Metric.Host) if err != nil { return err @@ -177,68 +188,15 @@ func (m *Command) SetupServer() error { InsecureSkipVerify: m.Config.TLS.SkipVerify, } - // TODO Review this location - TLSConfig = m.Server.TLS - } c := pilosa.GetHTTPClient(TLSConfig) m.Server.RemoteClient = c m.Server.Handler.RemoteClient = c + m.Server.Cluster.RemoteClient = c - // Set internal port (string). - gossipPortStr := pilosa.DefaultGossipPort - // Config.GossipPort is deprecated, so Config.Gossip.Port has priority - if m.Config.Gossip.Port != "" { - gossipPortStr = m.Config.Gossip.Port - } else if m.Config.GossipPort != "" { - gossipPortStr = m.Config.GossipPort - } - - switch m.Config.Cluster.Type { - case pilosa.ClusterGossip: - gossipPort, err := strconv.Atoi(gossipPortStr) - if err != nil { - return err - } - gossipSeed := pilosa.DefaultHost + ":" + pilosa.DefaultGossipPort - // Config.GossipSeed is deprecated, so Config.Gossip.Seed has priority - if m.Config.Gossip.Seed != "" { - gossipSeed = m.Config.Gossip.Seed - } else if m.Config.GossipSeed != "" { - gossipSeed = m.Config.GossipSeed - } - - var gossipKey []byte - if m.Config.Gossip.Key != "" { - gossipKey, err = ioutil.ReadFile(m.Config.Gossip.Key) - if err != nil { - return err - } - } - - // get the host portion of addr to use for binding - gossipHost := uri.Host() - gossipNodeSet, err := gossip.NewGossipNodeSet(uri.HostPort(), gossipHost, gossipPort, gossipSeed, m.Server, gossipKey) - if err != nil { - return err - } - m.Server.Cluster.NodeSet = gossipNodeSet - m.Server.Broadcaster = m.Server - m.Server.BroadcastReceiver = gossipNodeSet - m.Server.Gossiper = gossipNodeSet - case pilosa.ClusterStatic, pilosa.ClusterNone: - m.Server.Broadcaster = pilosa.NopBroadcaster - m.Server.Cluster.NodeSet = pilosa.NewStaticNodeSet() - m.Server.BroadcastReceiver = pilosa.NopBroadcastReceiver - m.Server.Gossiper = pilosa.NopGossiper - err := m.Server.Cluster.NodeSet.(*pilosa.StaticNodeSet).Join(m.Server.Cluster.Nodes) - if err != nil { - return err - } - default: - return fmt.Errorf("'%v' is not a supported value for broadcaster type", m.Config.Cluster.Type) - } + // Statik file system. + m.Server.Handler.FileSystem = &statik.FileSystem{} // Set configuration options. m.Server.AntiEntropyInterval = time.Duration(m.Config.AntiEntropy.Interval) @@ -246,6 +204,71 @@ func (m *Command) SetupServer() error { return nil } +// SetupNetworking sets up internode communication based on the configuration. +func (m *Command) SetupNetworking() error { + + m.Server.NodeID = m.Server.LoadNodeID() + + if m.Config.Cluster.Disabled { + m.Server.Cluster.Static = true + m.Server.Cluster.Coordinator = m.Server.NodeID + for _, address := range m.Config.Cluster.Hosts { + uri, err := pilosa.NewURIFromAddress(address) + if err != nil { + return err + } + m.Server.Cluster.Nodes = append(m.Server.Cluster.Nodes, &pilosa.Node{ + URI: *uri, + }) + } + + m.Server.Broadcaster = pilosa.NopBroadcaster + m.Server.Cluster.MemberSet = pilosa.NewStaticMemberSet(m.Server.Cluster.Nodes) + m.Server.BroadcastReceiver = pilosa.NopBroadcastReceiver + m.Server.Gossiper = pilosa.NopGossiper + return nil + } + + // Set internal port (string). + gossipPortStr := pilosa.DefaultGossipPort + if m.Config.Gossip.Port != "" { + gossipPortStr = m.Config.Gossip.Port + } + + gossipPort, err := strconv.Atoi(gossipPortStr) + if err != nil { + return err + } + + // get the host portion of addr to use for binding + gossipHost := m.Server.URI.Host() + var transport *gossip.Transport + if m.GossipTransport != nil { + transport = m.GossipTransport + } else { + transport, err = gossip.NewTransport(gossipHost, gossipPort) + if err != nil { + return err + } + } + + // Set Coordinator. + if m.Config.Cluster.Coordinator || len(m.Config.Gossip.Seeds) == 0 { + m.Server.Cluster.Coordinator = m.Server.NodeID + } + + m.Server.Cluster.EventReceiver = gossip.NewGossipEventReceiver(m.Server.LogOutput) + gossipMemberSet, err := gossip.NewGossipMemberSetWithTransport(m.Server.NodeID, m.Config, transport, m.Server) + if err != nil { + return err + } + m.Server.Cluster.MemberSet = gossipMemberSet + m.Server.Broadcaster = m.Server + m.Server.BroadcastReceiver = gossipMemberSet + m.Server.Gossiper = gossipMemberSet + return nil +} + // GetLogWriter opens a file for logging, or a default io.Writer (such as stderr) for an empty path. func GetLogWriter(path string, defaultWriter io.Writer) (io.Writer, error) { // This is split out so it can be used in NewServeCmd as well as SetupServer diff --git a/server/server_test.go b/server/server_test.go index 4281541da..3852845a6 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -15,26 +15,20 @@ package server_test import ( - "bytes" "context" "encoding/json" "fmt" - "io" "io/ioutil" "math/rand" - "net/http" - "os" "reflect" "runtime" "sort" "strings" "testing" "testing/quick" - "time" "github.com/BurntSushi/toml" "github.com/pilosa/pilosa" - "github.com/pilosa/pilosa/server" "github.com/pilosa/pilosa/test" ) @@ -45,7 +39,7 @@ func TestMain_Set_Quick(t *testing.T) { } if err := quick.Check(func(cmds []SetCommand) bool { - m := MustRunMain() + m := test.MustRunMain() defer m.Close() // Create client. @@ -121,7 +115,7 @@ func TestMain_Set_Quick(t *testing.T) { // Ensure program can set row attributes and retrieve them. func TestMain_SetRowAttrs(t *testing.T) { - m := MustRunMain() + m := test.MustRunMain() defer m.Close() // Create frames. @@ -198,7 +192,7 @@ func TestMain_SetRowAttrs(t *testing.T) { // Ensure program can set column attributes and retrieve them. func TestMain_SetColumnAttrs(t *testing.T) { - m := MustRunMain() + m := test.MustRunMain() defer m.Close() // Create frames. @@ -242,7 +236,7 @@ func TestMain_SetColumnAttrs(t *testing.T) { // Ensure program can set column attributes with columnLabel option. func TestMain_SetColumnAttrsWithColumnOption(t *testing.T) { - m := MustRunMain() + m := test.MustRunMain() defer m.Close() // Create frames. @@ -276,11 +270,12 @@ func TestMain_SetColumnAttrsWithColumnOption(t *testing.T) { // Ensure program can set bits on one cluster and then restore to a second cluster. func TestMain_FrameRestore(t *testing.T) { - mains1 := NewMainArrayWithCluster(2) - m0 := mains1[0] + mains1 := test.MustRunMainWithCluster(t, 2) + m10 := mains1[0] + m11 := mains1[1] // Create frames. - client := m0.Client() + client := m10.Client() if err := client.CreateIndex(context.Background(), "i", pilosa.IndexOptions{}); err != nil && err != pilosa.ErrIndexExists { t.Fatal("create index:", err) } @@ -289,7 +284,7 @@ func TestMain_FrameRestore(t *testing.T) { } // Write data on first cluster. - if _, err := m0.Query("i", "", ` + if _, err := m10.Query("i", "", ` SetBit(rowID=1, frame="f", columnID=100) SetBit(rowID=1, frame="f", columnID=1000) SetBit(rowID=1, frame="f", columnID=100000) @@ -302,34 +297,45 @@ func TestMain_FrameRestore(t *testing.T) { } // Query row on first cluster. - if res, err := m0.Query("i", "", `Bitmap(rowID=1, frame="f")`); err != nil { + if res, err := m10.Query("i", "", `Bitmap(rowID=1, frame="f")`); err != nil { t.Fatal("bitmap query:", err) } else if res != `{"results":[{"attrs":{},"bits":[100,1000,100000,200000,400000,600000,800000]}]}`+"\n" { t.Fatalf("unexpected result: %s", res) } // Start second cluster. - mains2 := NewMainArrayWithCluster(2) - m2 := mains2[0] - defer m2.Close() + mains2 := test.MustRunMainWithCluster(t, 2) + m20 := mains2[0] + defer m20.Close() + m21 := mains2[1] + defer m21.Close() // Import from first cluster. - client, err := pilosa.NewInternalHTTPClient(m2.Server.URI.HostPort(), pilosa.GetHTTPClient(nil)) + client20, err := pilosa.NewInternalHTTPClient(m20.Server.URI.HostPort(), pilosa.GetHTTPClient(nil)) if err != nil { t.Fatal("new client:", err) } - if err := m2.Client().CreateIndex(context.Background(), "i", pilosa.IndexOptions{}); err != nil && err != pilosa.ErrIndexExists { + client21, err := pilosa.NewInternalHTTPClient(m21.Server.URI.HostPort(), pilosa.GetHTTPClient(nil)) + if err != nil { + t.Fatal("new client:", err) + } + + if err := m20.Client().CreateIndex(context.Background(), "i", pilosa.IndexOptions{}); err != nil && err != pilosa.ErrIndexExists { t.Fatal("create new index:", err) } - if err := m2.Client().CreateFrame(context.Background(), "i", "f", pilosa.FrameOptions{}); err != nil { + if err := m20.Client().CreateFrame(context.Background(), "i", "f", pilosa.FrameOptions{}); err != nil { t.Fatal("create new frame:", err) } - if err := client.RestoreFrame(context.Background(), m0.Server.URI.HostPort(), "i", "f"); err != nil { + + if err := client20.RestoreFrame(context.Background(), m10.Server.URI.HostPort(), "i", "f"); err != nil { + t.Fatal("restore frame:", err) + } + if err := client21.RestoreFrame(context.Background(), m11.Server.URI.HostPort(), "i", "f"); err != nil { t.Fatal("restore frame:", err) } // Query row on second cluster. - if res, err := m2.Query("i", "", `Bitmap(rowID=1, frame="f")`); err != nil { + if res, err := m20.Query("i", "", `Bitmap(rowID=1, frame="f")`); err != nil { t.Fatal("another bitmap query:", err) } else if res != `{"results":[{"attrs":{},"bits":[100,1000,100000,200000,400000,600000,800000]}]}`+"\n" { t.Fatalf("2unexpected result: %s", res) @@ -378,226 +384,45 @@ func TestCountOpenFiles(t *testing.T) { } } -// Ensure program can send/receive broadcast messages. -func TestMain_SendReceiveMessage(t *testing.T) { - mains := NewMainArrayWithCluster(2) - m0 := mains[0] - defer m0.Close() +func TestMain_RecalculateHashes(t *testing.T) { + const clusterSize = 5 + cluster := test.MustRunMainWithCluster(t, clusterSize) - m1 := mains[1] - defer m1.Close() - - // Expected indexes and Frames - expected := map[string][]string{ - "i": []string{"f"}, - } - - // Create a client for each node. - client0 := m0.Client() - client1 := m1.Client() - - // Create indexes and frames on one node. + // Create the schema. + client0 := cluster[0].Client() if err := client0.CreateIndex(context.Background(), "i", pilosa.IndexOptions{}); err != nil && err != pilosa.ErrIndexExists { - t.Fatal(err) - } else if err := client0.CreateFrame(context.Background(), "i", "f", pilosa.FrameOptions{}); err != nil { - t.Fatal(err) + t.Fatal("create index:", err) + } + if err := client0.CreateFrame(context.Background(), "i", "f", pilosa.FrameOptions{CacheType: "ranked"}); err != nil { + t.Fatal("create frame:", err) } - // Make sure node0 knows about the index and frame created. - schema0, err := client0.Schema(context.Background()) - if err != nil { - t.Fatal(err) - } - received0 := map[string][]string{} - for _, idx := range schema0 { - received0[idx.Name] = []string{} - for _, frame := range idx.Frames { - received0[idx.Name] = append(received0[idx.Name], frame.Name) + // Set some bits + data := []string{} + for rowID := 1; rowID < 10; rowID++ { + for columnID := 1; columnID < 100; columnID++ { + data = append(data, fmt.Sprintf(`SetBit(rowID=%d, frame="f", columnID=%d)`, rowID, columnID)) } } - if !reflect.DeepEqual(received0, expected) { - t.Fatalf("unexpected schema on node0: %s", received0) + if _, err := cluster[0].Query("i", "", strings.Join(data, "")); err != nil { + t.Fatal("setting bits:", err) } - // Make sure node1 knows about the index and frame created. - schema1, err := client1.Schema(context.Background()) - if err != nil { - t.Fatal(err) - } - received1 := map[string][]string{} - for _, idx := range schema1 { - received1[idx.Name] = []string{} - for _, frame := range idx.Frames { - received1[idx.Name] = append(received1[idx.Name], frame.Name) + // Calculate caches on the first node + cluster[0].RecalculateCaches() + target := `{"results":[[{"id":7,"count":99},{"id":1,"count":99},{"id":9,"count":99},{"id":5,"count":99},{"id":4,"count":99},{"id":8,"count":99},{"id":2,"count":99},{"id":6,"count":99},{"id":3,"count":99}]]}` + + // Run a TopN query on all nodes. The result should be the same as the target. + for _, m := range cluster { + res, err := m.Query("i", "", `TopN(frame="f")`) + if err != nil { + t.Fatal(err) + } + res = strings.TrimSpace(res) + if sortedString(target) != sortedString(res) { + t.Fatalf("%v != %v", target, res) } } - if !reflect.DeepEqual(received1, expected) { - t.Fatalf("unexpected schema on node1: %s", received1) - } - - // Write data on first node. - if _, err := m0.Query("i", "", ` - SetBit(rowID=1, frame="f", columnID=1) - SetBit(rowID=1, frame="f", columnID=2400000) - `); err != nil { - t.Fatal(err) - } - - // We have to wait for the broadcast message to be sent before checking state. - time.Sleep(1 * time.Second) - - // Make sure node0 knows about the latest MaxSlice. - maxSlices0, err := client0.MaxSliceByIndex(context.Background()) - if err != nil { - t.Fatal(err) - } - if maxSlices0["i"] != 2 { - t.Fatalf("unexpected maxSlice on node0: %d", maxSlices0["i"]) - } - - // Make sure node1 knows about the latest MaxSlice. - maxSlices1, err := client1.MaxSliceByIndex(context.Background()) - if err != nil { - t.Fatal(err) - } - if maxSlices1["i"] != 2 { - t.Fatalf("unexpected maxSlice on node1: %d", maxSlices1["i"]) - } - - // Write input definition to the first node. - if _, err := m0.CreateDefinition("i", "test", `{ - "frames": [{"name": "event-time", - "options": { - "cacheType": "ranked", - "timeQuantum": "YMD" - }}], - "fields": [{"name": "columnID", - "primaryKey": true - }]} - `); err != nil { - t.Fatal(err) - } - - // We have to wait for the broadcast message to be sent before checking state. - time.Sleep(1 * time.Second) - - frame0 := m0.Server.Holder.Frame("i", "event-time") - if frame0 == nil { - t.Fatal("frame not found") - } - frame1 := m1.Server.Holder.Frame("i", "event-time") - if frame1 == nil { - t.Fatal("frame not found") - } -} - -// Main represents a test wrapper for main.Main. -type Main struct { - *server.Command - - Stdin bytes.Buffer - Stdout bytes.Buffer - Stderr bytes.Buffer -} - -// NewMain returns a new instance of Main with a temporary data directory and random port. -func NewMain() *Main { - path, err := ioutil.TempDir("", "pilosa-") - if err != nil { - panic(err) - } - - m := &Main{Command: server.NewCommand(os.Stdin, os.Stdout, os.Stderr)} - m.Server.Network = *test.Network - m.Config.DataDir = path - m.Config.Bind = "localhost:0" - m.Config.Cluster.Type = "static" - m.Command.Stdin = &m.Stdin - m.Command.Stdout = &m.Stdout - m.Command.Stderr = &m.Stderr - - if testing.Verbose() { - m.Command.Stdout = io.MultiWriter(os.Stdout, m.Command.Stdout) - m.Command.Stderr = io.MultiWriter(os.Stderr, m.Command.Stderr) - } - - return m -} - -func NewMainArrayWithCluster(size int) []*Main { - cluster, err := test.NewServerCluster(size) - if err != nil { - panic(err) - } - mainArray := make([]*Main, size) - for i := 0; i < size; i++ { - mainArray[i] = &Main{Command: cluster.Servers[i]} - } - return mainArray -} - -// MustRunMain returns a new, running Main. Panic on error. -func MustRunMain() *Main { - m := NewMain() - if err := m.Run(); err != nil { - panic(err) - } - return m -} - -// Close closes the program and removes the underlying data directory. -func (m *Main) Close() error { - defer os.RemoveAll(m.Config.DataDir) - return m.Command.Close() -} - -// Reopen closes the program and reopens it. -func (m *Main) Reopen() error { - if err := m.Command.Close(); err != nil { - return err - } - - // Create new main with the same config. - config := m.Config - m.Command = server.NewCommand(os.Stdin, os.Stdout, os.Stderr) - m.Server.Network = *test.Network - m.Config = config - - // Run new program. - if err := m.Run(); err != nil { - return err - } - return nil -} - -// URL returns the base URL string for accessing the running program. -func (m *Main) URL() string { return "http://" + m.Server.Addr().String() } - -// Client returns a client to connect to the program. -func (m *Main) Client() *pilosa.InternalHTTPClient { - client, err := pilosa.NewInternalHTTPClient(m.Server.URI.HostPort(), pilosa.GetHTTPClient(nil)) - if err != nil { - panic(err) - } - return client -} - -// Query executes a query against the program through the HTTP API. -func (m *Main) Query(index, rawQuery, query string) (string, error) { - resp := MustDo("POST", m.URL()+fmt.Sprintf("/index/%s/query?", index)+rawQuery, query) - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("invalid status: %d, body=%s", resp.StatusCode, resp.Body) - } - return resp.Body, nil -} - -// CreateDefinition. -func (m *Main) CreateDefinition(index, def, query string) (string, error) { - resp := MustDo("POST", m.URL()+fmt.Sprintf("/index/%s/input-definition/%s", index, def), query) - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("invalid status: %d, body=%s", resp.StatusCode, resp.Body) - } - return resp.Body, nil } // SetCommand represents a command to set a bit. @@ -656,32 +481,6 @@ func ParseConfig(s string) (pilosa.Config, error) { return c, err } -// MustDo executes http.Do() with an http.NewRequest(). Panic on error. -func MustDo(method, urlStr string, body string) *httpResponse { - req, err := http.NewRequest(method, urlStr, strings.NewReader(body)) - if err != nil { - panic(err) - } - resp, err := http.DefaultClient.Do(req) - if err != nil { - panic(err) - } - defer resp.Body.Close() - - buf, err := ioutil.ReadAll(resp.Body) - if err != nil { - panic(err) - } - - return &httpResponse{Response: resp, Body: string(buf)} -} - -// httpResponse is a wrapper for http.Response that holds the Body as a string. -type httpResponse struct { - *http.Response - Body string -} - // MustMarshalJSON marshals v into a string. Panic on error. func MustMarshalJSON(v interface{}) string { buf, err := json.Marshal(v) @@ -691,6 +490,12 @@ func MustMarshalJSON(v interface{}) string { return string(buf) } +func sortedString(s string) string { + arr := strings.Split(s, "") + sort.Strings(arr) + return strings.Join(arr, "") +} + // uint64Slice represents a sortable slice of uint64 numbers. type uint64Slice []uint64 diff --git a/statik/.gitignore b/statik/.gitignore index 514ee40a1..485c0c57d 100644 --- a/statik/.gitignore +++ b/statik/.gitignore @@ -1 +1 @@ -statik.go +/statik.go diff --git a/statik/doc.go b/statik/doc.go deleted file mode 100644 index 85edd9e6f..000000000 --- a/statik/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package statik contains static assets for the Web UI. `go generate` will -// produce statik.go, which is ignored by git. -package statik diff --git a/statik/filesystem.go b/statik/filesystem.go new file mode 100644 index 000000000..e3bf95cb1 --- /dev/null +++ b/statik/filesystem.go @@ -0,0 +1,37 @@ +// Copyright 2017 Pilosa Corp. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//go:generate statik -src=../webui -dest=.. +// +// Package statik contains static assets for the Web UI. `go generate` or +// `make generate-statik` will produce statik.go, which is ignored by git. +package statik + +import ( + "net/http" + + "github.com/pilosa/pilosa" + "github.com/rakyll/statik/fs" +) + +// Ensure nopFileSystem implements interface. +var _ pilosa.FileSystem = &FileSystem{} + +// FileSystem represents a static FileSystem. +type FileSystem struct{} + +// New is a statik implementation of FileSystem New method. +func (s *FileSystem) New() (http.FileSystem, error) { + return fs.New() +} diff --git a/test/attr.go b/test/attr.go index 9363011b1..16e8ff334 100644 --- a/test/attr.go +++ b/test/attr.go @@ -22,15 +22,16 @@ import ( "testing" "github.com/pilosa/pilosa" + "github.com/pilosa/pilosa/boltdb" ) // AttrStore represents a test wrapper for pilosa.AttrStore. type AttrStore struct { - *pilosa.AttrStore + pilosa.AttrStore } // NewAttrStore returns a new instance of AttrStore. -func NewAttrStore() *AttrStore { +func NewAttrStore(string) pilosa.AttrStore { f, err := ioutil.TempFile("", "pilosa-attr-") if err != nil { panic(err) @@ -38,7 +39,7 @@ func NewAttrStore() *AttrStore { f.Close() os.Remove(f.Name()) - return &AttrStore{AttrStore: pilosa.NewAttrStore(f.Name())} + return &AttrStore{boltdb.NewAttrStore(f.Name())} } func BenchmarkAttrStore_Duplicate(b *testing.B) { @@ -74,8 +75,8 @@ func BenchmarkAttrStore_Duplicate(b *testing.B) { } // MustOpenAttrStore returns a new, opened attribute store at a temporary path. Panic on error. -func MustOpenAttrStore() *AttrStore { - s := NewAttrStore() +func MustOpenAttrStore() pilosa.AttrStore { + s := NewAttrStore("") if err := s.Open(); err != nil { panic(err) } diff --git a/test/cluster.go b/test/cluster.go index adf82fc0a..8362ca964 100644 --- a/test/cluster.go +++ b/test/cluster.go @@ -15,24 +15,42 @@ package test import ( + "bufio" + "bytes" "fmt" + "io/ioutil" + "path/filepath" + "sync" + "time" + "github.com/gogo/protobuf/proto" "github.com/pilosa/pilosa" + "github.com/pilosa/pilosa/internal" ) // NewCluster returns a cluster with n nodes and uses a mod-based hasher. func NewCluster(n int) *pilosa.Cluster { + path, err := ioutil.TempDir("", "pilosa-cluster-") + if err != nil { + panic(err) + } + c := pilosa.NewCluster() c.ReplicaN = 1 c.Hasher = NewModHasher() + c.Path = path + c.Topology = pilosa.NewTopology() for i := 0; i < n; i++ { c.Nodes = append(c.Nodes, &pilosa.Node{ - Scheme: "http", - Host: fmt.Sprintf("host%d", i), + ID: fmt.Sprintf("node%d", i), + URI: NewURI("http", fmt.Sprintf("host%d", i), uint16(0)), }) } + c.Node = c.Nodes[0] + c.Coordinator = c.Nodes[0].ID + return c } @@ -53,3 +71,379 @@ type ConstHasher struct { func NewConstHasher(i int) *ConstHasher { return &ConstHasher{i: i} } func (h *ConstHasher) Hash(key uint64, n int) int { return h.i } + +// NewURI is a test URI creator that intentionally swallows errors. +func NewURI(scheme, host string, port uint16) pilosa.URI { + uri := pilosa.DefaultURI() + uri.SetScheme(scheme) + uri.SetHost(host) + uri.SetPort(port) + return *uri +} + +func NewURIFromHostPort(host string, port uint16) pilosa.URI { + uri := pilosa.DefaultURI() + uri.SetHost(host) + uri.SetPort(port) + return *uri +} + +// TestCluster represents a cluster of test nodes, each of which +// has a pilosa.Cluster. +type TestCluster struct { + Clusters []*pilosa.Cluster + + common *commonClusterSettings + + mu sync.RWMutex + resizing bool + resizeDone chan struct{} +} + +type commonClusterSettings struct { + Nodes []*pilosa.Node +} + +func (t *TestCluster) CreateIndex(name string) error { + for _, c := range t.Clusters { + if _, err := c.Holder.CreateIndexIfNotExists(name, pilosa.IndexOptions{}); err != nil { + return err + } + } + return nil +} + +func (t *TestCluster) CreateFrame(index, frame string, opt pilosa.FrameOptions) error { + for _, c := range t.Clusters { + idx, err := c.Holder.CreateIndexIfNotExists(index, pilosa.IndexOptions{}) + if err != nil { + return err + } + if _, err := idx.CreateFrame(frame, opt); err != nil { + return err + } + } + return nil +} +func (t *TestCluster) SetBit(index, frame, view string, rowID, colID uint64, x *time.Time) error { + // Determine which node should receive the SetBit. + c0 := t.Clusters[0] // use the first node's cluster to determine slice location. + slice := colID / pilosa.SliceWidth + nodes := c0.FragmentNodes(index, slice) + + for _, node := range nodes { + c := t.clusterByID(node.ID) + if c == nil { + continue + } + f := c.Holder.Frame(index, frame) + if f == nil { + return fmt.Errorf("index/frame does not exist: %s/%s", index, frame) + } + _, err := f.SetBit(view, rowID, colID, x) + if err != nil { + return err + } + } + + return nil +} + +func (t *TestCluster) SetFieldValue(index, frame string, columnID uint64, name string, value int64) error { + // Determine which node should receive the SetFieldValue. + c0 := t.Clusters[0] // use the first node's cluster to determine slice location. + slice := columnID / pilosa.SliceWidth + nodes := c0.FragmentNodes(index, slice) + + for _, node := range nodes { + c := t.clusterByID(node.ID) + if c == nil { + continue + } + f := c.Holder.Frame(index, frame) + if f == nil { + return fmt.Errorf("index/frame does not exist: %s/%s", index, frame) + } + _, err := f.SetFieldValue(columnID, name, value) + if err != nil { + return err + } + } + + return nil +} + +func (t *TestCluster) clusterByID(id string) *pilosa.Cluster { + for _, c := range t.Clusters { + if c.Node.ID == id { + return c + } + } + return nil +} + +// AddNode adds a node to the cluster and (potentially) starts a resize job. +func (t *TestCluster) AddNode(saveTopology bool) error { + id := len(t.Clusters) + + c, err := t.addCluster(id, saveTopology) + if err != nil { + return err + } + + // Send NodeJoin event to coordinator. + if id > 0 { + coord := t.Clusters[0] + ev := &pilosa.NodeEvent{ + Event: pilosa.NodeJoin, + Node: c.Node, + } + + if err := coord.ReceiveEvent(ev); err != nil { + return err + } + + // Wait for the AddNode job to finish. + if c.State() != pilosa.ClusterStateNormal { + t.resizeDone = make(chan struct{}) + t.mu.Lock() + t.resizing = true + t.mu.Unlock() + <-t.resizeDone + } + } + + return nil +} + +// WriteTopology writes the given topology to disk. +func (t *TestCluster) WriteTopology(path string, top *pilosa.Topology) error { + if buf, err := proto.Marshal(top.Encode()); err != nil { + return err + } else if err := ioutil.WriteFile(filepath.Join(path, ".topology"), buf, 0666); err != nil { + return err + } + return nil +} + +func (t *TestCluster) addCluster(i int, saveTopology bool) (*pilosa.Cluster, error) { + + id := fmt.Sprintf("node%d", i) + uri := NewURI("http", fmt.Sprintf("host%d", i), uint16(0)) + + node := &pilosa.Node{ + ID: id, + URI: uri, + } + + // add URI to common + //t.common.NodeIDs = append(t.common.NodeIDs, id) + //sort.Sort(t.common.NodeIDs) + + // add node to common + t.common.Nodes = append(t.common.Nodes, node) + + // create node-specific temp directory + path, err := ioutil.TempDir("", fmt.Sprintf("pilosa-cluster-node-%d-", i)) + if err != nil { + return nil, err + } + + // holder + h := pilosa.NewHolder() + h.Path = path + + // cluster + c := pilosa.NewCluster() + c.ReplicaN = 1 + c.Hasher = NewModHasher() + c.Path = path + c.Topology = pilosa.NewTopology() + c.Holder = h + c.MemberSet = pilosa.NewStaticMemberSet(c.Nodes) + c.Node = node + c.Coordinator = t.common.Nodes[0].ID // the first node is the coordinator + c.Broadcaster = t + + // add nodes + if saveTopology { + for _, n := range t.common.Nodes { + c.AddNode(n) + } + } + + // Add this node to the TestCluster. + t.Clusters = append(t.Clusters, c) + + return c, nil +} + +// NewTestCluster returns a new instance of test.Cluster. +func NewTestCluster(n int) *TestCluster { + + tc := &TestCluster{ + common: &commonClusterSettings{}, + } + + // add clusters + for i := 0; i < n; i++ { + _, err := tc.addCluster(i, true) + if err != nil { + panic(err) + } + } + return tc +} + +// SetState sets the state of the cluster on each node. +func (t *TestCluster) SetState(state string) { + for _, c := range t.Clusters { + c.SetState(state) + } +} + +// Open opens all clusters in the test cluster. +func (t *TestCluster) Open() error { + for _, c := range t.Clusters { + if err := c.Open(); err != nil { + return err + } + if err := c.Holder.Open(); err != nil { + return err + } + if err := c.SetNodeState(pilosa.NodeStateReady); err != nil { + return err + } + } + + // Start the listener on the coordinator. + if len(t.Clusters) == 0 { + return nil + } + t.Clusters[0].ListenForJoins() + + return nil +} + +// Close closes all clusters in the test cluster. +func (t *TestCluster) Close() error { + for _, c := range t.Clusters { + err := c.Close() + if err != nil { + return err + } + } + return nil +} + +// TestCluster implements Broadcaster interface. + +// SendSync is a test implemenetation of Broadcaster SendSync method. +func (t *TestCluster) SendSync(pb proto.Message) error { + switch obj := pb.(type) { + case *internal.ClusterStatus: + // Apply the send message to all nodes (except the coordinator). + for _, c := range t.Clusters { + c.MergeClusterStatus(obj) + } + t.mu.RLock() + if obj.State == pilosa.ClusterStateNormal && t.resizing { + close(t.resizeDone) + } + t.mu.RUnlock() + } + + return nil +} + +// SendAsync is a test implemenetation of Broadcaster SendAsync method. +func (t *TestCluster) SendAsync(pb proto.Message) error { + return nil +} + +// SendTo is a test implemenetation of Broadcaster SendTo method. +func (t *TestCluster) SendTo(to *pilosa.Node, pb proto.Message) error { + switch obj := pb.(type) { + case *internal.ResizeInstruction: + err := t.FollowResizeInstruction(obj) + if err != nil { + return err + } + case *internal.ResizeInstructionComplete: + coord := t.clusterByID(to.ID) + go coord.MarkResizeInstructionComplete(obj) + } + return nil +} + +// FollowResizeInstruction is a version of cluster.FollowResizeInstruction used for testing. +func (t *TestCluster) FollowResizeInstruction(instr *internal.ResizeInstruction) error { + + // Prepare the return message. + complete := &internal.ResizeInstructionComplete{ + JobID: instr.JobID, + Node: instr.Node, + Error: "", + } + + // Stop processing on any error. + if err := func() error { + + // figure out which node it was meant for, then call the operation on that cluster + // basically need to mimic this: client.RetrieveSliceFromURI(context.Background(), src.Index, src.Frame, src.View, src.Slice, srcURI) + instrNode := pilosa.DecodeNode(instr.Node) + destCluster := t.clusterByID(instrNode.ID) + + // Sync the schema received in the resize instruction. + if err := destCluster.Holder.ApplySchema(instr.Schema); err != nil { + return err + } + + for _, src := range instr.Sources { + srcNode := pilosa.DecodeNode(src.Node) + srcCluster := t.clusterByID(srcNode.ID) + + srcFragment := srcCluster.Holder.Fragment(src.Index, src.Frame, src.View, src.Slice) + destFragment := destCluster.Holder.Fragment(src.Index, src.Frame, src.View, src.Slice) + if destFragment == nil { + // Create fragment on destination if it doesn't exist. + f := destCluster.Holder.Frame(src.Index, src.Frame) + v := f.View(src.View) + var err error + destFragment, err = v.CreateFragmentIfNotExists(src.Slice) + if err != nil { + return err + } + } + + buf := bytes.NewBuffer(nil) + + bw := bufio.NewWriter(buf) + br := bufio.NewReader(buf) + + // Get the fragment from source. + if _, err := srcFragment.WriteTo(bw); err != nil { + return err + } + + // Flush the bufio.buf to the io.Writer (buf). + bw.Flush() + + // Write data to destination. + if _, err := destFragment.ReadFrom(br); err != nil { + return err + } + } + + return nil + }(); err != nil { + complete.Error = err.Error() + } + + node := pilosa.DecodeNode(instr.Coordinator) + if err := t.SendTo(node, complete); err != nil { + return err + } + + return nil +} diff --git a/test/executor.go b/test/executor.go index 9b4233035..afe244ad7 100644 --- a/test/executor.go +++ b/test/executor.go @@ -34,14 +34,13 @@ func init() { } // NewExecutor returns a new instance of Executor. -// The executor always matches the hostname of the first cluster node. +// The executor always matches the uri of the first cluster node. func NewExecutor(holder *pilosa.Holder, cluster *pilosa.Cluster) *Executor { executor := pilosa.NewExecutor(remoteClient) e := &Executor{Executor: executor} e.Holder = holder e.Cluster = cluster - e.Scheme = cluster.Nodes[0].Scheme - e.Host = cluster.Nodes[0].Host + e.Node = cluster.Nodes[0] return e } diff --git a/test/fragment.go b/test/fragment.go index b208e7a3a..39caa8db2 100644 --- a/test/fragment.go +++ b/test/fragment.go @@ -27,7 +27,7 @@ const SliceWidth = pilosa.SliceWidth // Fragment is a test wrapper for pilosa.Fragment. type Fragment struct { *pilosa.Fragment - RowAttrStore *AttrStore + RowAttrStore pilosa.AttrStore } // NewFragment returns a new instance of Fragment with a temporary path. @@ -43,7 +43,7 @@ func NewFragment(index, frame, view string, slice uint64, cacheType string) *Fra RowAttrStore: MustOpenAttrStore(), } f.Fragment.CacheType = cacheType - f.Fragment.RowAttrStore = f.RowAttrStore.AttrStore + f.Fragment.RowAttrStore = f.RowAttrStore return f } @@ -78,7 +78,7 @@ func (f *Fragment) Reopen() error { f.Fragment = pilosa.NewFragment(path, f.Index(), f.Frame(), f.View(), f.Slice()) f.Fragment.CacheType = cacheType - f.Fragment.RowAttrStore = f.RowAttrStore.AttrStore + f.Fragment.RowAttrStore = f.RowAttrStore if err := f.Open(); err != nil { return err } diff --git a/test/handler.go b/test/handler.go index 4743ea30c..ea44bef9a 100644 --- a/test/handler.go +++ b/test/handler.go @@ -17,7 +17,6 @@ package test import ( "context" "encoding/json" - "errors" "io" "io/ioutil" "net/http" @@ -47,6 +46,8 @@ func NewHandler() *Handler { // Handler test messages can no-op. h.Broadcaster = pilosa.NopBroadcaster + h.SetNormal() + return h } @@ -80,43 +81,36 @@ func NewServer() *Server { if err != nil { panic(err) } - s.Handler.URI = uri // Handler test messages can no-op. s.Handler.Broadcaster = pilosa.NopBroadcaster // Create a default cluster on the handler s.Handler.Cluster = NewCluster(1) - s.Handler.Cluster.Nodes[0].Host = s.Host() + s.Handler.Cluster.Nodes[0].URI = *uri + + s.Handler.Node = s.Handler.Cluster.Nodes[0] return s } -// LocalStatus returns the state of the local node as well as the -// holder (indexes/frames) according to the local node. +// LocalStatus exists so that test.Server implements StatusHandler. func (s *Server) LocalStatus() (proto.Message, error) { - if s.Handler.Holder == nil { - return nil, errors.New("Server.Holder is nil") - } - - ns := internal.NodeStatus{ - Host: s.Handler.Handler.URI.HostPort(), - State: pilosa.NodeStateUp, - Indexes: pilosa.EncodeIndexes(s.Handler.Holder.Indexes()), - } - - // Append Slice list per this Node's indexes - for _, index := range ns.Indexes { - index.Slices = s.Handler.Cluster.OwnsSlices(index.Name, index.MaxSlice, s.Handler.URI.HostPort()) - } - - return &ns, nil + return nil, nil } -// ClusterStatus returns the NodeState for all nodes in the cluster. +// ClusterStatus exists so that test.Server implements StatusHandler. func (s *Server) ClusterStatus() (proto.Message, error) { - // Assuming we are only testing this with one Node - // So just return its status - return s.LocalStatus() + id := "test-node" + uri := pilosa.DefaultURI() + node := &pilosa.Node{ + ID: id, + URI: *uri, + } + return &internal.ClusterStatus{ + ClusterID: "", + State: pilosa.ClusterStateNormal, + Nodes: pilosa.EncodeNodes([]*pilosa.Node{node}), + }, nil } // HandleRemoteStatus just need to implement a nop to complete the Interface @@ -125,12 +119,12 @@ func (s *Server) HandleRemoteStatus(pb proto.Message) error { return nil } // Host returns the hostname of the running server. func (s *Server) Host() string { return MustParseURLHost(s.URL) } -func (s *Server) HostURI() *pilosa.URI { +func (s *Server) HostURI() pilosa.URI { uri, err := pilosa.NewURIFromAddress(s.URL) if err != nil { panic(err) } - return uri + return *uri } // MustParseURLHost parses rawurl and returns the hostname. Panic on error. diff --git a/test/holder.go b/test/holder.go index c9d21d2f2..59402cea0 100644 --- a/test/holder.go +++ b/test/holder.go @@ -20,6 +20,7 @@ import ( "os" "github.com/pilosa/pilosa" + "github.com/pilosa/pilosa/boltdb" ) // Holder is a test wrapper for pilosa.Holder. @@ -38,6 +39,7 @@ func NewHolder() *Holder { h := &Holder{Holder: pilosa.NewHolder()} h.Path = path h.Holder.LogOutput = &h.LogOutput + h.Holder.NewAttrStore = boltdb.NewAttrStore return h } @@ -64,6 +66,7 @@ func (h *Holder) Reopen() error { h.Holder = pilosa.NewHolder() h.Holder.Path = path h.Holder.LogOutput = logOutput + h.Holder.NewAttrStore = boltdb.NewAttrStore if err := h.Holder.Open(); err != nil { return err } diff --git a/test/pilosa.go b/test/pilosa.go index 97c4321c9..86623c239 100644 --- a/test/pilosa.go +++ b/test/pilosa.go @@ -16,140 +16,268 @@ package test import ( "bytes" + "fmt" + "io" "io/ioutil" - "net" - "strconv" + "net/http" + "os" + "strings" "testing" + "github.com/pilosa/pilosa" + "github.com/pilosa/pilosa/boltdb" + "github.com/pilosa/pilosa/gossip" "github.com/pilosa/pilosa/server" "github.com/pkg/errors" ) -func MustNewRunningServer(t *testing.T) *server.Command { - s, err := newServer() - if err != nil { - t.Fatalf("getting new server: %v", err) - } - - err = s.Run() - if err != nil { - t.Fatalf("running new pilosa server: %v", err) - } - return s -} - -func newServer() (*server.Command, error) { - s := server.NewCommand(&bytes.Buffer{}, ioutil.Discard, ioutil.Discard) - - port, err := findPort() - if err != nil { - return nil, errors.Wrap(err, "getting port") - } - s.Config.Bind = "localhost:" + strconv.Itoa(port) - - gport, err := findPort() - if err != nil { - return nil, errors.Wrap(err, "getting gossip port") - } - s.Config.GossipPort = strconv.Itoa(gport) - - s.Config.GossipSeed = "localhost:" + s.Config.GossipPort - s.Config.Cluster.Type = "gossip" - td, err := ioutil.TempDir("", "") - if err != nil { - return nil, errors.Wrap(err, "temp dir") - } - s.Config.DataDir = td - return s, nil -} - -func findPort() (int, error) { - addr, err := net.ResolveTCPAddr("tcp", ":0") - if err != nil { - return 0, errors.Wrap(err, "resolving new port addr") - } - l, err := net.ListenTCP("tcp", addr) - if err != nil { - return 0, errors.Wrap(err, "listening to get new port") - } - port := l.Addr().(*net.TCPAddr).Port - err = l.Close() - if err != nil { - return port, errors.Wrap(err, "closing listener") - } - return port, nil - -} - -func MustFindPort(t *testing.T) int { - port, err := findPort() - if err != nil { - t.Fatalf("allocating new port: %v", err) - } - return port -} - -type Cluster struct { - Servers []*server.Command -} - -func MustNewServerCluster(t *testing.T, size int) *Cluster { - cluster, err := NewServerCluster(size) - if err != nil { - t.Fatalf("new cluster: %v", err) - } - return cluster -} - -// **** below exists to have interface compatibility with cluster-resize, -// **** we can remove when cluster resize gets merged *****************// - -type M struct { +//////////////////////////////////////////////////////////////////////////////////// +// Main represents a test wrapper for main.Main. +type Main struct { *server.Command + Stdin bytes.Buffer Stdout bytes.Buffer Stderr bytes.Buffer } -func MustRunMainWithCluster(t *testing.T, size int) []*M { - cluster := MustNewServerCluster(t, size) - mains := make([]*M, 0) - for _, s := range cluster.Servers { - mains = append(mains, &M{Command: s}) +// NewMain returns a new instance of Main with a temporary data directory and random port. +func NewMain() *Main { + path, err := ioutil.TempDir("", "pilosa-") + if err != nil { + panic(err) } - return mains + + m := &Main{Command: server.NewCommand(os.Stdin, os.Stdout, os.Stderr)} + m.Server.Network = *Network + m.Server.NewAttrStore = NewAttrStore + m.Server.Holder.NewAttrStore = NewAttrStore + 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 + + if testing.Verbose() { + m.Command.Stdout = io.MultiWriter(os.Stdout, m.Command.Stdout) + m.Command.Stderr = io.MultiWriter(os.Stderr, m.Command.Stderr) + } + + return m } -// ***********************************************************************************// +// NewMainWithCluster returns a new instance of Main with clustering enabled. +func NewMainWithCluster(isCoordinator bool) *Main { + m := NewMain() + m.Config.Cluster.Disabled = false + m.Config.Cluster.Coordinator = isCoordinator + return m +} -func NewServerCluster(size int) (cluster *Cluster, err error) { - cluster = &Cluster{ - Servers: make([]*server.Command, size), +// 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) []*Main { + ma, err := runMainWithCluster(size) + if err != nil { + t.Fatalf("new main array with cluster: %v", err) } - hosts := make([]string, size) + return ma +} + +// runMainWithCluster runs an array of *Main where all nodes are +// joined via memberlist (i.e. clustering enabled). +func runMainWithCluster(size int) ([]*Main, error) { + if size == 0 { + return nil, errors.New("cluster must contain at least one node") + } + + mains := make([]*Main, size) + + gossipHost := "localhost" + gossipPort := 0 + var err error + var gossipSeeds = make([]string, size) + for i := 0; i < size; i++ { - s, err := newServer() + m := NewMainWithCluster(i == 0) + + gossipSeeds[i], err = m.RunWithTransport(gossipHost, gossipPort, gossipSeeds[:i]) if err != nil { - return nil, errors.Wrap(err, "new server") + return nil, errors.Wrap(err, "RunWithTransport") } - cluster.Servers[i] = s - hosts[i] = s.Config.Bind - s.Config.GossipSeed = cluster.Servers[0].Config.GossipSeed + mains[i] = m } - for _, s := range cluster.Servers { - s.Config.Cluster.Hosts = hosts - } - for i, s := range cluster.Servers { - err := s.Run() - if err != nil { - for j := 0; j <= i; j++ { - cluster.Servers[j].Close() - } - return nil, errors.Wrapf(err, "starting server %d of %d. Config: %#v", i+1, size, s.Config) - } - } - - return cluster, nil + return mains, nil +} + +// MustRunMain returns a new, running Main. Panic on error. +func MustRunMain() *Main { + m := NewMain() + m.Config.Metric.Diagnostics = false // Disable diagnostics. + if err := m.Run(); err != nil { + panic(err) + } + return m +} + +// Close closes the program and removes the underlying data directory. +func (m *Main) Close() error { + defer os.RemoveAll(m.Config.DataDir) + return m.Command.Close() +} + +// Reopen closes the program and reopens it. +func (m *Main) Reopen() error { + if err := m.Command.Close(); err != nil { + return err + } + + // Create new main with the same config. + config := m.Config + m.Command = server.NewCommand(os.Stdin, os.Stdout, os.Stderr) + m.Server.Network = *Network + m.Server.NewAttrStore = boltdb.NewAttrStore + m.Server.Holder.NewAttrStore = m.Server.NewAttrStore + m.Config = config + + // Run new program. + if err := m.Run(); err != nil { + return err + } + return nil +} + +// RunWithTransport runs Main and returns the dynamically allocated gossip port. +func (m *Main) RunWithTransport(host string, bindPort int, joinSeeds []string) (seed string, err error) { + defer close(m.Started) + + /* + TEST: + - SetupServer (just static settings from config) + - OpenListener (sets Server.Name to use in gossip) + - NewTransport (gossip) + - SetupNetworking (does the gossip or static stuff) - uses Server.Name + - Open server + + PRODUCTION: + - SetupServer (just static settings from config) + - SetupNetworking (does the gossip or static stuff) - calls NewTransport + - Open server - calls OpenListener + */ + + // SetupServer + err = m.SetupServer() + if err != nil { + return seed, err + } + + // Open server listener. + err = m.Server.OpenListener() + if err != nil { + return seed, err + } + + // Open gossip transport to use in SetupServer. + transport, err := gossip.NewTransport(host, bindPort) + if err != nil { + return seed, err + } + m.GossipTransport = transport + + if len(joinSeeds) != 0 { + m.Config.Gossip.Seeds = joinSeeds + } else { + m.Config.Gossip.Seeds = []string{transport.URI.String()} + } + + seed = transport.URI.String() + + // SetupNetworking + err = m.SetupNetworking() + if err != nil { + return seed, err + } + + if err = m.Server.BroadcastReceiver.Start(m.Server); err != nil { + return seed, err + } + + m.Server.Cluster.Static = false + + // Initialize server. + err = m.Server.Open() + if err != nil { + return seed, err + } + + return seed, nil +} + +// URL returns the base URL string for accessing the running program. +func (m *Main) URL() string { return "http://" + m.Server.Addr().String() } + +// Client returns a client to connect to the program. +func (m *Main) Client() *pilosa.InternalHTTPClient { + client, err := pilosa.NewInternalHTTPClient(m.Server.URI.HostPort(), pilosa.GetHTTPClient(nil)) + if err != nil { + panic(err) + } + return client +} + +// Query executes a query against the program through the HTTP API. +func (m *Main) Query(index, rawQuery, query string) (string, error) { + resp := MustDo("POST", m.URL()+fmt.Sprintf("/index/%s/query?", index)+rawQuery, query) + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("invalid status: %d, body=%s", resp.StatusCode, resp.Body) + } + return resp.Body, nil +} + +// CreateDefinition. +func (m *Main) CreateDefinition(index, def, query string) (string, error) { + resp := MustDo("POST", m.URL()+fmt.Sprintf("/index/%s/input-definition/%s", index, def), query) + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("invalid status: %d, body=%s", resp.StatusCode, resp.Body) + } + return resp.Body, nil +} + +func (m *Main) RecalculateCaches() error { + resp := MustDo("POST", fmt.Sprintf("%s/recalculate-caches", m.URL()), "") + if resp.StatusCode != 204 { + return fmt.Errorf("invalid status: %d, body=%s", resp.StatusCode, resp.Body) + } + return nil +} + +//////////////////////////////////////////////////////////////////////////////////// + +// MustDo executes http.Do() with an http.NewRequest(). Panic on error. +func MustDo(method, urlStr string, body string) *httpResponse { + req, err := http.NewRequest(method, urlStr, strings.NewReader(body)) + if err != nil { + panic(err) + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + panic(err) + } + defer resp.Body.Close() + + buf, err := ioutil.ReadAll(resp.Body) + if err != nil { + panic(err) + } + + return &httpResponse{Response: resp, Body: string(buf)} +} + +// httpResponse is a wrapper for http.Response that holds the Body as a string. +type httpResponse struct { + *http.Response + Body string } diff --git a/test/pilosa_test.go b/test/pilosa_test.go index ca2104866..583a244c0 100644 --- a/test/pilosa_test.go +++ b/test/pilosa_test.go @@ -15,28 +15,35 @@ package test_test import ( + "encoding/json" "net/http" "testing" - "encoding/json" - + "github.com/pilosa/pilosa" "github.com/pilosa/pilosa/test" ) func TestNewCluster(t *testing.T) { - cluster := test.MustNewServerCluster(t, 3) - response, err := http.Get("http://" + cluster.Servers[0].Server.Addr().String() + "/status") + numNodes := 3 + cluster := test.MustRunMainWithCluster(t, numNodes) + coordinator := cluster[0].Server.Cluster.Coordinator + for i := 1; i < numNodes; i++ { + if coordi := cluster[i].Server.Cluster.Coordinator; coordi != coordinator { + t.Fatalf("node %d does not have the same coordinator as node 0. '%v' and '%v' respectively", i, coordi, coordinator) + } + } + + response, err := http.Get("http://" + cluster[0].Server.Addr().String() + "/status") if err != nil { t.Fatalf("getting schema: %v", err) } dec := json.NewDecoder(response.Body) body := struct { - Status struct { - Nodes []struct { - Host string - Schema string - State string - } + State string + Nodes []struct { + Scheme string + Host string + Port int } }{} @@ -50,13 +57,11 @@ func TestNewCluster(t *testing.T) { t.Fatalf("encoding: %v", err) } - if len(body.Status.Nodes) != 3 { + if len(body.Nodes) != 3 { t.Fatalf("wrong number of nodes in status: %s", bytes) } - for i, node := range body.Status.Nodes { - if node.State != "UP" { - t.Fatalf("node %d should be up but is %s", i, node.State) - } + if body.State != pilosa.ClusterStateNormal { + t.Fatalf("cluster state should be %s but is %s", pilosa.ClusterStateNormal, body.State) } } diff --git a/uri.go b/uri.go index 21028fe44..beb5060b5 100644 --- a/uri.go +++ b/uri.go @@ -15,11 +15,14 @@ package pilosa import ( + "encoding/json" "errors" "fmt" "regexp" "strconv" "strings" + + "github.com/pilosa/pilosa/internal" ) var schemeRegexp = regexp.MustCompile("^[+a-z]+$") @@ -40,9 +43,9 @@ var addressRegexp = regexp.MustCompile("^(([+a-z]+):\\/\\/)?([0-9a-z.-]+|\\[[:0- // localhost // :10101 type URI struct { - scheme string - host string - port uint16 + scheme string `json:"scheme"` + host string `json:"host"` + port uint16 `json:"port"` } // DefaultURI creates and returns the default URI. @@ -54,6 +57,16 @@ func DefaultURI() *URI { } } +type URIs []URI + +func (u URIs) HostPortStrings() []string { + s := make([]string, len(u)) + for i, a := range u { + s[i] = a.HostPort() + } + return s +} + // NewURIFromHostPort returns a URI with specified host and port. func NewURIFromHostPort(host string, port uint16) (*URI, error) { uri := DefaultURI() @@ -67,11 +80,7 @@ func NewURIFromHostPort(host string, port uint16) (*URI, error) { // NewURIFromAddress parses the passed address and returns a URI. func NewURIFromAddress(address string) (*URI, error) { - uri, err := parseAddress(address) - if err != nil { - return nil, err - } - return uri, err + return parseAddress(address) } // Scheme returns the scheme of this URI. @@ -144,9 +153,7 @@ func (u URI) Equals(other *URI) bool { if other == nil { return false } - return u.scheme == other.scheme && - u.host == other.host && - u.port == other.port + return u == *other } // Path returns URI with path @@ -198,3 +205,82 @@ func parseAddress(address string) (uri *URI, err error) { } return uri, nil } + +// Encode converts o into its internal representation. +func (u URI) Encode() *internal.URI { + return encodeURI(u) +} + +func encodeURI(u URI) *internal.URI { + return &internal.URI{ + Scheme: u.scheme, + Host: u.host, + Port: uint32(u.port), + } +} + +func DecodeURI(i *internal.URI) URI { + return decodeURI(i) +} + +func decodeURI(i *internal.URI) URI { + if i == nil { + return URI{} + } + return URI{ + scheme: i.Scheme, + host: i.Host, + port: uint16(i.Port), + } +} + +func encodeURIs(a []URI) []*internal.URI { + if len(a) == 0 { + return nil + } + other := make([]*internal.URI, len(a)) + for i := range a { + other[i] = encodeURI(a[i]) + } + return other +} + +func decodeURIs(a []*internal.URI) []URI { + if len(a) == 0 { + return nil + } + other := make([]URI, len(a)) + for i := range a { + other[i] = decodeURI(a[i]) + } + return other +} + +// MarshalJSON marshals URI into a JSON-encoded byte slice. +func (u *URI) MarshalJSON() ([]byte, error) { + var output struct { + Scheme string `json:"scheme,omitempty"` + Host string `json:"host,omitempty"` + Port uint16 `json:"port,omitempty"` + } + output.Scheme = u.scheme + output.Host = u.host + output.Port = u.port + + return json.Marshal(output) +} + +func (u *URI) UnmarshalJSON(b []byte) error { + var input struct { + Scheme string `json:"scheme,omitempty"` + Host string `json:"host,omitempty"` + Port uint16 `json:"port,omitempty"` + } + if err := json.Unmarshal(b, &input); err != nil { + return err + } + u.scheme = input.Scheme + u.host = input.Host + u.port = input.Port + return nil +} diff --git a/view.go b/view.go index 5da97d644..dfedebbe3 100644 --- a/view.go +++ b/view.go @@ -18,6 +18,7 @@ import ( "fmt" "io" "io/ioutil" + "log" "os" "path/filepath" "strconv" @@ -62,7 +63,7 @@ type View struct { broadcaster Broadcaster stats StatsClient - RowAttrStore *AttrStore + RowAttrStore AttrStore LogOutput io.Writer } @@ -98,6 +99,12 @@ func (v *View) Path() string { return v.path } // Open opens and initializes the view. func (v *View) Open() error { + + // Never keep a cache for field views. + if strings.HasPrefix(v.name, ViewFieldPrefix) { + v.cacheType = CacheTypeNone + } + if err := func() error { // Ensure the view's path exists. if err := os.MkdirAll(v.path, 0777); err != nil { @@ -119,6 +126,9 @@ func (v *View) Open() error { return nil } +// logger returns a logger instance for the view. +func (v *View) logger() *log.Logger { return log.New(v.LogOutput, "", log.LstdFlags) } + // openFragments opens and initializes the fragments inside the view. func (v *View) openFragments() error { file, err := os.Open(filepath.Join(v.path, "fragments")) @@ -270,6 +280,36 @@ func (v *View) newFragment(path string, slice uint64) *Fragment { return frag } +// DeleteFragment removes the fragment from the view. +func (v *View) DeleteFragment(slice uint64) error { + + fragment := v.fragments[slice] + if fragment == nil { + return ErrFragmentNotFound + } + + v.logger().Printf("delete fragment: (%s/%s/%s) %d", v.index, v.frame, v.name, slice) + + // Close data files before deletion. + if err := fragment.Close(); err != nil { + return err + } + + // Delete fragment file. + if err := os.Remove(fragment.Path()); err != nil { + return err + } + + // Delete fragment cache file. + if err := os.Remove(fragment.CachePath()); err != nil { + v.logger().Printf("no cache file to delete for slice %d", slice) + } + + delete(v.fragments, slice) + + return nil +} + // SetBit sets a bit within the view. func (v *View) SetBit(rowID, columnID uint64) (changed bool, err error) { slice := columnID / SliceWidth diff --git a/view_test.go b/view_test.go index 5f5ce94e2..87ed8e628 100644 --- a/view_test.go +++ b/view_test.go @@ -17,6 +17,7 @@ package pilosa_test import ( "io/ioutil" "os" + "testing" "github.com/pilosa/pilosa" "github.com/pilosa/pilosa/test" @@ -25,22 +26,21 @@ import ( // View is a test wrapper for pilosa.View. type View struct { *pilosa.View - RowAttrStore *test.AttrStore + RowAttrStore pilosa.AttrStore } // NewView returns a new instance of View with a temporary path. func NewView(index, frame, name string) *View { - file, err := ioutil.TempFile("", "pilosa-view-") + path, err := ioutil.TempDir("", "pilosa-view-") if err != nil { panic(err) } - file.Close() v := &View{ - View: pilosa.NewView(file.Name(), index, frame, name, pilosa.DefaultCacheSize), + View: pilosa.NewView(path, index, frame, name, pilosa.DefaultCacheSize), RowAttrStore: test.MustOpenAttrStore(), } - v.View.RowAttrStore = v.RowAttrStore.AttrStore + v.View.RowAttrStore = v.RowAttrStore return v } @@ -68,7 +68,7 @@ func (v *View) Reopen() error { } v.View = pilosa.NewView(path, v.Index(), v.Frame(), v.Name(), pilosa.DefaultCacheSize) - v.View.RowAttrStore = v.RowAttrStore.AttrStore + v.View.RowAttrStore = v.RowAttrStore if err := v.Open(); err != nil { return err } @@ -93,3 +93,36 @@ func (v *View) MustClearBits(rowID uint64, columnIDs ...uint64) { } } } + +// Ensure view can open and retrieve a fragment. +func TestView_DeleteFragment(t *testing.T) { + v := MustOpenView("i", "f", "v") + defer v.Close() + + slice := uint64(9) + + // Create fragment. + fragment, err := v.CreateFragmentIfNotExists(slice) + if err != nil { + t.Fatal(err) + } else if fragment == nil { + t.Fatal("expected fragment") + } + + err = v.DeleteFragment(slice) + if err != nil { + t.Fatal(err) + } + + if v.Fragment(slice) != nil { + t.Fatal("fragment still exists in view") + } + + // Recreate fragment with same slice, verify that the old fragment was not reused. + fragment2, err := v.CreateFragmentIfNotExists(slice) + if err != nil { + t.Fatal(err) + } else if fragment == fragment2 { + t.Fatal("failed to create new fragment") + } +}