├── .gitignore ├── .travis.yml ├── CHANGES.md ├── LICENSE ├── Makefile ├── README.md ├── bootstrap.sh ├── discovery ├── jsonfile │ ├── lib.go │ └── lib_test.go ├── statichosts │ └── lib.go ├── test │ └── invalidhosts.json └── types.go ├── errors.go ├── events ├── events.go ├── events_test.go ├── mock_event_listener_test.go └── test │ └── mocks │ └── event_listener.go ├── examples ├── keyvalue │ ├── .gitignore │ ├── README.md │ ├── gen-go │ │ └── keyvalue │ │ │ ├── ringpop-keyvalue.go │ │ │ └── tchan-keyvalue.go │ ├── keyvalue.thrift │ └── main.go ├── ping-json │ ├── .gitignore │ ├── README.md │ └── main.go ├── ping-thrift-gen │ ├── .gitignore │ ├── README.md │ ├── gen-go │ │ └── ping │ │ │ ├── GoUnusedProtection__.go │ │ │ ├── ping-consts.go │ │ │ ├── ping.go │ │ │ ├── ringpop-ping.go │ │ │ └── tchan-ping.go │ ├── main.go │ └── ping.thrift ├── ping-thrift │ ├── .gitignore │ ├── README.md │ ├── gen-go │ │ └── ping │ │ │ ├── GoUnusedProtection__.go │ │ │ ├── ping-consts.go │ │ │ ├── ping.go │ │ │ └── tchan-ping.go │ ├── main.go │ └── ping.thrift └── role-labels │ ├── .gitignore │ ├── README.md │ ├── gen-go │ └── role │ │ ├── GoUnusedProtection__.go │ │ ├── ringpop-role.go │ │ ├── role-consts.go │ │ ├── role.go │ │ └── tchan-role.go │ ├── main.go │ └── role.thrift ├── forward ├── events.go ├── forwarder.go ├── forwarder_test.go ├── mock_sender_test.go ├── request_sender.go └── request_sender_test.go ├── go.mod ├── go.sum ├── handlers.go ├── hashring ├── checksummer.go ├── checksummer_test.go ├── hashring.go ├── hashring_test.go ├── rbtree.go ├── rbtree_test.go └── util_test.go ├── logging ├── default.go ├── default_test.go ├── facility.go ├── facility_test.go ├── level.go ├── level_test.go ├── named.go ├── named_test.go └── nologger.go ├── membership ├── events.go └── interface.go ├── options.go ├── options_test.go ├── replica ├── replicator.go └── replicator_test.go ├── ringpop.go ├── ringpop.thrift-gen ├── ringpop_test.go ├── router ├── router.go └── router_test.go ├── scripts ├── go-get-version.sh ├── lint │ ├── Makefile │ ├── lint-excludes │ ├── lint-warn │ ├── run-vet │ └── test │ │ ├── .gitignore │ │ ├── lint-warn.t │ │ ├── test-excludes │ │ ├── test-lint-all-fail.log │ │ ├── test-lint-mix.log │ │ └── test-lint-ok.log ├── pre-commit ├── testpop │ ├── statter.go │ └── testpop.go └── travis │ ├── .gitignore │ ├── get-cram.sh │ ├── get-thrift-gen.sh │ └── get-thrift.sh ├── shared ├── interfaces.go └── shared.go ├── stats_handler.go ├── swim ├── disseminator.go ├── disseminator_test.go ├── events.go ├── gossip.go ├── gossip_test.go ├── handlers.go ├── handlers_test.go ├── heal_partition.go ├── heal_partition_test.go ├── heal_via_discover_provider.go ├── join_delayer.go ├── join_delayer_test.go ├── join_handler.go ├── join_sender.go ├── join_test.go ├── labels.go ├── labels_test.go ├── member.go ├── member_doc_test.go ├── member_predicate.go ├── member_predicate_test.go ├── member_test.go ├── memberlist.go ├── memberlist_iter.go ├── memberlist_iter_test.go ├── memberlist_test.go ├── mock_self_evict_hook_test.go ├── node.go ├── node_bootstrap_test.go ├── node_test.go ├── ping_handler.go ├── ping_request_handler.go ├── ping_request_sender.go ├── ping_request_test.go ├── ping_sender.go ├── ping_test.go ├── schedule.go ├── self_evict.go ├── self_evict_test.go ├── state_transitions.go ├── state_transitions_test.go ├── stats.go ├── stats_test.go └── utils_test.go ├── test ├── .gitignore ├── gen-testfiles ├── go-test-prettify ├── lib.sh ├── mocks │ ├── README │ ├── client_factory.go │ ├── logger.go │ ├── logger │ │ └── logger.go │ ├── ringpop.go │ ├── stats_reporter.go │ ├── swim_node.go │ └── t_chan_client.go ├── remoteservice │ ├── .gitignore │ ├── remoteservice.thrift │ ├── remoteservice_test.go │ ├── shared.thrift │ └── unused.thrift ├── run-example-tests ├── run-integration-tests ├── thrift │ ├── pingpong.thrift │ └── pingpong │ │ ├── GoUnusedProtection__.go │ │ ├── mock_t_chan_ping_pong.go │ │ ├── pingpong-consts.go │ │ ├── pingpong.go │ │ └── tchan-pingpong.go ├── travis └── update-coveralls ├── util.go ├── util ├── util.go └── util_test.go └── utils_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | testpop 3 | hosts.json 4 | coverage.out 5 | lint.log 6 | /vendor/ 7 | /_venv/ 8 | README.md.err 9 | tick-cluster.log 10 | .idea/ 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - stable 4 | 5 | addons: 6 | apt: 7 | packages: 8 | - python-virtualenv 9 | 10 | go_import_path: github.com/temporalio/ringpop-go 11 | 12 | install: 13 | - go get -u github.com/Masterminds/glide 14 | - go get github.com/axw/gocov/gocov 15 | - go get github.com/mattn/goveralls 16 | - go get golang.org/x/tools/cmd/cover 17 | - ./scripts/travis/get-thrift.sh 18 | - ./scripts/travis/get-thrift-gen.sh 19 | - ./scripts/travis/get-cram.sh 20 | - npm install -g tcurl@v4.22.2 21 | - make setup 22 | 23 | env: 24 | - RUN="make test-unit test-race test-examples lint coveralls" 25 | - RUN="make test-integration" 26 | 27 | matrix: 28 | fast_finish: true 29 | 30 | script: 31 | - test/travis 32 | 33 | cache: 34 | directories: 35 | - $HOME/.glide/cache 36 | - _venv 37 | 38 | before_cache: # glide touches ORIG_HEAD on `glide install` 39 | - find $HOME/.glide/cache -name ORIG_HEAD -exec rm {} \; 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Uber Technologies, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean clean-common clean-mocks coveralls testpop lint mocks out setup test test-integration test-unit test-race 2 | 3 | SHELL = /bin/bash 4 | 5 | export GO15VENDOREXPERIMENT=1 6 | NOVENDOR = $(shell GO15VENDOREXPERIMENT=1 glide novendor) 7 | 8 | export PATH := $(shell pwd)/scripts/travis/thrift-release/linux-x86_64:$(PATH) 9 | export PATH := $(shell pwd)/scripts/travis/thrift-gen-release/linux-x86_64:$(PATH) 10 | 11 | export ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 12 | export GOPKG=$(shell go list) 13 | 14 | 15 | # Automatically gather packages 16 | PKGS = $(shell find . -maxdepth 3 -type d \ 17 | ! -path '*/.git*' \ 18 | ! -path '*/_*' \ 19 | ! -path '*/vendor*' \ 20 | ! -path '*/test*' \ 21 | ! -path '*/gen-go*' \ 22 | ) 23 | 24 | out: test 25 | 26 | clean: 27 | rm -f testpop 28 | 29 | clean-common: 30 | rm -rf test/ringpop-common 31 | 32 | clean-mocks: 33 | rm -f test/mocks/*.go forward/mock_*.go 34 | rm -rf test/thrift/pingpong/ 35 | 36 | coveralls: 37 | test/update-coveralls 38 | 39 | lint: 40 | @:>lint.log 41 | 42 | @-golint ./... | grep -Ev '(^vendor|test|gen-go)/' | tee -a lint.log 43 | 44 | @for pkg in $(PKGS); do \ 45 | scripts/lint/run-vet "$$pkg" | tee -a lint.log; \ 46 | done; 47 | 48 | @[ ! -s lint.log ] 49 | @rm -f lint.log 50 | 51 | mocks: 52 | test/gen-testfiles 53 | 54 | dev_deps: 55 | command -v pip >/dev/null 2>&1 || { echo >&2 "'pip' required but not found. Please install. Aborting."; exit 1; } 56 | 57 | pip install cram 58 | command -v cram >/dev/null 2>&1 || { echo >&2 "'cram' required but not found. Please install. Aborting."; exit 1; } 59 | 60 | pip install virtualenv 61 | command -v virtualenv >/dev/null 2>&1 || { echo >&2 "'virtualenv' required but not found. Please install. Aborting."; exit 1; } 62 | 63 | pip install npm 64 | command -v npm >/dev/null 2>&1 || { echo >&2 "'npm' required but not found. Please install. Aborting."; exit 1; } 65 | 66 | npm install -g tcurl@4.22.0 67 | command -v tcurl >/dev/null 2>&1 || { echo >&2 "'tcurl' installed but not found on path. Aborting."; exit 1; } 68 | 69 | go get -u github.com/temporalio/tchannel-go/thrift/thrift-gen 70 | command -v thrift-gen >/dev/null 2>&1 || { echo >&2 "'thrift-gen' installed but not found on path. Aborting."; exit 1; } 71 | 72 | go get -u golang.org/x/lint/golint... 73 | 74 | # Thrift commit matches glide version 75 | go get -u github.com/apache/thrift@2a93df80f27739ccabb5b885cb12a8dc7595ecdf 76 | command -v thrift >/dev/null 2>&1 || { echo >&2 "'thrift' installed but not found on path. Aborting."; exit 1; } 77 | 78 | go get -u github.com/vektra/mockery/@130a05e 79 | command -v mockery >/dev/null 2>&1 || { echo >&2 "'mockery' installed but not found on path. Aborting."; exit 1; } 80 | 81 | 82 | setup: dev_deps 83 | @if ! which thrift | grep -q /; then \ 84 | echo "thrift not in PATH. (brew install thrift?)" >&2; \ 85 | exit 1; \ 86 | fi 87 | 88 | ln -sf ../../scripts/pre-commit .git/hooks/pre-commit 89 | 90 | # lint should happen after test-unit and test-examples as it relies on objects 91 | # being created during these phases 92 | test: test-unit test-race test-examples lint test-integration 93 | 94 | test-integration: 95 | test/run-integration-tests 96 | 97 | test-unit: 98 | go generate $(NOVENDOR) 99 | test/go-test-prettify $(NOVENDOR) 100 | 101 | test-examples: _venv/bin/cram 102 | . _venv/bin/activate && ./test/run-example-tests 103 | 104 | test-race: 105 | go generate $(NOVENDOR) 106 | test/go-test-prettify -race $(NOVENDOR) 107 | 108 | _venv/bin/cram: 109 | ./scripts/travis/get-cram.sh 110 | 111 | testpop: clean 112 | go build ./scripts/testpop/ 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ringpop-go [![Build Status](https://travis-ci.org/uber/ringpop-go.svg?branch=master)](https://travis-ci.org/uber/ringpop-go) [![Coverage Status](https://coveralls.io/repos/uber/ringpop-go/badge.svg?branch=master&service=github)](https://coveralls.io/github/uber/ringpop-go?branch=master) 2 | ========== 3 | 4 | **(This project is no longer under active development. Temporal will eventually deprecate usage of Ringpop.)** 5 | 6 | Ringpop is a library that brings cooperation and coordination to distributed 7 | applications ([see Uber announcement blogpost](https://eng.uber.com/ringpop-open-source-nodejs-library/)). It maintains a consistent hash ring on top of a membership 8 | protocol and provides request forwarding as a routing convenience. It can be 9 | used to shard your application in a way that's scalable and fault tolerant. 10 | 11 | Getting started 12 | --------------- 13 | 14 | To install ringpop-go: 15 | 16 | ``` 17 | go get github.com/temporalio/ringpop-go 18 | ``` 19 | 20 | Developing 21 | ---------- 22 | 23 | First make certain that `thrift` v0.9.3 24 | (OSX: `brew install https://gist.githubusercontent.com/chrislusf/8b4e7c19551ba220232f037b43c0eaf3/raw/01465b867b8ef9af7c7c3fa830c83666c825122d/thrift.rb`) and `glide` are 25 | in your path (above). Then, 26 | 27 | ``` 28 | make setup 29 | ``` 30 | 31 | to install remaining golang dependencies and install the pre-commit hook. 32 | 33 | Finally, run the tests by doing (note ensure you have enough file descriptors using `ulimit -n` - atleast 8192 reccomended.): 34 | 35 | ``` 36 | make test 37 | ``` 38 | 39 | Documentation 40 | -------------- 41 | 42 | Interested in where to go from here? Read the docs at 43 | [ringpop.readthedocs.org](https://ringpop.readthedocs.org) 44 | -------------------------------------------------------------------------------- /bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Same as `go get` but you can specify a git commit ref. 4 | # 5 | set -e 6 | 7 | 8 | read -p "This git resets, cleans, builds and tests both unit tests and examples. Are you sure?" -n 1 -r 9 | echo # (optional) move to a new line 10 | if [[ $REPLY =~ ^[Yy]$ ]] 11 | then 12 | git reset --hard 13 | git clean -xffd 14 | 15 | make setup && make out 16 | fi -------------------------------------------------------------------------------- /discovery/jsonfile/lib.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package jsonfile 22 | 23 | import ( 24 | "encoding/json" 25 | "io/ioutil" 26 | ) 27 | 28 | // HostList is a DiscoverProvider that reads a list of hosts from a JSON file. 29 | type HostList struct { 30 | filePath string 31 | } 32 | 33 | // Hosts reads hosts from a JSON file. 34 | func (p *HostList) Hosts() ([]string, error) { 35 | var hosts []string 36 | 37 | data, err := ioutil.ReadFile(p.filePath) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | err = json.Unmarshal(data, &hosts) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | return hosts, nil 48 | } 49 | 50 | // New creates a provider which reads the JSON file for the hosts. 51 | func New(filePath string) *HostList { 52 | return &HostList{filePath} 53 | } 54 | -------------------------------------------------------------------------------- /discovery/jsonfile/lib_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package jsonfile 22 | 23 | import ( 24 | "io/ioutil" 25 | "os" 26 | 27 | "testing" 28 | 29 | "github.com/stretchr/testify/assert" 30 | ) 31 | 32 | func TestInvalidJSONFile(t *testing.T) { 33 | provider := New("./invalid") 34 | _, err := provider.Hosts() 35 | assert.Error(t, err, "open /invalid: no such file or directory", "should fail to open file") 36 | } 37 | 38 | func TestMalformedJSONFile(t *testing.T) { 39 | provider := New("../test/invalidhosts.json") 40 | _, err := provider.Hosts() 41 | assert.Error(t, err, "invalid character 'T' looking for beginning of value", "should fail to unmarhsal JSON") 42 | } 43 | 44 | func TestNiceJSONFile(t *testing.T) { 45 | content := []byte("[\"127.0.0.1:3000\", \"127.0.0.1:3042\"]") 46 | 47 | tmpfile, err := ioutil.TempFile("", "jsonfiletest") 48 | assert.NoError(t, err, "failed to create a temp file") 49 | defer os.Remove(tmpfile.Name()) 50 | 51 | _, err = tmpfile.Write(content) 52 | assert.NoError(t, err, "failed to write contents to the temp file") 53 | 54 | err = tmpfile.Close() 55 | assert.NoError(t, err, "failed to write contents to the temp file") 56 | 57 | // Now actually test the DiscoverProvider:Hosts() will return the two 58 | provider := New(tmpfile.Name()) 59 | res, err := provider.Hosts() 60 | assert.NoError(t, err, "hosts call failed") 61 | assert.Equal(t, []string{"127.0.0.1:3000", "127.0.0.1:3042"}, res) 62 | } 63 | -------------------------------------------------------------------------------- /discovery/statichosts/lib.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package statichosts 22 | 23 | // HostList is a static list of hosts to bootstrap from. 24 | type HostList struct { 25 | hosts []string 26 | } 27 | 28 | // Hosts just returns the static list of hosts that was provided to this struct 29 | // on construction. 30 | func (p *HostList) Hosts() ([]string, error) { 31 | return p.hosts, nil 32 | } 33 | 34 | // New creates a provider with a static list of hosts. 35 | func New(hosts ...string) *HostList { 36 | return &HostList{hosts} 37 | } 38 | -------------------------------------------------------------------------------- /discovery/test/invalidhosts.json: -------------------------------------------------------------------------------- 1 | This is a malformed JSON file for testing 2 | 3 | "192.0.2.2:1", 4 | "192.0.2.2:2", 5 | ] 6 | -------------------------------------------------------------------------------- /discovery/types.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package discovery 22 | 23 | // A DiscoverProvider is a interface that provides a list of peers for a node 24 | // to bootstrap from. 25 | type DiscoverProvider interface { 26 | Hosts() ([]string, error) 27 | } 28 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package ringpop 2 | 3 | import "errors" 4 | 5 | var ( 6 | // ErrNotBootstrapped is returned by public methods which require the ring to 7 | // be bootstrapped before they can operate correctly. 8 | ErrNotBootstrapped = errors.New("ringpop is not bootstrapped") 9 | 10 | // ErrEphemeralAddress is returned by the address resolver if TChannel is 11 | // using port 0 and is not listening (and thus has not been assigned a port by 12 | // the OS). 13 | ErrEphemeralAddress = errors.New("unable to resolve this node's address from channel that is not yet listening") 14 | 15 | // ErrChannelNotListening is returned on bootstrap if TChannel is not 16 | // listening. 17 | ErrChannelNotListening = errors.New("tchannel is not listening") 18 | 19 | // ErrInvalidIdentity is returned when the identity value is invalid. 20 | ErrInvalidIdentity = errors.New("a hostport is not valid as an identity") 21 | ) 22 | -------------------------------------------------------------------------------- /events/mock_event_listener_test.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import "github.com/stretchr/testify/mock" 4 | 5 | type MockEventListener struct { 6 | mock.Mock 7 | } 8 | 9 | // HandleEvent provides a mock function with given fields: event 10 | func (_m *MockEventListener) HandleEvent(event Event) { 11 | _m.Called(event) 12 | } 13 | -------------------------------------------------------------------------------- /events/test/mocks/event_listener.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "github.com/stretchr/testify/mock" 5 | "github.com/temporalio/ringpop-go/events" 6 | ) 7 | 8 | type EventListener struct { 9 | mock.Mock 10 | } 11 | 12 | // HandleEvent provides a mock function with given fields: event 13 | func (_m *EventListener) HandleEvent(event events.Event) { 14 | _m.Called(event) 15 | } 16 | -------------------------------------------------------------------------------- /examples/keyvalue/.gitignore: -------------------------------------------------------------------------------- 1 | hosts.json 2 | keyvalue 3 | -------------------------------------------------------------------------------- /examples/keyvalue/README.md: -------------------------------------------------------------------------------- 1 | This application shows a complex example where a node makes requests to itself from a sharded endpoint. 2 | 3 | Note: this file can be [cram][3]-executed using `make test-examples`. That's why some of the example outputs below are a bit unusual. 4 | 5 | # Running the example 6 | 7 | All commands are relative to this directory: 8 | 9 | $ cd ${TESTDIR} # examples/keyvalue 10 | 11 | (optional, the files are already included) Generate the thrift code: 12 | 13 | $ go generate 14 | 15 | Build the example binary: 16 | 17 | $ go build 18 | 19 | Start a custer of 5 nodes using [tick-cluster][1]: 20 | 21 | $ tick-cluster.js --interface=127.0.0.1 -n 5 keyvalue &> tick-cluster.log & 22 | $ sleep 5 23 | 24 | Set some reference keys that are sharded around the cluster using [tcurl][2]: 25 | 26 | $ tcurl keyvalue -P hosts.json --thrift ./keyvalue.thrift KeyValueService::Set '{"key":"127.0.0.1:30010", "value": "foo"}' # key 127.0.0.1:30010 is the first replica point on the node with identity `127.0.0.1:3001`. The identity is comprised of the host:port + replica index eg '127.0.0.1:3001' + '0' 27 | {"ok":true,"head":{},"headers":{"as":"thrift"},"trace":"*"} (glob) 28 | $ tcurl keyvalue -P hosts.json --thrift ./keyvalue.thrift KeyValueService::Set '{"key":"127.0.0.1:30020", "value": "bar"}' 29 | {"ok":true,"head":{},"headers":{"as":"thrift"},"trace":"*"} (glob) 30 | $ tcurl keyvalue -P hosts.json --thrift ./keyvalue.thrift KeyValueService::Set '{"key":"127.0.0.1:30040", "value": "baz"}' 31 | {"ok":true,"head":{},"headers":{"as":"thrift"},"trace":"*"} (glob) 32 | 33 | Use GetAll on the node that should answer to make sure self requests work if the first call is not forwarded 34 | 35 | $ tcurl keyvalue -p 127.0.0.1:3004 --thrift ./keyvalue.thrift KeyValueService::GetAll '{"keys":["127.0.0.1:30010","127.0.0.1:30020"]}' 36 | {"ok":true,"head":{},"body":["foo","bar"],"headers":{"as":"thrift"},"trace":"*"} (glob) 37 | 38 | Use GetAll on the node that should not answer to make sure self requests work after forwarding 39 | 40 | $ tcurl keyvalue -p 127.0.0.1:3000 --thrift ./keyvalue.thrift KeyValueService::GetAll '{"keys":["127.0.0.1:30010","127.0.0.1:30020"]}' 41 | {"ok":true,"head":{},"body":["foo","bar"],"headers":{"as":"thrift"},"trace":"*"} (glob) 42 | 43 | Now do the same but also with a key stored on the node executing the fanout 44 | 45 | $ tcurl keyvalue -p 127.0.0.1:3004 --thrift ./keyvalue.thrift KeyValueService::GetAll '{"keys":["127.0.0.1:30010","127.0.0.1:30020","127.0.0.1:30040"]}' 46 | {"ok":true,"head":{},"body":["foo","bar","baz"],"headers":{"as":"thrift"},"trace":"*"} (glob) 47 | $ tcurl keyvalue -p 127.0.0.1:3000 --thrift ./keyvalue.thrift KeyValueService::GetAll '{"keys":["127.0.0.1:30010","127.0.0.1:30020","127.0.0.1:30040"]}' 48 | {"ok":true,"head":{},"body":["foo","bar","baz"],"headers":{"as":"thrift"},"trace":"*"} (glob) 49 | 50 | And to top it off we will now lookup the local key first 51 | 52 | $ tcurl keyvalue -p 127.0.0.1:3004 --thrift ./keyvalue.thrift KeyValueService::GetAll '{"keys":["127.0.0.1:30040","127.0.0.1:30010","127.0.0.1:30020"]}' 53 | {"ok":true,"head":{},"body":["baz","foo","bar"],"headers":{"as":"thrift"},"trace":"*"} (glob) 54 | $ tcurl keyvalue -p 127.0.0.1:3000 --thrift ./keyvalue.thrift KeyValueService::GetAll '{"keys":["127.0.0.1:30040","127.0.0.1:30010","127.0.0.1:30020"]}' 55 | {"ok":true,"head":{},"body":["baz","foo","bar"],"headers":{"as":"thrift"},"trace":"*"} (glob) 56 | 57 | In the end you should kill tick cluster via: 58 | 59 | $ kill %1 60 | 61 | [1]:https://github.com/uber/ringpop-common/ 62 | [2]:https://github.com/uber/tcurl 63 | [3]:https://pypi.python.org/pypi/cram 64 | -------------------------------------------------------------------------------- /examples/keyvalue/keyvalue.thrift: -------------------------------------------------------------------------------- 1 | service KeyValueService { 2 | void Set(1: string key, 2: string value) 3 | string Get(1: string key) 4 | list GetAll (1: list keys) 5 | } 6 | -------------------------------------------------------------------------------- /examples/keyvalue/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | //go:generate thrift-gen --generateThrift --outputDir gen-go --template github.com/temporalio/ringpop-go/ringpop.thrift-gen --inputFile keyvalue.thrift 22 | 23 | package main 24 | 25 | import ( 26 | "flag" 27 | "sync" 28 | 29 | log "github.com/sirupsen/logrus" 30 | "github.com/uber-common/bark" 31 | "github.com/temporalio/ringpop-go" 32 | "github.com/temporalio/ringpop-go/discovery/jsonfile" 33 | gen "github.com/temporalio/ringpop-go/examples/keyvalue/gen-go/keyvalue" 34 | "github.com/temporalio/ringpop-go/swim" 35 | "github.com/temporalio/tchannel-go" 36 | "github.com/temporalio/tchannel-go/thrift" 37 | ) 38 | 39 | var ( 40 | hostport = flag.String("listen", "127.0.0.1:3000", "hostport to start service on") 41 | hostfile = flag.String("hosts", "./hosts.json", "path to hosts file") 42 | ) 43 | 44 | type worker struct { 45 | adapter gen.TChanKeyValueService 46 | 47 | memoryLock sync.RWMutex 48 | memory map[string]string 49 | } 50 | 51 | func (w *worker) Set(ctx thrift.Context, key string, value string) error { 52 | log.Printf("setting key %q to %q", key, value) 53 | 54 | w.memoryLock.Lock() 55 | defer w.memoryLock.Unlock() 56 | 57 | if w.memory == nil { 58 | w.memory = make(map[string]string) 59 | } 60 | w.memory[key] = value 61 | 62 | return nil 63 | } 64 | 65 | func (w *worker) Get(ctx thrift.Context, key string) (string, error) { 66 | log.Printf("getting key %q", key) 67 | 68 | w.memoryLock.RLock() 69 | defer w.memoryLock.RUnlock() 70 | 71 | value := w.memory[key] 72 | 73 | return value, nil 74 | } 75 | 76 | func (w *worker) GetAll(ctx thrift.Context, keys []string) ([]string, error) { 77 | log.Printf("getting keys: %v", keys) 78 | 79 | m := make([]string, 0, len(keys)) 80 | for _, key := range keys { 81 | // sharded self call 82 | value, err := w.adapter.Get(ctx, key) 83 | if err != nil { 84 | return nil, err 85 | } 86 | m = append(m, value) 87 | } 88 | return m, nil 89 | } 90 | 91 | func main() { 92 | flag.Parse() 93 | 94 | ch, err := tchannel.NewChannel("keyvalue", nil) 95 | if err != nil { 96 | log.Fatalf("channel did not create successfully: %v", err) 97 | } 98 | 99 | logger := log.StandardLogger() 100 | 101 | rp, err := ringpop.New("keyvalue", 102 | ringpop.Channel(ch), 103 | ringpop.Logger(bark.NewLoggerFromLogrus(logger)), 104 | ) 105 | if err != nil { 106 | log.Fatalf("Unable to create Ringpop: %v", err) 107 | } 108 | 109 | worker := &worker{} 110 | adapter, _ := gen.NewRingpopKeyValueServiceAdapter(worker, rp, ch, 111 | gen.KeyValueServiceConfiguration{ 112 | Get: &gen.KeyValueServiceGetConfiguration{ 113 | Key: func(ctx thrift.Context, key string) (shardKey string, err error) { 114 | return key, nil 115 | }, 116 | }, 117 | 118 | GetAll: &gen.KeyValueServiceGetAllConfiguration{ 119 | Key: func(ctx thrift.Context, keys []string) (shardKey string, err error) { 120 | // use the node listening on 127.0.0.1:3004 as the node that should answer 121 | return "127.0.0.1:30040", nil 122 | }, 123 | }, 124 | 125 | Set: &gen.KeyValueServiceSetConfiguration{ 126 | Key: func(ctx thrift.Context, key string, value string) (shardKey string, err error) { 127 | return key, nil 128 | }, 129 | }, 130 | }, 131 | ) 132 | worker.adapter = adapter 133 | 134 | // register sharded endpoits 135 | thrift.NewServer(ch).Register(gen.NewTChanKeyValueServiceServer(adapter)) 136 | 137 | if err := ch.ListenAndServe(*hostport); err != nil { 138 | log.Fatalf("could not listen on given hostport: %v", err) 139 | } 140 | 141 | bootstrapOpts := &swim.BootstrapOptions{ 142 | DiscoverProvider: jsonfile.New(*hostfile), 143 | } 144 | if _, err := rp.Bootstrap(bootstrapOpts); err != nil { 145 | log.Fatalf("ringpop bootstrap failed: %v", err) 146 | } 147 | 148 | select {} 149 | } 150 | -------------------------------------------------------------------------------- /examples/ping-json/.gitignore: -------------------------------------------------------------------------------- 1 | ping-json 2 | /vendor 3 | -------------------------------------------------------------------------------- /examples/ping-json/README.md: -------------------------------------------------------------------------------- 1 | A simple ping-pong service implementation that that integrates ringpop to forward requests between nodes. 2 | 3 | Note: this file can be [cram][3]-executed using `make test-examples`. That's why some of the example outputs below are a bit unusual. 4 | 5 | # Running the example 6 | 7 | All commands are relative to this directory: 8 | 9 | $ cd ${TESTDIR} # examples/ping-json 10 | $ go build 11 | 12 | Start a custer of 5 nodes using [tick-cluster][1]: 13 | 14 | $ tick-cluster.js --interface=127.0.0.1 -n 5 ping-json &> tick-cluster.log & 15 | $ sleep 5 16 | 17 | Lookup the node `my_key` key belongs to using [tcurl][2]: 18 | 19 | $ tcurl ringpop -P hosts.json /admin/lookup '{"key": "my_key"}' 20 | {"ok":true,"head":null,"body":{"dest":"127.0.0.1:300?"},"headers":{"as":"json"},"trace":"*"} (glob) 21 | 22 | Call the `/ping` endpoint (multiple times) and see the request being forwarded. Each request is sent to a random node in the cluster because of the `-P hosts.json` argument--but is always handled by the node owning the key. This can be seen in the `from` field of the response: 23 | 24 | $ tcurl pingchannel -P hosts.json /ping '{"key": "my_key"}' 25 | {"ok":true,"head":null,"body":{"message":"Hello, world!","from":"127.0.0.1:300?","pheader":""},"headers":{"as":"json"},"trace":"*"} (glob) 26 | 27 | Optionally, set the `p` header. This value will be forwarded together with the request body to the node owning the key. Its value is returned in the response body in the `pheader` field: 28 | 29 | $ tcurl pingchannel -P hosts.json /ping '{"key": "my_key"}' --headers '{"p": "my_header"}' 30 | {"ok":true,"head":null,"body":{"message":"Hello, world!","from":"127.0.0.1:300?","pheader":"my_header"},"headers":{"as":"json"},"trace":"*"} (glob) 31 | 32 | $ kill %1 33 | 34 | [1]:https://github.com/uber/ringpop-common/ 35 | [2]:https://github.com/uber/tcurl 36 | [3]:https://pypi.python.org/pypi/cram 37 | -------------------------------------------------------------------------------- /examples/ping-json/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | import ( 24 | json2 "encoding/json" 25 | "flag" 26 | 27 | log "github.com/sirupsen/logrus" 28 | "github.com/uber-common/bark" 29 | "github.com/temporalio/ringpop-go" 30 | "github.com/temporalio/ringpop-go/discovery/jsonfile" 31 | "github.com/temporalio/ringpop-go/forward" 32 | "github.com/temporalio/ringpop-go/swim" 33 | "github.com/temporalio/tchannel-go" 34 | "github.com/temporalio/tchannel-go/json" 35 | "golang.org/x/net/context" 36 | ) 37 | 38 | var ( 39 | hostport = flag.String("listen", "127.0.0.1:3000", "hostport to start ringpop on") 40 | hostfile = flag.String("hosts", "./hosts.json", "path to hosts file") 41 | ) 42 | 43 | type worker struct { 44 | ringpop *ringpop.Ringpop 45 | channel *tchannel.Channel 46 | logger *log.Logger 47 | } 48 | 49 | // Ping is a request 50 | type Ping struct { 51 | Key string `json:"key"` 52 | } 53 | 54 | // Bytes returns the byets for a ping 55 | func (p Ping) Bytes() []byte { 56 | data, _ := json2.Marshal(p) 57 | return data 58 | } 59 | 60 | // Pong is a ping response 61 | type Pong struct { 62 | Message string `json:"message"` 63 | From string `json:"from"` 64 | PHeader string `json:"pheader"` 65 | } 66 | 67 | func (w *worker) RegisterPing() error { 68 | hmap := map[string]interface{}{"/ping": w.PingHandler} 69 | 70 | return json.Register(w.channel, hmap, func(ctx context.Context, err error) { 71 | w.logger.Debug("error occurred", err) 72 | }) 73 | } 74 | 75 | func (w *worker) PingHandler(ctx json.Context, request *Ping) (*Pong, error) { 76 | var pong Pong 77 | var res []byte 78 | 79 | headers := ctx.Headers() 80 | marshaledHeaders, err := json2.Marshal(ctx.Headers()) 81 | if err != nil { 82 | return nil, err 83 | } 84 | forwardOptions := &forward.Options{Headers: []byte(marshaledHeaders)} 85 | handle, err := w.ringpop.HandleOrForward(request.Key, request.Bytes(), &res, "pingchannel", "/ping", tchannel.JSON, forwardOptions) 86 | if handle { 87 | address, err := w.ringpop.WhoAmI() 88 | if err != nil { 89 | return nil, err 90 | } 91 | return &Pong{"Hello, world!", address, headers["p"]}, nil 92 | } 93 | 94 | if err := json2.Unmarshal(res, &pong); err != nil { 95 | return nil, err 96 | } 97 | 98 | // else request was forwarded 99 | return &pong, err 100 | } 101 | 102 | func main() { 103 | flag.Parse() 104 | 105 | ch, err := tchannel.NewChannel("pingchannel", nil) 106 | if err != nil { 107 | log.Fatalf("channel did not create successfully: %v", err) 108 | } 109 | 110 | logger := log.StandardLogger() 111 | 112 | rp, err := ringpop.New("ping-app", 113 | ringpop.Channel(ch), 114 | ringpop.Address(*hostport), 115 | ringpop.Logger(bark.NewLoggerFromLogrus(logger)), 116 | ) 117 | if err != nil { 118 | log.Fatalf("Unable to create Ringpop: %v", err) 119 | } 120 | 121 | worker := &worker{ 122 | channel: ch, 123 | ringpop: rp, 124 | logger: logger, 125 | } 126 | 127 | if err := worker.RegisterPing(); err != nil { 128 | log.Fatalf("could not register ping handler: %v", err) 129 | } 130 | 131 | if err := worker.channel.ListenAndServe(*hostport); err != nil { 132 | log.Fatalf("could not listen on given hostport: %v", err) 133 | } 134 | 135 | opts := new(swim.BootstrapOptions) 136 | opts.DiscoverProvider = jsonfile.New(*hostfile) 137 | 138 | if _, err := worker.ringpop.Bootstrap(opts); err != nil { 139 | log.Fatalf("ringpop bootstrap failed: %v", err) 140 | } 141 | 142 | select {} 143 | } 144 | -------------------------------------------------------------------------------- /examples/ping-thrift-gen/.gitignore: -------------------------------------------------------------------------------- 1 | ping-thrift-gen 2 | /vendor 3 | -------------------------------------------------------------------------------- /examples/ping-thrift-gen/README.md: -------------------------------------------------------------------------------- 1 | A simple ping-pong service implementation that that integrates ringpop to forward requests between nodes. 2 | 3 | Note: this file can be [cram][3]-executed using `make test-examples`. That's why some of the example outputs below are a bit unusual. 4 | 5 | This example it's different than the others because it uses ringpop specific generated code to avoid some of the boilerplate. 6 | 7 | # Running the example 8 | 9 | All commands are relative to this directory: 10 | 11 | $ cd ${TESTDIR} # examples/ping-thrift-gen 12 | 13 | (optional, the files are already included) Generate the thrift code: 14 | 15 | $ go generate 16 | 17 | Build the example binary: 18 | 19 | $ go build 20 | 21 | Start a custer of 5 nodes using [tick-cluster][1]: 22 | 23 | $ tick-cluster.js --interface=127.0.0.1 -n 5 ping-thrift-gen &> tick-cluster.log & 24 | $ sleep 5 25 | 26 | Lookup the node `my_key` key belongs to using [tcurl][2]: 27 | 28 | $ tcurl ringpop -P hosts.json /admin/lookup '{"key": "my_key"}' 29 | {"ok":true,"head":null,"body":{"dest":"127.0.0.1:300?"},"headers":{"as":"json"},"trace":"*"} (glob) 30 | 31 | Call the `PingPongService::Ping` endpoint (multiple times) and see the request being forwarded. Each request is sent to a random node in the cluster because of the `-P hosts.json` argument--but is always handled by the node owning the key. This can be seen in the `from` field of the response: 32 | 33 | $ tcurl pingchannel -P hosts.json --thrift ./ping.thrift PingPongService::Ping '{"request": {"key": "my_key"}}' 34 | {"ok":true,"head":{},"body":{"message":"Hello, world!","from_":"127.0.0.1:300?","pheader":""},"headers":{"as":"thrift"},"trace":"*"} (glob) 35 | 36 | Optionally, set the `p` header. This value will be forwarded together with the request body to the node owning the key. Its value is returned in the response body in the `pheader` field: 37 | 38 | $ tcurl pingchannel -P hosts.json --thrift ./ping.thrift PingPongService::Ping '{"request": {"key": "my_key"}}' --headers '{"p": "my_header"}' 39 | {"ok":true,"head":{},"body":{"message":"Hello, world!","from_":"127.0.0.1:300?","pheader":"my_header"},"headers":{"as":"thrift"},"trace":"*"} (glob) 40 | 41 | $ kill %1 42 | 43 | [1]:https://github.com/uber/ringpop-common/ 44 | [2]:https://github.com/uber/tcurl 45 | [3]:https://pypi.python.org/pypi/cram 46 | -------------------------------------------------------------------------------- /examples/ping-thrift-gen/gen-go/ping/GoUnusedProtection__.go: -------------------------------------------------------------------------------- 1 | // Code generated by Thrift Compiler (0.15.0). DO NOT EDIT. 2 | 3 | package ping 4 | 5 | var GoUnusedProtection__ int; 6 | 7 | -------------------------------------------------------------------------------- /examples/ping-thrift-gen/gen-go/ping/ping-consts.go: -------------------------------------------------------------------------------- 1 | // Code generated by Thrift Compiler (0.15.0). DO NOT EDIT. 2 | 3 | package ping 4 | 5 | import ( 6 | "bytes" 7 | "context" 8 | "fmt" 9 | "time" 10 | thrift "github.com/apache/thrift/lib/go/thrift" 11 | ) 12 | 13 | // (needed to ensure safety because of naive import list construction.) 14 | var _ = thrift.ZERO 15 | var _ = fmt.Printf 16 | var _ = context.Background 17 | var _ = time.Now 18 | var _ = bytes.Equal 19 | 20 | 21 | func init() { 22 | } 23 | 24 | -------------------------------------------------------------------------------- /examples/ping-thrift-gen/gen-go/ping/ringpop-ping.go: -------------------------------------------------------------------------------- 1 | // @generated Code generated by thrift-gen. Do not modify. 2 | 3 | package ping 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | 9 | "github.com/temporalio/ringpop-go" 10 | "github.com/temporalio/ringpop-go/forward" 11 | "github.com/temporalio/ringpop-go/router" 12 | "github.com/temporalio/tchannel-go" 13 | "github.com/temporalio/tchannel-go/thrift" 14 | ) 15 | 16 | type RingpopPingPongServiceAdapter struct { 17 | impl TChanPingPongService 18 | ringpop ringpop.Interface 19 | ch *tchannel.Channel 20 | config PingPongServiceConfiguration 21 | router router.Router 22 | } 23 | 24 | // PingPongServiceConfiguration contains the forwarding configuration for the PingPongService service. It has a field for every endpoint defined in the service. In this field the endpoint specific forward configuration can be stored. Populating these fields is optional, default behaviour is to call the service implementation locally to the process where the call came in. 25 | type PingPongServiceConfiguration struct { 26 | // Ping holds the forwarding configuration for the Ping endpoint defined in the service 27 | Ping *PingPongServicePingConfiguration 28 | } 29 | 30 | func (c *PingPongServiceConfiguration) validate() error { 31 | if c.Ping != nil { 32 | if c.Ping.Key == nil { 33 | return errors.New("configuration for endpoint Ping is missing a Key function") 34 | } 35 | } 36 | return nil 37 | } 38 | 39 | // NewRingpopPingPongServiceAdapter creates an implementation of the TChanPingPongService interface. This specific implementation will use to configuration provided during construction to deterministically route calls to nodes from a ringpop cluster. The channel should be the channel on which the service exposes its endpoints. Forwarded calls, calls to unconfigured endpoints and calls that already were executed on the right machine will be passed on the the implementation passed in during construction. 40 | // 41 | // Example usage: 42 | // import "github.com/temporalio/tchannel-go/thrift" 43 | // 44 | // var server thrift.Server 45 | // server = ... 46 | // 47 | // var handler TChanPingPongService 48 | // handler = &YourImplementation{} 49 | // 50 | // adapter, _ := NewRingpopPingPongServiceAdapter(handler, ringpop, channel, 51 | // PingPongServiceConfiguration{ 52 | // Ping: &PingPongServicePingConfiguration: { 53 | // Key: func(ctx thrift.Context, request *Ping) (shardKey string, err error) { 54 | // return "calculated-shard-key", nil 55 | // }, 56 | // }, 57 | // }, 58 | // ) 59 | // server.Register(NewTChanPingPongServiceServer(adapter)) 60 | func NewRingpopPingPongServiceAdapter( 61 | impl TChanPingPongService, 62 | rp ringpop.Interface, 63 | ch *tchannel.Channel, 64 | config PingPongServiceConfiguration, 65 | ) (TChanPingPongService, error) { 66 | err := config.validate() 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | adapter := &RingpopPingPongServiceAdapter{ 72 | impl: impl, 73 | ringpop: rp, 74 | ch: ch, 75 | config: config, 76 | } 77 | // create ringpop router for routing based on ring membership 78 | adapter.router = router.New(rp, adapter, ch) 79 | 80 | return adapter, nil 81 | } 82 | 83 | // GetLocalClient satisfies the ClientFactory interface of ringpop-go/router 84 | func (a *RingpopPingPongServiceAdapter) GetLocalClient() interface{} { 85 | return a.impl 86 | } 87 | 88 | // MakeRemoteClient satisfies the ClientFactory interface of ringpop-go/router 89 | func (a *RingpopPingPongServiceAdapter) MakeRemoteClient(client thrift.TChanClient) interface{} { 90 | return NewTChanPingPongServiceClient(client) 91 | } 92 | 93 | // PingPongServicePingConfiguration contains the configuration on how to route calls to the thrift endpoint PingPongService::Ping. 94 | type PingPongServicePingConfiguration struct { 95 | // Key is a closure that generates a routable key based on the parameters of the incomming request. 96 | Key func(ctx thrift.Context, request *Ping) (string, error) 97 | } 98 | 99 | // Ping satisfies the TChanPingPongService interface. This function uses the configuration for Ping to determine the host to execute the call on. When it decides the call needs to be executed in the current process it will forward the invocation to its local implementation. 100 | func (a *RingpopPingPongServiceAdapter) Ping(ctx thrift.Context, request *Ping) (r *Pong, err error) { 101 | // check if the function should be called locally 102 | if a.config.Ping == nil || forward.DeleteForwardedHeader(ctx) { 103 | return a.impl.Ping(ctx, request) 104 | } 105 | 106 | // find the key to shard on 107 | ringpopKey, err := a.config.Ping.Key(ctx, request) 108 | if err != nil { 109 | return r, fmt.Errorf("could not get key: %q", err) 110 | } 111 | 112 | clientInterface, isRemote, err := a.router.GetClient(ringpopKey) 113 | if err != nil { 114 | return r, err 115 | } 116 | 117 | client := clientInterface.(TChanPingPongService) 118 | if isRemote { 119 | ctx = forward.SetForwardedHeader(ctx, []string{ringpopKey}) 120 | } 121 | return client.Ping(ctx, request) 122 | } 123 | -------------------------------------------------------------------------------- /examples/ping-thrift-gen/gen-go/ping/tchan-ping.go: -------------------------------------------------------------------------------- 1 | // @generated Code generated by thrift-gen. Do not modify. 2 | 3 | // Package ping is generated code used to make or handle TChannel calls using Thrift. 4 | package ping 5 | 6 | import ( 7 | "fmt" 8 | 9 | athrift "github.com/apache/thrift/lib/go/thrift" 10 | "github.com/temporalio/tchannel-go/thrift" 11 | ) 12 | 13 | // Interfaces for the service and client for the services defined in the IDL. 14 | 15 | // TChanPingPongService is the interface that defines the server handler and client interface. 16 | type TChanPingPongService interface { 17 | Ping(ctx thrift.Context, request *Ping) (*Pong, error) 18 | } 19 | 20 | // Implementation of a client and service handler. 21 | 22 | type tchanPingPongServiceClient struct { 23 | thriftService string 24 | client thrift.TChanClient 25 | } 26 | 27 | func NewTChanPingPongServiceInheritedClient(thriftService string, client thrift.TChanClient) *tchanPingPongServiceClient { 28 | return &tchanPingPongServiceClient{ 29 | thriftService, 30 | client, 31 | } 32 | } 33 | 34 | // NewTChanPingPongServiceClient creates a client that can be used to make remote calls. 35 | func NewTChanPingPongServiceClient(client thrift.TChanClient) TChanPingPongService { 36 | return NewTChanPingPongServiceInheritedClient("PingPongService", client) 37 | } 38 | 39 | func (c *tchanPingPongServiceClient) Ping(ctx thrift.Context, request *Ping) (*Pong, error) { 40 | var resp PingPongServicePingResult 41 | args := PingPongServicePingArgs{ 42 | Request: request, 43 | } 44 | success, err := c.client.Call(ctx, c.thriftService, "Ping", &args, &resp) 45 | if err == nil && !success { 46 | switch { 47 | default: 48 | err = fmt.Errorf("received no result or unknown exception for Ping") 49 | } 50 | } 51 | 52 | return resp.GetSuccess(), err 53 | } 54 | 55 | type tchanPingPongServiceServer struct { 56 | handler TChanPingPongService 57 | } 58 | 59 | // NewTChanPingPongServiceServer wraps a handler for TChanPingPongService so it can be 60 | // registered with a thrift.Server. 61 | func NewTChanPingPongServiceServer(handler TChanPingPongService) thrift.TChanServer { 62 | return &tchanPingPongServiceServer{ 63 | handler, 64 | } 65 | } 66 | 67 | func (s *tchanPingPongServiceServer) Service() string { 68 | return "PingPongService" 69 | } 70 | 71 | func (s *tchanPingPongServiceServer) Methods() []string { 72 | return []string{ 73 | "Ping", 74 | } 75 | } 76 | 77 | func (s *tchanPingPongServiceServer) Handle(ctx thrift.Context, methodName string, protocol athrift.TProtocol) (bool, athrift.TStruct, error) { 78 | switch methodName { 79 | case "Ping": 80 | return s.handlePing(ctx, protocol) 81 | 82 | default: 83 | return false, nil, fmt.Errorf("method %v not found in service %v", methodName, s.Service()) 84 | } 85 | } 86 | 87 | func (s *tchanPingPongServiceServer) handlePing(ctx thrift.Context, protocol athrift.TProtocol) (bool, athrift.TStruct, error) { 88 | var req PingPongServicePingArgs 89 | var res PingPongServicePingResult 90 | 91 | if err := req.Read(ctx, protocol); err != nil { 92 | return false, nil, err 93 | } 94 | 95 | r, err := 96 | s.handler.Ping(ctx, req.Request) 97 | 98 | if err != nil { 99 | return false, nil, err 100 | } else { 101 | res.Success = r 102 | } 103 | 104 | return err == nil, &res, nil 105 | } 106 | -------------------------------------------------------------------------------- /examples/ping-thrift-gen/ping.thrift: -------------------------------------------------------------------------------- 1 | struct ping { 2 | 1: required string key, 3 | } 4 | 5 | struct pong { 6 | 1: required string message, 7 | 2: required string from_, 8 | 3: optional string pheader, 9 | } 10 | 11 | service PingPongService { 12 | pong Ping(1: ping request) 13 | } 14 | -------------------------------------------------------------------------------- /examples/ping-thrift/.gitignore: -------------------------------------------------------------------------------- 1 | ping-thrift 2 | /vendor 3 | -------------------------------------------------------------------------------- /examples/ping-thrift/README.md: -------------------------------------------------------------------------------- 1 | A simple ping-pong service implementation that that integrates ringpop to forward requests between nodes. 2 | 3 | Note: this file can be [cram][3]-executed using `make test-examples`. That's why some of the example outputs below are a bit unusual. 4 | 5 | # Running the example 6 | 7 | All commands are relative to this directory: 8 | 9 | $ cd ${TESTDIR} # examples/ping-thrift 10 | 11 | (optional, the files are already included) Generate the thrift code: 12 | 13 | $ thrift-gen --generateThrift --outputDir gen-go --inputFile ping.thrift 14 | 15 | Build the example binary: 16 | 17 | $ go build 18 | 19 | 20 | Start a custer of 5 nodes using [tick-cluster][1]: 21 | 22 | $ tick-cluster.js --interface=127.0.0.1 -n 5 ping-thrift &> tick-cluster.log & 23 | $ sleep 5 24 | 25 | Lookup the node `my_key` key belongs to using [tcurl][2]: 26 | 27 | $ tcurl ringpop -P hosts.json /admin/lookup '{"key": "my_key"}' 28 | {"ok":true,"head":null,"body":{"dest":"127.0.0.1:300?"},"headers":{"as":"json"},"trace":"*"} (glob) 29 | 30 | Call the `PingPongService::Ping` endpoint (multiple times) and see the request being forwarded. Each request is sent to a random node in the cluster because of the `-P hosts.json` argument--but is always handled by the node owning the key. This can be seen in the `from` field of the response: 31 | 32 | $ tcurl pingchannel -P hosts.json --thrift ./ping.thrift PingPongService::Ping '{"request": {"key": "my_key"}}' 33 | {"ok":true,"head":{},"body":{"message":"Hello, world!","from_":"127.0.0.1:300?","pheader":""},"headers":{"as":"thrift"},"trace":"*"} (glob) 34 | 35 | Optionally, set the `p` header. This value will be forwarded together with the request body to the node owning the key. Its value is returned in the response body in the `pheader` field: 36 | 37 | $ tcurl pingchannel -P hosts.json --thrift ./ping.thrift PingPongService::Ping '{"request": {"key": "my_key"}}' --headers '{"p": "my_header"}' 38 | {"ok":true,"head":{},"body":{"message":"Hello, world!","from_":"127.0.0.1:300?","pheader":"my_header"},"headers":{"as":"thrift"},"trace":"*"} (glob) 39 | 40 | $ kill %1 41 | 42 | [1]:https://github.com/uber/ringpop-common/ 43 | [2]:https://github.com/uber/tcurl 44 | [3]:https://pypi.python.org/pypi/cram 45 | -------------------------------------------------------------------------------- /examples/ping-thrift/gen-go/ping/GoUnusedProtection__.go: -------------------------------------------------------------------------------- 1 | // Code generated by Thrift Compiler (0.15.0). DO NOT EDIT. 2 | 3 | package ping 4 | 5 | var GoUnusedProtection__ int; 6 | 7 | -------------------------------------------------------------------------------- /examples/ping-thrift/gen-go/ping/ping-consts.go: -------------------------------------------------------------------------------- 1 | // Code generated by Thrift Compiler (0.15.0). DO NOT EDIT. 2 | 3 | package ping 4 | 5 | import ( 6 | "bytes" 7 | "context" 8 | "fmt" 9 | "time" 10 | thrift "github.com/apache/thrift/lib/go/thrift" 11 | ) 12 | 13 | // (needed to ensure safety because of naive import list construction.) 14 | var _ = thrift.ZERO 15 | var _ = fmt.Printf 16 | var _ = context.Background 17 | var _ = time.Now 18 | var _ = bytes.Equal 19 | 20 | 21 | func init() { 22 | } 23 | 24 | -------------------------------------------------------------------------------- /examples/ping-thrift/gen-go/ping/tchan-ping.go: -------------------------------------------------------------------------------- 1 | // @generated Code generated by thrift-gen. Do not modify. 2 | 3 | // Package ping is generated code used to make or handle TChannel calls using Thrift. 4 | package ping 5 | 6 | import ( 7 | "fmt" 8 | 9 | athrift "github.com/apache/thrift/lib/go/thrift" 10 | "github.com/temporalio/tchannel-go/thrift" 11 | ) 12 | 13 | // Interfaces for the service and client for the services defined in the IDL. 14 | 15 | // TChanPingPongService is the interface that defines the server handler and client interface. 16 | type TChanPingPongService interface { 17 | Ping(ctx thrift.Context, request *Ping) (*Pong, error) 18 | } 19 | 20 | // Implementation of a client and service handler. 21 | 22 | type tchanPingPongServiceClient struct { 23 | thriftService string 24 | client thrift.TChanClient 25 | } 26 | 27 | func NewTChanPingPongServiceInheritedClient(thriftService string, client thrift.TChanClient) *tchanPingPongServiceClient { 28 | return &tchanPingPongServiceClient{ 29 | thriftService, 30 | client, 31 | } 32 | } 33 | 34 | // NewTChanPingPongServiceClient creates a client that can be used to make remote calls. 35 | func NewTChanPingPongServiceClient(client thrift.TChanClient) TChanPingPongService { 36 | return NewTChanPingPongServiceInheritedClient("PingPongService", client) 37 | } 38 | 39 | func (c *tchanPingPongServiceClient) Ping(ctx thrift.Context, request *Ping) (*Pong, error) { 40 | var resp PingPongServicePingResult 41 | args := PingPongServicePingArgs{ 42 | Request: request, 43 | } 44 | success, err := c.client.Call(ctx, c.thriftService, "Ping", &args, &resp) 45 | if err == nil && !success { 46 | switch { 47 | default: 48 | err = fmt.Errorf("received no result or unknown exception for Ping") 49 | } 50 | } 51 | 52 | return resp.GetSuccess(), err 53 | } 54 | 55 | type tchanPingPongServiceServer struct { 56 | handler TChanPingPongService 57 | } 58 | 59 | // NewTChanPingPongServiceServer wraps a handler for TChanPingPongService so it can be 60 | // registered with a thrift.Server. 61 | func NewTChanPingPongServiceServer(handler TChanPingPongService) thrift.TChanServer { 62 | return &tchanPingPongServiceServer{ 63 | handler, 64 | } 65 | } 66 | 67 | func (s *tchanPingPongServiceServer) Service() string { 68 | return "PingPongService" 69 | } 70 | 71 | func (s *tchanPingPongServiceServer) Methods() []string { 72 | return []string{ 73 | "Ping", 74 | } 75 | } 76 | 77 | func (s *tchanPingPongServiceServer) Handle(ctx thrift.Context, methodName string, protocol athrift.TProtocol) (bool, athrift.TStruct, error) { 78 | switch methodName { 79 | case "Ping": 80 | return s.handlePing(ctx, protocol) 81 | 82 | default: 83 | return false, nil, fmt.Errorf("method %v not found in service %v", methodName, s.Service()) 84 | } 85 | } 86 | 87 | func (s *tchanPingPongServiceServer) handlePing(ctx thrift.Context, protocol athrift.TProtocol) (bool, athrift.TStruct, error) { 88 | var req PingPongServicePingArgs 89 | var res PingPongServicePingResult 90 | 91 | if err := req.Read(ctx, protocol); err != nil { 92 | return false, nil, err 93 | } 94 | 95 | r, err := 96 | s.handler.Ping(ctx, req.Request) 97 | 98 | if err != nil { 99 | return false, nil, err 100 | } else { 101 | res.Success = r 102 | } 103 | 104 | return err == nil, &res, nil 105 | } 106 | -------------------------------------------------------------------------------- /examples/ping-thrift/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | import ( 24 | "bytes" 25 | "flag" 26 | 27 | log "github.com/sirupsen/logrus" 28 | "github.com/uber-common/bark" 29 | "github.com/temporalio/ringpop-go" 30 | "github.com/temporalio/ringpop-go/discovery/jsonfile" 31 | gen "github.com/temporalio/ringpop-go/examples/ping-thrift/gen-go/ping" 32 | "github.com/temporalio/ringpop-go/forward" 33 | "github.com/temporalio/ringpop-go/swim" 34 | "github.com/temporalio/tchannel-go" 35 | "github.com/temporalio/tchannel-go/thrift" 36 | ) 37 | 38 | var ( 39 | hostport = flag.String("listen", "127.0.0.1:3000", "hostport to start ringpop on") 40 | hostfile = flag.String("hosts", "./hosts.json", "path to hosts file") 41 | ) 42 | 43 | type worker struct { 44 | ringpop *ringpop.Ringpop 45 | channel *tchannel.Channel 46 | logger *log.Logger 47 | } 48 | 49 | func (w *worker) RegisterPing() error { 50 | server := thrift.NewServer(w.channel) 51 | server.Register(gen.NewTChanPingPongServiceServer(w)) 52 | return nil 53 | } 54 | 55 | func (w *worker) Ping(ctx thrift.Context, request *gen.Ping) (*gen.Pong, error) { 56 | var pongResult gen.PingPongServicePingResult 57 | var res []byte 58 | 59 | headers := ctx.Headers() 60 | var marshaledHeaders bytes.Buffer 61 | err := thrift.WriteHeaders(&marshaledHeaders, headers) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | pingArgs := &gen.PingPongServicePingArgs{ 67 | Request: request, 68 | } 69 | req, err := ringpop.SerializeThrift(ctx, pingArgs) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | forwardOptions := &forward.Options{Headers: marshaledHeaders.Bytes()} 75 | handle, err := w.ringpop.HandleOrForward(request.Key, req, &res, "pingchannel", "PingPongService::Ping", tchannel.Thrift, forwardOptions) 76 | if handle { 77 | address, err := w.ringpop.WhoAmI() 78 | if err != nil { 79 | return nil, err 80 | } 81 | pHeader := headers["p"] 82 | return &gen.Pong{ 83 | Message: "Hello, world!", 84 | From_: address, 85 | Pheader: &pHeader, 86 | }, nil 87 | } 88 | 89 | if err := ringpop.DeserializeThrift(ctx, res, &pongResult); err != nil { 90 | return nil, err 91 | } 92 | 93 | // else request was forwarded 94 | return pongResult.Success, err 95 | } 96 | 97 | func main() { 98 | flag.Parse() 99 | 100 | ch, err := tchannel.NewChannel("pingchannel", nil) 101 | if err != nil { 102 | log.Fatalf("channel did not create successfully: %v", err) 103 | } 104 | 105 | logger := log.StandardLogger() 106 | 107 | rp, err := ringpop.New("ping-app", 108 | ringpop.Channel(ch), 109 | ringpop.Address(*hostport), 110 | ringpop.Logger(bark.NewLoggerFromLogrus(logger)), 111 | ) 112 | if err != nil { 113 | log.Fatalf("Unable to create Ringpop: %v", err) 114 | } 115 | 116 | worker := &worker{ 117 | channel: ch, 118 | ringpop: rp, 119 | logger: logger, 120 | } 121 | 122 | if err := worker.RegisterPing(); err != nil { 123 | log.Fatalf("could not register ping handler: %v", err) 124 | } 125 | 126 | if err := worker.channel.ListenAndServe(*hostport); err != nil { 127 | log.Fatalf("could not listen on given hostport: %v", err) 128 | } 129 | 130 | opts := new(swim.BootstrapOptions) 131 | opts.DiscoverProvider = jsonfile.New(*hostfile) 132 | 133 | if _, err := worker.ringpop.Bootstrap(opts); err != nil { 134 | log.Fatalf("ringpop bootstrap failed: %v", err) 135 | } 136 | 137 | select {} 138 | } 139 | -------------------------------------------------------------------------------- /examples/ping-thrift/ping.thrift: -------------------------------------------------------------------------------- 1 | struct ping { 2 | 1: required string key, 3 | } 4 | 5 | struct pong { 6 | 1: required string message, 7 | 2: required string from_, 8 | 3: optional string pheader, 9 | } 10 | 11 | service PingPongService { 12 | pong Ping(1: ping request) 13 | } 14 | -------------------------------------------------------------------------------- /examples/role-labels/.gitignore: -------------------------------------------------------------------------------- 1 | role-labels 2 | -------------------------------------------------------------------------------- /examples/role-labels/README.md: -------------------------------------------------------------------------------- 1 | This application shows a simple use-case of labels where the labels of a node will describe the role the member is fulfilling in the cluster. All nodes are able to list the nodes that fulfill a specific role. 2 | 3 | Note: this file can be [cram][3]-executed using `make test-examples`. That's why some of the example outputs below are a bit unusual. 4 | 5 | # Running the example 6 | 7 | All commands are relative to this directory: 8 | 9 | $ cd ${TESTDIR} # examples/role-labels 10 | 11 | (optional, the files are already included) Generate the thrift code: 12 | 13 | $ go generate 14 | 15 | Build the example binary: 16 | 17 | $ go build 18 | 19 | Start a custer of 5 nodes using [tick-cluster][1]: 20 | 21 | $ tick-cluster.js --interface=127.0.0.1 -n 5 role-labels &> tick-cluster.log & 22 | $ sleep 5 23 | 24 | Show all nodes that fulfill the `roleA` role using [tcurl][2]: 25 | 26 | $ tcurl role -P hosts.json --thrift ./role.thrift RoleService::GetMembers '{"role":"roleA"}' 27 | {"ok":true,"head":{},"body":["127.0.0.1:300?","127.0.0.1:300?","127.0.0.1:300?","127.0.0.1:300?","127.0.0.1:300?"],"headers":{"as":"thrift"},"trace":"*"} (glob) 28 | 29 | This will give you the list of all instances as they all start with their role set to `roleA`. The role of an instance can be changed by running: 30 | 31 | $ tcurl role -P hosts.json --thrift ./role.thrift RoleService::SetRole '{"role":"roleB"}' 32 | {"ok":true,"head":{},"headers":{"as":"thrift"},"trace":"*"} (glob) 33 | $ sleep 2 # give the cluster of 5 some time to converge on the new state of the membership 34 | 35 | To validate that this worked we can now lookup both the members for `roleA` and `roleB` and see that the membership lists contain respectively 4 and 1 member: 36 | 37 | $ tcurl role -P hosts.json --thrift ./role.thrift RoleService::GetMembers '{"role":"roleA"}' 38 | {"ok":true,"head":{},"body":["127.0.0.1:300?","127.0.0.1:300?","127.0.0.1:300?","127.0.0.1:300?"],"headers":{"as":"thrift"},"trace":"*"} (glob) 39 | 40 | $ tcurl role -P hosts.json --thrift ./role.thrift RoleService::GetMembers '{"role":"roleB"}' 41 | {"ok":true,"head":{},"body":["127.0.0.1:300?"],"headers":{"as":"thrift"},"trace":"*"} (glob) 42 | 43 | 44 | In the end you should kill tick cluster via: 45 | 46 | $ kill %1 47 | 48 | [1]:https://github.com/uber/ringpop-common/ 49 | [2]:https://github.com/uber/tcurl 50 | [3]:https://pypi.python.org/pypi/cram 51 | -------------------------------------------------------------------------------- /examples/role-labels/gen-go/role/GoUnusedProtection__.go: -------------------------------------------------------------------------------- 1 | // Code generated by Thrift Compiler (0.15.0). DO NOT EDIT. 2 | 3 | package role 4 | 5 | var GoUnusedProtection__ int; 6 | 7 | -------------------------------------------------------------------------------- /examples/role-labels/gen-go/role/role-consts.go: -------------------------------------------------------------------------------- 1 | // Code generated by Thrift Compiler (0.15.0). DO NOT EDIT. 2 | 3 | package role 4 | 5 | import ( 6 | "bytes" 7 | "context" 8 | "fmt" 9 | "time" 10 | thrift "github.com/apache/thrift/lib/go/thrift" 11 | ) 12 | 13 | // (needed to ensure safety because of naive import list construction.) 14 | var _ = thrift.ZERO 15 | var _ = fmt.Printf 16 | var _ = context.Background 17 | var _ = time.Now 18 | var _ = bytes.Equal 19 | 20 | 21 | func init() { 22 | } 23 | 24 | -------------------------------------------------------------------------------- /examples/role-labels/gen-go/role/tchan-role.go: -------------------------------------------------------------------------------- 1 | // @generated Code generated by thrift-gen. Do not modify. 2 | 3 | // Package role is generated code used to make or handle TChannel calls using Thrift. 4 | package role 5 | 6 | import ( 7 | "fmt" 8 | 9 | athrift "github.com/apache/thrift/lib/go/thrift" 10 | "github.com/temporalio/tchannel-go/thrift" 11 | ) 12 | 13 | // Interfaces for the service and client for the services defined in the IDL. 14 | 15 | // TChanRoleService is the interface that defines the server handler and client interface. 16 | type TChanRoleService interface { 17 | GetMembers(ctx thrift.Context, role string) ([]string, error) 18 | SetRole(ctx thrift.Context, role string) error 19 | } 20 | 21 | // Implementation of a client and service handler. 22 | 23 | type tchanRoleServiceClient struct { 24 | thriftService string 25 | client thrift.TChanClient 26 | } 27 | 28 | func NewTChanRoleServiceInheritedClient(thriftService string, client thrift.TChanClient) *tchanRoleServiceClient { 29 | return &tchanRoleServiceClient{ 30 | thriftService, 31 | client, 32 | } 33 | } 34 | 35 | // NewTChanRoleServiceClient creates a client that can be used to make remote calls. 36 | func NewTChanRoleServiceClient(client thrift.TChanClient) TChanRoleService { 37 | return NewTChanRoleServiceInheritedClient("RoleService", client) 38 | } 39 | 40 | func (c *tchanRoleServiceClient) GetMembers(ctx thrift.Context, role string) ([]string, error) { 41 | var resp RoleServiceGetMembersResult 42 | args := RoleServiceGetMembersArgs{ 43 | Role: role, 44 | } 45 | success, err := c.client.Call(ctx, c.thriftService, "GetMembers", &args, &resp) 46 | if err == nil && !success { 47 | switch { 48 | default: 49 | err = fmt.Errorf("received no result or unknown exception for GetMembers") 50 | } 51 | } 52 | 53 | return resp.GetSuccess(), err 54 | } 55 | 56 | func (c *tchanRoleServiceClient) SetRole(ctx thrift.Context, role string) error { 57 | var resp RoleServiceSetRoleResult 58 | args := RoleServiceSetRoleArgs{ 59 | Role: role, 60 | } 61 | success, err := c.client.Call(ctx, c.thriftService, "SetRole", &args, &resp) 62 | if err == nil && !success { 63 | switch { 64 | default: 65 | err = fmt.Errorf("received no result or unknown exception for SetRole") 66 | } 67 | } 68 | 69 | return err 70 | } 71 | 72 | type tchanRoleServiceServer struct { 73 | handler TChanRoleService 74 | } 75 | 76 | // NewTChanRoleServiceServer wraps a handler for TChanRoleService so it can be 77 | // registered with a thrift.Server. 78 | func NewTChanRoleServiceServer(handler TChanRoleService) thrift.TChanServer { 79 | return &tchanRoleServiceServer{ 80 | handler, 81 | } 82 | } 83 | 84 | func (s *tchanRoleServiceServer) Service() string { 85 | return "RoleService" 86 | } 87 | 88 | func (s *tchanRoleServiceServer) Methods() []string { 89 | return []string{ 90 | "GetMembers", 91 | "SetRole", 92 | } 93 | } 94 | 95 | func (s *tchanRoleServiceServer) Handle(ctx thrift.Context, methodName string, protocol athrift.TProtocol) (bool, athrift.TStruct, error) { 96 | switch methodName { 97 | case "GetMembers": 98 | return s.handleGetMembers(ctx, protocol) 99 | case "SetRole": 100 | return s.handleSetRole(ctx, protocol) 101 | 102 | default: 103 | return false, nil, fmt.Errorf("method %v not found in service %v", methodName, s.Service()) 104 | } 105 | } 106 | 107 | func (s *tchanRoleServiceServer) handleGetMembers(ctx thrift.Context, protocol athrift.TProtocol) (bool, athrift.TStruct, error) { 108 | var req RoleServiceGetMembersArgs 109 | var res RoleServiceGetMembersResult 110 | 111 | if err := req.Read(ctx, protocol); err != nil { 112 | return false, nil, err 113 | } 114 | 115 | r, err := 116 | s.handler.GetMembers(ctx, req.Role) 117 | 118 | if err != nil { 119 | return false, nil, err 120 | } else { 121 | res.Success = r 122 | } 123 | 124 | return err == nil, &res, nil 125 | } 126 | 127 | func (s *tchanRoleServiceServer) handleSetRole(ctx thrift.Context, protocol athrift.TProtocol) (bool, athrift.TStruct, error) { 128 | var req RoleServiceSetRoleArgs 129 | var res RoleServiceSetRoleResult 130 | 131 | if err := req.Read(ctx, protocol); err != nil { 132 | return false, nil, err 133 | } 134 | 135 | err := 136 | s.handler.SetRole(ctx, req.Role) 137 | 138 | if err != nil { 139 | return false, nil, err 140 | } else { 141 | } 142 | 143 | return err == nil, &res, nil 144 | } 145 | -------------------------------------------------------------------------------- /examples/role-labels/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | //go:generate thrift-gen --generateThrift --outputDir gen-go --template github.com/temporalio/ringpop-go/ringpop.thrift-gen --inputFile role.thrift 22 | 23 | package main 24 | 25 | import ( 26 | "flag" 27 | 28 | log "github.com/sirupsen/logrus" 29 | "github.com/uber-common/bark" 30 | "github.com/temporalio/ringpop-go" 31 | "github.com/temporalio/ringpop-go/discovery/jsonfile" 32 | gen "github.com/temporalio/ringpop-go/examples/role-labels/gen-go/role" 33 | "github.com/temporalio/ringpop-go/swim" 34 | "github.com/temporalio/tchannel-go" 35 | "github.com/temporalio/tchannel-go/thrift" 36 | ) 37 | 38 | const ( 39 | roleKey = "role" 40 | ) 41 | 42 | var ( 43 | hostport = flag.String("listen", "127.0.0.1:3000", "hostport to start service on") 44 | hostfile = flag.String("hosts", "./hosts.json", "path to hosts file") 45 | rolename = flag.String("role", "roleA", "name of the role this node takes in the cluster") 46 | ) 47 | 48 | type worker struct { 49 | ringpop *ringpop.Ringpop 50 | channel *tchannel.Channel 51 | logger *log.Logger 52 | } 53 | 54 | func (w *worker) GetMembers(ctx thrift.Context, role string) ([]string, error) { 55 | return w.ringpop.GetReachableMembers(swim.MemberWithLabelAndValue(roleKey, role)) 56 | } 57 | 58 | func (w *worker) SetRole(ctx thrift.Context, role string) error { 59 | labels, err := w.ringpop.Labels() 60 | if err != nil { 61 | return err 62 | } 63 | labels.Set(roleKey, role) 64 | return nil 65 | } 66 | 67 | func (w *worker) RegisterRoleService() error { 68 | server := thrift.NewServer(w.channel) 69 | server.Register(gen.NewTChanRoleServiceServer(w)) 70 | return nil 71 | } 72 | 73 | func main() { 74 | flag.Parse() 75 | 76 | ch, err := tchannel.NewChannel("role", nil) 77 | if err != nil { 78 | log.Fatalf("channel did not create successfully: %v", err) 79 | } 80 | 81 | logger := log.StandardLogger() 82 | 83 | rp, err := ringpop.New("ping-app", 84 | ringpop.Channel(ch), 85 | ringpop.Address(*hostport), 86 | ringpop.Logger(bark.NewLoggerFromLogrus(logger)), 87 | ) 88 | if err != nil { 89 | log.Fatalf("Unable to create Ringpop: %v", err) 90 | } 91 | 92 | worker := &worker{ 93 | channel: ch, 94 | ringpop: rp, 95 | logger: logger, 96 | } 97 | 98 | if err := worker.RegisterRoleService(); err != nil { 99 | log.Fatalf("could not register role service: %v", err) 100 | } 101 | 102 | if err := worker.channel.ListenAndServe(*hostport); err != nil { 103 | log.Fatalf("could not listen on given hostport: %v", err) 104 | } 105 | 106 | opts := new(swim.BootstrapOptions) 107 | opts.DiscoverProvider = jsonfile.New(*hostfile) 108 | 109 | log.Println("Bootstrapping") 110 | 111 | if _, err := worker.ringpop.Bootstrap(opts); err != nil { 112 | log.Fatalf("ringpop bootstrap failed: %v", err) 113 | } 114 | 115 | log.Println("Bootstrapped") 116 | 117 | if labels, err := worker.ringpop.Labels(); err != nil { 118 | log.Fatalf("unable to get access to ringpop labels: %v", err) 119 | } else { 120 | labels.Set(roleKey, *rolename) 121 | } 122 | 123 | log.Println("Started") 124 | 125 | select {} 126 | } 127 | -------------------------------------------------------------------------------- /examples/role-labels/role.thrift: -------------------------------------------------------------------------------- 1 | service RoleService { 2 | void SetRole(1: string role) 3 | list GetMembers(1: string role) 4 | } 5 | -------------------------------------------------------------------------------- /forward/events.go: -------------------------------------------------------------------------------- 1 | package forward 2 | 3 | // A RequestForwardedEvent is emitted for every forwarded request 4 | type RequestForwardedEvent struct{} 5 | 6 | // A InflightRequestsChangedEvent is emitted everytime the number of inflight requests change 7 | type InflightRequestsChangedEvent struct { 8 | Inflight int64 9 | } 10 | 11 | // InflightCountOperation indicates the operation being performed on the inflight counter 12 | type InflightCountOperation string 13 | 14 | const ( 15 | // InflightIncrement indicates that the inflight number was being incremented 16 | InflightIncrement InflightCountOperation = "increment" 17 | 18 | // InflightDecrement indicates that the inflight number was being decremented 19 | InflightDecrement InflightCountOperation = "decrement" 20 | ) 21 | 22 | // A InflightRequestsMiscountEvent is emitted when a miscount happend for the inflight requests 23 | type InflightRequestsMiscountEvent struct { 24 | Operation InflightCountOperation 25 | } 26 | 27 | // A SuccessEvent is emitted when the forwarded request responded without an error 28 | type SuccessEvent struct{} 29 | 30 | // A FailedEvent is emitted when the forwarded request responded with an error 31 | type FailedEvent struct{} 32 | 33 | // A MaxRetriesEvent is emitted when the sender failed to complete the request after the maximum specified amount of retries 34 | type MaxRetriesEvent struct { 35 | MaxRetries int 36 | } 37 | 38 | // A RetryAttemptEvent is emitted when a retry is initiated during forwarding 39 | type RetryAttemptEvent struct{} 40 | 41 | // A RetryAbortEvent is emitted when a retry has been aborted. The reason for abortion is embedded 42 | type RetryAbortEvent struct { 43 | Reason string 44 | } 45 | 46 | // A RerouteEvent is emitted when a forwarded request is being rerouted to a new destination 47 | type RerouteEvent struct { 48 | OldDestination string 49 | NewDestination string 50 | } 51 | 52 | // A RetrySuccessEvent is emitted after a retry resulted in a successful forwarded request 53 | type RetrySuccessEvent struct { 54 | NumRetries int 55 | } 56 | -------------------------------------------------------------------------------- /forward/mock_sender_test.go: -------------------------------------------------------------------------------- 1 | package forward 2 | 3 | import "github.com/stretchr/testify/mock" 4 | 5 | type MockSender struct { 6 | mock.Mock 7 | } 8 | 9 | // WhoAmI provides a mock function with given fields: 10 | func (_m *MockSender) WhoAmI() (string, error) { 11 | ret := _m.Called() 12 | 13 | var r0 string 14 | if rf, ok := ret.Get(0).(func() string); ok { 15 | r0 = rf() 16 | } else { 17 | r0 = ret.Get(0).(string) 18 | } 19 | 20 | var r1 error 21 | if rf, ok := ret.Get(1).(func() error); ok { 22 | r1 = rf() 23 | } else { 24 | r1 = ret.Error(1) 25 | } 26 | 27 | return r0, r1 28 | } 29 | 30 | // Lookup provides a mock function with given fields: _a0 31 | func (_m *MockSender) Lookup(_a0 string) (string, error) { 32 | ret := _m.Called(_a0) 33 | 34 | var r0 string 35 | if rf, ok := ret.Get(0).(func(string) string); ok { 36 | r0 = rf(_a0) 37 | } else { 38 | r0 = ret.Get(0).(string) 39 | } 40 | 41 | var r1 error 42 | if rf, ok := ret.Get(1).(func(string) error); ok { 43 | r1 = rf(_a0) 44 | } else { 45 | r1 = ret.Error(1) 46 | } 47 | 48 | return r0, r1 49 | } 50 | -------------------------------------------------------------------------------- /forward/request_sender_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package forward 22 | 23 | import ( 24 | "testing" 25 | 26 | "github.com/stretchr/testify/mock" 27 | "github.com/stretchr/testify/suite" 28 | "github.com/temporalio/ringpop-go/events" 29 | "github.com/temporalio/ringpop-go/shared" 30 | "github.com/temporalio/ringpop-go/test/mocks/logger" 31 | "github.com/temporalio/tchannel-go" 32 | ) 33 | 34 | type requestSenderTestSuite struct { 35 | suite.Suite 36 | requestSender *requestSender 37 | mockSender *MockSender 38 | } 39 | 40 | type dummies struct { 41 | channel shared.SubChannel 42 | dest string 43 | emitter events.EventEmitter 44 | endpoint string 45 | format tchannel.Format 46 | keys []string 47 | options *Options 48 | request []byte 49 | service string 50 | } 51 | 52 | func (s *requestSenderTestSuite) SetupTest() { 53 | mockSender := &MockSender{} 54 | mockSender.On("WhoAmI").Return("", nil) 55 | dummies := s.newDummies(mockSender) 56 | s.requestSender = newRequestSender(mockSender, dummies.emitter, 57 | dummies.channel, dummies.request, dummies.keys, dummies.dest, 58 | dummies.service, dummies.endpoint, dummies.format, dummies.options) 59 | s.mockSender = mockSender 60 | } 61 | 62 | func (s *requestSenderTestSuite) newDummies(mockSender *MockSender) *dummies { 63 | channel, err := tchannel.NewChannel("dummychannel", nil) 64 | s.NoError(err, "no error creating TChannel") 65 | 66 | return &dummies{ 67 | channel: channel, 68 | dest: "dummydest", 69 | endpoint: "/dummyendpoint", 70 | emitter: NewForwarder(mockSender, channel), 71 | format: tchannel.Thrift, 72 | keys: []string{}, 73 | options: &Options{}, 74 | request: []byte{}, 75 | service: "dummyservice", 76 | } 77 | } 78 | 79 | func newDummyLogger() *mocklogger.Logger { 80 | logger := &mocklogger.Logger{} 81 | logger.On("WithFields", mock.Anything).Return(logger) 82 | logger.On("Warn", mock.Anything).Return(nil) 83 | return logger 84 | } 85 | 86 | func stubLookupWithKeys(mockSender *MockSender, dest string, keys ...string) { 87 | for _, key := range keys { 88 | mockSender.On("Lookup", key).Return(dest, nil) 89 | } 90 | } 91 | 92 | func (s *requestSenderTestSuite) TestAttemptRetrySendsMultipleKeys() { 93 | s.mockSender.On("WhoAmI").Return("192.0.2.1:0", nil) 94 | stubLookupWithKeys(s.mockSender, "192.0.2.1:1", "key1", "key2") 95 | 96 | // Mutate keys that are looked up prior to attempted retry. 97 | s.requestSender.keys = []string{"key1", "key2"} 98 | 99 | _, err := s.requestSender.AttemptRetry() 100 | s.NotEqual(errDestinationsDiverged, err, "not a diverged error") 101 | } 102 | 103 | func (s *requestSenderTestSuite) TestLookupKeysDedupes() { 104 | // 2 groups of 2 keys. Each group hashes to a different destination. 105 | stubLookupWithKeys(s.mockSender, "192.0.2.1:1", "key1", "key2") 106 | stubLookupWithKeys(s.mockSender, "192.0.2.1:2", "key3", "key4") 107 | 108 | dests := s.requestSender.LookupKeys([]string{"key1", "key2"}) 109 | s.Len(dests, 1, "dedupes single destination for multiple keys") 110 | 111 | dests = s.requestSender.LookupKeys([]string{"key1", "key2", "key3", "key4"}) 112 | s.Len(dests, 2, "dedupes multiple destinations for multiple keys") 113 | } 114 | 115 | func TestRequestSenderTestSuite(t *testing.T) { 116 | suite.Run(t, new(requestSenderTestSuite)) 117 | } 118 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/temporalio/ringpop-go 2 | 3 | go 1.11 4 | 5 | require ( 6 | github.com/apache/thrift v0.16.0 7 | github.com/benbjohnson/clock v0.0.0-20160125162948-a620c1cc9866 8 | github.com/cactus/go-statsd-client/statsd v0.0.0-20200423205355-cb0885a1018c 9 | github.com/dgryski/go-farm v0.0.0-20140601200337-fc41e106ee0e 10 | github.com/rcrowley/go-metrics v0.0.0-20141108142129-dee209f2455f 11 | github.com/sirupsen/logrus v1.0.2-0.20170726183946-abee6f9b0679 12 | github.com/stretchr/objx v0.1.1 // indirect 13 | github.com/stretchr/testify v1.7.0 14 | github.com/temporalio/tchannel-go v1.22.1-0.20220818200552-1be8d8cffa5b 15 | github.com/uber-common/bark v1.0.0 16 | golang.org/x/net v0.17.0 17 | ) 18 | -------------------------------------------------------------------------------- /handlers.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package ringpop 22 | 23 | import ( 24 | "github.com/temporalio/tchannel-go/json" 25 | "golang.org/x/net/context" 26 | ) 27 | 28 | // TODO: EVERYTHING! 29 | 30 | // Arg is a blank arg 31 | type Arg struct{} 32 | 33 | func (rp *Ringpop) registerHandlers() error { 34 | handlers := map[string]interface{}{ 35 | "/health": rp.health, 36 | "/admin/stats": rp.adminStatsHandler, 37 | "/admin/lookup": rp.adminLookupHandler, 38 | } 39 | 40 | return json.Register(rp.subChannel, handlers, func(ctx context.Context, err error) { 41 | rp.logger.WithField("error", err).Info("error occured") 42 | }) 43 | } 44 | 45 | func (rp *Ringpop) health(ctx json.Context, req *Arg) (*Arg, error) { 46 | return nil, nil 47 | } 48 | 49 | func (rp *Ringpop) adminStatsHandler(ctx json.Context, req *Arg) (map[string]interface{}, error) { 50 | return handleStats(rp), nil 51 | } 52 | 53 | type lookupRequest struct { 54 | Key string `json:"key"` 55 | } 56 | 57 | type lookupResponse struct { 58 | Dest string `json:"dest"` 59 | } 60 | 61 | func (rp *Ringpop) adminLookupHandler(ctx json.Context, req *lookupRequest) (*lookupResponse, error) { 62 | dest, err := rp.Lookup(req.Key) 63 | if err != nil { 64 | return nil, err 65 | } 66 | return &lookupResponse{Dest: dest}, nil 67 | } 68 | 69 | func (rp *Ringpop) adminReloadHandler(ctx json.Context, req *Arg) (*Arg, error) { 70 | return nil, nil 71 | } 72 | -------------------------------------------------------------------------------- /hashring/checksummer.go: -------------------------------------------------------------------------------- 1 | package hashring 2 | 3 | import ( 4 | "bytes" 5 | "sort" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/dgryski/go-farm" 10 | ) 11 | 12 | // Checksummer computes a checksum for an instance of a HashRing. The 13 | // checksum can be used to compare two rings for equality. 14 | type Checksummer interface { 15 | // Checksum calculates the checksum for the hashring that is passed in. 16 | // Compute will be called while having at least a read-lock on the hashring so 17 | // it is safe to read from the ring, but not safe to change the ring. There 18 | // might be multiple Checksum Computes initiated at the same time, but every 19 | // Checksum will only be called once per hashring at once 20 | Checksum(ring *HashRing) (checksum uint32) 21 | } 22 | 23 | type identityChecksummer struct{} 24 | 25 | func (i *identityChecksummer) Checksum(ring *HashRing) uint32 { 26 | identitySet := make(map[string]struct{}) 27 | ring.tree.root.traverseWhile(func(node *redBlackNode) bool { 28 | identitySet[node.key.(replicaPoint).identity] = struct{}{} 29 | return true 30 | }) 31 | 32 | identities := make([]string, 0, len(identitySet)) 33 | for identity := range identitySet { 34 | identities = append(identities, identity) 35 | } 36 | 37 | sort.Strings(identities) 38 | bytes := []byte(strings.Join(identities, ";")) 39 | return farm.Fingerprint32(bytes) 40 | } 41 | 42 | type replicaPointChecksummer struct{} 43 | 44 | func (r *replicaPointChecksummer) Checksum(ring *HashRing) uint32 { 45 | buffer := bytes.Buffer{} 46 | 47 | ring.tree.root.traverseWhile(func(node *redBlackNode) bool { 48 | buffer.WriteString(strconv.Itoa(node.key.(replicaPoint).hash)) 49 | buffer.WriteString("-") 50 | buffer.WriteString(node.value.(string)) 51 | buffer.WriteString(";") 52 | return true 53 | }) 54 | 55 | return farm.Fingerprint32(buffer.Bytes()) 56 | } 57 | -------------------------------------------------------------------------------- /hashring/checksummer_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package hashring 22 | 23 | import ( 24 | "sort" 25 | "strings" 26 | "testing" 27 | 28 | "github.com/dgryski/go-farm" 29 | "github.com/stretchr/testify/assert" 30 | ) 31 | 32 | // addressChecksum implements the now obsolete checksum method that didn't support identities. 33 | // It's moved to this test file to test backwards compatibility of the identityChecksummer 34 | type addressChecksummer struct{} 35 | 36 | func (i *addressChecksummer) Checksum(ring *HashRing) uint32 { 37 | addresses := ring.copyServersNoLock() 38 | sort.Strings(addresses) 39 | bytes := []byte(strings.Join(addresses, ";")) 40 | return farm.Fingerprint32(bytes) 41 | } 42 | 43 | func TestAddressChecksum_Compute(t *testing.T) { 44 | members := genMembers(1, 1, 10, false) 45 | ring := New(farm.Fingerprint32, 1) 46 | ring.AddMembers(members...) 47 | checksum := &addressChecksummer{} 48 | 49 | addresses := make([]string, 0, 10) 50 | for _, members := range members { 51 | addresses = append(addresses, members.GetAddress()) 52 | } 53 | 54 | sort.Strings(addresses) 55 | bytes := []byte(strings.Join(addresses, ";")) 56 | 57 | expected := farm.Fingerprint32(bytes) 58 | actual := checksum.Checksum(ring) 59 | 60 | assert.Equal(t, expected, actual) 61 | } 62 | 63 | func TestIdentityChecksum_Compute(t *testing.T) { 64 | identityChecksummer := &identityChecksummer{} 65 | 66 | ringWithoutIdentities := New(farm.Fingerprint32, 1) 67 | ringWithoutIdentities.AddMembers(genMembers(1, 1, 10, false)...) 68 | 69 | legacyChecksum := (&addressChecksummer{}).Checksum(ringWithoutIdentities) 70 | identityChecksum := identityChecksummer.Checksum(ringWithoutIdentities) 71 | 72 | assert.Equal(t, legacyChecksum, identityChecksum, "Identity checksum should be the same as legacy on ring without identities") 73 | 74 | ringWithIdentities := New(farm.Fingerprint32, 1) 75 | ringWithIdentities.AddMembers(genMembers(1, 1, 10, true)...) 76 | 77 | identityChecksum = identityChecksummer.Checksum(ringWithIdentities) 78 | 79 | assert.NotEqual(t, legacyChecksum, identityChecksum, "IdentityChecksummer should not match legacy checksummer on ring with identites ") 80 | } 81 | 82 | func TestReplicaPointChecksum_Compute(t *testing.T) { 83 | replicaPointChecksummer := &replicaPointChecksummer{} 84 | members := genMembers(1, 1, 10, false) 85 | 86 | ring1ReplicaPoint := New(farm.Fingerprint32, 1) 87 | ring1ReplicaPoint.AddMembers(members...) 88 | 89 | ring2ReplicaPoints := New(farm.Fingerprint32, 2) 90 | ring2ReplicaPoints.AddMembers(members...) 91 | 92 | checksum1ReplicaPoint := replicaPointChecksummer.Checksum(ring1ReplicaPoint) 93 | checksum2ReplicaPoints := replicaPointChecksummer.Checksum(ring2ReplicaPoints) 94 | 95 | assert.NotEqual(t, checksum1ReplicaPoint, checksum2ReplicaPoints, "Checksum should not match with different replica point counts") 96 | } 97 | -------------------------------------------------------------------------------- /hashring/util_test.go: -------------------------------------------------------------------------------- 1 | package hashring 2 | 3 | type fakeMember struct { 4 | address string 5 | identity string 6 | } 7 | 8 | func (f fakeMember) GetAddress() string { 9 | return f.address 10 | } 11 | 12 | func (f fakeMember) Label(key string) (value string, has bool) { 13 | return "", false 14 | } 15 | 16 | func (f fakeMember) Identity() string { 17 | if f.identity != "" { 18 | return f.identity 19 | } 20 | return f.address 21 | } 22 | -------------------------------------------------------------------------------- /logging/default.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package logging 22 | 23 | import ( 24 | "github.com/uber-common/bark" 25 | ) 26 | 27 | var defaultFacility = NewFacility(nil) 28 | 29 | // SetLogger sets the underlying logger for the default facility. 30 | func SetLogger(log bark.Logger) { defaultFacility.SetLogger(log) } 31 | 32 | // SetLevel sets the severity level for a named logger on the default facility. 33 | func SetLevel(logName string, level Level) error { return defaultFacility.SetLevel(logName, level) } 34 | 35 | // SetLevels same as SetLevels but for multiple named loggers. 36 | func SetLevels(levels map[string]Level) error { return defaultFacility.SetLevels(levels) } 37 | 38 | // Logger returns a named logger from the default facility. 39 | func Logger(logName string) bark.Logger { return defaultFacility.Logger(logName) } 40 | -------------------------------------------------------------------------------- /logging/default_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package logging 22 | 23 | import ( 24 | "testing" 25 | 26 | "github.com/stretchr/testify/mock" 27 | "github.com/stretchr/testify/suite" 28 | "github.com/temporalio/ringpop-go/test/mocks/logger" 29 | ) 30 | 31 | type DefaultLoggingTestSuite struct { 32 | suite.Suite 33 | mockLogger *mocklogger.Logger 34 | } 35 | 36 | func (s *DefaultLoggingTestSuite) SetupTest() { 37 | s.mockLogger = &mocklogger.Logger{} 38 | SetLogger(s.mockLogger) 39 | // Set expected calls 40 | s.mockLogger.On("Warn", mock.Anything) 41 | } 42 | 43 | func (s *DefaultLoggingTestSuite) TestMessagePropagation() { 44 | Logger("named").Warn("msg") 45 | s.mockLogger.AssertCalled(s.T(), "Warn", []interface{}{"msg"}) 46 | } 47 | 48 | func (s *DefaultLoggingTestSuite) TestSetLevel() { 49 | SetLevel("name", Fatal) 50 | Logger("named").Warn("msg") 51 | s.mockLogger.AssertCalled(s.T(), "Warn", []interface{}{"msg"}) 52 | } 53 | 54 | func (s *DefaultLoggingTestSuite) TestSetLevels() { 55 | SetLevels(map[string]Level{"named": Fatal}) 56 | Logger("named").Warn("msg") 57 | s.mockLogger.AssertNotCalled(s.T(), "Warn", []interface{}{"msg"}) 58 | } 59 | 60 | func TestDefaultLoggingTestSuite(t *testing.T) { 61 | suite.Run(t, new(DefaultLoggingTestSuite)) 62 | } 63 | -------------------------------------------------------------------------------- /logging/facility_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package logging 22 | 23 | import ( 24 | "strings" 25 | "testing" 26 | 27 | "github.com/stretchr/testify/mock" 28 | "github.com/stretchr/testify/suite" 29 | "github.com/uber-common/bark" 30 | "github.com/temporalio/ringpop-go/test/mocks/logger" 31 | ) 32 | 33 | type LogFacilityTestSuite struct { 34 | suite.Suite 35 | mockLogger *mocklogger.Logger 36 | facility *Facility 37 | } 38 | 39 | func (s *LogFacilityTestSuite) SetupTest() { 40 | s.mockLogger = &mocklogger.Logger{} 41 | s.facility = NewFacility(s.mockLogger) 42 | // Set expected calls 43 | for _, meth := range []string{"Debug", "Info", "Warn", "Error", "Fatal", "Panic"} { 44 | s.mockLogger.On(meth, mock.Anything) 45 | s.mockLogger.On(meth+"f", mock.Anything, mock.Anything) 46 | } 47 | s.mockLogger.On("WithFields", mock.Anything).Return(s.mockLogger) 48 | s.mockLogger.On("WithField", mock.Anything, mock.Anything).Return(s.mockLogger) 49 | } 50 | 51 | func (s *LogFacilityTestSuite) TestForwarding() { 52 | levels := []Level{Debug, Info, Warn, Error, Fatal, Panic} 53 | for _, level := range levels { 54 | methName := strings.Title(level.String()) 55 | msg := []interface{}{"msg"} 56 | 57 | fields := bark.Fields{"a": 1} 58 | s.facility.Log("name", level, fields, msg) 59 | s.mockLogger.AssertCalled(s.T(), methName, msg) 60 | s.mockLogger.AssertCalled(s.T(), "WithFields", fields) 61 | 62 | fields = bark.Fields{"b": 2} 63 | s.facility.Logf("name", level, fields, "format %s", msg) 64 | s.mockLogger.AssertCalled(s.T(), methName+"f", "format %s", msg) 65 | s.mockLogger.AssertCalled(s.T(), "WithFields", fields) 66 | } 67 | } 68 | 69 | func (s *LogFacilityTestSuite) TestSetLogger() { 70 | newLogger := &mocklogger.Logger{} 71 | s.facility.SetLogger(newLogger) 72 | newLogger.On("Debug", mock.Anything) 73 | msg := []interface{}{"msg"} 74 | s.facility.Log("name", Debug, nil, msg) 75 | newLogger.AssertCalled(s.T(), "Debug", msg) 76 | } 77 | 78 | func (s *LogFacilityTestSuite) TestSetLevel() { 79 | s.facility.SetLevel("name", Warn) 80 | s.afterLevelIsSet() 81 | } 82 | 83 | func (s *LogFacilityTestSuite) TestSetLevels() { 84 | s.facility.SetLevels(map[string]Level{"name": Warn}) 85 | s.afterLevelIsSet() 86 | } 87 | 88 | func (s *LogFacilityTestSuite) afterLevelIsSet() { 89 | msg := []interface{}{"msg"} 90 | s.facility.Log("name", Debug, nil, msg) 91 | s.facility.Logf("name", Debug, nil, "format %s", msg) 92 | s.mockLogger.AssertNotCalled(s.T(), "Debug", msg) 93 | s.mockLogger.AssertNotCalled(s.T(), "Debugf", "format %s", msg) 94 | } 95 | 96 | func (s *LogFacilityTestSuite) TestSetLevelError() { 97 | s.Error(s.facility.SetLevel("name", Panic), "Setting a severity level above Fatal should fail.") 98 | } 99 | 100 | func (s *LogFacilityTestSuite) TestSetLevelsError() { 101 | s.Error(s.facility.SetLevels(map[string]Level{"name": Panic}), "Setting a severity level above Fatal should fail.") 102 | } 103 | 104 | func (s *LogFacilityTestSuite) TestNamedLogger() { 105 | logger := s.facility.Logger("name") 106 | logger.Debug("msg") 107 | s.mockLogger.AssertCalled(s.T(), "Debug", []interface{}{"msg"}) 108 | } 109 | 110 | func TestLogFacilityTestSuite(t *testing.T) { 111 | suite.Run(t, new(LogFacilityTestSuite)) 112 | } 113 | -------------------------------------------------------------------------------- /logging/level.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package logging 22 | 23 | import ( 24 | "fmt" 25 | "strconv" 26 | ) 27 | 28 | // Level is the severity level of a log message. 29 | type Level uint8 30 | 31 | // Additional levels can be created from integers between Debug and maxLevel. 32 | const ( 33 | // Panic log level 34 | Panic Level = iota 35 | // Fatal log level 36 | Fatal 37 | // Error log level 38 | Error 39 | // Warn log level 40 | Warn 41 | // Info log level 42 | Info 43 | // Debug log level 44 | Debug 45 | 46 | // maxLevel is the maximum valid log level. 47 | // This is used internally for boundary check. 48 | maxLevel = int(255) 49 | ) 50 | 51 | // String converts a log level to its string representation. 52 | func (lvl Level) String() string { 53 | switch lvl { 54 | case Panic: 55 | return "panic" 56 | case Fatal: 57 | return "fatal" 58 | case Error: 59 | return "error" 60 | case Warn: 61 | return "warn" 62 | case Info: 63 | return "info" 64 | case Debug: 65 | return "debug" 66 | } 67 | return strconv.Itoa(int(lvl)) 68 | } 69 | 70 | // Parse converts a string to a log level. 71 | func Parse(lvl string) (Level, error) { 72 | switch lvl { 73 | case "fatal": 74 | return Fatal, nil 75 | case "panic": 76 | return Panic, nil 77 | case "error": 78 | return Error, nil 79 | case "warn": 80 | return Warn, nil 81 | case "info": 82 | return Info, nil 83 | case "debug": 84 | return Debug, nil 85 | } 86 | 87 | level, err := strconv.Atoi(lvl) 88 | if level > maxLevel { 89 | err = fmt.Errorf("invalid level value: %s", lvl) 90 | } 91 | return Level(level), err 92 | } 93 | -------------------------------------------------------------------------------- /logging/level_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package logging 22 | 23 | import ( 24 | "testing" 25 | 26 | "github.com/stretchr/testify/suite" 27 | ) 28 | 29 | type LogLevelTestSuite struct { 30 | suite.Suite 31 | } 32 | 33 | // Test that level -> string -> level conversion produces the same level 34 | func (s *LogLevelTestSuite) TestLevels() { 35 | // This is a custom level, higer than Debug 36 | customLevel := Debug + 10 37 | levels := []Level{Panic, Fatal, Error, Warn, Info, Debug, customLevel} 38 | for _, level := range levels { 39 | gotLevel, err := Parse(level.String()) 40 | s.NoError(err, "Converting a Level to a string and back should not fail.") 41 | s.Equal(gotLevel, level, "Converting a Level to a string and back should produce the same Level.") 42 | } 43 | } 44 | 45 | func (s *LogLevelTestSuite) TestInvalidLevel() { 46 | levels := []string{"", "1234", "abc"} 47 | for _, level := range levels { 48 | _, err := Parse(level) 49 | s.Error(err, "Converting an invalid string to a Level should fail.") 50 | } 51 | } 52 | 53 | func TestLogLevelTestSuite(t *testing.T) { 54 | suite.Run(t, new(LogLevelTestSuite)) 55 | } 56 | -------------------------------------------------------------------------------- /logging/named.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package logging 22 | 23 | import ( 24 | "github.com/uber-common/bark" 25 | ) 26 | 27 | // namedLogger is a bark.Logger implementation that has a name. It forwards all 28 | // log requests to a logReceiver, adding its own name in the process. 29 | type namedLogger struct { 30 | name string 31 | forwardTo logReceiver 32 | err error 33 | fields bark.Fields 34 | } 35 | 36 | type logReceiver interface { 37 | Log(logName string, wantLevel Level, fields bark.Fields, msg []interface{}) 38 | Logf(logName string, wantLevel Level, fields bark.Fields, format string, msg []interface{}) 39 | } 40 | 41 | // Forward all method calls to the logReceiver. The logReceiver can decide, 42 | // based on the name of the logger and the desired severity level if the 43 | // message should be silenced or not. 44 | 45 | func (l *namedLogger) Debug(args ...interface{}) { l.forwardTo.Log(l.name, Debug, l.fields, args) } 46 | func (l *namedLogger) Info(args ...interface{}) { l.forwardTo.Log(l.name, Info, l.fields, args) } 47 | func (l *namedLogger) Warn(args ...interface{}) { l.forwardTo.Log(l.name, Warn, l.fields, args) } 48 | func (l *namedLogger) Error(args ...interface{}) { l.forwardTo.Log(l.name, Error, l.fields, args) } 49 | func (l *namedLogger) Fatal(args ...interface{}) { l.forwardTo.Log(l.name, Fatal, l.fields, args) } 50 | func (l *namedLogger) Panic(args ...interface{}) { l.forwardTo.Log(l.name, Panic, l.fields, args) } 51 | func (l *namedLogger) Debugf(fmt string, args ...interface{}) { 52 | l.forwardTo.Logf(l.name, Debug, l.fields, fmt, args) 53 | } 54 | func (l *namedLogger) Infof(fmt string, args ...interface{}) { 55 | l.forwardTo.Logf(l.name, Info, l.fields, fmt, args) 56 | } 57 | func (l *namedLogger) Warnf(fmt string, args ...interface{}) { 58 | l.forwardTo.Logf(l.name, Warn, l.fields, fmt, args) 59 | } 60 | func (l *namedLogger) Errorf(fmt string, args ...interface{}) { 61 | l.forwardTo.Logf(l.name, Error, l.fields, fmt, args) 62 | } 63 | func (l *namedLogger) Fatalf(fmt string, args ...interface{}) { 64 | l.forwardTo.Logf(l.name, Fatal, l.fields, fmt, args) 65 | } 66 | func (l *namedLogger) Panicf(fmt string, args ...interface{}) { 67 | l.forwardTo.Logf(l.name, Panic, l.fields, fmt, args) 68 | } 69 | 70 | // WithField creates a new namedLogger that retains the name but has an updated 71 | // copy of the fields. 72 | func (l *namedLogger) WithField(key string, value interface{}) bark.Logger { 73 | newSize := len(l.fields) + 1 74 | newFields := make(map[string]interface{}, newSize) // Hold the updated copy 75 | for k, v := range l.fields { 76 | newFields[k] = v 77 | } 78 | newFields[key] = value // Set the new key. 79 | 80 | return &namedLogger{ 81 | name: l.name, 82 | forwardTo: l.forwardTo, 83 | fields: newFields, 84 | } 85 | } 86 | 87 | func (l *namedLogger) WithFields(fields bark.LogFields) bark.Logger { 88 | other := fields.Fields() 89 | newSize := len(l.fields) + len(other) 90 | newFields := make(map[string]interface{}, newSize) // Hold the updated copy 91 | for k, v := range l.fields { 92 | newFields[k] = v 93 | } 94 | // The fields passed to the method call override any previously defined 95 | // fields with the same name. 96 | for k, v := range other { 97 | newFields[k] = v 98 | } 99 | 100 | return &namedLogger{ 101 | name: l.name, 102 | forwardTo: l.forwardTo, 103 | fields: newFields, 104 | } 105 | } 106 | 107 | // Return a new named logger with the error set to be included in a subsequent 108 | // normal logging call 109 | func (l *namedLogger) WithError(err error) bark.Logger { 110 | return &namedLogger{ 111 | name: l.name, 112 | forwardTo: l.forwardTo, 113 | err: err, 114 | fields: l.Fields(), 115 | } 116 | } 117 | 118 | // This is needed to fully implement the bark.Logger interface. 119 | func (l *namedLogger) Fields() bark.Fields { 120 | return l.fields 121 | } 122 | -------------------------------------------------------------------------------- /logging/named_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package logging 22 | 23 | import ( 24 | "testing" 25 | 26 | "github.com/stretchr/testify/suite" 27 | "github.com/uber-common/bark" 28 | ) 29 | 30 | type NamedLoggerTestSuite struct { 31 | suite.Suite 32 | recv *recv 33 | logger *namedLogger 34 | } 35 | 36 | type recv struct { 37 | LogName string 38 | WantLevel Level 39 | Fields bark.Fields 40 | Format string 41 | Msg []interface{} 42 | } 43 | 44 | func (r *recv) Log(logName string, wantLevel Level, fields bark.Fields, msg []interface{}) { 45 | r.LogName, r.WantLevel, r.Fields, r.Format, r.Msg = logName, wantLevel, fields, "", msg 46 | } 47 | 48 | func (r *recv) Logf(logName string, wantLevel Level, fields bark.Fields, format string, msg []interface{}) { 49 | r.LogName, r.WantLevel, r.Fields, r.Format, r.Msg = logName, wantLevel, fields, format, msg 50 | } 51 | 52 | func (s *NamedLoggerTestSuite) SetupTest() { 53 | s.recv = &recv{} 54 | s.logger = &namedLogger{ 55 | name: "name", 56 | forwardTo: s.recv, 57 | fields: bark.Fields{"key": 1}, 58 | } 59 | } 60 | 61 | func (s *NamedLoggerTestSuite) TestForwarding() { 62 | cases := []struct { 63 | level Level 64 | logMeth func(l *namedLogger, args ...interface{}) 65 | logMethf func(l *namedLogger, format string, args ...interface{}) 66 | }{ 67 | {Debug, (*namedLogger).Debug, (*namedLogger).Debugf}, 68 | {Info, (*namedLogger).Info, (*namedLogger).Infof}, 69 | {Warn, (*namedLogger).Warn, (*namedLogger).Warnf}, 70 | {Error, (*namedLogger).Error, (*namedLogger).Errorf}, 71 | {Fatal, (*namedLogger).Fatal, (*namedLogger).Fatalf}, 72 | {Panic, (*namedLogger).Panic, (*namedLogger).Panicf}, 73 | } 74 | for _, c := range cases { 75 | // logger.Debug("msg") 76 | c.logMeth(s.logger, "msg") 77 | 78 | expected := &recv{ 79 | LogName: "name", 80 | WantLevel: c.level, 81 | Fields: bark.Fields{"key": 1}, 82 | Format: "", 83 | Msg: []interface{}{"msg"}, 84 | } 85 | 86 | s.Equal(s.recv, expected, "Log messages should be forwarded unmodified.") 87 | 88 | *s.recv = recv{} // reset 89 | 90 | // logger.Debugf("format", "msg") 91 | c.logMethf(s.logger, "format", "msg") 92 | 93 | expected.Format = "format" 94 | s.Equal(s.recv, expected, "Log messages should be forwarded unmodified.") 95 | } 96 | } 97 | 98 | func (s *NamedLoggerTestSuite) TestWithFields() { 99 | l := s.logger.WithField("a", 1).WithFields(bark.Fields{"b": 2, "a": 3}) 100 | l.Debug("msg") 101 | expected := &recv{ 102 | LogName: "name", 103 | WantLevel: Debug, 104 | Fields: bark.Fields{"key": 1, "a": 3, "b": 2}, 105 | Msg: []interface{}{"msg"}, 106 | } 107 | s.Equal(s.recv, expected, "Multiple fields should be merged.") 108 | } 109 | 110 | func (s *NamedLoggerTestSuite) TestFields() { 111 | s.Equal(s.logger.Fields(), bark.Fields{"key": 1}, "A logger should return its fields.") 112 | } 113 | 114 | func TestNamedLoggerTestSuite(t *testing.T) { 115 | suite.Run(t, new(NamedLoggerTestSuite)) 116 | } 117 | -------------------------------------------------------------------------------- /logging/nologger.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package logging 22 | 23 | import ( 24 | "github.com/uber-common/bark" 25 | ) 26 | 27 | // NoLogger is a bark-compatible logger that does nothing with the log 28 | // messages. 29 | type noLogger struct{} 30 | 31 | func (l noLogger) Debug(args ...interface{}) {} 32 | func (l noLogger) Debugf(format string, args ...interface{}) {} 33 | func (l noLogger) Info(args ...interface{}) {} 34 | func (l noLogger) Infof(format string, args ...interface{}) {} 35 | func (l noLogger) Warn(args ...interface{}) {} 36 | func (l noLogger) Warnf(format string, args ...interface{}) {} 37 | func (l noLogger) Error(args ...interface{}) {} 38 | func (l noLogger) Errorf(format string, args ...interface{}) {} 39 | func (l noLogger) Fatal(args ...interface{}) {} 40 | func (l noLogger) Fatalf(format string, args ...interface{}) {} 41 | func (l noLogger) Panic(args ...interface{}) {} 42 | func (l noLogger) Panicf(format string, args ...interface{}) {} 43 | func (l noLogger) WithField(key string, value interface{}) bark.Logger { return l } 44 | func (l noLogger) WithFields(keyValues bark.LogFields) bark.Logger { return l } 45 | func (l noLogger) WithError(err error) bark.Logger { return l } 46 | func (l noLogger) Fields() bark.Fields { return nil } 47 | 48 | // NoLogger is the default logger used by logging facilities when a logger 49 | // is not passed. 50 | var NoLogger bark.Logger = noLogger{} 51 | -------------------------------------------------------------------------------- /membership/events.go: -------------------------------------------------------------------------------- 1 | package membership 2 | 3 | // MemberChange shows the state before and after the change of a Member 4 | type MemberChange struct { 5 | // Before is the state of the member before the change, if the 6 | // member is a new member the before state is nil 7 | Before Member 8 | // After is the state of the member after the change, if the 9 | // member left the after state will be nil 10 | After Member 11 | } 12 | 13 | // ChangeEvent indicates that the membership has changed. The event will contain 14 | // a list of changes that will show both the old and the new state of a member. 15 | // It is not guaranteed that any of the observable state of a member has in fact 16 | // changed, it might only be an interal state change for the underlying 17 | // membership. 18 | type ChangeEvent struct { 19 | // Changes is a slice of changes that is related to this event 20 | Changes []MemberChange 21 | } 22 | -------------------------------------------------------------------------------- /membership/interface.go: -------------------------------------------------------------------------------- 1 | package membership 2 | 3 | const ( 4 | // IdentityLabelKey is the key used to identify the identity label of a 5 | // Member 6 | IdentityLabelKey = "__identity" 7 | ) 8 | 9 | // Member defines a member of the membership. It can be used by applications to 10 | // apply specific business logic on Members. Examples are: 11 | // - Get the address of a member for RPC calls, both forwarding of internal 12 | // calls that should target a Member 13 | // - Decissions to include a Member in a query via predicates. 14 | type Member interface { 15 | // GetAddress returns the external address used by the rpc layer to 16 | // communicate to the member. 17 | // 18 | // Note: It is prefixed with Get for legacy reasons and can be removed after 19 | // a refactor of the swim.Member to free up the `Address` name. 20 | GetAddress() string 21 | 22 | // Label reads the label for a given key from the member. It also returns 23 | // wether or not the label was present on the member 24 | Label(key string) (value string, has bool) 25 | 26 | // Identity returns the logical identity the member takes within the 27 | // hashring, this is experimental and might move away from the membership to 28 | // the Hashring 29 | Identity() string 30 | } 31 | -------------------------------------------------------------------------------- /scripts/go-get-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Same as `go get` but you can specify a git commit ref. 4 | # 5 | set -e 6 | 7 | # 8 | # Resolve the path to the Go workspace from GOPATH. 9 | # 10 | _go_workspace() { 11 | local gopath 12 | 13 | # If GOPATH has multiple paths separate by a colon, use the first one as 14 | # the Go workspace. 15 | gopath="${GOPATH%%:*}" 16 | 17 | echo "$gopath" 18 | } 19 | 20 | # 21 | # List the Go package(s) in the current working directory, given the (optional) 22 | # path spec. 23 | # 24 | _go_list() { 25 | local path=$1 26 | go list ./${path} 27 | } 28 | 29 | # 30 | # Extract the version from the given string in the format "package@version" 31 | # 32 | _parse_version() { 33 | local package_spec="$1" 34 | if [ $(expr "$package_spec" : .*@) -ne 0 ]; then 35 | echo ${1##*@} 36 | fi 37 | } 38 | 39 | # 40 | # Extract the base repo from the package spec/URL. 41 | # 42 | _parse_repo() { 43 | local package_spec=$1 44 | 45 | # Split by '/' 46 | IFS='/' read -ra path_components <<< "$package_spec" 47 | 48 | echo "${path_components[0]}/${path_components[1]}/${path_components[2]}" 49 | } 50 | 51 | 52 | # 53 | # Extract the package path from the given package path. 54 | # 55 | # The path means the bit after the base (repo) name, for example given the 56 | # following package name: 57 | # 58 | # github.com/foo/mypackage/baz/.../ 59 | # 60 | # The path would be: 61 | # 62 | # /baz/.../ 63 | # 64 | _parse_path() { 65 | local package_spec=$1 66 | local package_base=$(_parse_repo "$package_spec") 67 | 68 | path=${package_spec#$package_base} # Strip base from front 69 | path=${path%%@*} # Strip version from back 70 | 71 | echo $path 72 | } 73 | 74 | # 75 | # Echos and runs the specified command. 76 | # 77 | run() { 78 | echo "+ $@" >&2 79 | "$@" 80 | } 81 | 82 | # 83 | # Print usage text to stderr. 84 | # 85 | _usage() { 86 | { 87 | echo 88 | echo "go gets packages at specific versions" 89 | echo 90 | echo "Usage: $0 " 91 | echo " e.g.: $0 github.com/foo/foo-package@31c913b github.com/foo/bar-package@3020345" 92 | echo 93 | } >&2 94 | } 95 | 96 | _main() { 97 | if [ "$#" -eq 0 ]; then 98 | _usage 99 | exit 99 100 | fi 101 | 102 | local package_repo 103 | local package_path 104 | local package_version 105 | 106 | local go_workspace="$(_go_workspace)" 107 | 108 | for package_spec in "$@"; do 109 | 110 | package_repo=$(_parse_repo "$package_spec") 111 | package_path=$(_parse_path "$package_spec") 112 | package_version=$(_parse_version "$package_spec") 113 | 114 | # echo package_repo: $package_repo 115 | # echo package_path: $package_path 116 | # echo package_version: $package_version 117 | # exit 118 | 119 | # Download package 120 | run go get -d "${package_repo}${package_path}" 121 | 122 | pushd "${go_workspace}/src/${package_repo}" >/dev/null 123 | 124 | if [ ! -z "$package_version" ]; then 125 | echo "# cd $PWD" >&2 126 | run git checkout -q $package_version 127 | # install dependencies for checked out version 128 | run go get ./... 129 | fi 130 | 131 | # Generate list of sub packages 132 | subpackages=$(_go_list "$package_path") 133 | 134 | popd >/dev/null 135 | 136 | # Build and install each package 137 | for subpackage in $subpackages; do 138 | pushd "${go_workspace}/src/${subpackage}" >/dev/null 139 | echo "# cd $PWD" >&2 140 | run go build 141 | run go install 142 | popd >/dev/null 143 | done 144 | done 145 | } 146 | 147 | _main "$@" 148 | -------------------------------------------------------------------------------- /scripts/lint/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean clean-tests setup test out update-tests 2 | 3 | out: test 4 | 5 | clean: clean-tests 6 | 7 | clean-tests: 8 | rm -vf test/*.err 9 | 10 | setup: 11 | pip install cram 12 | 13 | test: 14 | BASE_DIR="$$PWD" cram -v test/*.t 15 | 16 | update-tests: 17 | BASE_DIR="$$PWD" cram -v -i test/*.t 18 | -------------------------------------------------------------------------------- /scripts/lint/lint-excludes: -------------------------------------------------------------------------------- 1 | ./swim/disseminator.go:*: range var member copies Lock: swim.Member 2 | ./swim/member.go:*: address passes Lock by value: swim.Member 3 | ./swim/member.go:*: incarnation passes Lock by value: swim.Member 4 | ./swim/memberlist.go:*: Pingable passes Lock by value: swim.Member 5 | ./swim/member_test.go:*: newMember returns Lock by value: swim.Member 6 | ./swim/ping_request_sender.go:*: func passes Lock by value: swim.Member 7 | ./swim/stats_test.go:*: range var member copies Lock: swim.Member 8 | ./swim/test_utils.go:*: range var expected copies Lock: swim.Member 9 | ./swim/handlers.go:*: range var member copies Lock: swim.Member 10 | # go 1.6 travis - "Lock" is now lowercase "lock": 11 | ./swim/disseminator.go:*: range var member copies lock: swim.Member 12 | ./swim/member.go:*: address passes lock by value: swim.Member 13 | ./swim/member.go:*: incarnation passes lock by value: swim.Member 14 | ./swim/member_test.go:*: newMember returns lock by value: swim.Member 15 | ./swim/member_test.go:*: assignment copies lock value to m: swim.Member 16 | ./swim/member_test.go:*: assignment copies lock value to m: swim.Member 17 | ./swim/memberlist.go:*: Pingable passes lock by value: swim.Member 18 | ./swim/ping_request_sender.go:*: func passes lock by value: swim.Member 19 | ./swim/stats.go:*: assignment copies lock value to (\*s)\[i\]: swim.Member 20 | ./swim/stats.go:*: assignment copies lock value to (\*s)\[j\]: swim.Member 21 | ./swim/stats_test.go:*: range var member copies lock: swim.Member 22 | ./swim/test_utils.go:*: range var expected copies lock: swim.Member 23 | ./swim/handlers.go:*: range var member copies lock: swim.Member 24 | -------------------------------------------------------------------------------- /scripts/lint/lint-warn: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # lint-warn takes the output of `go tool vet` and matches lines against an 4 | # exclude file. If an output line matches an exclude, it is printed to stdout, 5 | # otherwise it is printed to stderr. 6 | # 7 | # If anything is printed to stderr, the program exits with exit code 1. 8 | set -ueo pipefail 9 | 10 | if [ $# -lt 1 ]; then 11 | echo "ERROR: Expect lint exclude file as argument" >&2 12 | exit 1 13 | fi 14 | 15 | # Load exclude patterns 16 | lint_exclude_patterns="$(cat $1)" 17 | 18 | declare buf= 19 | declare exit_code=0 20 | 21 | while IFS= read -r line; do 22 | buf="" 23 | 24 | IFS=$'\n' 25 | for exclude in $lint_exclude_patterns; do 26 | if [[ "$line" == $exclude ]]; then 27 | buf="WARNING: $line" 28 | break 29 | fi 30 | done 31 | 32 | if [ -z "$buf" ]; then 33 | # Print the error 34 | echo "ERROR: $line" >&2 35 | exit_code=1 36 | else 37 | # Print the warning 38 | echo "$buf" 39 | fi 40 | done 41 | 42 | exit $exit_code 43 | -------------------------------------------------------------------------------- /scripts/lint/run-vet: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # run-vet runs go vet on .go files in the specified directory. It runs the 4 | # output through lint-warn. It then swaps stdin/stdout so the error output 5 | # can be tee'd to a file. Warnings are thus printed to stderr. 6 | # 7 | set -ueo pipefail 8 | 9 | declare script_dir="${0%/*}" 10 | 11 | _go-vet() { 12 | find "$@" -maxdepth 1 -mindepth 1 -type f -name '*.go' \ 13 | -exec go vet -printfuncs Logf:3 {} + 2>&1 ; \ 14 | } 15 | 16 | _lint-warn() { 17 | # Run lint-warn, but swap stdout and stderr 18 | "$script_dir"/lint-warn "$script_dir"/lint-excludes 3>&2 2>&1 1>&3 19 | } 20 | 21 | _go-vet "$@" | _lint-warn 22 | -------------------------------------------------------------------------------- /scripts/lint/test/.gitignore: -------------------------------------------------------------------------------- 1 | *.err 2 | -------------------------------------------------------------------------------- /scripts/lint/test/lint-warn.t: -------------------------------------------------------------------------------- 1 | Test no args returns exit code 1: 2 | 3 | $ "$BASE_DIR"/lint-warn 4 | ERROR: Expect lint exclude file as argument 5 | [1] 6 | 7 | 8 | Test lint with only warnings returns 0 exit code: 9 | 10 | $ cat "$BASE_DIR"/test/test-lint-ok.log |"$BASE_DIR"/lint-warn "$BASE_DIR"/test/test-excludes 11 | WARNING: ./swim/member.go:53: address passes Lock by value: swim.Member 12 | WARNING: ./swim/member.go:57: incarnation passes Lock by value: swim.Member 13 | WARNING: ./swim/test_utils.go:135: range var expected copies Lock: swim.Member 14 | 15 | 16 | Test lint with a mix of warnings/errors returns exit code 1: 17 | 18 | $ cat "$BASE_DIR"/test/test-lint-mix.log |"$BASE_DIR"/lint-warn "$BASE_DIR"/test/test-excludes 19 | ERROR: ./swim/disseminator.go:107: range var member copies Lock: swim.Member 20 | WARNING: ./swim/member.go:53: address passes Lock by value: swim.Member 21 | WARNING: ./swim/member.go:57: incarnation passes Lock by value: swim.Member 22 | ERROR: ./swim/memberlist.go:128: Pingable passes Lock by value: swim.Member 23 | ERROR: ./swim/stats_test.go:101: range var member copies Lock: swim.Member 24 | WARNING: ./swim/test_utils.go:135: range var expected copies Lock: swim.Member 25 | [1] 26 | 27 | 28 | Test lint with only errors returns exit code 1: 29 | 30 | $ cat "$BASE_DIR"/test/test-lint-all-fail.log |"$BASE_DIR"/lint-warn "$BASE_DIR"/test/test-excludes 31 | ERROR: ./swim/disseminator.go:107: range var member copies Lock: swim.Member 32 | ERROR: ./swim/memberlist.go:128: Pingable passes Lock by value: swim.Member 33 | ERROR: ./swim/stats_test.go:101: range var member copies Lock: swim.Member 34 | [1] 35 | -------------------------------------------------------------------------------- /scripts/lint/test/test-excludes: -------------------------------------------------------------------------------- 1 | ./swim/member.go:*: address passes Lock by value: swim.Member 2 | ./swim/member.go:*: incarnation passes Lock by value: swim.Member 3 | ./swim/test_utils.go:*: range var expected copies Lock: swim.Member 4 | -------------------------------------------------------------------------------- /scripts/lint/test/test-lint-all-fail.log: -------------------------------------------------------------------------------- 1 | ./swim/disseminator.go:107: range var member copies Lock: swim.Member 2 | ./swim/memberlist.go:128: Pingable passes Lock by value: swim.Member 3 | ./swim/stats_test.go:101: range var member copies Lock: swim.Member 4 | -------------------------------------------------------------------------------- /scripts/lint/test/test-lint-mix.log: -------------------------------------------------------------------------------- 1 | ./swim/disseminator.go:107: range var member copies Lock: swim.Member 2 | ./swim/member.go:53: address passes Lock by value: swim.Member 3 | ./swim/member.go:57: incarnation passes Lock by value: swim.Member 4 | ./swim/memberlist.go:128: Pingable passes Lock by value: swim.Member 5 | ./swim/stats_test.go:101: range var member copies Lock: swim.Member 6 | ./swim/test_utils.go:135: range var expected copies Lock: swim.Member 7 | -------------------------------------------------------------------------------- /scripts/lint/test/test-lint-ok.log: -------------------------------------------------------------------------------- 1 | ./swim/member.go:53: address passes Lock by value: swim.Member 2 | ./swim/member.go:57: incarnation passes Lock by value: swim.Member 3 | ./swim/test_utils.go:135: range var expected copies Lock: swim.Member 4 | -------------------------------------------------------------------------------- /scripts/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | make lint 3 | -------------------------------------------------------------------------------- /scripts/testpop/statter.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "os" 8 | "time" 9 | 10 | "github.com/cactus/go-statsd-client/statsd" 11 | ) 12 | 13 | // fileSender is an adapter from io.WriteCloser to statsd.Sender 14 | type fileSender struct { 15 | bufWriter *bufio.Writer 16 | closer io.Closer 17 | } 18 | 19 | // Send implements the statsd.Sender interface 20 | func (fs *fileSender) Send(data []byte) (int, error) { 21 | line := fmt.Sprintf("%s: %s\n", time.Now().UTC().Format(time.RFC3339Nano), data) 22 | _, err := fs.bufWriter.Write([]byte(line)) 23 | // Because we're changing the underlying bytes sent, make sure: 24 | // written == len(data) on success 25 | // written < len(data) on error 26 | if err != nil { 27 | return 0, err 28 | } 29 | return len(data), nil 30 | } 31 | 32 | // Close implements the statsd.Sender interface 33 | func (fs *fileSender) Close() error { 34 | err := fs.bufWriter.Flush() 35 | fs.closer.Close() 36 | return err 37 | } 38 | 39 | // newFileSender returns a new fileSender 40 | func newFileSender(wc io.WriteCloser) *fileSender { 41 | return &fileSender{ 42 | bufWriter: bufio.NewWriter(wc), 43 | closer: wc} 44 | } 45 | 46 | // NewFileStatsd returns a statsd.Statter that writes to file. Each entry is 47 | // prefixed by a date/time with nanosecond resolution on it's own line 48 | func NewFileStatsd(name string) (statsd.Statter, error) { 49 | f, err := os.Create(name) 50 | if err != nil { 51 | return nil, err 52 | } 53 | fileSender := newFileSender(f) 54 | c, err := statsd.NewClientWithSender(fileSender, "") 55 | if err != nil { 56 | return nil, err 57 | } 58 | return c, nil 59 | } 60 | -------------------------------------------------------------------------------- /scripts/travis/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !*.sh -------------------------------------------------------------------------------- /scripts/travis/get-cram.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | if [[ ! -f "_venv/bin/activate" ]]; then 5 | virtualenv _venv 6 | fi 7 | 8 | . _venv/bin/activate 9 | 10 | if [[ -z "$(which cram)" ]]; then 11 | ./_venv/bin/pip install cram 12 | fi 13 | -------------------------------------------------------------------------------- /scripts/travis/get-thrift-gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")" 6 | rm -rf thrift-gen-release.tar.gz 7 | wget https://github.com/uber/tchannel-go/releases/download/v0.01/thrift-gen-release.tar.gz 8 | tar -xzf thrift-gen-release.tar.gz -------------------------------------------------------------------------------- /scripts/travis/get-thrift.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")" 6 | rm -rf thrift-release.zip 7 | wget https://github.com/prashantv/thrift/releases/download/p0.0.1/thrift-release.zip 8 | unzip thrift-release.zip 9 | 10 | -------------------------------------------------------------------------------- /shared/interfaces.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import "github.com/temporalio/tchannel-go" 4 | 5 | // The TChannel interface defines the dependencies for TChannel in Ringpop. 6 | type TChannel interface { 7 | GetSubChannel(string, ...tchannel.SubChannelOption) *tchannel.SubChannel 8 | PeerInfo() tchannel.LocalPeerInfo 9 | Register(h tchannel.Handler, methodName string) 10 | State() tchannel.ChannelState 11 | } 12 | 13 | // SubChannel represents a TChannel SubChannel as used in Ringpop. 14 | type SubChannel interface { 15 | tchannel.Registrar 16 | } 17 | -------------------------------------------------------------------------------- /shared/shared.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/temporalio/tchannel-go" 7 | 8 | "golang.org/x/net/context" 9 | ) 10 | 11 | var retryOptions = &tchannel.RetryOptions{ 12 | RetryOn: tchannel.RetryNever, 13 | } 14 | 15 | // NewTChannelContext creates a new TChannel context with default options 16 | // suitable for use in Ringpop. 17 | func NewTChannelContext(timeout time.Duration) (tchannel.ContextWithHeaders, context.CancelFunc) { 18 | return tchannel.NewContextBuilder(timeout). 19 | DisableTracing(). 20 | SetRetryOptions(retryOptions). 21 | Build() 22 | } 23 | -------------------------------------------------------------------------------- /stats_handler.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package ringpop 22 | 23 | import ( 24 | "os" 25 | "runtime" 26 | "strconv" 27 | "time" 28 | 29 | "github.com/temporalio/tchannel-go" 30 | ) 31 | 32 | func handleStats(rp *Ringpop) map[string]interface{} { 33 | var memStats runtime.MemStats 34 | runtime.ReadMemStats(&memStats) 35 | 36 | servers := rp.ring.Servers() 37 | 38 | type stats map[string]interface{} 39 | 40 | uptime, _ := rp.Uptime() 41 | 42 | return stats{ 43 | "hooks": nil, 44 | "membership": rp.node.MemberStats(), 45 | "process": stats{ 46 | "memory": stats{ 47 | "rss": memStats.Sys, 48 | "heapTotal": memStats.HeapSys, 49 | "heapUsed": memStats.HeapInuse, 50 | }, 51 | "pid": os.Getpid(), 52 | }, 53 | "protocol": rp.node.ProtocolStats(), 54 | "ring": stats{ 55 | "servers": servers, 56 | "checksum": rp.ring.Checksum(), 57 | "checksums": rp.ring.Checksums(), 58 | }, 59 | "version": "???", // TODO: version! 60 | "timestamp": time.Now().Unix(), 61 | "uptime": uptime, 62 | "tchannelVersion": strconv.Itoa(tchannel.CurrentProtocolVersion), // get proper version 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /swim/gossip_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package swim 22 | 23 | import ( 24 | "testing" 25 | 26 | "github.com/stretchr/testify/suite" 27 | ) 28 | 29 | type GossipTestSuite struct { 30 | suite.Suite 31 | tnode *testNode 32 | node *Node 33 | g *gossip 34 | m *memberlist 35 | incarnation int64 36 | } 37 | 38 | func (s *GossipTestSuite) SetupTest() { 39 | s.tnode = newChannelNode(s.T()) 40 | s.node = s.tnode.node 41 | 42 | s.g = s.node.gossip 43 | s.m = s.node.memberlist 44 | } 45 | 46 | func (s *GossipTestSuite) TearDownTest() { 47 | s.tnode.Destroy() 48 | } 49 | 50 | func (s *GossipTestSuite) TestStartStop() { 51 | s.True(s.g.Stopped(), "expected gossip to be stopped") 52 | 53 | s.g.Start() 54 | s.False(s.g.Stopped(), "expected gossip to be started") 55 | 56 | s.g.Stop() 57 | s.True(s.g.Stopped(), "expected gossip to be stopped") 58 | } 59 | 60 | func (s *GossipTestSuite) TestStartWhileRunning() { 61 | s.True(s.g.Stopped(), "expected gossip to be stopped") 62 | 63 | s.g.Start() 64 | s.False(s.g.Stopped(), "expected gossip to be started") 65 | 66 | s.g.Start() 67 | s.False(s.g.Stopped(), "expected gossip to still be started") 68 | } 69 | 70 | func (s *GossipTestSuite) StopWhileStopped() { 71 | s.True(s.g.Stopped(), "expected gossip to be stopped") 72 | 73 | s.g.Stop() 74 | s.True(s.g.Stopped(), "expected gossip to still be stopped") 75 | } 76 | 77 | func (s *GossipTestSuite) TestUpdatesArePropagated() { 78 | peer := newChannelNode(s.T()) 79 | defer peer.Destroy() 80 | defer peer.channel.Close() 81 | 82 | bootstrapNodes(s.T(), s.tnode, peer) 83 | waitForConvergence(s.T(), 100, s.tnode, peer) 84 | s.True(s.g.Stopped()) 85 | s.True(peer.node.gossip.Stopped()) 86 | 87 | s.node.disseminator.ClearChanges() 88 | peer.node.disseminator.ClearChanges() 89 | 90 | peer.node.memberlist.Update([]Change{ 91 | Change{Address: "127.0.0.1:3003", Incarnation: s.incarnation, Status: Alive}, 92 | Change{Address: "127.0.0.1:3004", Incarnation: s.incarnation, Status: Faulty}, 93 | Change{Address: "127.0.0.1:3005", Incarnation: s.incarnation, Status: Suspect}, 94 | Change{Address: "127.0.0.1:3006", Incarnation: s.incarnation, Status: Leave}, 95 | }) 96 | 97 | s.Len(peer.node.disseminator.changes, 4) 98 | 99 | s.g.ProtocolPeriod() 100 | 101 | expectedMembers := []Member{ 102 | Member{Address: "127.0.0.1:3003", Status: Alive}, 103 | Member{Address: "127.0.0.1:3004", Status: Faulty}, 104 | Member{Address: "127.0.0.1:3005", Status: Suspect}, 105 | Member{Address: "127.0.0.1:3006", Status: Leave}, 106 | } 107 | 108 | memberlistHasMembers(s.T(), s.node.memberlist, expectedMembers) 109 | } 110 | 111 | // TODO: move this test to node_test? 112 | func (s *GossipTestSuite) TestSuspicionStarted() { 113 | peers := genChannelNodes(s.T(), 3) 114 | defer destroyNodes(peers...) 115 | 116 | bootstrapNodes(s.T(), append(peers, s.tnode)...) 117 | 118 | s.node.memberiter = new(dummyIter) // always returns a member at 127.0.0.1:3010 119 | 120 | s.g.ProtocolPeriod() 121 | 122 | s.NotNil(s.node.stateTransitions.timer("127.0.0.1:3010"), "expected state timer to be set") 123 | } 124 | 125 | func TestGossipTestSuite(t *testing.T) { 126 | suite.Run(t, new(GossipTestSuite)) 127 | } 128 | -------------------------------------------------------------------------------- /swim/join_delayer_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | package swim 21 | 22 | import ( 23 | "testing" 24 | "time" 25 | 26 | "github.com/stretchr/testify/suite" 27 | ) 28 | 29 | // Null randomizer and sleeper used for join delay tests. 30 | var noRandom = func(n int) int { return n } 31 | var noSleep = func(d time.Duration) {} 32 | 33 | type joinDelayerTestSuite struct { 34 | suite.Suite 35 | delayer *exponentialDelayer 36 | expectedDelays [6]time.Duration 37 | } 38 | 39 | func (s *joinDelayerTestSuite) SetupTest() { 40 | opts := &delayOpts{ 41 | initial: 100 * time.Millisecond, 42 | max: 1 * time.Second, 43 | sleeper: noSleep, // Sleeper used for tests applies no actual delay 44 | } 45 | 46 | // Represents the steps of a complete exponential backoff 47 | // as implemented by the exponentialDelayer. 48 | s.expectedDelays = [...]time.Duration{ 49 | opts.initial, 50 | 200 * time.Millisecond, 51 | 400 * time.Millisecond, 52 | 800 * time.Millisecond, 53 | opts.max, // Backoff delay is capped at this point. 54 | opts.max, 55 | } 56 | 57 | delayer, err := newExponentialDelayer("dummyjoiner", opts) 58 | s.NoError(err, "expected valid exponential delayer") 59 | s.delayer = delayer 60 | } 61 | 62 | func (s *joinDelayerTestSuite) TestDelayWithRandomness() { 63 | for i, expectedDelay := range s.expectedDelays { 64 | delay := s.delayer.delay() 65 | if i == 0 { 66 | s.True(delay >= 0 && delay < expectedDelay, 67 | "first delay should be between 0 and min") 68 | } else { 69 | s.True(delay >= s.expectedDelays[i-1] && delay <= expectedDelay, 70 | "next delays should be within bounds") 71 | } 72 | } 73 | } 74 | 75 | func (s *joinDelayerTestSuite) TestDelayWithoutRandomness() { 76 | // Substitute delayer's randomizer with one that produces 77 | // no randomness whatsoever. 78 | s.delayer.randomizer = noRandom 79 | 80 | for _, expectedDelay := range s.expectedDelays { 81 | delay := s.delayer.delay() 82 | s.EqualValues(expectedDelay, delay, "join attempt delay is correct") 83 | } 84 | } 85 | 86 | func (s *joinDelayerTestSuite) TestMaxDelayReached() { 87 | for i := range s.expectedDelays { 88 | s.delayer.delay() 89 | // This condition assumes that the last two elements 90 | // of expectedDelay is equal to the max delay. 91 | if i < len(s.expectedDelays)-2 { 92 | s.False(s.delayer.maxDelayReached, "max delay not reached") 93 | } else { 94 | s.True(s.delayer.maxDelayReached, "max delay not reached") 95 | } 96 | } 97 | } 98 | 99 | func TestJoinDelayerTestSuite(t *testing.T) { 100 | suite.Run(t, new(joinDelayerTestSuite)) 101 | } 102 | -------------------------------------------------------------------------------- /swim/join_handler.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package swim 22 | 23 | import "fmt" 24 | 25 | // A JoinResponse is sent back as a response to a JoinRequest from a 26 | // remote node 27 | type joinResponse struct { 28 | App string `json:"app"` 29 | Coordinator string `json:"coordinator"` 30 | Membership []Change `json:"membership"` 31 | Checksum uint32 `json:"membershipChecksum"` 32 | } 33 | 34 | // TODO: Denying joins? 35 | 36 | func validateSourceAddress(node *Node, sourceAddress string) error { 37 | if node.address == sourceAddress { 38 | return fmt.Errorf("A node tried joining a cluster by attempting to join itself. "+ 39 | "The node ,%s, must join someone else.", sourceAddress) 40 | } 41 | return nil 42 | } 43 | 44 | func validateSourceApp(node *Node, sourceApp string) error { 45 | if node.app != sourceApp { 46 | return fmt.Errorf("A node tried joining a different app cluster. The "+ 47 | "expected app, %s, did not match the actual app ,%s", node.app, sourceApp) 48 | } 49 | return nil 50 | } 51 | 52 | func handleJoin(node *Node, req *joinRequest) (*joinResponse, error) { 53 | node.EmitEvent(JoinReceiveEvent{ 54 | Local: node.Address(), 55 | Source: req.Source, 56 | }) 57 | 58 | node.serverRate.Mark(1) 59 | node.totalRate.Mark(1) 60 | 61 | if err := validateSourceAddress(node, req.Source); err != nil { 62 | return nil, err 63 | } 64 | 65 | if err := validateSourceApp(node, req.App); err != nil { 66 | return nil, err 67 | } 68 | 69 | res := &joinResponse{ 70 | App: node.app, 71 | Coordinator: node.address, 72 | Membership: node.disseminator.MembershipAsChanges(), 73 | Checksum: node.memberlist.Checksum(), 74 | } 75 | 76 | return res, nil 77 | } 78 | -------------------------------------------------------------------------------- /swim/member_doc_test.go: -------------------------------------------------------------------------------- 1 | package swim 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | ) 7 | 8 | func ExampleMember_checksumString() { 9 | var b bytes.Buffer 10 | m := Member{ 11 | Address: "192.0.2.1:1234", 12 | Status: Alive, 13 | Incarnation: 42, 14 | } 15 | m.checksumString(&b) 16 | fmt.Println(b.String()) 17 | // Output: 192.0.2.1:1234alive42 18 | } 19 | 20 | func ExampleMember_checksumString_labels() { 21 | var b bytes.Buffer 22 | m := Member{ 23 | Address: "192.0.2.1:1234", 24 | Status: Alive, 25 | Incarnation: 42, 26 | Labels: LabelMap{ 27 | "hello": "world", 28 | }, 29 | } 30 | m.checksumString(&b) 31 | fmt.Println(b.String()) 32 | // Output: 192.0.2.1:1234alive42#labels975109414 33 | } 34 | 35 | func ExampleMember_checksumString_multilabels() { 36 | var b bytes.Buffer 37 | m := Member{ 38 | Address: "192.0.2.1:1234", 39 | Status: Alive, 40 | Incarnation: 42, 41 | Labels: LabelMap{ 42 | "hello": "world", 43 | "foo": "baz", 44 | }, 45 | } 46 | m.checksumString(&b) 47 | fmt.Println(b.String()) 48 | // Output: 192.0.2.1:1234alive42#labels-1625122257 49 | } 50 | -------------------------------------------------------------------------------- /swim/member_predicate.go: -------------------------------------------------------------------------------- 1 | package swim 2 | 3 | // MemberPredicate is a function that tests if a Member satisfies a condition. 4 | // It is advised to use exported functions on Member instead of its exported 5 | // fields in case we want to extract the functionality of Member to an Interface 6 | // in the future. This is likely to happen if we pursue plugable membership. 7 | type MemberPredicate func(member Member) bool 8 | 9 | // MemberMatchesPredicates can take multiple predicates and test them against a 10 | // member returning if the member satisfies all the predicates. This means that 11 | // if one test fails it will stop executing and return with false. 12 | func MemberMatchesPredicates(member Member, predicates ...MemberPredicate) bool { 13 | for _, p := range predicates { 14 | if !p(member) { 15 | return false 16 | } 17 | } 18 | return true 19 | } 20 | 21 | // memberIsReachable tests if a member is deemed to be reachable. This filters 22 | // out all members that are known to be unresponsive. Most operations will only 23 | // ever be concerned with Members that are in a Reachable state. In SWIM terms a 24 | // member is considered reachable when it is either in Alive status or in 25 | // Suspect status. All other Members are considered to not be reachable. 26 | func memberIsReachable(member Member) bool { 27 | return member.isReachable() 28 | } 29 | 30 | // MemberWithLabelAndValue returns a predicate able to test if the value of a 31 | // label on a member is equal to the provided value. 32 | func MemberWithLabelAndValue(key, value string) MemberPredicate { 33 | return func(member Member) bool { 34 | v, ok := member.Labels[key] 35 | 36 | if !ok { 37 | return false 38 | } 39 | 40 | // test if the values match 41 | return v == value 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /swim/member_predicate_test.go: -------------------------------------------------------------------------------- 1 | package swim 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | var memberIsReachableTests = []struct { 10 | member Member 11 | reachable bool 12 | }{ 13 | {Member{Address: "192.0.2.1:1234", Incarnation: 42, Status: Alive, Labels: nil}, true}, 14 | {Member{Address: "192.0.2.1:1234", Incarnation: 42, Status: Suspect, Labels: nil}, true}, 15 | {Member{Address: "192.0.2.1:1234", Incarnation: 42, Status: Faulty, Labels: nil}, false}, 16 | {Member{Address: "192.0.2.1:1234", Incarnation: 42, Status: Leave, Labels: nil}, false}, 17 | {Member{Address: "192.0.2.1:1234", Incarnation: 42, Status: Tombstone, Labels: nil}, false}, 18 | } 19 | 20 | func TestMemberIsReachable(t *testing.T) { 21 | var predicate MemberPredicate 22 | predicate = memberIsReachable 23 | 24 | for _, test := range memberIsReachableTests { 25 | assert.Equal(t, test.reachable, predicate(test.member), "member: %v expected: %b", test.member, test.reachable) 26 | } 27 | } 28 | 29 | var memberWithLabelAndValueTests = []struct { 30 | member Member 31 | reachable bool 32 | }{ 33 | {Member{Address: "192.0.2.1:1234", Incarnation: 42, Status: Alive, Labels: nil}, false}, 34 | {Member{Address: "192.0.2.1:1234", Incarnation: 42, Status: Alive, Labels: map[string]string{"hello": "world"}}, true}, 35 | {Member{Address: "192.0.2.1:1234", Incarnation: 42, Status: Alive, Labels: map[string]string{"hello": "world", "foo": "bar"}}, true}, 36 | {Member{Address: "192.0.2.1:1234", Incarnation: 42, Status: Alive, Labels: map[string]string{"foo": "bar"}}, false}, 37 | } 38 | 39 | func TestMemberWithLabelAndValue(t *testing.T) { 40 | var predicate MemberPredicate 41 | predicate = MemberWithLabelAndValue("hello", "world") 42 | 43 | for _, test := range memberWithLabelAndValueTests { 44 | assert.Equal(t, test.reachable, predicate(test.member), "member: %v expected: %b", test.member, test.reachable) 45 | } 46 | } 47 | 48 | var truePredicate = func(member Member) bool { return true } 49 | var falsePredicate = func(member Member) bool { return false } 50 | 51 | var memberMatchesPredicatesTests = []struct { 52 | predicates []MemberPredicate 53 | matches bool 54 | }{ 55 | {nil, true}, 56 | {[]MemberPredicate{}, true}, 57 | {[]MemberPredicate{truePredicate}, true}, 58 | {[]MemberPredicate{falsePredicate}, false}, 59 | {[]MemberPredicate{truePredicate, truePredicate}, true}, 60 | {[]MemberPredicate{truePredicate, falsePredicate}, false}, 61 | 62 | {[]MemberPredicate{MemberWithLabelAndValue("hello", "world")}, true}, 63 | {[]MemberPredicate{MemberWithLabelAndValue("foo", "bar")}, false}, 64 | } 65 | 66 | func TestMemberMatchesPredicates(t *testing.T) { 67 | member := Member{ 68 | Address: "192.0.2.1:1234", 69 | Incarnation: 42, 70 | Status: Alive, 71 | Labels: map[string]string{"hello": "world"}, 72 | } 73 | 74 | for _, test := range memberMatchesPredicatesTests { 75 | assert.Equal(t, test.matches, MemberMatchesPredicates(member, test.predicates...)) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /swim/memberlist_iter.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package swim 22 | 23 | type memberIter interface { 24 | Next() (*Member, bool) 25 | } 26 | 27 | // A memberlistIter iterates on a memberlist. Whenever the iterator runs out of 28 | // members, it shuffles the Memberlist and starts from the beginning. 29 | type memberlistIter struct { 30 | m *memberlist 31 | currentIndex int 32 | currentRound int 33 | } 34 | 35 | // NewMemberlistIter returns a new MemberlistIter 36 | func newMemberlistIter(m *memberlist) *memberlistIter { 37 | iter := &memberlistIter{ 38 | m: m, 39 | currentIndex: -1, 40 | currentRound: 0, 41 | } 42 | 43 | iter.m.Shuffle() 44 | 45 | return iter 46 | } 47 | 48 | // Next returns the next pingable member in the member list, if it 49 | // visits all members but none are pingable returns nil, false 50 | func (i *memberlistIter) Next() (*Member, bool) { 51 | for maxToVisit := i.m.NumMembers(); maxToVisit >= 0; maxToVisit-- { 52 | i.currentIndex++ 53 | 54 | member := i.m.MemberAt(i.currentIndex) 55 | if member == nil { 56 | i.currentIndex = 0 57 | i.currentRound++ 58 | i.m.Shuffle() 59 | member = i.m.MemberAt(i.currentIndex) 60 | if member == nil { 61 | return nil, false 62 | } 63 | } 64 | 65 | if i.m.Pingable(*member) { 66 | return member, true 67 | } 68 | } 69 | 70 | return nil, false 71 | } 72 | -------------------------------------------------------------------------------- /swim/memberlist_iter_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package swim 22 | 23 | import ( 24 | "testing" 25 | 26 | "github.com/stretchr/testify/suite" 27 | "github.com/temporalio/ringpop-go/util" 28 | ) 29 | 30 | type MemberlistIterTestSuite struct { 31 | suite.Suite 32 | node *Node 33 | m *memberlist 34 | i *memberlistIter 35 | incarnation int64 36 | } 37 | 38 | func (s *MemberlistIterTestSuite) SetupTest() { 39 | s.incarnation = util.TimeNowMS() 40 | s.node = NewNode("test", "127.0.0.1:3001", nil, nil) 41 | s.m = s.node.memberlist 42 | s.i = s.m.Iter() 43 | 44 | s.m.MakeAlive(s.node.Address(), s.incarnation) 45 | } 46 | 47 | func (s *MemberlistIterTestSuite) TearDownTest() { 48 | s.node.Destroy() 49 | } 50 | 51 | func (s *MemberlistIterTestSuite) TestNoneUseable() { 52 | // populate the membership with two unusable nodes. 53 | s.m.Update([]Change{ 54 | Change{ 55 | Address: "127.0.0.1:3002", 56 | Incarnation: s.incarnation, 57 | Status: Faulty, 58 | }, 59 | Change{ 60 | Address: "127.0.0.1:3003", 61 | Incarnation: s.incarnation, 62 | Status: Leave, 63 | }, 64 | }) 65 | 66 | member, ok := s.i.Next() 67 | s.Nil(member, "expected member to be nil") 68 | s.False(ok, "expected no usable members") 69 | } 70 | 71 | func (s *MemberlistIterTestSuite) TestIterOverFive() { 72 | addresses := fakeHostPorts(1, 1, 2, 6) 73 | 74 | for _, address := range addresses { 75 | s.m.MakeAlive(address, s.incarnation) 76 | } 77 | 78 | iterated := make(map[string]int) 79 | 80 | for i := 0; i < 20; i++ { 81 | member, ok := s.i.Next() 82 | s.Require().NotNil(member, "expected member to not be nil") 83 | s.True(ok, "expected a pingable member to be found") 84 | 85 | iterated[member.Address]++ 86 | } 87 | 88 | s.Len(iterated, 5, "expected only 5 members to be iterated over") 89 | for _, iterations := range iterated { 90 | s.Equal(4, iterations, "expected each member to be iterated over four times") 91 | } 92 | } 93 | 94 | func (s *MemberlistIterTestSuite) TestIterSkips() { 95 | s.m.Update([]Change{ 96 | Change{Address: "127.0.0.1:3002", Incarnation: s.incarnation, Status: Alive}, 97 | Change{Address: "127.0.0.1:3003", Incarnation: s.incarnation, Status: Faulty}, 98 | Change{Address: "127.0.0.1:3004", Incarnation: s.incarnation, Status: Alive}, 99 | Change{Address: "127.0.0.1:3005", Incarnation: s.incarnation, Status: Leave}, 100 | }) 101 | 102 | iterated := make(map[string]int) 103 | 104 | for i := 0; i < 10; i++ { 105 | member, ok := s.i.Next() 106 | s.Require().NotNil(member, "member cannot be nil") 107 | s.True(ok, "expected a pingable member to be found") 108 | 109 | iterated[member.Address]++ 110 | } 111 | 112 | s.Len(iterated, 2, "expected faulty, leave, local to be skipped") 113 | for _, iterations := range iterated { 114 | s.Equal(5, iterations, "expected pingable members to be iterated over twice") 115 | } 116 | } 117 | 118 | func TestMemberlistIterTestSuite(t *testing.T) { 119 | suite.Run(t, new(MemberlistIterTestSuite)) 120 | } 121 | -------------------------------------------------------------------------------- /swim/mock_self_evict_hook_test.go: -------------------------------------------------------------------------------- 1 | package swim 2 | 3 | import "github.com/stretchr/testify/mock" 4 | 5 | type MockSelfEvictHook struct { 6 | mock.Mock 7 | } 8 | 9 | // PreEvict provides a mock function with given fields: 10 | func (_m *MockSelfEvictHook) PreEvict() { 11 | _m.Called() 12 | } 13 | 14 | // PostEvict provides a mock function with given fields: 15 | func (_m *MockSelfEvictHook) PostEvict() { 16 | _m.Called() 17 | } 18 | -------------------------------------------------------------------------------- /swim/node_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package swim 22 | 23 | import ( 24 | "testing" 25 | 26 | "github.com/stretchr/testify/suite" 27 | "github.com/temporalio/ringpop-go/discovery/statichosts" 28 | "github.com/temporalio/ringpop-go/membership" 29 | ) 30 | 31 | type NodeTestSuite struct { 32 | suite.Suite 33 | testNode *testNode 34 | peers []*testNode 35 | } 36 | 37 | func (s *NodeTestSuite) SetupTest() { 38 | s.testNode = newChannelNode(s.T()) 39 | s.peers = append(s.peers, s.testNode) 40 | } 41 | 42 | func (s *NodeTestSuite) TearDownTest() { 43 | // Destroy any nodes added to the peer list during testing 44 | for _, node := range s.peers { 45 | if node != nil { 46 | node.Destroy() 47 | } 48 | } 49 | } 50 | 51 | func (s *NodeTestSuite) TestAppName() { 52 | s.Equal("test", s.testNode.node.App()) 53 | } 54 | 55 | func (s *NodeTestSuite) TestStartStop() { 56 | s.testNode.node.Bootstrap(&BootstrapOptions{ 57 | DiscoverProvider: statichosts.New(), 58 | }) 59 | 60 | s.testNode.node.Stop() 61 | 62 | s.True(s.testNode.node.gossip.Stopped(), "gossip should be stopped") 63 | s.True(s.testNode.node.Stopped(), "node should be stopped") 64 | s.False(s.testNode.node.stateTransitions.enabled, "suspicion should not be enabled") 65 | 66 | s.testNode.node.Start() 67 | 68 | s.True(s.testNode.node.stateTransitions.enabled, "suspicon should be enabled") 69 | s.False(s.testNode.node.Stopped(), "node should not be stopped") 70 | s.False(s.testNode.node.gossip.Stopped(), "gossip should not be stopped") 71 | } 72 | 73 | func (s *NodeTestSuite) TestStoppedBootstrapOption() { 74 | s.testNode.node.Bootstrap(&BootstrapOptions{ 75 | DiscoverProvider: statichosts.New(), 76 | Stopped: true, 77 | }) 78 | 79 | s.True(s.testNode.node.gossip.Stopped(), "gossip should be stopped") 80 | // TODO: Should these also be stopped? 81 | //s.True(s.testNode.node.Stopped(), "node should be stopped") 82 | //s.False(s.testNode.node.stateTransitions.enabled, "suspicion should not be enabled") 83 | } 84 | 85 | func (s *NodeTestSuite) TestSetIdentity() { 86 | _, has := s.testNode.node.Labels().Get(membership.IdentityLabelKey) 87 | s.False(has, "Identity label not set") 88 | 89 | s.testNode.node.SetIdentity("new_identity") 90 | 91 | value, has := s.testNode.node.Labels().Get(membership.IdentityLabelKey) 92 | s.True(has, "Identity label set") 93 | s.Equal("new_identity", value, "Identity label contains identity") 94 | } 95 | 96 | func TestNodeTestSuite(t *testing.T) { 97 | suite.Run(t, new(NodeTestSuite)) 98 | } 99 | -------------------------------------------------------------------------------- /swim/ping_handler.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package swim 22 | 23 | import ( 24 | "errors" 25 | "time" 26 | 27 | log "github.com/uber-common/bark" 28 | ) 29 | 30 | func handlePing(node *Node, req *ping) (*ping, error) { 31 | if !node.Ready() { 32 | node.EmitEvent(RequestBeforeReadyEvent{PingEndpoint}) 33 | return nil, ErrNodeNotReady 34 | } 35 | 36 | node.EmitEvent(PingReceiveEvent{ 37 | Local: node.Address(), 38 | Source: req.Source, 39 | Changes: req.Changes, 40 | }) 41 | 42 | if req.App == "" { 43 | if node.requiresAppInPing { 44 | node.logger.WithFields(log.Fields{ 45 | "source": req.Source, 46 | }).Warn("Rejected ping from unknown ringpop app") 47 | return nil, errors.New("Pinged ringpop requires app name") 48 | } 49 | } else { 50 | if req.App != node.app { 51 | node.logger.WithFields(log.Fields{ 52 | "app": req.App, 53 | "source": req.Source, 54 | }).Warn("Rejected ping from wrong ringpop app") 55 | return nil, errors.New("Pinged ringpop has a different app name") 56 | } 57 | } 58 | 59 | node.serverRate.Mark(1) 60 | node.totalRate.Mark(1) 61 | 62 | node.memberlist.Update(req.Changes) 63 | 64 | changes, fullSync := 65 | node.disseminator.IssueAsReceiver(req.Source, req.SourceIncarnation, req.Checksum) 66 | 67 | res := &ping{ 68 | Checksum: node.memberlist.Checksum(), 69 | Changes: changes, 70 | Source: node.Address(), 71 | SourceIncarnation: node.Incarnation(), 72 | } 73 | 74 | // Start bi-directional full sync. 75 | if fullSync { 76 | node.disseminator.tryStartReverseFullSync(req.Source, time.Second) 77 | } 78 | 79 | return res, nil 80 | } 81 | -------------------------------------------------------------------------------- /swim/ping_request_handler.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package swim 22 | 23 | import "time" 24 | 25 | // A PingResponse is the response from a successful ping request call 26 | type pingResponse struct { 27 | Ok bool `json:"pingStatus"` 28 | Target string `json:"target"` 29 | Changes []Change `json:"changes"` 30 | } 31 | 32 | func handlePingRequest(node *Node, req *pingRequest) (*pingResponse, error) { 33 | if !node.Ready() { 34 | node.EmitEvent(RequestBeforeReadyEvent{PingReqEndpoint}) 35 | return nil, ErrNodeNotReady 36 | } 37 | 38 | node.EmitEvent(PingRequestReceiveEvent{ 39 | Local: node.Address(), 40 | Source: req.Source, 41 | Target: req.Target, 42 | Changes: req.Changes, 43 | }) 44 | 45 | node.serverRate.Mark(1) 46 | node.totalRate.Mark(1) 47 | 48 | node.memberlist.Update(req.Changes) 49 | 50 | pingStartTime := time.Now() 51 | 52 | res, err := sendPing(node, req.Target, node.pingTimeout) 53 | pingOk := err == nil 54 | 55 | if pingOk { 56 | node.EmitEvent(PingRequestPingEvent{ 57 | Local: node.Address(), 58 | Source: req.Source, 59 | Target: req.Target, 60 | Duration: time.Now().Sub(pingStartTime), 61 | }) 62 | 63 | node.memberlist.Update(res.Changes) 64 | } 65 | 66 | changes, _ := 67 | node.disseminator.IssueAsReceiver(req.Source, req.SourceIncarnation, req.Checksum) 68 | 69 | // ignore full sync 70 | 71 | return &pingResponse{ 72 | Target: req.Target, 73 | Ok: pingOk, 74 | Changes: changes, 75 | }, nil 76 | } 77 | -------------------------------------------------------------------------------- /swim/ping_sender.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package swim 22 | 23 | import ( 24 | "errors" 25 | "time" 26 | 27 | log "github.com/uber-common/bark" 28 | 29 | "github.com/temporalio/ringpop-go/logging" 30 | "github.com/temporalio/ringpop-go/shared" 31 | "github.com/temporalio/tchannel-go/json" 32 | ) 33 | 34 | // A Ping is used as an Arg3 for the ping TChannel call / response 35 | type ping struct { 36 | Changes []Change `json:"changes"` 37 | Checksum uint32 `json:"checksum"` 38 | Source string `json:"source"` 39 | SourceIncarnation int64 `json:"sourceIncarnationNumber"` 40 | App string `json:"app"` 41 | } 42 | 43 | // sendPing sends a ping to target node that times out after timeout 44 | func sendPing(node *Node, target string, timeout time.Duration) (*ping, error) { 45 | changes, bumpPiggybackCounters := node.disseminator.IssueAsSender() 46 | 47 | res, err := sendPingWithChanges(node, target, changes, timeout) 48 | if err != nil { 49 | return res, err 50 | } 51 | 52 | // when ping was successful 53 | bumpPiggybackCounters() 54 | 55 | return res, err 56 | } 57 | 58 | // sendPingWithChanges sends a special ping to the target with the given changes. 59 | // In normal pings the disseminator is consulted to create issue the changes, 60 | // this is not the case in this function. Only the given changes are transmitted. 61 | func sendPingWithChanges(node *Node, target string, changes []Change, timeout time.Duration) (*ping, error) { 62 | req := ping{ 63 | Checksum: node.memberlist.Checksum(), 64 | Changes: changes, 65 | Source: node.Address(), 66 | SourceIncarnation: node.Incarnation(), 67 | App: node.app, 68 | } 69 | 70 | node.EmitEvent(PingSendEvent{ 71 | Local: node.Address(), 72 | Remote: target, 73 | Changes: req.Changes, 74 | }) 75 | 76 | logging.Logger("ping").WithFields(log.Fields{ 77 | "local": node.Address(), 78 | "remote": target, 79 | "changes": req.Changes, 80 | }).Debug("ping send") 81 | 82 | ctx, cancel := shared.NewTChannelContext(timeout) 83 | defer cancel() 84 | 85 | peer := node.channel.Peers().GetOrAdd(target) 86 | startTime := time.Now() 87 | 88 | // send the ping 89 | errC := make(chan error, 1) 90 | res := &ping{} 91 | go func() { 92 | errC <- json.CallPeer(ctx, peer, node.service, "/protocol/ping", req, res) 93 | }() 94 | 95 | // get result or timeout 96 | var err error 97 | select { 98 | case err = <-errC: 99 | case <-ctx.Done(): 100 | err = errors.New("ping timed out") 101 | } 102 | 103 | if err != nil { 104 | // ping failed 105 | logging.Logger("ping").WithFields(log.Fields{ 106 | "local": node.Address(), 107 | "remote": target, 108 | "error": err, 109 | }).Debug("ping failed") 110 | 111 | return nil, err 112 | } 113 | 114 | node.EmitEvent(PingSendCompleteEvent{ 115 | Local: node.Address(), 116 | Remote: target, 117 | Changes: req.Changes, 118 | Duration: time.Now().Sub(startTime), 119 | }) 120 | 121 | return res, err 122 | } 123 | -------------------------------------------------------------------------------- /swim/ping_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package swim 22 | 23 | import ( 24 | "testing" 25 | "time" 26 | 27 | "github.com/stretchr/testify/mock" 28 | "github.com/stretchr/testify/suite" 29 | "github.com/temporalio/ringpop-go/events/test/mocks" 30 | "github.com/temporalio/tchannel-go" 31 | ) 32 | 33 | type PingTestSuite struct { 34 | suite.Suite 35 | tnode, tpeer *testNode 36 | node, peer *Node 37 | } 38 | 39 | func (s *PingTestSuite) SetupSuite() { 40 | s.tnode = newChannelNode(s.T()) 41 | s.node = s.tnode.node 42 | s.tpeer = newChannelNode(s.T()) 43 | s.peer = s.tpeer.node 44 | 45 | bootstrapNodes(s.T(), s.tnode, s.tpeer) 46 | } 47 | 48 | func (s *PingTestSuite) TearDownSuite() { 49 | destroyNodes(s.tnode, s.tpeer) 50 | } 51 | 52 | func (s *PingTestSuite) TestPing() { 53 | res, err := sendPing(s.node, s.peer.Address(), time.Second) 54 | s.NoError(err, "expected a ping to succeed") 55 | s.NotNil(res, "expected a ping response") 56 | } 57 | 58 | func (s *PingTestSuite) TestPingFails() { 59 | // Create a channel with no handlers registered. Any requests to this 60 | // channel should result in an error being returned immediately. 61 | ch, err := tchannel.NewChannel("test", nil) 62 | ch.ListenAndServe("127.0.0.1:0") 63 | s.Require().NoError(err, "channel must create successfully") 64 | 65 | res, err := sendPing(s.node, ch.PeerInfo().HostPort, time.Second) 66 | s.Error(err, "expected ping to fail") 67 | s.Nil(res, "expected response to be nil") 68 | } 69 | 70 | func (s *PingTestSuite) TestPingTimesOut() { 71 | // Set the timeout so low that a ping response could never come back before 72 | // the timeout is reached. 73 | res, err := sendPing(s.node, s.peer.Address(), time.Nanosecond) 74 | s.Error(err, "expected ping to fail") 75 | s.Nil(res, "expected response to be nil") 76 | } 77 | 78 | func (s *PingTestSuite) TestPingBeforeReady() { 79 | testNode1 := newChannelNode(s.T()) 80 | defer testNode1.Destroy() 81 | 82 | testNode2 := newChannelNode(s.T()) 83 | defer testNode2.Destroy() 84 | 85 | // Register listener that should be fired with the correct event type 86 | listener := &mocks.EventListener{} 87 | listener.On("HandleEvent", mock.AnythingOfType("RequestBeforeReadyEvent")) 88 | testNode2.node.AddListener(listener) 89 | 90 | res, err := sendPing(testNode1.node, testNode2.node.Address(), 500*time.Millisecond) 91 | s.Error(err) 92 | s.Nil(res) 93 | 94 | // Assert the event was fired, but after a brief sleep, because the events 95 | // are fired in goroutines 96 | time.Sleep(10 * time.Millisecond) 97 | listener.AssertExpectations(s.T()) 98 | } 99 | 100 | func TestPingTestSuite(t *testing.T) { 101 | suite.Run(t, new(PingTestSuite)) 102 | } 103 | -------------------------------------------------------------------------------- /swim/schedule.go: -------------------------------------------------------------------------------- 1 | package swim 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/benbjohnson/clock" 7 | ) 8 | 9 | // scheduleRepeaditly runs a function in the background continually with a 10 | // dynamically computed delay in between invocations. The returned channels can 11 | // be used to stop the background execution and a channel that will be unblocked 12 | // when the background task is completely stopped. 13 | // 14 | // stop, wait := scheduleRepeaditly(func(){..}, func() time.Duration { return time.Second }, clock) 15 | // time.sleep(time.Duration(10) * time.Second) 16 | // // stop the background task 17 | // close(stop) 18 | // // wait till the background task is terminated 19 | // <-wait 20 | func scheduleRepeaditly(what func(), delayFn func() time.Duration, clock clock.Clock) (stop chan bool, wait <-chan bool) { 21 | stop = make(chan bool) 22 | 23 | internalWait := make(chan bool) 24 | wait = internalWait 25 | 26 | go func() { 27 | defer close(internalWait) 28 | for { 29 | delay := delayFn() 30 | what() 31 | select { 32 | case <-clock.After(delay): 33 | case <-stop: 34 | return 35 | } 36 | } 37 | }() 38 | 39 | return stop, wait 40 | } 41 | -------------------------------------------------------------------------------- /swim/stats.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package swim 22 | 23 | import ( 24 | "sort" 25 | "time" 26 | ) 27 | 28 | type members []Member 29 | 30 | // These methods exist to satisfy the sort.Interface for sorting. 31 | func (s *members) Len() int { return len(*s) } 32 | func (s *members) Less(i, j int) bool { return (*s)[i].Address < (*s)[j].Address } 33 | func (s *members) Swap(i, j int) { (*s)[i], (*s)[j] = (*s)[j], (*s)[i] } 34 | 35 | // MemberStats contains members in a memberlist and the checksum of those members 36 | type MemberStats struct { 37 | Checksum uint32 `json:"checksum"` 38 | Members []Member `json:"members"` 39 | } 40 | 41 | // GetChecksum returns the current checksum of the node's memberlist. 42 | func (n *Node) GetChecksum() uint32 { 43 | return n.memberlist.Checksum() 44 | } 45 | 46 | // MemberStats returns the current checksum of the node's memberlist and a slice 47 | // of the members in the memberlist in lexographically sorted order by address 48 | func (n *Node) MemberStats() MemberStats { 49 | members := members(n.memberlist.GetMembers()) 50 | sort.Sort(&members) 51 | return MemberStats{n.memberlist.Checksum(), members} 52 | } 53 | 54 | // ProtocolStats contains stats about the SWIM Protocol for the node 55 | type ProtocolStats struct { 56 | Timing Timing `json:"timing"` 57 | Rate time.Duration `json:"protocolRate"` 58 | ClientRate float64 `json:"clientRate"` 59 | ServerRate float64 `json:"serverRate"` 60 | TotalRate float64 `json:"totalRate"` 61 | } 62 | 63 | // Timing contains timing information for the SWIM protocol for the node 64 | type Timing struct { 65 | Type string `json:"type"` 66 | Min int64 `json:"min"` 67 | Max int64 `json:"max"` 68 | Sum int64 `json:"sum"` 69 | Variance float64 `json:"variance"` 70 | Mean float64 `json:"mean"` 71 | StdDev float64 `json:"std_dev"` 72 | Count int64 `json:"count"` 73 | Median float64 `json:"median"` 74 | P75 float64 `json:"p75"` 75 | P95 float64 `json:"p95"` 76 | P99 float64 `json:"p99"` 77 | P999 float64 `json:"p999"` 78 | } 79 | 80 | // ProtocolStats returns stats about the node's SWIM protocol. 81 | func (n *Node) ProtocolStats() ProtocolStats { 82 | timing := n.gossip.ProtocolTiming() 83 | return ProtocolStats{ 84 | Timing{ 85 | Type: "histogram", 86 | Min: timing.Min(), 87 | Max: timing.Max(), 88 | Sum: timing.Sum(), 89 | Variance: timing.Variance(), 90 | Mean: timing.Mean(), 91 | StdDev: timing.StdDev(), 92 | Count: timing.Count(), 93 | Median: timing.Percentile(0.5), 94 | P75: timing.Percentile(0.75), 95 | P95: timing.Percentile(0.95), 96 | P99: timing.Percentile(0.99), 97 | P999: timing.Percentile(0.999), 98 | }, 99 | n.gossip.ProtocolRate(), 100 | n.clientRate.Rate1(), 101 | n.serverRate.Rate1(), 102 | n.totalRate.Rate1(), 103 | } 104 | } 105 | 106 | // Uptime returns the amount of time the node has been running for 107 | func (n *Node) Uptime() time.Duration { 108 | return time.Now().Sub(n.startTime) 109 | } 110 | -------------------------------------------------------------------------------- /swim/stats_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package swim 22 | 23 | import ( 24 | "sort" 25 | "testing" 26 | "time" 27 | 28 | "github.com/stretchr/testify/suite" 29 | ) 30 | 31 | type StatsTestSuite struct { 32 | suite.Suite 33 | 34 | testNode *testNode 35 | 36 | cluster *swimCluster 37 | } 38 | 39 | func (s *StatsTestSuite) SetupTest() { 40 | // Create a test node, on its own. Tests can join this to the cluster for 41 | // testing, if they want. 42 | s.testNode = newChannelNode(s.T()) 43 | 44 | // Create a cluster for testing. Join these guys to each other. 45 | s.cluster = newSwimCluster(4) 46 | s.cluster.Bootstrap() 47 | } 48 | 49 | func (s *StatsTestSuite) TearDownTest() { 50 | if s.cluster != nil { 51 | s.cluster.Destroy() 52 | } 53 | } 54 | 55 | func (s *StatsTestSuite) TestUptime() { 56 | s.cluster.Add(s.testNode.node) 57 | s.True(s.testNode.node.Uptime() > 0, "expected uptime to be greater than zero") 58 | } 59 | 60 | // TestProtocolStats tests that the returned struct has non-zero values for all 61 | // fields. 62 | func (s *StatsTestSuite) TestProtocolStats() { 63 | if testing.Short() { 64 | s.T().Skip("skipping protocol stats test in short mode") 65 | } 66 | 67 | s.cluster.Add(s.testNode.node) 68 | 69 | // We need to sleep for at least 5 seconds, as this is the tick period for 70 | // the metrics.Meter and it cannot be easily changed. 71 | time.Sleep(6 * time.Second) 72 | 73 | stats := s.testNode.node.ProtocolStats() 74 | 75 | s.NotEmpty(stats.Timing.Type) 76 | s.NotZero(stats.Timing.Min) 77 | s.NotZero(stats.Timing.Max) 78 | s.NotZero(stats.Timing.Sum) 79 | s.NotZero(stats.Timing.Variance) 80 | s.NotZero(stats.Timing.Mean) 81 | s.NotZero(stats.Timing.StdDev) 82 | s.NotZero(stats.Timing.Count) 83 | s.NotZero(stats.Timing.Median) 84 | s.NotZero(stats.Timing.P75) 85 | s.NotZero(stats.Timing.P95) 86 | s.NotZero(stats.Timing.P99) 87 | s.NotZero(stats.Timing.P999) 88 | 89 | s.NotZero(stats.ServerRate) 90 | s.NotZero(stats.TotalRate) 91 | // TODO: Fix ClientRate, which is currently always 0 92 | //s.NotZero(stats.ClientRate) 93 | } 94 | 95 | func (s *StatsTestSuite) TestMemberStats() { 96 | s.cluster.WaitForConvergence(s.T(), 100) 97 | stats := s.cluster.Nodes()[0].MemberStats() 98 | 99 | // Extract addresses from the member list 100 | var memberAddresses []string 101 | for _, member := range stats.Members { 102 | memberAddresses = append(memberAddresses, member.Address) 103 | } 104 | 105 | sort.Strings(memberAddresses) 106 | 107 | clusterAddresses := s.cluster.Addresses() 108 | sort.Strings(clusterAddresses) 109 | 110 | s.Equal(clusterAddresses, memberAddresses, "member addresses should match cluster hosts") 111 | s.NotZero(stats.Checksum, "checksum should be non-zero") 112 | } 113 | 114 | func TestStatsTestSuite(t *testing.T) { 115 | suite.Run(t, new(StatsTestSuite)) 116 | } 117 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | ringpop-common/ 2 | -------------------------------------------------------------------------------- /test/gen-testfiles: -------------------------------------------------------------------------------- 1 | #!/bin/bash -x 2 | # 3 | # Generate mocks for ringpop and subpackages. 4 | # 5 | set -eo pipefail 6 | 7 | 8 | mock_directory="${0%/*}/mocks" 9 | thrift_directory="${0%/*}/thrift" 10 | 11 | CWD="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 12 | 13 | which thrift-gen 14 | thrift-gen -generateThrift -inputFile "$thrift_directory"/pingpong.thrift -outputDir "$thrift_directory" 15 | mockery -case=underscore -dir "$thrift_directory" -recursive -inpkg -name TChanPingPong 16 | 17 | # Generate mocks for dependencies 18 | mockery -case=underscore -dir "$GOPATH"/src/github.com/uber-common/bark -recursive -output "$mock_directory" -name Logger 19 | mockery -case=underscore -dir "$GOPATH"/src/github.com/uber-common/bark -recursive -output "$mock_directory" -name StatsReporter 20 | mockery -case=underscore -dir "$GOPATH"/src/github.com/uber/tchannel-go/thrift -recursive -output "$mock_directory" -name TChanClient 21 | 22 | # Remove vendor prefix from import as this is not supported 23 | #VENDORREPLACE=$GOPKG/vendor/ 24 | #IMPORT_SED=$(echo "s/${VENDORREPLACE//\//\\/}//g") 25 | #git grep -lz "$VENDORREPLACE" "$mock_directory" | xargs -0 sed -i "" -e "$IMPORT_SED" 26 | 27 | # Generate mock file for ringpop.Interface, but rename the mocked object to 28 | # mocks.Ringpop 29 | mockery -name Interface -print \ 30 | |sed 's/_m \*Interface/_m \*Ringpop/g' \ 31 | |sed 's/type Interface/type Ringpop/g' \ 32 | > "$mock_directory"/ringpop.go 33 | 34 | mockery -name NodeInterface -recursive -print \ 35 | |sed 's/_m \*NodeInterface/_m \*SwimNode/g' \ 36 | |sed 's/type NodeInterface/type SwimNode/g' \ 37 | > "$mock_directory"/swim_node.go 38 | 39 | mockery -case=underscore -dir router -recursive -output "$mock_directory" -name ClientFactory 40 | 41 | # mocks used in forward cannot go into test/mocks because circular dependency 42 | mockery -name Sender -dir forward -recursive -testonly -case=underscore -inpkg 43 | 44 | # mocks used in evenst for local use cause circular dependency when imported 45 | mockery -name EventListener -dir events -testonly -case=underscore -inpkg 46 | 47 | # mocks used in forward cannot go into test/mocks because circular dependency 48 | mockery -name SelfEvictHook -dir swim -recursive -testonly -case=underscore -inpkg 49 | 50 | # Mocks for events need to go in the events package to avoid circular deps 51 | mockery -case=underscore -dir events -output "${0%/*}/../events/test/mocks" -name EventListener 52 | -------------------------------------------------------------------------------- /test/go-test-prettify: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Runs `go test -v`, parses the output and prints a summary at the end. Also 4 | # only outputs errors by default unless -v is specified. 5 | # 6 | set -o pipefail 7 | 8 | declare -a go_args 9 | declare prettify_args= 10 | 11 | # Collect args for go but ignore -v and pass it through to prettify instead. 12 | for arg in "$@"; do 13 | if [ "$arg" == "-v" ]; then 14 | prettify_args="-v" 15 | else 16 | go_args+=("$arg") 17 | fi 18 | done 19 | 20 | # 21 | # Takes the output of "go test -v", only displays errors by default and prints 22 | # a summary at the bottom. 23 | # 24 | _prettify() { 25 | local verbose=false 26 | 27 | local -i run=0 28 | local -i passed=0 29 | local -i failed=0 30 | local -i skipped=0 31 | local -i errors=0 32 | local -i races=0 33 | 34 | if [[ $1 == "-v"* ]]; then 35 | verbose=true 36 | fi 37 | 38 | # Context is a buffer used to store the last test line, so we can print it 39 | # to give context about the current output. 40 | local context= 41 | 42 | while IFS= read -r line; do 43 | case $line in 44 | "--- PASS:"*) 45 | passed=$(($passed+1)) 46 | ;; 47 | 48 | "--- FAIL:"*) 49 | failed=$(($failed+1)) 50 | 51 | # Output fail lines 52 | if ! $verbose; then 53 | echo "$line" 54 | fi 55 | ;; 56 | 57 | "--- SKIP:"*) 58 | skipped=$(($skipped+1)) 59 | ;; 60 | 61 | # Skip output of "RUN" lines by default 62 | "=== RUN"*) 63 | run=$(($run+1)) 64 | context="$line" 65 | ;; 66 | 67 | # Skip output of mock.go success lines (unicode char is the green tick) 68 | *mock*$(echo -e "\xe2\x9c\x85")*) 69 | ;; 70 | 71 | # Compile errors 72 | FAIL*) 73 | errors=$(($errors+1)) 74 | 75 | if ! $verbose; then 76 | echo "$line" 77 | fi 78 | ;; 79 | 80 | # Data races 81 | "WARNING: DATA RACE"*) 82 | races=$(($races+1)) 83 | if ! $verbose; then 84 | echo "$context" 85 | echo "===" 86 | echo "$line" 87 | fi 88 | ;; 89 | 90 | # Skip and ignore printing junk 91 | PASS|FAIL|\?*) 92 | ;; 93 | 94 | *) 95 | # Output unknown lines 96 | if ! $verbose; then 97 | echo "$line" 98 | fi 99 | ;; 100 | esac 101 | 102 | if $verbose; then 103 | echo "$line" 104 | fi 105 | 106 | done 107 | 108 | echo 109 | echo "# tests: $run" 110 | [ $passed -ne 0 ] && echo "# passed: $passed" 111 | [ $failed -ne 0 ] && echo "# failed: $failed" 112 | [ $skipped -ne 0 ] && echo "# skipped: $skipped" 113 | [ $errors -ne 0 ] && echo "# errors: $errors" 114 | [ $races -ne 0 ] && echo "# races: $races" 115 | echo 116 | } 117 | 118 | go test -v "${go_args[@]}" |_prettify $prettify_args 119 | 120 | # Exit with return code from go 121 | exit $PIPESTATUS 122 | -------------------------------------------------------------------------------- /test/lib.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Common functions for test code 3 | 4 | declare project_root="${0%/*}/.." 5 | declare ringpop_common_dir="${0%/*}/ringpop-common" 6 | declare ringpop_common_branch="master" 7 | 8 | # 9 | # Clones or updates the ringpop-common repository. 10 | # Runs `npm install` in ringpop-common/$1. 11 | # 12 | fetch-ringpop-common() { 13 | if [ ! -e "$ringpop_common_dir" ]; then 14 | run git clone --depth=1 https://github.com/temporalio/ringpop-common.git "$ringpop_common_dir" --branch "$ringpop_common_branch" 15 | fi 16 | 17 | run cd "$ringpop_common_dir" 18 | #run git checkout latest version of $ringpop_common_branch 19 | run git fetch origin "$ringpop_common_branch" 20 | run git checkout "FETCH_HEAD" 21 | 22 | run cd - >/dev/null 23 | 24 | run cd "${ringpop_common_dir}/$1" 25 | if [[ ! -d "node_modules" && ! -d "../node_modules" ]]; then 26 | run npm install #>/dev/null 27 | fi 28 | run cd - >/dev/null 29 | } 30 | 31 | # 32 | # Copy stdin to stdout but prefix each line with the specified string. 33 | # 34 | prefix() { 35 | local _prefix= 36 | 37 | [ -n "$1" ] && _prefix="[$1] " 38 | while IFS= read -r -t 30 line; do 39 | echo "${_prefix}${line}" 40 | done 41 | } 42 | 43 | # 44 | # Echos and runs the specified command. 45 | # 46 | run() { 47 | echo "+ $@" >&2 48 | "$@" 49 | } 50 | -------------------------------------------------------------------------------- /test/mocks/README: -------------------------------------------------------------------------------- 1 | Files in this directory have been automatically generated using mockery. 2 | -------------------------------------------------------------------------------- /test/mocks/client_factory.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "github.com/stretchr/testify/mock" 5 | "github.com/temporalio/tchannel-go/thrift" 6 | ) 7 | 8 | type ClientFactory struct { 9 | mock.Mock 10 | } 11 | 12 | // GetLocalClient provides a mock function with given fields: 13 | func (_m *ClientFactory) GetLocalClient() interface{} { 14 | ret := _m.Called() 15 | 16 | var r0 interface{} 17 | if rf, ok := ret.Get(0).(func() interface{}); ok { 18 | r0 = rf() 19 | } else { 20 | if ret.Get(0) != nil { 21 | r0 = ret.Get(0).(interface{}) 22 | } 23 | } 24 | 25 | return r0 26 | } 27 | 28 | // MakeRemoteClient provides a mock function with given fields: client 29 | func (_m *ClientFactory) MakeRemoteClient(client thrift.TChanClient) interface{} { 30 | ret := _m.Called(client) 31 | 32 | var r0 interface{} 33 | if rf, ok := ret.Get(0).(func(thrift.TChanClient) interface{}); ok { 34 | r0 = rf(client) 35 | } else { 36 | if ret.Get(0) != nil { 37 | r0 = ret.Get(0).(interface{}) 38 | } 39 | } 40 | 41 | return r0 42 | } 43 | -------------------------------------------------------------------------------- /test/mocks/logger.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "github.com/stretchr/testify/mock" 5 | "github.com/uber-common/bark" 6 | ) 7 | 8 | type Logger struct { 9 | mock.Mock 10 | } 11 | 12 | // Debug provides a mock function with given fields: args 13 | func (_m *Logger) Debug(args ...interface{}) { 14 | _m.Called(args) 15 | } 16 | 17 | // Debugf provides a mock function with given fields: format, args 18 | func (_m *Logger) Debugf(format string, args ...interface{}) { 19 | _m.Called(format, args) 20 | } 21 | 22 | // Info provides a mock function with given fields: args 23 | func (_m *Logger) Info(args ...interface{}) { 24 | _m.Called(args) 25 | } 26 | 27 | // Infof provides a mock function with given fields: format, args 28 | func (_m *Logger) Infof(format string, args ...interface{}) { 29 | _m.Called(format, args) 30 | } 31 | 32 | // Warn provides a mock function with given fields: args 33 | func (_m *Logger) Warn(args ...interface{}) { 34 | _m.Called(args) 35 | } 36 | 37 | // Warnf provides a mock function with given fields: format, args 38 | func (_m *Logger) Warnf(format string, args ...interface{}) { 39 | _m.Called(format, args) 40 | } 41 | 42 | // Error provides a mock function with given fields: args 43 | func (_m *Logger) Error(args ...interface{}) { 44 | _m.Called(args) 45 | } 46 | 47 | // Errorf provides a mock function with given fields: format, args 48 | func (_m *Logger) Errorf(format string, args ...interface{}) { 49 | _m.Called(format, args) 50 | } 51 | 52 | // Fatal provides a mock function with given fields: args 53 | func (_m *Logger) Fatal(args ...interface{}) { 54 | _m.Called(args) 55 | } 56 | 57 | // Fatalf provides a mock function with given fields: format, args 58 | func (_m *Logger) Fatalf(format string, args ...interface{}) { 59 | _m.Called(format, args) 60 | } 61 | 62 | // Panic provides a mock function with given fields: args 63 | func (_m *Logger) Panic(args ...interface{}) { 64 | _m.Called(args) 65 | } 66 | 67 | // Panicf provides a mock function with given fields: format, args 68 | func (_m *Logger) Panicf(format string, args ...interface{}) { 69 | _m.Called(format, args) 70 | } 71 | 72 | // WithField provides a mock function with given fields: key, value 73 | func (_m *Logger) WithField(key string, value interface{}) bark.Logger { 74 | ret := _m.Called(key, value) 75 | 76 | var r0 bark.Logger 77 | if rf, ok := ret.Get(0).(func(string, interface{}) bark.Logger); ok { 78 | r0 = rf(key, value) 79 | } else { 80 | r0 = ret.Get(0).(bark.Logger) 81 | } 82 | 83 | return r0 84 | } 85 | 86 | // WithFields provides a mock function with given fields: keyValues 87 | func (_m *Logger) WithFields(keyValues bark.LogFields) bark.Logger { 88 | ret := _m.Called(keyValues) 89 | 90 | var r0 bark.Logger 91 | if rf, ok := ret.Get(0).(func(bark.LogFields) bark.Logger); ok { 92 | r0 = rf(keyValues) 93 | } else { 94 | r0 = ret.Get(0).(bark.Logger) 95 | } 96 | 97 | return r0 98 | } 99 | 100 | // WithError provides a mock function with given fields: err 101 | func (_m *Logger) WithError(err error) bark.Logger { 102 | ret := _m.Called(err) 103 | 104 | var r0 bark.Logger 105 | if rf, ok := ret.Get(0).(func(error) bark.Logger); ok { 106 | r0 = rf(err) 107 | } else { 108 | r0 = ret.Get(0).(bark.Logger) 109 | } 110 | 111 | return r0 112 | } 113 | 114 | // Fields provides a mock function with given fields: 115 | func (_m *Logger) Fields() bark.Fields { 116 | ret := _m.Called() 117 | 118 | var r0 bark.Fields 119 | if rf, ok := ret.Get(0).(func() bark.Fields); ok { 120 | r0 = rf() 121 | } else { 122 | r0 = ret.Get(0).(bark.Fields) 123 | } 124 | 125 | return r0 126 | } 127 | -------------------------------------------------------------------------------- /test/mocks/logger/logger.go: -------------------------------------------------------------------------------- 1 | package mocklogger 2 | 3 | import "github.com/uber-common/bark" 4 | import "github.com/stretchr/testify/mock" 5 | 6 | type Logger struct { 7 | mock.Mock 8 | } 9 | 10 | // Debug provides a mock function with given fields: args 11 | func (_m *Logger) Debug(args ...interface{}) { 12 | _m.Called(args) 13 | } 14 | 15 | // Debugf provides a mock function with given fields: format, args 16 | func (_m *Logger) Debugf(format string, args ...interface{}) { 17 | _m.Called(format, args) 18 | } 19 | 20 | // Info provides a mock function with given fields: args 21 | func (_m *Logger) Info(args ...interface{}) { 22 | _m.Called(args) 23 | } 24 | 25 | // Infof provides a mock function with given fields: format, args 26 | func (_m *Logger) Infof(format string, args ...interface{}) { 27 | _m.Called(format, args) 28 | } 29 | 30 | // Warn provides a mock function with given fields: args 31 | func (_m *Logger) Warn(args ...interface{}) { 32 | _m.Called(args) 33 | } 34 | 35 | // Warnf provides a mock function with given fields: format, args 36 | func (_m *Logger) Warnf(format string, args ...interface{}) { 37 | _m.Called(format, args) 38 | } 39 | 40 | // Error provides a mock function with given fields: args 41 | func (_m *Logger) Error(args ...interface{}) { 42 | _m.Called(args) 43 | } 44 | 45 | // Errorf provides a mock function with given fields: format, args 46 | func (_m *Logger) Errorf(format string, args ...interface{}) { 47 | _m.Called(format, args) 48 | } 49 | 50 | // Fatal provides a mock function with given fields: args 51 | func (_m *Logger) Fatal(args ...interface{}) { 52 | _m.Called(args) 53 | } 54 | 55 | // Fatalf provides a mock function with given fields: format, args 56 | func (_m *Logger) Fatalf(format string, args ...interface{}) { 57 | _m.Called(format, args) 58 | } 59 | 60 | // Panic provides a mock function with given fields: args 61 | func (_m *Logger) Panic(args ...interface{}) { 62 | _m.Called(args) 63 | } 64 | 65 | // Panicf provides a mock function with given fields: format, args 66 | func (_m *Logger) Panicf(format string, args ...interface{}) { 67 | _m.Called(format, args) 68 | } 69 | 70 | // WithField provides a mock function with given fields: key, value 71 | func (_m *Logger) WithField(key string, value interface{}) bark.Logger { 72 | ret := _m.Called(key, value) 73 | 74 | var r0 bark.Logger 75 | if rf, ok := ret.Get(0).(func(string, interface{}) bark.Logger); ok { 76 | r0 = rf(key, value) 77 | } else { 78 | r0 = ret.Get(0).(bark.Logger) 79 | } 80 | 81 | return r0 82 | } 83 | 84 | // WithFields provides a mock function with given fields: keyValues 85 | func (_m *Logger) WithFields(keyValues bark.LogFields) bark.Logger { 86 | ret := _m.Called(keyValues) 87 | 88 | var r0 bark.Logger 89 | if rf, ok := ret.Get(0).(func(bark.LogFields) bark.Logger); ok { 90 | r0 = rf(keyValues) 91 | } else { 92 | r0 = ret.Get(0).(bark.Logger) 93 | } 94 | 95 | return r0 96 | } 97 | 98 | // WithError provides a mock function with given fields: err 99 | func (_m *Logger) WithError(err error) bark.Logger { 100 | ret := _m.Called(err) 101 | 102 | var r0 bark.Logger 103 | if rf, ok := ret.Get(0).(func(error) bark.Logger); ok { 104 | r0 = rf(err) 105 | } else { 106 | r0 = ret.Get(0).(bark.Logger) 107 | } 108 | 109 | return r0 110 | } 111 | 112 | // Fields provides a mock function with given fields: 113 | func (_m *Logger) Fields() bark.Fields { 114 | ret := _m.Called() 115 | 116 | var r0 bark.Fields 117 | if rf, ok := ret.Get(0).(func() bark.Fields); ok { 118 | r0 = rf() 119 | } else { 120 | r0 = ret.Get(0).(bark.Fields) 121 | } 122 | 123 | return r0 124 | } 125 | -------------------------------------------------------------------------------- /test/mocks/stats_reporter.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/stretchr/testify/mock" 7 | "github.com/uber-common/bark" 8 | ) 9 | 10 | type StatsReporter struct { 11 | mock.Mock 12 | } 13 | 14 | // IncCounter provides a mock function with given fields: name, tags, value 15 | func (_m *StatsReporter) IncCounter(name string, tags bark.Tags, value int64) { 16 | _m.Called(name, tags, value) 17 | } 18 | 19 | // UpdateGauge provides a mock function with given fields: name, tags, value 20 | func (_m *StatsReporter) UpdateGauge(name string, tags bark.Tags, value int64) { 21 | _m.Called(name, tags, value) 22 | } 23 | 24 | // RecordTimer provides a mock function with given fields: name, tags, d 25 | func (_m *StatsReporter) RecordTimer(name string, tags bark.Tags, d time.Duration) { 26 | _m.Called(name, tags, d) 27 | } 28 | -------------------------------------------------------------------------------- /test/mocks/t_chan_client.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "github.com/stretchr/testify/mock" 5 | "github.com/temporalio/tchannel-go/thrift" 6 | 7 | athrift "github.com/apache/thrift/lib/go/thrift" 8 | ) 9 | 10 | type TChanClient struct { 11 | mock.Mock 12 | } 13 | 14 | // Call provides a mock function with given fields: ctx, serviceName, methodName, req, resp 15 | func (_m *TChanClient) Call(ctx thrift.Context, serviceName string, methodName string, req athrift.TStruct, resp athrift.TStruct) (bool, error) { 16 | ret := _m.Called(ctx, serviceName, methodName, req, resp) 17 | 18 | var r0 bool 19 | if rf, ok := ret.Get(0).(func(thrift.Context, string, string, athrift.TStruct, athrift.TStruct) bool); ok { 20 | r0 = rf(ctx, serviceName, methodName, req, resp) 21 | } else { 22 | r0 = ret.Get(0).(bool) 23 | } 24 | 25 | var r1 error 26 | if rf, ok := ret.Get(1).(func(thrift.Context, string, string, athrift.TStruct, athrift.TStruct) error); ok { 27 | r1 = rf(ctx, serviceName, methodName, req, resp) 28 | } else { 29 | r1 = ret.Error(1) 30 | } 31 | 32 | return r0, r1 33 | } 34 | -------------------------------------------------------------------------------- /test/remoteservice/.gitignore: -------------------------------------------------------------------------------- 1 | *.go 2 | !*_test.go 3 | mocks/ 4 | .gen/ 5 | -------------------------------------------------------------------------------- /test/remoteservice/remoteservice.thrift: -------------------------------------------------------------------------------- 1 | include "./shared.thrift" 2 | 3 | // the unused file contains types that are not used in service endpoints. These 4 | // types might actually be used in embedded structs here, but since the 5 | // generated code could potentially contain the import of an unused package in 6 | // the RingpopAdapter. To prevent this the generator uses `GoUnusedProtection__` 7 | include "./unused.thrift" 8 | 9 | service RemoteService { 10 | void RemoteCall(1: shared.Name name) 11 | } 12 | -------------------------------------------------------------------------------- /test/remoteservice/shared.thrift: -------------------------------------------------------------------------------- 1 | typedef string Name 2 | -------------------------------------------------------------------------------- /test/remoteservice/unused.thrift: -------------------------------------------------------------------------------- 1 | typedef string UnusedType 2 | -------------------------------------------------------------------------------- /test/run-example-tests: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Runs the ringpop-admin tests. 4 | # 5 | # This script launches a ringpop cluster using tick-cluster and then runs 6 | # a series of tests against the ringpop-admin command output. 7 | # 8 | set -euo pipefail 9 | 10 | 11 | declare cram_opts="-v --shell=/bin/bash --indent=4" 12 | declare prog_cram="cram" 13 | declare test_files="${0%/*}/../examples/*/README.md" 14 | 15 | # Import common functions 16 | . "${0%/*}/lib.sh" 17 | 18 | # 19 | # Print the specified text to stdout, prefixing with a timestamp. 20 | # 21 | # $1: Text to print 22 | # 23 | _print() { 24 | echo -e "[$(date)] ${0##*/}: $@" 25 | } 26 | 27 | # 28 | # Echos the full normalised path of the specified path, or nothing if the path 29 | # does not exist. 30 | # 31 | _normalise_path() { 32 | path="$(cd "$1" && echo $PWD)" 33 | if [ $? -gt 0 ]; then 34 | return 1 35 | else 36 | echo $path 37 | return 0 38 | fi 39 | } 40 | 41 | if [ $# -gt 0 ]; then 42 | 43 | if [ "$1" == "--help" -o "$1" == "-h" ]; then 44 | { 45 | echo 46 | echo "Run the tests for ringpop-go/examples." 47 | echo 48 | echo "Usage: $0 [--update] [test_file] [...]" 49 | echo 50 | echo " --update When a test fails, prompt to update the saved test output." 51 | echo 52 | } >&2 53 | exit 1 54 | fi 55 | 56 | if [ "$1" == "--update" ]; then 57 | cram_opts="$cram_opts -i" 58 | shift 59 | fi 60 | 61 | fi 62 | 63 | # Check cram is installed 64 | if ! type cram &>/dev/null; then 65 | echo "$0 requires cram to be installed (try: 'sudo pip install cram')." >&2 66 | echo 67 | exit 1 68 | fi 69 | 70 | fetch-ringpop-common "tools" 2>&1 71 | 72 | export PATH=$PATH:$(_normalise_path "${ringpop_common_dir}/tools") 73 | 74 | run_test() { 75 | # Check test files exist 76 | if ! ls "$@" >/dev/null; then 77 | echo "ERROR: Test files missing." >&2 78 | exit 1 79 | fi 80 | 81 | # Run the tests 82 | _print "Running tests..." 83 | exit_code=0 84 | cram $cram_opts "$@" || exit_code=1 85 | 86 | return $exit_code 87 | } 88 | 89 | declare test_result=0 90 | # Accept test files as arguments 91 | if [ $# -gt 0 ]; then 92 | run_test "$@" || test_result=1 93 | else 94 | _print "Testing $test_files" 95 | run_test $test_files 96 | fi 97 | 98 | 99 | if [ $test_result -eq 0 ]; then 100 | _print "\033[0;32mSuccess!\033[0m" 101 | fi 102 | 103 | _print "exiting $test_result" 104 | 105 | exit $test_result 106 | -------------------------------------------------------------------------------- /test/run-integration-tests: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Run integration tests for ringpop-go. 4 | # 5 | set -eo pipefail 6 | 7 | # Import common functions and variables 8 | . "${0%/*}/lib.sh" 9 | 10 | declare tap_filter="${ringpop_common_dir}/test/tap-filter" 11 | 12 | declare test_cluster_sizes="1 2 3 4 5 10" 13 | declare test_result= 14 | 15 | declare temp_dir="$(mktemp -d)" 16 | 17 | # Check node is installed 18 | if ! type node &>/dev/null; then 19 | echo "ERROR: missing 'node'" >&2 20 | exit 1 21 | fi 22 | 23 | # 24 | # Same as builtin wait, but return code is the number of background processes 25 | # that exited with a non-zero code. 26 | # 27 | wait-all() { 28 | local -i failed=0 29 | 30 | # We need to explicitly loop through all background jobs and specify the 31 | # pids to `wait`, otherwise `wait` doesn't return the exit code. 32 | for pid in $(jobs -p); do 33 | wait $pid || let "failed+=1" 34 | done 35 | 36 | return $failed 37 | } 38 | 39 | # 40 | # Echos and runs the specified command. 41 | # 42 | run() { 43 | echo "+ $@" >&2 44 | "$@" 45 | } 46 | 47 | # Build the testpop binary. 48 | # 49 | build-testpop() { 50 | cd "$project_root" 51 | run make testpop 52 | } 53 | 54 | # 55 | # Run test with specified cluster size. 56 | # 57 | # $1: cluster size 58 | # 59 | run-test-for-cluster-size() { 60 | local cluster_size=$1 61 | local err=0 62 | local output_file="${temp_dir}/${cluster_size}.out" 63 | 64 | # Run the tests and buffer the output to a log file. We'll display it later 65 | # if the test fails. This avoids interleaving of output to the terminal 66 | # when tests are running in parallel. 67 | node "${ringpop_common_dir}/test/it-tests.js" \ 68 | -s "[$1]" \ 69 | --enable-feature reaping-faulty-nodes \ 70 | --enable-feature bidirectional-full-syncs \ 71 | --enable-feature partition-healing \ 72 | --enable-feature labels \ 73 | --enable-feature self-eviction \ 74 | --enable-feature identity \ 75 | "${project_root}/testpop" &>$output_file || err=$? 76 | 77 | if [ $PIPESTATUS -gt 0 ]; then 78 | echo "ERROR: Test errored for cluster size $cluster_size" | \ 79 | prefix "test-errors-${cluster_size}" >&2 80 | return 1 81 | fi 82 | 83 | if [ $err -ne 0 ]; then 84 | # If the test failed, print a message and display the failures 85 | { 86 | echo "FAIL: Test failed for cluster size $cluster_size" 87 | # Output the test data through tap-filter, which discards success 88 | cat "$output_file" |$tap_filter 2>&1 89 | 90 | } | prefix "test-errors-${cluster_size}" >&2 91 | 92 | return 1 93 | fi 94 | } 95 | 96 | # 97 | # Run the integration tests against the testpop binary, in parallel. 98 | # 99 | run-tests() { 100 | for cluster_size in $test_cluster_sizes; do 101 | echo "Spawning test for cluster size ${cluster_size}..." |prefix "test-runner" 102 | run-test-for-cluster-size $cluster_size & 103 | done 104 | 105 | { 106 | echo 107 | echo "Waiting for tests to complete." 108 | echo 109 | echo "To monitor test output (verbose), run:" 110 | echo " tail -f ${temp_dir}/*.out" 111 | echo 112 | } \ 113 | |prefix "test-runner" 114 | 115 | wait-all 116 | } 117 | 118 | # 119 | # Run the integration tests against the testpop binary, in serial. 120 | # 121 | run-tests-serial() { 122 | local exit_code=0 123 | 124 | for cluster_size in $test_cluster_sizes; do 125 | echo "Running test for cluster size ${cluster_size}..." |prefix "test-runner" 126 | run-test-for-cluster-size $cluster_size || exit_code=1 127 | done 128 | 129 | return $exit_code 130 | } 131 | 132 | # Fetch and build in parallel 133 | { fetch-ringpop-common "test" 2>&1|prefix "fetch ringpop-common"; } & 134 | { build-testpop 2>&1|prefix "build testpop"; } & 135 | wait-all 136 | 137 | # Run integration tests 138 | #run-tests 139 | run-tests-serial 140 | test_result=$? 141 | 142 | if [ $test_result -eq 0 ]; then 143 | echo "Tests passed" 144 | rm -rf "$temp_dir" 145 | else 146 | echo "Tests failed" >&2 147 | fi 148 | 149 | exit $test_result 150 | -------------------------------------------------------------------------------- /test/thrift/pingpong.thrift: -------------------------------------------------------------------------------- 1 | struct ping { 2 | 1: required string key, 3 | } 4 | 5 | struct pong { 6 | 1: required string source, 7 | } 8 | 9 | exception PingError {} 10 | 11 | service PingPong { 12 | pong Ping(1: ping request) throws (1: PingError pingError) 13 | } 14 | -------------------------------------------------------------------------------- /test/thrift/pingpong/GoUnusedProtection__.go: -------------------------------------------------------------------------------- 1 | // Code generated by Thrift Compiler (0.15.0). DO NOT EDIT. 2 | 3 | package pingpong 4 | 5 | var GoUnusedProtection__ int; 6 | 7 | -------------------------------------------------------------------------------- /test/thrift/pingpong/mock_t_chan_ping_pong.go: -------------------------------------------------------------------------------- 1 | package pingpong 2 | 3 | import ( 4 | "github.com/stretchr/testify/mock" 5 | "github.com/temporalio/tchannel-go/thrift" 6 | ) 7 | 8 | type MockTChanPingPong struct { 9 | mock.Mock 10 | } 11 | 12 | // Ping provides a mock function with given fields: ctx, request 13 | func (_m *MockTChanPingPong) Ping(ctx thrift.Context, request *Ping) (*Pong, error) { 14 | ret := _m.Called(ctx, request) 15 | 16 | var r0 *Pong 17 | if rf, ok := ret.Get(0).(func(thrift.Context, *Ping) *Pong); ok { 18 | r0 = rf(ctx, request) 19 | } else { 20 | if ret.Get(0) != nil { 21 | r0 = ret.Get(0).(*Pong) 22 | } 23 | } 24 | 25 | var r1 error 26 | if rf, ok := ret.Get(1).(func(thrift.Context, *Ping) error); ok { 27 | r1 = rf(ctx, request) 28 | } else { 29 | r1 = ret.Error(1) 30 | } 31 | 32 | return r0, r1 33 | } 34 | -------------------------------------------------------------------------------- /test/thrift/pingpong/pingpong-consts.go: -------------------------------------------------------------------------------- 1 | // Code generated by Thrift Compiler (0.15.0). DO NOT EDIT. 2 | 3 | package pingpong 4 | 5 | import ( 6 | "bytes" 7 | "context" 8 | "fmt" 9 | "time" 10 | thrift "github.com/apache/thrift/lib/go/thrift" 11 | ) 12 | 13 | // (needed to ensure safety because of naive import list construction.) 14 | var _ = thrift.ZERO 15 | var _ = fmt.Printf 16 | var _ = context.Background 17 | var _ = time.Now 18 | var _ = bytes.Equal 19 | 20 | 21 | func init() { 22 | } 23 | 24 | -------------------------------------------------------------------------------- /test/thrift/pingpong/tchan-pingpong.go: -------------------------------------------------------------------------------- 1 | // @generated Code generated by thrift-gen. Do not modify. 2 | 3 | // Package pingpong is generated code used to make or handle TChannel calls using Thrift. 4 | package pingpong 5 | 6 | import ( 7 | "fmt" 8 | 9 | athrift "github.com/apache/thrift/lib/go/thrift" 10 | "github.com/temporalio/tchannel-go/thrift" 11 | ) 12 | 13 | // Interfaces for the service and client for the services defined in the IDL. 14 | 15 | // TChanPingPong is the interface that defines the server handler and client interface. 16 | type TChanPingPong interface { 17 | Ping(ctx thrift.Context, request *Ping) (*Pong, error) 18 | } 19 | 20 | // Implementation of a client and service handler. 21 | 22 | type tchanPingPongClient struct { 23 | thriftService string 24 | client thrift.TChanClient 25 | } 26 | 27 | func NewTChanPingPongInheritedClient(thriftService string, client thrift.TChanClient) *tchanPingPongClient { 28 | return &tchanPingPongClient{ 29 | thriftService, 30 | client, 31 | } 32 | } 33 | 34 | // NewTChanPingPongClient creates a client that can be used to make remote calls. 35 | func NewTChanPingPongClient(client thrift.TChanClient) TChanPingPong { 36 | return NewTChanPingPongInheritedClient("PingPong", client) 37 | } 38 | 39 | func (c *tchanPingPongClient) Ping(ctx thrift.Context, request *Ping) (*Pong, error) { 40 | var resp PingPongPingResult 41 | args := PingPongPingArgs{ 42 | Request: request, 43 | } 44 | success, err := c.client.Call(ctx, c.thriftService, "Ping", &args, &resp) 45 | if err == nil && !success { 46 | switch { 47 | case resp.PingError != nil: 48 | err = resp.PingError 49 | default: 50 | err = fmt.Errorf("received no result or unknown exception for Ping") 51 | } 52 | } 53 | 54 | return resp.GetSuccess(), err 55 | } 56 | 57 | type tchanPingPongServer struct { 58 | handler TChanPingPong 59 | } 60 | 61 | // NewTChanPingPongServer wraps a handler for TChanPingPong so it can be 62 | // registered with a thrift.Server. 63 | func NewTChanPingPongServer(handler TChanPingPong) thrift.TChanServer { 64 | return &tchanPingPongServer{ 65 | handler, 66 | } 67 | } 68 | 69 | func (s *tchanPingPongServer) Service() string { 70 | return "PingPong" 71 | } 72 | 73 | func (s *tchanPingPongServer) Methods() []string { 74 | return []string{ 75 | "Ping", 76 | } 77 | } 78 | 79 | func (s *tchanPingPongServer) Handle(ctx thrift.Context, methodName string, protocol athrift.TProtocol) (bool, athrift.TStruct, error) { 80 | switch methodName { 81 | case "Ping": 82 | return s.handlePing(ctx, protocol) 83 | 84 | default: 85 | return false, nil, fmt.Errorf("method %v not found in service %v", methodName, s.Service()) 86 | } 87 | } 88 | 89 | func (s *tchanPingPongServer) handlePing(ctx thrift.Context, protocol athrift.TProtocol) (bool, athrift.TStruct, error) { 90 | var req PingPongPingArgs 91 | var res PingPongPingResult 92 | 93 | if err := req.Read(ctx, protocol); err != nil { 94 | return false, nil, err 95 | } 96 | 97 | r, err := 98 | s.handler.Ping(ctx, req.Request) 99 | 100 | if err != nil { 101 | switch v := err.(type) { 102 | case *PingError: 103 | if v == nil { 104 | return false, nil, fmt.Errorf("Handler for pingError returned non-nil error type *PingError but nil value") 105 | } 106 | res.PingError = v 107 | default: 108 | return false, nil, err 109 | } 110 | } else { 111 | res.Success = r 112 | } 113 | 114 | return err == nil, &res, nil 115 | } 116 | -------------------------------------------------------------------------------- /test/travis: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Run Travis CI scripts from an environment variable. 4 | # 5 | # This script evaluates and executes the command string from the RUN 6 | # environment variable. 7 | # 8 | # It allows you to execute different commands in a test matrix on Travis CI, 9 | # based on env vars in your .travis.yml. 10 | # 11 | declare RUN_DEFAULT="" 12 | 13 | # Perform default action if none specified 14 | if [ -z "$RUN" ]; then 15 | RUN=$RUN_DEFAULT 16 | fi 17 | 18 | # Exit with error if there's nothing to do 19 | if [ -z "$RUN" ]; then 20 | echo "ERROR: No actions specified and no default actions. Exiting." >&2 21 | exit 1 22 | fi 23 | 24 | # Execute the command string; echo the commands as they run 25 | eval "set -x; $RUN" 26 | -------------------------------------------------------------------------------- /test/update-coveralls: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Send coverage data to coveralls.io 4 | # 5 | # `go test -coverprofile=` only supports generating a coverage profile for one 6 | # package at a time. This script combines coverage data from multiple packages 7 | # and then sends the coverage data to coveralls.io 8 | # 9 | echo "mode: set" > acc.out 10 | FAIL=0 11 | 12 | packages="$(find . -type d -maxdepth 3 \ 13 | ! -path './.git*' \ 14 | ! -path '*/_*' \ 15 | ! -path './Godeps*' \ 16 | ! -path './test*' \ 17 | ! -path './vendor*' \ 18 | )" 19 | 20 | # Standard go tooling behavior is to ignore dirs with leading underscores 21 | for dir in $packages; 22 | do 23 | if ls $dir/*.go &> /dev/null; then 24 | go test -coverprofile=profile.out $dir || FAIL=$? 25 | if [ -f profile.out ] 26 | then 27 | cat profile.out | grep -v "mode: set" | grep -v "mocks.go" >> acc.out 28 | rm profile.out 29 | fi 30 | fi 31 | done 32 | 33 | # Failures have incomplete results, so don't send 34 | if [ "$FAIL" -eq 0 ]; then 35 | goveralls -service=travis-ci -v -coverprofile=acc.out 36 | fi 37 | 38 | rm -f acc.out 39 | 40 | exit $FAIL 41 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package ringpop 22 | 23 | import ( 24 | "strings" 25 | "time" 26 | 27 | "github.com/uber-common/bark" 28 | ) 29 | 30 | type noopStatsReporter struct{} 31 | 32 | func (noopStatsReporter) IncCounter(name string, tags bark.Tags, value int64) {} 33 | func (noopStatsReporter) UpdateGauge(name string, tags bark.Tags, value int64) {} 34 | func (noopStatsReporter) RecordTimer(name string, tags bark.Tags, d time.Duration) {} 35 | 36 | func genStatsHostport(hostport string) string { 37 | return strings.Replace(strings.Replace(hostport, ".", "_", -1), ":", "_", -1) 38 | } 39 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package ringpop 22 | 23 | import ( 24 | "fmt" 25 | "sync" 26 | "time" 27 | 28 | "github.com/temporalio/ringpop-go/membership" 29 | "github.com/temporalio/ringpop-go/swim" 30 | "github.com/temporalio/ringpop-go/util" 31 | 32 | "github.com/uber-common/bark" 33 | ) 34 | 35 | // fake membership.Member 36 | type fakeMember struct { 37 | address string 38 | identity string 39 | } 40 | 41 | func (f fakeMember) GetAddress() string { 42 | return f.address 43 | } 44 | 45 | func (f fakeMember) Label(key string) (value string, has bool) { 46 | return "", false 47 | } 48 | 49 | func (f fakeMember) Identity() string { 50 | if f.identity != "" { 51 | return f.identity 52 | } 53 | return f.address 54 | } 55 | 56 | // fake stats 57 | type dummyStats struct { 58 | sync.RWMutex 59 | _vals map[string]int64 60 | } 61 | 62 | func newDummyStats() *dummyStats { 63 | return &dummyStats{ 64 | _vals: make(map[string]int64), 65 | } 66 | } 67 | 68 | func (s *dummyStats) IncCounter(key string, tags bark.Tags, val int64) { 69 | s.Lock() 70 | defer s.Unlock() 71 | 72 | s._vals[key] += val 73 | } 74 | 75 | func (s *dummyStats) read(key string) int64 { 76 | s.RLock() 77 | defer s.RUnlock() 78 | 79 | return s._vals[key] 80 | } 81 | 82 | func (s *dummyStats) has(key string) bool { 83 | s.Lock() 84 | defer s.Unlock() 85 | 86 | _, has := s._vals[key] 87 | 88 | return has 89 | } 90 | 91 | func (s *dummyStats) clear() { 92 | s.Lock() 93 | defer s.Unlock() 94 | 95 | s._vals = make(map[string]int64) 96 | } 97 | 98 | func (s *dummyStats) UpdateGauge(key string, tags bark.Tags, val int64) { 99 | s.Lock() 100 | defer s.Unlock() 101 | 102 | s._vals[key] = val 103 | } 104 | 105 | func (s *dummyStats) RecordTimer(key string, tags bark.Tags, d time.Duration) { 106 | s.Lock() 107 | defer s.Unlock() 108 | 109 | s._vals[key] += util.MS(d) 110 | } 111 | 112 | func genAddresses(host, fromPort, toPort int) []string { 113 | var addresses []string 114 | 115 | for i := fromPort; i <= toPort; i++ { 116 | addresses = append(addresses, fmt.Sprintf("127.0.0.%v:%v", host, 3000+i)) 117 | } 118 | 119 | return addresses 120 | } 121 | 122 | func genChanges(addresses []string, statuses ...string) (changes []swim.Change) { 123 | for _, address := range addresses { 124 | for _, status := range statuses { 125 | changes = append(changes, swim.Change{ 126 | Address: address, 127 | Status: status, 128 | }) 129 | } 130 | } 131 | 132 | return changes 133 | } 134 | 135 | func genMembers(addresses []string) (members []membership.Member) { 136 | for _, address := range addresses { 137 | members = append(members, fakeMember{ 138 | address: address, 139 | }) 140 | } 141 | return 142 | } 143 | 144 | type MembershipChangeField int 145 | 146 | const ( 147 | BeforeMemberField MembershipChangeField = 1 << iota 148 | AfterMemberField = 1 << iota 149 | ) 150 | 151 | func genMembershipChanges(members []membership.Member, fields MembershipChangeField) (changes []membership.MemberChange) { 152 | for _, member := range members { 153 | var change membership.MemberChange 154 | 155 | if fields&BeforeMemberField != 0 { 156 | change.Before = member 157 | } 158 | if fields&AfterMemberField != 0 { 159 | change.After = member 160 | } 161 | 162 | changes = append(changes, change) 163 | } 164 | return 165 | } 166 | --------------------------------------------------------------------------------