├── .github ├── pull_request_template.md └── workflows │ ├── check.yml │ └── main.yml ├── .gitignore ├── .golangci.yaml ├── .luacheckrc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Makefile ├── README.md ├── README_ru.md ├── api.go ├── api_test.go ├── concurrent_test.go ├── config.lua ├── discovery.go ├── docs ├── doc.md ├── doc_ru.md └── static │ ├── direct.png │ ├── logo.png │ ├── not-direct.png │ └── providers.png ├── error.go ├── examples └── customer │ ├── README.md │ ├── README.ru.md │ ├── go-service │ ├── Makefile │ ├── config.yaml │ ├── docs │ │ ├── docs.go │ │ ├── swagger.json │ │ └── swagger.yaml │ ├── go.mod │ ├── go.sum │ └── main.go │ └── tarantool │ ├── .luarocks-config.lua │ ├── .tarantoolctl │ ├── Makefile │ ├── customer-scm-1.rockspec │ ├── localcfg.lua │ ├── router.lua │ ├── router_1.lua │ ├── storage.lua │ ├── storage_1_a.lua │ ├── storage_1_b.lua │ ├── storage_1_c.lua │ ├── storage_2_a.lua │ └── storage_2_b.lua ├── go.mod ├── go.sum ├── logger.go ├── mocks ├── pool │ └── pooler.go └── topology │ └── topology_controller.go ├── providers.go ├── providers ├── etcd │ ├── README.md │ ├── doc.go │ ├── provider.go │ └── provider_test.go ├── prometheus │ ├── prometheus.go │ ├── prometheus_example_test.go │ └── prometheus_test.go ├── slog │ ├── slog.go │ └── slog_test.go ├── static │ ├── provider.go │ └── provider_test.go └── viper │ ├── README.md │ ├── moonlibs │ ├── config.go │ ├── convert.go │ └── convert_test.go │ ├── provider.go │ ├── provider_test.go │ ├── tarantool3 │ ├── config.go │ └── convert.go │ └── testdata │ ├── config-direct.yaml │ ├── config-sub.yaml │ └── config-tarantool3.yaml ├── providers_test.go ├── replicaset.go ├── replicaset_test.go ├── sugar.go ├── tarantool_test.go ├── test_helper └── helper.go ├── topology.go ├── topology_test.go ├── vshard.go ├── vshard_shadow_test.go └── vshard_test.go /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | What has been done? Why? What problem is being solved? 2 | 3 | I didn't forget about (remove if it is not applicable): 4 | 5 | - [ ] Changelog (see [documentation](https://keepachangelog.com/en/1.0.0/) for changelog format) 6 | 7 | Related issues: 8 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Run checks 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | luacheck: 11 | runs-on: ubuntu-22.04 12 | steps: 13 | - uses: actions/checkout@master 14 | 15 | - name: Setup Tarantool 16 | uses: tarantool/setup-tarantool@v2 17 | with: 18 | tarantool-version: '2.8' 19 | 20 | - name: Setup tt 21 | run: | 22 | curl -L https://tarantool.io/release/2/installer.sh | sudo bash 23 | sudo apt install -y tt 24 | tt version 25 | 26 | - name: Setup luacheck 27 | run: tt rocks install luacheck 0.25.0 28 | 29 | - name: Run luacheck 30 | run: ./.rocks/bin/luacheck . 31 | 32 | golangci-lint: 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/setup-go@v2 36 | 37 | - uses: actions/checkout@v2 38 | 39 | - name: golangci-lint 40 | uses: golangci/golangci-lint-action@v3 41 | continue-on-error: true 42 | with: 43 | # The first run is for GitHub Actions error format. 44 | args: --config=.golangci.yaml 45 | 46 | - name: golangci-lint 47 | uses: golangci/golangci-lint-action@v3 48 | with: 49 | # The second run is for human-readable error format with a file name 50 | # and a line number. 51 | args: --out-${NO_FUTURE}format colored-line-number --config=.golangci.yaml -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: testing 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | all-tests: 14 | # We could replace it with ubuntu-latest after fixing the bug: 15 | # https://github.com/tarantool/setup-tarantool/issues/37 16 | runs-on: ubuntu-22.04 17 | 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | golang: 22 | - '1.22' 23 | - 'stable' 24 | tarantool: 25 | - '2.8' 26 | - '2.10' 27 | - 'master' 28 | include: 29 | - tarantool: 'master' 30 | golang: '1.22' 31 | coverage: true 32 | 33 | steps: 34 | - name: Clone the connector 35 | uses: actions/checkout@v3 36 | 37 | - name: Setup tt 38 | run: | 39 | curl -L https://tarantool.io/release/2/installer.sh | sudo bash 40 | sudo apt install -y tt 41 | 42 | - name: Setup tt environment 43 | run: tt init 44 | 45 | # https://github.com/tarantool/checks/issues/64 46 | - name: Install specific CMake version 47 | run: pip3 install cmake==3.15.3 48 | 49 | - name: Setup Tarantool ${{ matrix.tarantool }} 50 | if: matrix.tarantool != 'master' 51 | uses: tarantool/setup-tarantool@v2 52 | with: 53 | tarantool-version: ${{ matrix.tarantool }} 54 | 55 | - name: Get Tarantool master commit 56 | if: matrix.tarantool == 'master' 57 | run: | 58 | commit_hash=$(git ls-remote https://github.com/tarantool/tarantool.git --branch master | head -c 8) 59 | echo "LATEST_COMMIT=${commit_hash}" >> $GITHUB_ENV 60 | shell: bash 61 | 62 | - name: Cache Tarantool master 63 | if: matrix.tarantool == 'master' 64 | id: cache-latest 65 | uses: actions/cache@v3 66 | with: 67 | path: | 68 | ${{ github.workspace }}/bin 69 | ${{ github.workspace }}/include 70 | key: cache-latest-${{ env.LATEST_COMMIT }} 71 | 72 | - name: Setup Tarantool master 73 | if: matrix.tarantool == 'master' && steps.cache-latest.outputs.cache-hit != 'true' 74 | run: | 75 | sudo pip3 install cmake==3.15.3 76 | sudo tt install tarantool master 77 | 78 | - name: Add Tarantool master to PATH 79 | if: matrix.tarantool == 'master' 80 | run: echo "${GITHUB_WORKSPACE}/bin" >> $GITHUB_PATH 81 | 82 | - name: Setup golang for the connector and tests 83 | uses: actions/setup-go@v3 84 | with: 85 | go-version: ${{ matrix.golang }} 86 | 87 | - name: Run tests 88 | run: | 89 | export START_PORT=33000 90 | export NREPLICASETS=5 91 | make test 92 | 93 | - name: Install goveralls 94 | if: ${{ matrix.coverage }} 95 | run: go install github.com/mattn/goveralls@latest 96 | 97 | - name: Send coverage 98 | if: ${{ matrix.coverage }} 99 | env: 100 | COVERALLS_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 101 | run: goveralls -coverprofile=coverage.out -service=github -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE Files 2 | .idea 3 | .vscode 4 | 5 | # TT 6 | .rocks 7 | *.snap 8 | *.xlog 9 | *.log 10 | 11 | # System 12 | *.pid 13 | 14 | # Go output 15 | *.out 16 | 17 | # Tmp 18 | *.tmp 19 | tests/tnt/tmp -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 10m 3 | issues-exit-code: 1 4 | tests: true 5 | 6 | output: 7 | print-issued-lines: true 8 | print-linter-name: true 9 | 10 | linters-settings: 11 | dupl: 12 | threshold: 100 13 | goconst: 14 | min-len: 2 15 | min-occurrences: 2 16 | revive: 17 | rules: 18 | - name: string-format 19 | severity: error 20 | disabled: false 21 | arguments: 22 | - - 'fmt.Errorf[0]' 23 | - "/^[^A-Z].*$/" 24 | - error messages must not start with capital letter 25 | - - 'log.Printf[0]' 26 | - "/^[^a-z].*$/" 27 | - log messages must not start with lowercase letter 28 | misspell: 29 | # Correct spellings using locale preferences for US or UK. 30 | # Default is to use a neutral variety of English. 31 | # Setting locale to US will correct the British spelling of 'colour' to 'color'. 32 | locale: US 33 | ignore-words: 34 | - GitLab 35 | 36 | linters: 37 | disable-all: true 38 | enable: 39 | - errcheck 40 | - goconst 41 | - goimports 42 | - gosec 43 | - gosimple 44 | - govet 45 | - ineffassign 46 | - revive 47 | - typecheck 48 | - prealloc 49 | # - wls # excluded from linters list because produces too many noise 50 | - staticcheck 51 | - unused 52 | - contextcheck 53 | - durationcheck 54 | - errname 55 | - exhaustive 56 | - gocritic 57 | - gofmt 58 | - nilerr 59 | - nilnil 60 | - usestdlibvars 61 | - misspell 62 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | std = "luajit" 2 | codes = true 3 | 4 | read_globals = { 5 | -- Tarantool vars: 6 | "box", 7 | "tonumber64", 8 | "package", 9 | "spacer", 10 | "F", 11 | "T", 12 | 13 | package = { 14 | fields = { 15 | reload = { 16 | fields = { 17 | "count", 18 | "register", 19 | } 20 | } 21 | } 22 | }, 23 | 24 | -- Exported by package 'config' 25 | "config", 26 | } 27 | 28 | max_line_length = 200 29 | 30 | ignore = { 31 | "212", 32 | "213", 33 | "411", -- ignore was previously defined 34 | "422", -- ignore shadowing 35 | "111", -- ignore non standart functions; for tarantool there is global functions 36 | } 37 | 38 | local conf_rules = { 39 | read_globals = { 40 | "instance_name", 41 | }, 42 | globals = { 43 | "box", "etcd", 44 | } 45 | } 46 | 47 | exclude_files = { 48 | "tests/tnt/.rocks/*", 49 | "examples/customer/tarantool/.rocks/*", 50 | "tests/tnt/tmp/*", -- ignore tmp tarantool files 51 | "examples/customer/tarantool/*", -- TODO: now we ignore examples from original vshard example 52 | ".rocks/*", -- ignore rocks after tests prepare 53 | } 54 | 55 | 56 | files["etc/*.lua"] = conf_rules 57 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at maksim.konovalov@vk.team. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Konovalov Maxim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Default timeout for tests 2 | TEST_TIMEOUT?=100s 3 | # Timeout for extended tests 4 | EXTENDED_TEST_TIMEOUT=1m 5 | # Command to invoke Go 6 | GO_CMD?=go 7 | # Path to local binary output directory 8 | LOCAL_BIN:=$(CURDIR)/bin 9 | # Version tag for golangci-lint 10 | GOLANGCI_TAG:=latest 11 | # Path to the golangci-lint binary 12 | GOLANGCI_BIN:=$(GOPATH)/bin/golangci-lint 13 | 14 | 15 | # HELP 16 | # This will output the help for each task 17 | # thanks to https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html 18 | .PHONY: help 19 | 20 | .DEFAULT_GOAL := help 21 | 22 | help: ## This help. 23 | @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) 24 | 25 | .PHONY: install-lint 26 | install-lint: 27 | ifeq ($(wildcard $(GOLANGCI_BIN)),) 28 | $(info #Downloading swaggo latest) 29 | $(GO_CMD) install github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_TAG) 30 | endif 31 | 32 | test: 33 | tt rocks install vshard 0.1.26 34 | $(GO_CMD) test ./... -race -parallel=10 -timeout=$(TEST_TIMEOUT) -covermode=atomic -coverprofile=coverage.out.tmp -coverpkg="./..." 35 | @cat coverage.out.tmp | grep -v "mock" > coverage.out 36 | @rm coverage.out.tmp 37 | 38 | cover: test ## Generate and open the HTML report for test coverage. 39 | $(GO_CMD) tool cover -html=coverage.out 40 | 41 | generate/mocks: 42 | mockery --name=Pooler --case=underscore --output=mocks/pool --outpkg=mockpool # need fix it later 43 | mockery --name=TopologyController --case=underscore --output=mocks/topology --outpkg=mocktopology 44 | 45 | .PHONY: lint 46 | lint: install-lint ## Run GolangCI-Lint to check code quality and style. 47 | $(GOLANGCI_BIN) run --config=.golangci.yaml ./... 48 | 49 | testrace: BUILD_TAGS+=testonly 50 | testrace: 51 | @CGO_ENABLED=1 \ 52 | $(GO_CMD) test -tags='$(BUILD_TAGS)' -race -timeout=$(EXTENDED_TEST_TIMEOUT) -parallel=20 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go VShard Router 2 | 3 | логотип go vshard router 4 | 5 | [![Go Reference](https://pkg.go.dev/badge/github.com/tarantool/go-vshard-router.svg)](https://pkg.go.dev/github.com/tarantool/go-vshard-router) 6 | [![Actions Status][actions-badge]][actions-url] 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/tarantool/go-vshard-router)](https://goreportcard.com/report/github.com/tarantool/go-vshard-router) 8 | [![Code Coverage][coverage-badge]][coverage-url] 9 | [![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/KaymeKaydex/go-vshard-router/master/LICENSE) 10 | 11 | Translations: 12 | - [Русский](https://github.com/tarantool/go-vshard-router/blob/master/README_ru.md) 13 | 14 | 15 | go-vshard-router is a library for sending requests to a sharded tarantool cluster directly, 16 | without using tarantool-router. This library based on [tarantool vhsard library router](https://github.com/tarantool/vshard/blob/master/vshard/router/init.lua) and [go-tarantool connector](https://github.com/tarantool/go-tarantool). go-vshard-router takes a new approach to creating your cluster 17 | 18 | ```mermaid 19 | graph TD 20 | %% Old Cluster Schema 21 | subgraph Old_Cluster_Schema["Old Cluster Schema"] 22 | direction LR 23 | subgraph Tarantool Database Cluster 24 | subgraph Replicaset 1 25 | Master_001_1 26 | Replica_001_2 27 | end 28 | end 29 | 30 | ROUTER1["Tarantool vshard-router 1_1"] --> Master_001_1 31 | ROUTER2["Tarantool vshard-router 1_2"] --> Master_001_1 32 | ROUTER3["Tarantool vshard-router 1_3"] --> Master_001_1 33 | ROUTER1["Tarantool vshard-router 1_1"] --> Replica_001_2 34 | ROUTER2["Tarantool vshard-router 1_2"] --> Replica_001_2 35 | ROUTER3["Tarantool vshard-router 1_3"] --> Replica_001_2 36 | 37 | GO["Golang service"] 38 | GO --> ROUTER1 39 | GO --> ROUTER2 40 | GO --> ROUTER3 41 | end 42 | 43 | %% New Cluster Schema 44 | subgraph New_Cluster_Schema["New Cluster Schema"] 45 | direction LR 46 | subgraph Application Host 47 | Golang_Service 48 | end 49 | 50 | Golang_Service --> |iproto| MASTER1 51 | Golang_Service --> |iproto| REPLICA1 52 | 53 | MASTER1["Master 001_1"] 54 | REPLICA1["Replica 001_2"] 55 | 56 | subgraph Tarantool Database Cluster 57 | subgraph Replicaset 1 58 | MASTER1 59 | REPLICA1 60 | end 61 | end 62 | end 63 | ``` 64 | 65 | ## Getting started 66 | ### Prerequisites 67 | 68 | - **[Go](https://go.dev/)**: any one of the **two latest major** [releases](https://go.dev/doc/devel/release) (we test it with these). 69 | 70 | ### Getting Go-Vshard-Router 71 | With [Go module](https://github.com/golang/go/wiki/Modules) support, simply add the following import 72 | 73 | ``` 74 | import "github.com/tarantool/go-vshard-router/v2" 75 | ``` 76 | to your code, and then `go [build|run|test]` will automatically fetch the necessary dependencies. 77 | 78 | Otherwise, run the following Go command to install the `go-vshard-router` package: 79 | 80 | ```sh 81 | $ go get -u github.com/tarantool/go-vshard-router/v2 82 | ``` 83 | 84 | ### Running Go-Vshard-Router 85 | 86 | First you need to import Go-Vshard-Router package for using Go-Vshard-Router 87 | 88 | ```go 89 | package main 90 | 91 | import ( 92 | "context" 93 | "fmt" 94 | "strconv" 95 | "time" 96 | 97 | vshardrouter "github.com/tarantool/go-vshard-router/v2" 98 | "github.com/tarantool/go-vshard-router/v2/providers/static" 99 | 100 | "github.com/google/uuid" 101 | "github.com/tarantool/go-tarantool/v2" 102 | ) 103 | 104 | func main() { 105 | ctx := context.Background() 106 | 107 | directRouter, err := vshardrouter.NewRouter(ctx, vshardrouter.Config{ 108 | DiscoveryTimeout: time.Minute, 109 | DiscoveryMode: vshardrouter.DiscoveryModeOn, 110 | TopologyProvider: static.NewProvider(map[vshardrouter.ReplicasetInfo][]vshardrouter.InstanceInfo{ 111 | {Name: "replcaset_1", UUID: uuid.New()}: { 112 | {Addr: "127.0.0.1:1001", Name: "1_1"}, 113 | {Addr: "127.0.0.1:1002", Name: "1_2"}, 114 | }, 115 | {Name: "replcaset_2", UUID: uuid.New()}: { 116 | {Addr: "127.0.0.1:2001", Name: "2_1"}, 117 | {Addr: "127.0.0.1:2002", Name: "2_2"}, 118 | }, 119 | }), 120 | TotalBucketCount: 128000, 121 | PoolOpts: tarantool.Opts{ 122 | Timeout: time.Second, 123 | }, 124 | }) 125 | if err != nil { 126 | panic(err) 127 | } 128 | 129 | user := struct { 130 | ID uint64 131 | }{ 132 | ID: 123, 133 | } 134 | 135 | bucketID := directRouter.BucketIDStrCRC32(strconv.FormatUint(user.ID, 10)) 136 | 137 | resp, err := directRouter.Call( 138 | ctx, 139 | bucketID, 140 | vshardrouter.CallModeBRO, 141 | "storage.api.get_user_info", 142 | []interface{}{&struct { 143 | BucketID uint64 `msgpack:"bucket_id" json:"bucket_id,omitempty"` 144 | Body map[string]interface{} `msgpack:"body"` 145 | }{ 146 | BucketID: bucketID, 147 | Body: map[string]interface{}{ 148 | "user_id": "123456", 149 | }, 150 | }}, vshardrouter.CallOpts{Timeout: time.Second * 2}, 151 | ) 152 | if err != nil { 153 | panic(err) 154 | } 155 | 156 | info := &struct { 157 | BirthDay int 158 | }{} 159 | 160 | err = resp.GetTyped(&[]interface{}{info}) 161 | if err != nil { 162 | panic(err) 163 | } 164 | 165 | interfaceResult, err := resp.Get() 166 | if err != nil { 167 | panic(err) 168 | } 169 | 170 | fmt.Printf("interface result: %v", interfaceResult) 171 | fmt.Printf("get typed result: %v", info) 172 | } 173 | ``` 174 | ### Providers 175 | topology 176 | 177 | We understand that the implementations of loggers, metrics, 178 | and topology sources can vary significantly in both implementation and usage. 179 | Therefore, go-vshard-router gives you the flexibility to choose the tools you deem appropriate 180 | or implement them yourself by using interfaces. 181 | 182 | #### Topology 183 | You can use topology (configuration) providers as the source of router configuration. 184 | Currently, the following providers are supported: 185 | 186 | - **etcd** (for configurations similar to [moonlibs/config](https://github.com/moonlibs/config?tab=readme-ov-file#example-of-etcd-configuration-for-vshard-based-applications-etcdclustervshard) in etcd v2 for Tarantool versions below 3) 187 | - **static** (for specifying configuration directly in the code for ease of testing) 188 | - **[viper](providers/viper/README.md)** 189 | - etcd v3 190 | - consul 191 | - files 192 | 193 | #### Metrics 194 | Metrics providers are also available, 195 | you can use ready-made metrics providers that can be registered in prometheus and passed to go-vshard-router. 196 | This will allow you not to think about options and metrics settings. 197 | The following providers are currently available: 198 | - **[prometheus](providers/prometheus)** 199 | 200 | #### Logs 201 | 202 | - stdout (builtin) 203 | - **[slog](providers/slog)** 204 | 205 | ### Learn more examples 206 | #### Quick Start 207 | Learn with th [Quick Start](docs/doc.md), which include examples and theory. 208 | #### [Customer service](examples/customer/README.md) 209 | Service with go-vshard-router on top of the tarantool example from the original vshard library using raft 210 | 211 | ## Benchmarks 212 | ### Go Bench 213 | 214 | | Benchmark | Runs | Time (ns/op) | Memory (B/op) | Allocations (allocs/op) | 215 | |---------------------------------------|--------|---------------|----------------|-------------------------| 216 | | BenchmarkCallSimpleInsert_GO-12 | 14929 | 77443 | 2308 | 34 | 217 | | BenchmarkCallSimpleInsert_Lua-12 | 10196 | 116101 | 1098 | 19 | 218 | | BenchmarkCallSimpleSelect_GO-12 | 20065 | 60521 | 2534 | 40 | 219 | | BenchmarkCallSimpleSelect_Lua-12 | 12064 | 99874 | 1153 | 20 | 220 | 221 | 222 | ### [K6](https://github.com/grafana/k6) 223 | Topology: 224 | - 4 replicasets (x2 instances per rs) 225 | - 4 tarantool proxy 226 | - 1 golang service 227 | 228 | constant VUes scenario: 229 | at a load close to production 230 | 231 | ```select``` 232 | - go-vshard-router: uncritically worse latency, but 3 times more rps 233 | ![Image alt](docs/static/direct.png) 234 | - tarantool-router: (80% cpu, heavy rps kills proxy at 100% cpu) 235 | ![Image alt](docs/static/not-direct.png) 236 | 237 | 238 | [actions-badge]: https://github.com/tarantool/go-vshard-router/actions/workflows/main.yml/badge.svg 239 | [actions-url]: https://github.com/tarantool/go-vshard-router/actions/workflows/main.yml 240 | [coverage-badge]: https://coveralls.io/repos/github/tarantool/go-vshard-router/badge.svg?branch=master 241 | [coverage-url]: https://coveralls.io/github/tarantool/go-vshard-router?branch=master -------------------------------------------------------------------------------- /README_ru.md: -------------------------------------------------------------------------------- 1 | # Go VShard Router 2 | 3 | go vshard router logo 4 | 5 | [![Go Reference](https://pkg.go.dev/badge/github.com/tarantool/go-vshard-router.svg)](https://pkg.go.dev/github.com/tarantool/go-vshard-router) 6 | [![Actions Status][actions-badge]][actions-url] 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/tarantool/go-vshard-router)](https://goreportcard.com/report/github.com/tarantool/go-vshard-router) 8 | [![Code Coverage][coverage-badge]][coverage-url] 9 | [![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/KaymeKaydex/go-vshard-router/master/LICENSE) 10 | 11 | Translations: 12 | 13 | - [English](https://github.com/tarantool/go-vshard-router/blob/main/README.md) 14 | 15 | go-vshard-router — библиотека для отправки запросов напрямую в стораджа в шардированный кластер tarantool, 16 | без использования tarantool-router. Эта библиотека написана на 17 | основе [модуля библиотеки tarantool vhsard router](https://github.com/tarantool/vshard/blob/master/vshard/router/init.lua) 18 | и [коннектора go-tarantool](https://github.com/tarantool/go-tarantool). go-vshard-router применяет новый подход к 19 | созданию кластера 20 | 21 | ```mermaid 22 | graph TD 23 | %% Старая схема кластера 24 | subgraph Old_Cluster_Schema["Old Cluster Schema"] 25 | direction LR 26 | subgraph Tarantool Database Cluster 27 | subgraph Replicaset 1 28 | Master_001_1 29 | Replica_001_2 30 | end 31 | end 32 | 33 | ROUTER1["Tarantool vshard-router 1_1"] --> Master_001_1 34 | ROUTER2["Tarantool vshard-router 1_2"] --> Master_001_1 35 | ROUTER3["Tarantool vshard-router 1_3"] --> Master_001_1 36 | ROUTER1["Tarantool vshard-router 1_1"] --> Replica_001_2 37 | ROUTER2["Tarantool vshard-router 1_2"] --> Replica_001_2 38 | ROUTER3["Tarantool vshard-router 1_3"] --> Replica_001_2 39 | 40 | GO["Golang service"] 41 | GO --> ROUTER1 42 | GO --> ROUTER2 43 | GO --> ROUTER3 44 | end 45 | 46 | %% Новая схема кластера 47 | subgraph New_Cluster_Schema["New Cluster Schema"] 48 | direction LR 49 | subgraph Application Host 50 | Golang_Service 51 | end 52 | 53 | Golang_Service --> |iproto| MASTER1 54 | Golang_Service --> |iproto| REPLICA1 55 | 56 | MASTER1["Master 001_1"] 57 | REPLICA1["Replica 001_2"] 58 | 59 | subgraph Tarantool Database Cluster 60 | subgraph Replicaset 1 61 | MASTER1 62 | REPLICA1 63 | end 64 | end 65 | end 66 | ``` 67 | 68 | ## Как начать использовать? 69 | 70 | ### Предварительные условия 71 | 72 | - **[Go](https://go.dev/)**: любая из **двух последних мажорных версий** [releases](https://go.dev/doc/devel/release). 73 | 74 | ### Установка Go-Vshard-Router 75 | 76 | С помощью [Go module](https://github.com/golang/go/wiki/Modules) можно добавить следующий импорт 77 | 78 | ``` 79 | import "github.com/tarantool/go-vshard-router/v2" 80 | ``` 81 | 82 | в ваш код, а затем `go [build|run|test]` автоматически получит необходимые зависимости. 83 | 84 | В противном случае выполните следующую команду Go, чтобы установить пакет go-vshard-router: 85 | 86 | ```sh 87 | $ go get -u github.com/tarantool/go-vshard-router/v2 88 | ``` 89 | 90 | ### Использование Go-Vshard-Router 91 | 92 | Сначала вам необходимо импортировать пакет go-vshard-router для его использования. 93 | 94 | ```go 95 | package main 96 | 97 | import ( 98 | "context" 99 | "fmt" 100 | "strconv" 101 | "time" 102 | 103 | vshardrouter "github.com/tarantool/go-vshard-router/v2" 104 | "github.com/tarantool/go-vshard-router/v2/providers/static" 105 | 106 | "github.com/google/uuid" 107 | "github.com/tarantool/go-tarantool/v2" 108 | ) 109 | 110 | func main() { 111 | ctx := context.Background() 112 | 113 | directRouter, err := vshardrouter.NewRouter(ctx, vshardrouter.Config{ 114 | DiscoveryTimeout: time.Minute, 115 | DiscoveryMode: vshardrouter.DiscoveryModeOn, 116 | TopologyProvider: static.NewProvider(map[vshardrouter.ReplicasetInfo][]vshardrouter.InstanceInfo{ 117 | {Name: "replcaset_1", UUID: uuid.New()}: { 118 | {Addr: "127.0.0.1:1001", Name: "1_1"}, 119 | {Addr: "127.0.0.1:1002", Name: "1_2"}, 120 | }, 121 | {Name: "replcaset_2", UUID: uuid.New()}: { 122 | {Addr: "127.0.0.1:2001", Name: "2_1"}, 123 | {Addr: "127.0.0.1:2002", Name: "2_2"}, 124 | }, 125 | }), 126 | TotalBucketCount: 128000, 127 | PoolOpts: tarantool.Opts{ 128 | Timeout: time.Second, 129 | }, 130 | }) 131 | if err != nil { 132 | panic(err) 133 | } 134 | 135 | user := struct { 136 | ID uint64 137 | }{ 138 | ID: 123, 139 | } 140 | 141 | bucketID := directRouter.BucketIDStrCRC32(strconv.FormatUint(user.ID, 10)) 142 | 143 | resp, err := directRouter.Call( 144 | ctx, 145 | bucketID, 146 | vshardrouter.CallModeBRO, 147 | "storage.api.get_user_info", 148 | []interface{}{&struct { 149 | BucketID uint64 `msgpack:"bucket_id" json:"bucket_id,omitempty"` 150 | Body map[string]interface{} `msgpack:"body"` 151 | }{ 152 | BucketID: bucketID, 153 | Body: map[string]interface{}{ 154 | "user_id": "123456", 155 | }, 156 | }}, vshardrouter.CallOpts{Timeout: time.Second * 2}, 157 | ) 158 | if err != nil { 159 | panic(err) 160 | } 161 | 162 | info := &struct { 163 | BirthDay int 164 | }{} 165 | 166 | err = resp.GetTyped(&[]interface{}{info}) 167 | if err != nil { 168 | panic(err) 169 | } 170 | 171 | interfaceResult, err := resp.Get() 172 | if err != nil { 173 | panic(err) 174 | } 175 | 176 | fmt.Printf("interface result: %v", interfaceResult) 177 | fmt.Printf("get typed result: %v", info) 178 | } 179 | ``` 180 | 181 | ### Провайдеры 182 | 183 | topology 184 | 185 | Мы понимаем, что реализации логгеров, метрик, 186 | а так же источников топологии могут сильно отличаться в реализации и использовании. 187 | Поэтому go-vshard-router дает возможность вам самим выбирать инструменты, 188 | которые вы считаете правильным использовать или реализовать их самим за счет использования интерфейсов. 189 | 190 | #### Топология 191 | Как источник конфигурации вы можете использовать провайдеры топологии(конфигурации). 192 | На данный момент есть поддержка следующих провайдеров: 193 | 194 | - **etcd** (для конфигурации 195 | аналогичной [moonlibs/config](https://github.com/moonlibs/config?tab=readme-ov-file#example-of-etcd-configuration-for-vshard-based-applications-etcdclustervshard) 196 | в etcd v2 для tarantool версии ниже 3) 197 | - **static** (для указания конфигурации прямо из кода и простоты тестирования) 198 | - **[viper](providers/viper/README.md)** 199 | - etcd v3 200 | - consul 201 | - files 202 | 203 | #### Метрики 204 | 205 | Также доступны провайдеры метрик, вы можете использовать готовые провайдеры метрик, 206 | которые можно зарегестрировать в prometheus и передать go-vshard-router. 207 | Что позволит вам не задумываться над опциями и настройкой метрик. 208 | На данный момент доступны следующие провайдеры: 209 | - **[prometheus](providers/prometheus)** 210 | 211 | #### Логирование 212 | 213 | - stdout (builtin) 214 | - **[slog](providers/slog)** 215 | 216 | 217 | ### Ознакомьтесь с другими примерами 218 | 219 | #### Быстрое начало 220 | 221 | Познакомьтесь с [Полной документацией](docs/doc_ru.md), которая включает в себя примеры и теорию. 222 | 223 | #### [Customer service](examples/customer/README.ru.md) 224 | 225 | Сервис с go-vshard-router поверх примера тарантула из оригинальной библиотеки vshard с использованием raft. 226 | 227 | ## Бенчмарки 228 | 229 | ### Go Bench 230 | 231 | | Бенчмарк | Число запусков | Время (ns/op) | Память (B/op) | Аллокации (allocs/op) | 232 | |----------------------------------|----------------|---------------|---------------|-----------------------| 233 | | BenchmarkCallSimpleInsert_GO-12 | 14929 | 77443 | 2308 | 34 | 234 | | BenchmarkCallSimpleInsert_Lua-12 | 10196 | 116101 | 1098 | 19 | 235 | | BenchmarkCallSimpleSelect_GO-12 | 20065 | 60521 | 2534 | 40 | 236 | | BenchmarkCallSimpleSelect_Lua-12 | 12064 | 99874 | 1153 | 20 | 237 | 238 | ### [K6](https://github.com/grafana/k6) 239 | 240 | Топология: 241 | 242 | - 4 репликасета (x2 инстанса на репликасет) 243 | - 4 тарантул прокси 244 | - 1 инстанс гошного сервиса 245 | 246 | сценарий constant VUes: 247 | в нагрузке близкой к продовой 248 | 249 | ```select``` 250 | 251 | - go-vshard-router: uncritically worse latency, but 3 times more rps 252 | ![Image alt](docs/static/direct.png) 253 | - tarantool-router: (80% cpu, heavy rps kills proxy at 100% cpu) 254 | ![Image alt](docs/static/not-direct.png) 255 | 256 | [actions-badge]: https://github.com/tarantool/go-vshard-router/actions/workflows/main.yml/badge.svg 257 | 258 | [actions-url]: https://github.com/tarantool/go-vshard-router/actions/workflows/main.yml 259 | 260 | [coverage-badge]: https://coveralls.io/repos/github/tarantool/go-vshard-router/badge.svg?branch=master 261 | 262 | [coverage-url]: https://coveralls.io/github/tarantool/go-vshard-router?branch=master -------------------------------------------------------------------------------- /api_test.go: -------------------------------------------------------------------------------- 1 | package vshard_router // nolint: revive 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | "github.com/vmihailenco/msgpack/v5" 9 | "github.com/vmihailenco/msgpack/v5/msgpcode" 10 | ) 11 | 12 | func TestVshardMode_String_NotEmpty(t *testing.T) { 13 | t.Parallel() 14 | require.NotEmpty(t, ReadMode.String()) 15 | require.NotEmpty(t, WriteMode.String()) 16 | } 17 | 18 | func TestRouter_RouterRouteAll(t *testing.T) { 19 | t.Parallel() 20 | var emptyRouter = &Router{ 21 | cfg: Config{ 22 | TotalBucketCount: uint64(10), 23 | Loggerf: emptyLogfProvider, 24 | Metrics: emptyMetricsProvider, 25 | }, 26 | } 27 | emptyRouter.setEmptyNameToReplicaset() 28 | m := emptyRouter.RouteAll() 29 | require.Empty(t, m) 30 | } 31 | 32 | func TestVshardStorageCallResponseProto_DecodeMsgpack_ProtocolViolation(t *testing.T) { 33 | t.Parallel() 34 | 35 | tCases := map[string]func() *bytes.Buffer{ 36 | "0 arr len": func() *bytes.Buffer { 37 | buf := bytes.NewBuffer(nil) 38 | 39 | return buf 40 | }, 41 | "one bool false": func() *bytes.Buffer { 42 | buf := bytes.NewBuffer(nil) 43 | buf.WriteByte(msgpcode.FixedArrayLow | byte(1)) 44 | buf.WriteByte(msgpcode.False) 45 | 46 | return buf 47 | }, 48 | "double": func() *bytes.Buffer { 49 | buf := bytes.NewBuffer(nil) 50 | buf.WriteByte(msgpcode.FixedArrayLow | byte(1)) 51 | buf.WriteByte(msgpcode.Double) 52 | 53 | return buf 54 | }, 55 | } 56 | 57 | for tCaseName, bufGenerator := range tCases { 58 | protoResp := vshardStorageCallResponseProto{} 59 | 60 | t.Run(tCaseName, func(t *testing.T) { 61 | buf := bufGenerator() 62 | err := protoResp.DecodeMsgpack(msgpack.NewDecoder(buf)) 63 | require.Error(t, err) 64 | }) 65 | } 66 | } 67 | 68 | func TestVshardStorageCallResponseProto_DecodeMsgpack_StorageCallError(t *testing.T) { 69 | t.Parallel() 70 | prepareBuf := func() *bytes.Buffer { 71 | buf := bytes.NewBuffer(nil) 72 | buf.WriteByte(msgpcode.FixedArrayLow | byte(2)) 73 | buf.WriteByte(msgpcode.Nil) 74 | 75 | return buf 76 | } 77 | 78 | tCases := map[string]func() (*bytes.Buffer, StorageCallVShardError){ 79 | "empty storage call error": func() (*bytes.Buffer, StorageCallVShardError) { 80 | buf := prepareBuf() 81 | e := StorageCallVShardError{} 82 | 83 | err := msgpack.NewEncoder(buf).Encode(e) 84 | require.NoError(t, err) 85 | 86 | return buf, e 87 | }, 88 | 89 | "name": func() (*bytes.Buffer, StorageCallVShardError) { 90 | buf := prepareBuf() 91 | e := StorageCallVShardError{ 92 | Name: "test", 93 | } 94 | 95 | err := msgpack.NewEncoder(buf).Encode(e) 96 | require.NoError(t, err) 97 | 98 | return buf, e 99 | }, 100 | } 101 | 102 | for tCaseName, bufGenerator := range tCases { 103 | t.Run(tCaseName, func(t *testing.T) { 104 | t.Parallel() 105 | 106 | protoResp := vshardStorageCallResponseProto{} 107 | 108 | buf, storageErr := bufGenerator() 109 | 110 | err := protoResp.DecodeMsgpack(msgpack.NewDecoder(buf)) 111 | require.NoError(t, err) 112 | 113 | require.NotNil(t, protoResp.VshardError) 114 | require.Nil(t, protoResp.CallResp.buf) 115 | require.Nil(t, protoResp.AssertError) 116 | 117 | require.EqualValues(t, storageErr, *protoResp.VshardError) 118 | }) 119 | 120 | } 121 | } 122 | 123 | func TestVshardStorageCallResponseProto_DecodeMsgpack_AssertError(t *testing.T) { 124 | t.Parallel() 125 | prepareBuf := func() *bytes.Buffer { 126 | buf := bytes.NewBuffer(nil) 127 | buf.WriteByte(msgpcode.FixedArrayLow | byte(2)) 128 | buf.WriteByte(msgpcode.False) 129 | 130 | return buf 131 | } 132 | 133 | tCases := map[string]func() (*bytes.Buffer, assertError){ 134 | "empty assert call error": func() (*bytes.Buffer, assertError) { 135 | buf := prepareBuf() 136 | e := assertError{} 137 | 138 | err := msgpack.NewEncoder(buf).Encode(e) 139 | require.NoError(t, err) 140 | 141 | return buf, e 142 | }, 143 | } 144 | 145 | for tCaseName, bufGenerator := range tCases { 146 | t.Run(tCaseName, func(t *testing.T) { 147 | t.Parallel() 148 | 149 | protoResp := vshardStorageCallResponseProto{} 150 | 151 | buf, storageErr := bufGenerator() 152 | 153 | err := protoResp.DecodeMsgpack(msgpack.NewDecoder(buf)) 154 | require.NoError(t, err) 155 | 156 | require.NotNil(t, protoResp.AssertError) 157 | require.Nil(t, protoResp.VshardError) 158 | require.Nil(t, protoResp.CallResp.buf) 159 | 160 | require.EqualValues(t, storageErr, *protoResp.AssertError) 161 | }) 162 | 163 | } 164 | } 165 | 166 | func TestVshardStorageCallResponseProto_DecodeMsgpack_GetNonTyped(t *testing.T) { 167 | t.Parallel() 168 | prepareBuf := func() *bytes.Buffer { 169 | buf := bytes.NewBuffer(nil) 170 | buf.WriteByte(msgpcode.FixedArrayLow | byte(2)) 171 | buf.WriteByte(msgpcode.True) 172 | 173 | return buf 174 | } 175 | 176 | tCases := map[string]func() (*bytes.Buffer, []interface{}){ 177 | "one string": func() (*bytes.Buffer, []interface{}) { 178 | buf := prepareBuf() 179 | val := []interface{}{"test", "test"} 180 | 181 | err := msgpack.NewEncoder(buf).Encode(val) 182 | require.NoError(t, err) 183 | 184 | return buf, []interface{}{val} 185 | }, 186 | } 187 | 188 | for tCaseName, bufGenerator := range tCases { 189 | t.Run(tCaseName, func(t *testing.T) { 190 | t.Parallel() 191 | 192 | protoResp := vshardStorageCallResponseProto{} 193 | 194 | buf, respExpect := bufGenerator() 195 | 196 | err := protoResp.DecodeMsgpack(msgpack.NewDecoder(buf)) 197 | require.NoError(t, err) 198 | 199 | require.Nil(t, protoResp.AssertError) 200 | require.Nil(t, protoResp.VshardError) 201 | require.NotNil(t, protoResp.CallResp.buf) 202 | 203 | resp, err := protoResp.CallResp.Get() 204 | require.NoError(t, err) 205 | 206 | require.Equal(t, respExpect, resp) 207 | }) 208 | 209 | } 210 | } 211 | 212 | func BenchmarkVshardStorageCallResponseProto_DecodeMsgpack_Ok(b *testing.B) { 213 | // Skip in timer buffer creation information 214 | b.StopTimer() 215 | 216 | // We need a lot of different examples to avoid caching the data in CPU caches. 217 | const examplesCount = 100_000 218 | var bufBytesArr [][]byte 219 | 220 | for i := 0; i < examplesCount; i++ { 221 | buf := bytes.NewBuffer(nil) 222 | 223 | err := msgpack.NewEncoder(buf).Encode([]interface{}{true, i}) 224 | require.NoError(b, err) 225 | 226 | bufBytesArr = append(bufBytesArr, buf.Bytes()) 227 | } 228 | 229 | // allocate a bytesReader and msgpackDecoder only once to eliminate memory allocation intervention to benchmark 230 | bytesReader := bytes.NewReader(nil) 231 | msgpackDecoder := msgpack.NewDecoder(nil) 232 | 233 | // detects errors as count to minimize `require` module intervention to benchmark 234 | var errCount uint64 235 | var err error 236 | 237 | b.StartTimer() 238 | 239 | for i := 0; i < b.N; i++ { 240 | // benchmark will include some redundant things such Reset call below, it is ok for us now. 241 | // NOTE: get a new bufBytes each time to flush out CPU caches 242 | bytesReader.Reset(bufBytesArr[i%examplesCount]) 243 | msgpackDecoder.Reset(bytesReader) 244 | 245 | // benchmark core parts are below 246 | // - protoResp.DecodeMsgpack 247 | // - protoResp.CallResp.Get() 248 | protoResp := vshardStorageCallResponseProto{} 249 | err = protoResp.DecodeMsgpack(msgpackDecoder) 250 | if err != nil { 251 | errCount++ 252 | } 253 | 254 | _, err = protoResp.CallResp.Get() 255 | if err != nil { 256 | errCount++ 257 | } 258 | } 259 | b.StopTimer() 260 | 261 | require.True(b, errCount == 0) 262 | b.ReportAllocs() 263 | } 264 | -------------------------------------------------------------------------------- /concurrent_test.go: -------------------------------------------------------------------------------- 1 | package vshard_router_test 2 | 3 | import ( 4 | "context" 5 | "math/rand" 6 | "sync" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/require" 11 | vshardrouter "github.com/tarantool/go-vshard-router/v2" 12 | ) 13 | 14 | type concurrentTopologyProvider struct { 15 | done chan struct{} 16 | closed chan struct{} 17 | t *testing.T 18 | } 19 | 20 | func (c *concurrentTopologyProvider) Init(tc vshardrouter.TopologyController) error { 21 | ctx := context.Background() 22 | 23 | var cfg = make(map[vshardrouter.ReplicasetInfo][]vshardrouter.InstanceInfo) 24 | for k, v := range topology { 25 | cfg[k] = v 26 | } 27 | 28 | err := tc.AddReplicasets(ctx, cfg) 29 | require.NoError(c.t, err) 30 | 31 | c.done = make(chan struct{}) 32 | c.closed = make(chan struct{}) 33 | 34 | added := cfg 35 | removed := make(map[vshardrouter.ReplicasetInfo][]vshardrouter.InstanceInfo) 36 | 37 | go func() { 38 | defer close(c.closed) 39 | //nolint:errcheck 40 | defer tc.AddReplicasets(ctx, removed) 41 | 42 | type actiont int 43 | 44 | const add actiont = 0 45 | const remove actiont = 1 46 | 47 | for { 48 | select { 49 | case <-c.done: 50 | return 51 | default: 52 | } 53 | 54 | canAdd := len(removed) > 0 55 | canRemove := len(added) > 0 56 | 57 | var action actiont 58 | 59 | switch { 60 | case canAdd && canRemove: 61 | //nolint:gosec 62 | action = actiont(rand.Int() % 2) 63 | case canAdd: 64 | action = add 65 | case canRemove: 66 | action = remove 67 | default: 68 | require.Failf(c.t, "unreachable case", "%v, %v", added, removed) 69 | } 70 | 71 | switch action { 72 | case add: 73 | var keys []vshardrouter.ReplicasetInfo 74 | for k := range removed { 75 | keys = append(keys, k) 76 | } 77 | //nolint:gosec 78 | key := keys[rand.Int()%len(keys)] 79 | 80 | added[key] = removed[key] 81 | delete(removed, key) 82 | 83 | _ = tc.AddReplicaset(ctx, key, added[key]) 84 | case remove: 85 | var keys []vshardrouter.ReplicasetInfo 86 | for k := range added { 87 | keys = append(keys, k) 88 | } 89 | //nolint:gosec 90 | key := keys[rand.Int()%len(keys)] 91 | 92 | removed[key] = added[key] 93 | delete(added, key) 94 | 95 | _ = tc.RemoveReplicaset(ctx, key.UUID.String()) 96 | default: 97 | require.Fail(c.t, "unreachable case") 98 | } 99 | } 100 | }() 101 | 102 | return nil 103 | } 104 | 105 | func (c *concurrentTopologyProvider) Close() { 106 | close(c.done) 107 | <-c.closed 108 | } 109 | 110 | func TestConncurrentTopologyChange(t *testing.T) { 111 | /* What we do: 112 | 1) Addreplicaset + Removereplicaset by random in one goroutine 113 | 2) Call ReplicaCall, MapRw and etc. in another goroutines 114 | */ 115 | 116 | // Don't run this parallel with other tests, because this test is heavy and used to detect data races. 117 | // Therefore this test may impact other ones. 118 | // t.Parallel() 119 | 120 | tc := &concurrentTopologyProvider{t: t} 121 | 122 | router, err := vshardrouter.NewRouter(context.Background(), vshardrouter.Config{ 123 | TopologyProvider: tc, 124 | DiscoveryTimeout: 5 * time.Second, 125 | DiscoveryMode: vshardrouter.DiscoveryModeOn, 126 | TotalBucketCount: totalBucketCount, 127 | User: username, 128 | }) 129 | 130 | require.Nil(t, err, "NewRouter finished successfully") 131 | 132 | wg := sync.WaitGroup{} 133 | 134 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 135 | defer cancel() 136 | 137 | const concurrentCalls = 100 138 | callCntArr := make([]int, concurrentCalls) 139 | for i := 0; i < 100; i++ { 140 | i := i 141 | wg.Add(1) 142 | go func() { 143 | defer wg.Done() 144 | 145 | for { 146 | select { 147 | case <-ctx.Done(): 148 | return 149 | default: 150 | } 151 | 152 | bucketID := randBucketID(totalBucketCount) 153 | args := []interface{}{"arg1"} 154 | 155 | callOpts := vshardrouter.CallOpts{} 156 | 157 | _, _ = router.Call(ctx, bucketID, vshardrouter.CallModeBRO, "echo", args, callOpts) 158 | callCntArr[i]++ 159 | } 160 | }() 161 | } 162 | 163 | var mapCnt int 164 | wg.Add(1) 165 | go func() { 166 | defer wg.Done() 167 | 168 | for { 169 | select { 170 | case <-ctx.Done(): 171 | return 172 | default: 173 | } 174 | 175 | args := []interface{}{"arg1"} 176 | _, _ = vshardrouter.RouterMapCallRW[interface{}](router, ctx, "echo", args, vshardrouter.RouterMapCallRWOptions{}) 177 | mapCnt++ 178 | } 179 | }() 180 | 181 | wg.Wait() 182 | 183 | var callCnt int 184 | for _, v := range callCntArr { 185 | callCnt += v 186 | } 187 | 188 | t.Logf("Call cnt=%d, map cnt=%d", callCnt, mapCnt) 189 | 190 | tc.Close() 191 | } 192 | -------------------------------------------------------------------------------- /config.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | require('strict').on() 4 | 5 | local uuid = require('uuid') 6 | local vshard = require 'vshard' 7 | 8 | -- Get instance name 9 | local NAME = os.getenv("TEST_TNT_WORK_DIR") 10 | local fiber = require('fiber') 11 | 12 | -- Check if we are running under test-run 13 | if os.getenv('ADMIN') then 14 | test_run = require('test_run').new() 15 | require('console').listen(os.getenv('ADMIN')) 16 | end 17 | 18 | -- Call a configuration provider 19 | local cfg = { 20 | sharding = { 21 | ['cbf06940-0790-498b-948d-042b62cf3d29'] = { -- replicaset #1 22 | replicas = { 23 | ['8a274925-a26d-47fc-9e1b-af88ce939412'] = { 24 | uri = 'storage:storage@127.0.0.1:3301', 25 | name = 'storage_1_a', 26 | master = true, 27 | }, 28 | ['3de2e3e1-9ebe-4d0d-abb1-26d301b84633'] = { 29 | uri = 'storage:storage@127.0.0.1:3302', 30 | name = 'storage_1_b' 31 | }, 32 | }, 33 | }, -- replicaset #1 34 | ['ac522f65-aa94-4134-9f64-51ee384f1a54'] = { -- replicaset #2 35 | replicas = { 36 | ['1e02ae8a-afc0-4e91-ba34-843a356b8ed7'] = { 37 | uri = 'storage:storage@127.0.0.1:3303', 38 | name = 'storage_2_a', 39 | master = true, 40 | }, 41 | ['001688c3-66f8-4a31-8e19-036c17d489c2'] = { 42 | uri = 'storage:storage@127.0.0.1:3304', 43 | name = 'storage_2_b' 44 | } 45 | }, 46 | }, -- replicaset #2 47 | }, -- sharding 48 | replication_connect_quorum = 0, -- its oke if we havent some replicas 49 | } 50 | 51 | 52 | -- Name to uuid map 53 | local names = { 54 | ['storage_1_a'] = '8a274925-a26d-47fc-9e1b-af88ce939412', 55 | ['storage_1_b'] = '3de2e3e1-9ebe-4d0d-abb1-26d301b84633', 56 | ['storage_1_c'] = '3de2e3e1-9ebe-4d0d-abb1-26d301b84635', 57 | ['storage_2_a'] = '1e02ae8a-afc0-4e91-ba34-843a356b8ed7', 58 | ['storage_2_b'] = '001688c3-66f8-4a31-8e19-036c17d489c2', 59 | } 60 | 61 | replicasets = {'cbf06940-0790-498b-948d-042b62cf3d29', 62 | 'ac522f65-aa94-4134-9f64-51ee384f1a54'} 63 | 64 | 65 | 66 | rawset(_G, 'vshard', vshard) -- set as global variable 67 | 68 | -- Если мы являемся роутером, то применяем другой конфиг 69 | if NAME == "router" then 70 | cfg.listen = "0.0.0.0:12000" 71 | cfg['bucket_count'] = 100 72 | local router = vshard.router.new('router', cfg) 73 | rawset(_G, 'router', router) -- set as global variable 74 | 75 | local api = {} 76 | rawset(_G, 'api', api) 77 | 78 | router:bootstrap({timeout = 4, if_not_bootstrapped = true}) 79 | 80 | function api.add_product(product) 81 | local bucket_id = router:bucket_id_strcrc32(product.id) 82 | product.bucket_id = bucket_id 83 | 84 | return router:call(bucket_id, 'write', 'product_add', {product}) 85 | end 86 | 87 | function api.get_product(req) 88 | local bucket_id = router:bucket_id_strcrc32(req.id) 89 | 90 | return router:call(bucket_id, 'read', 'product_get', {req}) 91 | end 92 | else 93 | vshard.storage.cfg(cfg, names[NAME]) 94 | 95 | -- everything below is copypasted from storage.lua in vshard example: 96 | -- https://github.com/tarantool/vshard/blob/dfa2cc8a2aff221d5f421298851a9a229b2e0434/example/storage.lua 97 | box.once("testapp:schema:1", function() 98 | local customer = box.schema.space.create('customer') 99 | customer:format({ 100 | {'customer_id', 'unsigned'}, 101 | {'bucket_id', 'unsigned'}, 102 | {'name', 'string'}, 103 | }) 104 | customer:create_index('customer_id', {parts = {'customer_id'}}) 105 | customer:create_index('bucket_id', {parts = {'bucket_id'}, unique = false}) 106 | 107 | -- create products for easy bench 108 | local products = box.schema.space.create('products') 109 | products:format({ 110 | {'id', 'uuid'}, 111 | {'bucket_id', 'unsigned'}, 112 | {'name', 'string'}, 113 | {'count', 'unsigned'}, 114 | }) 115 | products:create_index('id', {parts = {'id'}}) 116 | products:create_index('bucket_id', {parts = {'bucket_id'}, unique = false}) 117 | 118 | 119 | local account = box.schema.space.create('account') 120 | account:format({ 121 | {'account_id', 'unsigned'}, 122 | {'customer_id', 'unsigned'}, 123 | {'bucket_id', 'unsigned'}, 124 | {'balance', 'unsigned'}, 125 | {'name', 'string'}, 126 | }) 127 | account:create_index('account_id', {parts = {'account_id'}}) 128 | account:create_index('customer_id', {parts = {'customer_id'}, unique = false}) 129 | account:create_index('bucket_id', {parts = {'bucket_id'}, unique = false}) 130 | box.snapshot() 131 | 132 | box.schema.func.create('customer_lookup') 133 | box.schema.role.grant('public', 'execute', 'function', 'customer_lookup') 134 | box.schema.func.create('customer_add') 135 | box.schema.role.grant('public', 'execute', 'function', 'customer_add') 136 | box.schema.func.create('echo') 137 | box.schema.role.grant('public', 'execute', 'function', 'echo') 138 | box.schema.func.create('sleep') 139 | box.schema.role.grant('public', 'execute', 'function', 'sleep') 140 | box.schema.func.create('raise_luajit_error') 141 | box.schema.role.grant('public', 'execute', 'function', 'raise_luajit_error') 142 | box.schema.func.create('raise_client_error') 143 | box.schema.role.grant('public', 'execute', 'function', 'raise_client_error') 144 | 145 | box.schema.user.grant('storage', 'super') 146 | box.schema.user.create('tarantool') 147 | box.schema.user.grant('tarantool', 'super') 148 | end) 149 | 150 | 151 | local function insert_customer(customer) 152 | box.space.customer:insert({customer.customer_id, customer.bucket_id, customer.name}) 153 | for _, account in ipairs(customer.accounts) do 154 | box.space.account:insert({ 155 | account.account_id, 156 | customer.customer_id, 157 | customer.bucket_id, 158 | 0, 159 | account.name 160 | }) 161 | end 162 | end 163 | 164 | function customer_add(customer) 165 | local ares = box.atomic(insert_customer, customer) 166 | 167 | return {res = ares } 168 | end 169 | 170 | function customer_lookup(customer_id) 171 | if type(customer_id) ~= 'number' then 172 | error('Usage: customer_lookup(customer_id)') 173 | end 174 | 175 | local customer = box.space.customer:get(customer_id) 176 | if customer == nil then 177 | return nil 178 | end 179 | customer = { 180 | customer_id = customer.customer_id; 181 | name = customer.name; 182 | } 183 | local accounts = {} 184 | for _, account in box.space.account.index.customer_id:pairs(customer_id) do 185 | table.insert(accounts, { 186 | account_id = account.account_id; 187 | name = account.name; 188 | balance = account.balance; 189 | }) 190 | end 191 | customer.accounts = accounts; 192 | return customer, {test=123} 193 | end 194 | 195 | function echo(...) 196 | return ... 197 | end 198 | 199 | function sleep(time) 200 | fiber.sleep(time) 201 | return true 202 | end 203 | 204 | function raise_luajit_error() 205 | assert(1 == 2) 206 | end 207 | 208 | function raise_client_error() 209 | box.error(box.error.UNKNOWN) 210 | end 211 | 212 | -- product_add - simple add some product to storage 213 | function product_add(product) 214 | local id = uuid.fromstr(product.id) 215 | 216 | box.space.products:insert({ id, product.bucket_id, product.name, product.count}) 217 | 218 | return true 219 | end 220 | 221 | -- product_get - simple select for benches 222 | function product_get(req) 223 | local product = box.space.products:get(uuid.fromstr(req.id)) 224 | 225 | return { 226 | name = product.name, 227 | id = product.id:str() 228 | } 229 | end 230 | end 231 | 232 | box.once('access:v1', function() 233 | box.schema.user.grant('guest', 'read,write,execute', 'universe') 234 | end) 235 | -------------------------------------------------------------------------------- /docs/doc.md: -------------------------------------------------------------------------------- 1 | # Go-Vshsard Quick Start 2 | -------------------------------------------------------------------------------- /docs/static/direct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarantool/go-vshard-router/94e50dbb65f7edcf2ebbcea87768349fe0e055d7/docs/static/direct.png -------------------------------------------------------------------------------- /docs/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarantool/go-vshard-router/94e50dbb65f7edcf2ebbcea87768349fe0e055d7/docs/static/logo.png -------------------------------------------------------------------------------- /docs/static/not-direct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarantool/go-vshard-router/94e50dbb65f7edcf2ebbcea87768349fe0e055d7/docs/static/not-direct.png -------------------------------------------------------------------------------- /docs/static/providers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarantool/go-vshard-router/94e50dbb65f7edcf2ebbcea87768349fe0e055d7/docs/static/providers.png -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package vshard_router //nolint:revive 2 | 3 | import "fmt" 4 | 5 | // VShard error codes 6 | const ( 7 | VShardErrCodeWrongBucket = 1 8 | VShardErrCodeNonMaster = 2 9 | VShardErrCodeBucketAlreadyExists = 3 10 | VShardErrCodeNoSuchReplicaset = 4 11 | VShardErrCodeMoveToSelf = 5 12 | VShardErrCodeMissingMaster = 6 13 | VShardErrCodeTransferIsInProgress = 7 14 | VShardErrCodeUnreachableReplicaset = 8 15 | VShardErrCodeNoRouteToBucket = 9 16 | VShardErrCodeNonEmpty = 10 17 | VShardErrCodeUnreachableMaster = 11 18 | VShardErrCodeOutOfSync = 12 19 | VShardErrCodeHighReplicationLag = 13 20 | VShardErrCodeUnreachableReplica = 14 21 | VShardErrCodeLowRedundancy = 15 22 | VShardErrCodeInvalidRebalancing = 16 23 | VShardErrCodeSuboptimalReplica = 17 24 | VShardErrCodeUnknownBuckets = 18 25 | VShardErrCodeReplicasetIsLocked = 19 26 | VShardErrCodeObjectIsOutdated = 20 27 | VShardErrCodeRouterAlreadyExists = 21 28 | VShardErrCodeBucketIsLocked = 22 29 | VShardErrCodeInvalidCfg = 23 30 | VShardErrCodeBucketIsPinned = 24 31 | VShardErrCodeTooManyReceiving = 25 32 | VShardErrCodeStorageIsReferenced = 26 33 | VShardErrCodeStorageRefAdd = 27 34 | VShardErrCodeStorageRefUse = 28 35 | VShardErrCodeStorageRefDel = 29 36 | VShardErrCodeBucketRecvDataError = 30 37 | VShardErrCodeMultipleMastersFound = 31 38 | VShardErrCodeReplicasetInBackoff = 32 39 | VShardErrCodeStorageIsDisabled = 33 40 | VShardErrCodeBucketIsCorrupted = 34 41 | VShardErrCodeRouterIsDisabled = 35 42 | VShardErrCodeBucketGCError = 36 43 | VShardErrCodeStorageCfgIsInProgress = 37 44 | VShardErrCodeRouterCfgIsInProgress = 38 45 | VShardErrCodeBucketInvalidUpdate = 39 46 | VShardErrCodeVhandshakeNotComplete = 40 47 | VShardErrCodeInstanceNameMismatch = 41 48 | ) 49 | 50 | // VShard error names 51 | const ( 52 | VShardErrNameWrongBucket = "WRONG_BUCKET" 53 | VShardErrNameNonMaster = "NON_MASTER" 54 | VShardErrNameBucketAlreadyExists = "BUCKET_ALREADY_EXISTS" 55 | VShardErrNameNoSuchReplicaset = "NO_SUCH_REPLICASET" 56 | VShardErrNameMoveToSelf = "MOVE_TO_SELF" 57 | VShardErrNameMissingMaster = "MISSING_MASTER" 58 | VShardErrNameTransferIsInProgress = "TRANSFER_IS_IN_PROGRESS" 59 | VShardErrNameUnreachableReplicaset = "UNREACHABLE_REPLICASET" 60 | VShardErrNameNoRouteToBucket = "NO_ROUTE_TO_BUCKET" 61 | VShardErrNameNonEmpty = "NON_EMPTY" 62 | VShardErrNameUnreachableMaster = "UNREACHABLE_MASTER" 63 | VShardErrNameOutOfSync = "OUT_OF_SYNC" 64 | VShardErrNameHighReplicationLag = "HIGH_REPLICATION_LAG" 65 | VShardErrNameUnreachableReplica = "UNREACHABLE_REPLICA" 66 | VShardErrNameLowRedundancy = "LOW_REDUNDANCY" 67 | VShardErrNameInvalidRebalancing = "INVALID_REBALANCING" 68 | VShardErrNameSuboptimalReplica = "SUBOPTIMAL_REPLICA" 69 | VShardErrNameUnknownBuckets = "UNKNOWN_BUCKETS" 70 | VShardErrNameReplicasetIsLocked = "REPLICASET_IS_LOCKED" 71 | VShardErrNameObjectIsOutdated = "OBJECT_IS_OUTDATED" 72 | VShardErrNameRouterAlreadyExists = "ROUTER_ALREADY_EXISTS" 73 | VShardErrNameBucketIsLocked = "BUCKET_IS_LOCKED" 74 | VShardErrNameInvalidCfg = "INVALID_CFG" 75 | VShardErrNameBucketIsPinned = "BUCKET_IS_PINNED" 76 | VShardErrNameTooManyReceiving = "TOO_MANY_RECEIVING" 77 | VShardErrNameStorageIsReferenced = "STORAGE_IS_REFERENCED" 78 | VShardErrNameStorageRefAdd = "STORAGE_REF_ADD" 79 | VShardErrNameStorageRefUse = "STORAGE_REF_USE" 80 | VShardErrNameStorageRefDel = "STORAGE_REF_DEL" 81 | VShardErrNameBucketRecvDataError = "BUCKET_RECV_DATA_ERROR" 82 | VShardErrNameMultipleMastersFound = "MULTIPLE_MASTERS_FOUND" 83 | VShardErrNameReplicasetInBackoff = "REPLICASET_IN_BACKOFF" 84 | VShardErrNameStorageIsDisabled = "STORAGE_IS_DISABLED" 85 | VShardErrNameBucketIsCorrupted = "BUCKET_IS_CORRUPTED" 86 | VShardErrNameRouterIsDisabled = "ROUTER_IS_DISABLED" 87 | VShardErrNameBucketGCError = "BUCKET_GC_ERROR" 88 | VShardErrNameStorageCfgIsInProgress = "STORAGE_CFG_IS_IN_PROGRESS" 89 | VShardErrNameRouterCfgIsInProgress = "ROUTER_CFG_IS_IN_PROGRESS" 90 | VShardErrNameBucketInvalidUpdate = "BUCKET_INVALID_UPDATE" 91 | VShardErrNameVhandshakeNotComplete = "VHANDSHAKE_NOT_COMPLETE" 92 | VShardErrNameInstanceNameMismatch = "INSTANCE_NAME_MISMATCH" 93 | ) 94 | 95 | func newVShardErrorNoRouteToBucket(bucketID uint64) error { 96 | return &StorageCallVShardError{ 97 | Name: VShardErrNameNoRouteToBucket, 98 | Code: VShardErrCodeNoRouteToBucket, 99 | Type: "ShardingError", 100 | BucketID: bucketID, 101 | Message: fmt.Sprintf("Bucket %d cannot be found. Is rebalancing in progress?", bucketID), 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /examples/customer/README.md: -------------------------------------------------------------------------------- 1 | # Customer service 2 | ## What it is? 3 | 4 | This example is taken directly from the vshard repository example. A go service was written on top. 5 | Includes only 2 endpoints (see [swagger](go-service/docs/swagger.yaml)): count, record sales information. 6 | Only a few changes have been made: 7 | 8 | - slightly modified Makefile 9 | - by default, the example was created for fault tolerance according to Raft (only for replicaset 1, since there are 3 instances) 10 | - the number of buckets is set to 10k 11 | - minor fixes to critical errors (for example, the “transaction” attempt was replaced by box.atomic) 12 | - added 1 more instance to replicaset 1 so that raft could select a new master 13 | ## How to start? 14 | 15 | 1. Start the cluster 16 | 17 | ```sh 18 | $ cd tarantool 19 | $ make start 20 | ``` 21 | 22 | 2. Launch the service 23 | ```sh 24 | $ cd go-service # from the customer directory 25 | $ make start 26 | ``` -------------------------------------------------------------------------------- /examples/customer/README.ru.md: -------------------------------------------------------------------------------- 1 | # Customer service 2 | ## Что это такое? 3 | 4 | Этот пример взят напрямую из примера репозитория vshard. Поверх был написан сервис на go. 5 | Включает лишь 2 эндпоинта (см. [swagger](go-service/docs/swagger.yaml)): считать, записать информацию о продаже. 6 | Допущены лишь несколько изменений: 7 | 8 | - несколько модифицирован Makefile 9 | - по умолчанию пример создан для отказоустойчивости по Raft(только для репликасета 1, поскольку там 3 инстанса) 10 | - количество бакетов установлено в 10k 11 | - минорные исправления критических ошибок (например попытка "транзакции" заменена на box.atomic) 12 | - добавлен еще 1 инстанс в репликасет 1, чтобы raft мог выбрать нового мастера 13 | ## Как запустить? 14 | 15 | 1. Запускаем кластер 16 | 17 | ```sh 18 | $ cd tarantool 19 | $ make start 20 | ``` 21 | 22 | 2. Запускаем сервис 23 | ```sh 24 | $ cd go-service # из директории customer 25 | $ make start 26 | ``` 27 | -------------------------------------------------------------------------------- /examples/customer/go-service/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: install-swaggo 2 | install-swaggo: 3 | ifeq ($(wildcard $(SWAGGO_BIN)),) 4 | $(info #Downloading swaggo latest) 5 | go install github.com/swaggo/swag/cmd/swag@latest 6 | endif 7 | 8 | start: 9 | go run main.go config.yaml 10 | 11 | # генерирует свагер 12 | .PHONY: generate 13 | generate: install-swaggo 14 | swag init -g main.go -------------------------------------------------------------------------------- /examples/customer/go-service/config.yaml: -------------------------------------------------------------------------------- 1 | listener: 2 | address: :8096 3 | routers: 4 | addrs: 5 | - "127.0.0.1:12002" 6 | storage: 7 | direct: true 8 | total_bucket_count: 10000 9 | topology: 10 | clusters: 11 | storage_1: 12 | replicaset_uuid: cbf06940-0790-498b-948d-042b62cf3d29 13 | storage_2: 14 | replicaset_uuid: ac522f65-aa94-4134-9f64-51ee384f1a54 15 | instances: 16 | storage_1_a: 17 | cluster: storage_1 18 | box: 19 | listen: '127.0.0.1:3301' 20 | instance_uuid: '6E35AC64-1241-0001-0001-000000000000' 21 | storage_1_b: 22 | cluster: storage_1 23 | box: 24 | listen: '127.0.0.1:3302' 25 | instance_uuid: '6E35AC64-1241-0001-0002-000000000000' 26 | storage_1_c: 27 | cluster: storage_1 28 | box: 29 | listen: '127.0.0.1:3305' 30 | instance_uuid: '6E35AC64-1241-0001-0003-000000000000' 31 | storage_2_a: 32 | cluster: storage_2 33 | box: 34 | listen: '127.0.0.1:3303' 35 | instance_uuid: '6E35AC64-1241-0002-0001-000000000000' 36 | storage_2_b: 37 | cluster: storage_2 38 | box: 39 | listen: '127.0.0.1:3304' 40 | instance_uuid: '6E35AC64-1241-0002-0002-000000000000' -------------------------------------------------------------------------------- /examples/customer/go-service/docs/docs.go: -------------------------------------------------------------------------------- 1 | // Package docs Code generated by swaggo/swag. DO NOT EDIT 2 | package docs 3 | 4 | import "github.com/swaggo/swag" 5 | 6 | const docTemplate = `{ 7 | "schemes": {{ marshal .Schemes }}, 8 | "swagger": "2.0", 9 | "info": { 10 | "description": "{{escape .Description}}", 11 | "title": "{{.Title}}", 12 | "termsOfService": "http://swagger.io/terms/", 13 | "contact": { 14 | "name": "API Support", 15 | "url": "http://quedafoe.ru", 16 | "email": "djassange@ya.ru" 17 | }, 18 | "version": "{{.Version}}" 19 | }, 20 | "host": "{{.Host}}", 21 | "basePath": "{{.BasePath}}", 22 | "paths": { 23 | "/customer_add": { 24 | "post": { 25 | "description": "Todo", 26 | "produces": [ 27 | "application/json" 28 | ], 29 | "tags": [ 30 | "Set" 31 | ], 32 | "summary": "Todo", 33 | "parameters": [ 34 | { 35 | "description": "Обязательно не должно быть пустым", 36 | "name": "Request", 37 | "in": "body", 38 | "required": true, 39 | "schema": { 40 | "$ref": "#/definitions/main.CustomerAddRequest" 41 | } 42 | } 43 | ], 44 | "responses": { 45 | "200": { 46 | "description": "ok", 47 | "schema": { 48 | "type": "string" 49 | } 50 | } 51 | } 52 | } 53 | }, 54 | "/customer_lookup": { 55 | "get": { 56 | "description": "Todo", 57 | "produces": [ 58 | "application/json" 59 | ], 60 | "tags": [ 61 | "Get" 62 | ], 63 | "summary": "Todo", 64 | "parameters": [ 65 | { 66 | "type": "string", 67 | "description": "id of customer", 68 | "name": "id", 69 | "in": "query" 70 | } 71 | ], 72 | "responses": { 73 | "200": { 74 | "description": "OK", 75 | "schema": { 76 | "$ref": "#/definitions/main.CustomerLookupResponse" 77 | } 78 | } 79 | } 80 | } 81 | } 82 | }, 83 | "definitions": { 84 | "main.CustomerAddRequest": { 85 | "type": "object", 86 | "properties": { 87 | "accounts": { 88 | "type": "array", 89 | "items": { 90 | "type": "object", 91 | "properties": { 92 | "account_id": { 93 | "type": "integer" 94 | }, 95 | "name": { 96 | "type": "string" 97 | } 98 | } 99 | } 100 | }, 101 | "customer_id": { 102 | "type": "integer" 103 | }, 104 | "name": { 105 | "type": "string" 106 | } 107 | } 108 | }, 109 | "main.CustomerLookupResponse": { 110 | "type": "object", 111 | "properties": { 112 | "accounts": { 113 | "type": "array", 114 | "items": { 115 | "type": "object", 116 | "properties": { 117 | "account_id": { 118 | "type": "integer" 119 | }, 120 | "balance": { 121 | "type": "integer" 122 | }, 123 | "name": { 124 | "type": "string" 125 | } 126 | } 127 | } 128 | }, 129 | "customer_id": { 130 | "type": "integer" 131 | }, 132 | "name": { 133 | "type": "string" 134 | } 135 | } 136 | } 137 | } 138 | }` 139 | 140 | // SwaggerInfo holds exported Swagger Info so clients can modify it 141 | var SwaggerInfo = &swag.Spec{ 142 | Version: "1.0", 143 | Host: "localhost:8096", 144 | BasePath: "", 145 | Schemes: []string{}, 146 | Title: "Example customer service Swagger API", 147 | Description: "Just example :)", 148 | InfoInstanceName: "swagger", 149 | SwaggerTemplate: docTemplate, 150 | LeftDelim: "{{", 151 | RightDelim: "}}", 152 | } 153 | 154 | func init() { 155 | swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) 156 | } 157 | -------------------------------------------------------------------------------- /examples/customer/go-service/docs/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "description": "Just example :)", 5 | "title": "Example customer service Swagger API", 6 | "termsOfService": "http://swagger.io/terms/", 7 | "contact": { 8 | "name": "API Support", 9 | "url": "http://quedafoe.ru", 10 | "email": "djassange@ya.ru" 11 | }, 12 | "version": "1.0" 13 | }, 14 | "host": "localhost:8096", 15 | "paths": { 16 | "/customer_add": { 17 | "post": { 18 | "description": "Todo", 19 | "produces": [ 20 | "application/json" 21 | ], 22 | "tags": [ 23 | "Set" 24 | ], 25 | "summary": "Todo", 26 | "parameters": [ 27 | { 28 | "description": "Обязательно не должно быть пустым", 29 | "name": "Request", 30 | "in": "body", 31 | "required": true, 32 | "schema": { 33 | "$ref": "#/definitions/main.CustomerAddRequest" 34 | } 35 | } 36 | ], 37 | "responses": { 38 | "200": { 39 | "description": "ok", 40 | "schema": { 41 | "type": "string" 42 | } 43 | } 44 | } 45 | } 46 | }, 47 | "/customer_lookup": { 48 | "get": { 49 | "description": "Todo", 50 | "produces": [ 51 | "application/json" 52 | ], 53 | "tags": [ 54 | "Get" 55 | ], 56 | "summary": "Todo", 57 | "parameters": [ 58 | { 59 | "type": "string", 60 | "description": "id of customer", 61 | "name": "id", 62 | "in": "query" 63 | } 64 | ], 65 | "responses": { 66 | "200": { 67 | "description": "OK", 68 | "schema": { 69 | "$ref": "#/definitions/main.CustomerLookupResponse" 70 | } 71 | } 72 | } 73 | } 74 | } 75 | }, 76 | "definitions": { 77 | "main.CustomerAddRequest": { 78 | "type": "object", 79 | "properties": { 80 | "accounts": { 81 | "type": "array", 82 | "items": { 83 | "type": "object", 84 | "properties": { 85 | "account_id": { 86 | "type": "integer" 87 | }, 88 | "name": { 89 | "type": "string" 90 | } 91 | } 92 | } 93 | }, 94 | "customer_id": { 95 | "type": "integer" 96 | }, 97 | "name": { 98 | "type": "string" 99 | } 100 | } 101 | }, 102 | "main.CustomerLookupResponse": { 103 | "type": "object", 104 | "properties": { 105 | "accounts": { 106 | "type": "array", 107 | "items": { 108 | "type": "object", 109 | "properties": { 110 | "account_id": { 111 | "type": "integer" 112 | }, 113 | "balance": { 114 | "type": "integer" 115 | }, 116 | "name": { 117 | "type": "string" 118 | } 119 | } 120 | } 121 | }, 122 | "customer_id": { 123 | "type": "integer" 124 | }, 125 | "name": { 126 | "type": "string" 127 | } 128 | } 129 | } 130 | } 131 | } -------------------------------------------------------------------------------- /examples/customer/go-service/docs/swagger.yaml: -------------------------------------------------------------------------------- 1 | definitions: 2 | main.CustomerAddRequest: 3 | properties: 4 | accounts: 5 | items: 6 | properties: 7 | account_id: 8 | type: integer 9 | name: 10 | type: string 11 | type: object 12 | type: array 13 | customer_id: 14 | type: integer 15 | name: 16 | type: string 17 | type: object 18 | main.CustomerLookupResponse: 19 | properties: 20 | accounts: 21 | items: 22 | properties: 23 | account_id: 24 | type: integer 25 | balance: 26 | type: integer 27 | name: 28 | type: string 29 | type: object 30 | type: array 31 | customer_id: 32 | type: integer 33 | name: 34 | type: string 35 | type: object 36 | host: localhost:8096 37 | info: 38 | contact: 39 | email: djassange@ya.ru 40 | name: API Support 41 | url: http://quedafoe.ru 42 | description: Just example :) 43 | termsOfService: http://swagger.io/terms/ 44 | title: Example customer service Swagger API 45 | version: "1.0" 46 | paths: 47 | /customer_add: 48 | post: 49 | description: Todo 50 | parameters: 51 | - description: Обязательно не должно быть пустым 52 | in: body 53 | name: Request 54 | required: true 55 | schema: 56 | $ref: '#/definitions/main.CustomerAddRequest' 57 | produces: 58 | - application/json 59 | responses: 60 | "200": 61 | description: ok 62 | schema: 63 | type: string 64 | summary: Todo 65 | tags: 66 | - Set 67 | /customer_lookup: 68 | get: 69 | description: Todo 70 | parameters: 71 | - description: id of customer 72 | in: query 73 | name: id 74 | type: string 75 | produces: 76 | - application/json 77 | responses: 78 | "200": 79 | description: OK 80 | schema: 81 | $ref: '#/definitions/main.CustomerLookupResponse' 82 | summary: Todo 83 | tags: 84 | - Get 85 | swagger: "2.0" 86 | -------------------------------------------------------------------------------- /examples/customer/go-service/go.mod: -------------------------------------------------------------------------------- 1 | module customer 2 | 3 | go 1.22.2 4 | 5 | require ( 6 | github.com/google/uuid v1.6.0 7 | github.com/spf13/viper v1.19.0 8 | github.com/swaggo/swag v1.16.3 9 | github.com/tarantool/go-tarantool/v2 v2.2.1 10 | github.com/tarantool/go-vshard-router v1.3.2 11 | ) 12 | 13 | require ( 14 | github.com/KyleBanks/depth v1.2.1 // indirect 15 | github.com/PuerkitoBio/purell v1.1.1 // indirect 16 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect 17 | github.com/fsnotify/fsnotify v1.7.0 // indirect 18 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 19 | github.com/go-openapi/jsonreference v0.19.6 // indirect 20 | github.com/go-openapi/spec v0.20.4 // indirect 21 | github.com/go-openapi/swag v0.19.15 // indirect 22 | github.com/hashicorp/hcl v1.0.0 // indirect 23 | github.com/josharian/intern v1.0.0 // indirect 24 | github.com/magiconair/properties v1.8.7 // indirect 25 | github.com/mailru/easyjson v0.7.6 // indirect 26 | github.com/mitchellh/mapstructure v1.5.0 // indirect 27 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 28 | github.com/sagikazarmark/locafero v0.4.0 // indirect 29 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 30 | github.com/snksoft/crc v1.1.0 // indirect 31 | github.com/sourcegraph/conc v0.3.0 // indirect 32 | github.com/spf13/afero v1.11.0 // indirect 33 | github.com/spf13/cast v1.6.0 // indirect 34 | github.com/spf13/pflag v1.0.5 // indirect 35 | github.com/subosito/gotenv v1.6.0 // indirect 36 | github.com/tarantool/go-iproto v1.1.0 // indirect 37 | github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect 38 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 39 | go.uber.org/atomic v1.9.0 // indirect 40 | go.uber.org/multierr v1.9.0 // indirect 41 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect 42 | golang.org/x/net v0.33.0 // indirect 43 | golang.org/x/sync v0.10.0 // indirect 44 | golang.org/x/sys v0.28.0 // indirect 45 | golang.org/x/text v0.21.0 // indirect 46 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect 47 | gopkg.in/ini.v1 v1.67.0 // indirect 48 | gopkg.in/yaml.v2 v2.4.0 // indirect 49 | gopkg.in/yaml.v3 v3.0.1 // indirect 50 | ) 51 | 52 | replace github.com/tarantool/go-vshard-router v0.0.9 => ../../../ 53 | -------------------------------------------------------------------------------- /examples/customer/go-service/go.sum: -------------------------------------------------------------------------------- 1 | github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= 2 | github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= 3 | github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= 4 | github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 5 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= 6 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 7 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 11 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 13 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 14 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 15 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 16 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 17 | github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= 18 | github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 19 | github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= 20 | github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= 21 | github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= 22 | github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= 23 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 24 | github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= 25 | github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= 26 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 27 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 28 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 29 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 30 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 31 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 32 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 33 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 34 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 35 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 36 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 37 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 38 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 39 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 40 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 41 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 42 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 43 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 44 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 45 | github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= 46 | github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 47 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 48 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 49 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 50 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 51 | github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= 52 | github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 53 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 54 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 55 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 56 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 57 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 58 | github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= 59 | github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= 60 | github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= 61 | github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= 62 | github.com/snksoft/crc v1.1.0 h1:HkLdI4taFlgGGG1KvsWMpz78PkOC9TkPVpTV/cuWn48= 63 | github.com/snksoft/crc v1.1.0/go.mod h1:5/gUOsgAm7OmIhb6WJzw7w5g2zfJi4FrHYgGPdshE+A= 64 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 65 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 66 | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= 67 | github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= 68 | github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= 69 | github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 70 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 71 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 72 | github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= 73 | github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= 74 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 75 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 76 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 77 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 78 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 79 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 80 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 81 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 82 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 83 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 84 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 85 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 86 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 87 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 88 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 89 | github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= 90 | github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk= 91 | github.com/tarantool/go-iproto v1.1.0 h1:HULVOIHsiehI+FnHfM7wMDntuzUddO09DKqu2WnFQ5A= 92 | github.com/tarantool/go-iproto v1.1.0/go.mod h1:LNCtdyZxojUed8SbOiYHoc3v9NvaZTB7p96hUySMlIo= 93 | github.com/tarantool/go-tarantool/v2 v2.2.1 h1:ldzMVfkmTuJl4ie3ByMIr+mmPSKDVTcSkN8XlVZEows= 94 | github.com/tarantool/go-tarantool/v2 v2.2.1/go.mod h1:hKKeZeCP8Y8+U6ZFS32ot1jHV/n4WKVP4fjRAvQznMY= 95 | github.com/tarantool/go-vshard-router v1.3.2 h1:30ZQIZGj5U6TaAK8NXgTUYgpns938KDQKApnkcqz2Ps= 96 | github.com/tarantool/go-vshard-router v1.3.2/go.mod h1:+ZRedQeNcP5EUjtQg2BKHS9b40u2A3V+IEo2QqTpKJY= 97 | github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= 98 | github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= 99 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= 100 | github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 101 | go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= 102 | go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 103 | go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= 104 | go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= 105 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= 106 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= 107 | golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= 108 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 109 | golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= 110 | golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 111 | golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 112 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 113 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 114 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 115 | golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 116 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 117 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 118 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 119 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 120 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 121 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 122 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 123 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 124 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= 125 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 126 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 127 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 128 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 129 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 130 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 131 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 132 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 133 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 134 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 135 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 136 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 137 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 138 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 139 | -------------------------------------------------------------------------------- /examples/customer/go-service/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "os" 10 | "strconv" 11 | "time" 12 | 13 | "github.com/google/uuid" 14 | "github.com/spf13/viper" 15 | "github.com/tarantool/go-tarantool/v2" 16 | vshardrouter "github.com/tarantool/go-vshard-router" 17 | "github.com/tarantool/go-vshard-router/providers/static" 18 | ) 19 | 20 | // @title Example customer service Swagger API 21 | // @version 1.0 22 | // @description Just example :) 23 | // @termsOfService http://swagger.io/terms/ 24 | 25 | // @contact.name API Support 26 | // @contact.url http://quedafoe.ru 27 | // @contact.email djassange@ya.ru 28 | 29 | // @host localhost:8096 30 | func main() { 31 | ctx := context.Background() 32 | var err error 33 | 34 | if len(os.Args) == 1 { 35 | log.Println("write config file path as argument") 36 | 37 | os.Exit(2) 38 | } 39 | 40 | cfg := readCfg(os.Args[1]) 41 | 42 | vshardRouter, err := vshardrouter.NewRouter(ctx, vshardrouter.Config{ 43 | Loggerf: &vshardrouter.StdoutLoggerf{}, 44 | DiscoveryTimeout: time.Minute, 45 | DiscoveryMode: vshardrouter.DiscoveryModeOn, 46 | TopologyProvider: static.NewProvider(cfg.Storage.Topology), 47 | TotalBucketCount: cfg.Storage.TotalBucketCount, 48 | PoolOpts: tarantool.Opts{ 49 | Timeout: time.Second, 50 | }, 51 | }) 52 | 53 | ctrl := controller{ 54 | router: vshardRouter, 55 | } 56 | 57 | mux := http.NewServeMux() 58 | mux.HandleFunc("/customer_lookup", ctrl.CustomerLookupHandler) 59 | mux.HandleFunc("/customer_add", ctrl.CustomerAddHandler) 60 | 61 | log.Println("new mux server created") 62 | 63 | s := &http.Server{ 64 | Addr: cfg.ListenerConfig.Address, 65 | WriteTimeout: time.Minute, 66 | ReadTimeout: time.Minute, 67 | IdleTimeout: time.Minute, 68 | Handler: mux, 69 | } 70 | 71 | log.Printf("start listening on %s", cfg.ListenerConfig.Address) 72 | err = s.ListenAndServe() 73 | if err != nil { 74 | log.Println(err) 75 | 76 | return 77 | } 78 | } 79 | 80 | // --------- HANDLERS ---------- 81 | type controller struct { 82 | router *vshardrouter.Router 83 | } 84 | 85 | type CustomerAddRequest struct { 86 | CustomerID int `json:"customer_id" msgpack:"customer_id"` 87 | Name string `json:"name" msgpack:"name"` 88 | BucketId uint64 `json:"-" msgpack:"bucket_id"` 89 | Accounts []struct { 90 | AccountId int `json:"account_id" msgpack:"account_id"` 91 | Name string `json:"name" msgpack:"name"` 92 | } `json:"accounts" msgpack:"accounts"` 93 | } 94 | 95 | // CustomerAddHandler godoc 96 | // @Summary Todo 97 | // @Description Todo 98 | // @Produce json 99 | // @Param Request body CustomerAddRequest true "Обязательно не должно быть пустым" 100 | // @Success 200 {string} string "ok" 101 | // @Router /customer_add [post] 102 | // @Tags Set 103 | func (c *controller) CustomerAddHandler(w http.ResponseWriter, r *http.Request) { 104 | ctx := r.Context() 105 | if r.Method != http.MethodPost { 106 | w.WriteHeader(http.StatusMethodNotAllowed) 107 | return 108 | } 109 | 110 | req := &CustomerAddRequest{} 111 | 112 | err := json.NewDecoder(r.Body).Decode(req) 113 | if err != nil { 114 | w.WriteHeader(http.StatusBadRequest) 115 | return 116 | } 117 | 118 | bucketID := c.router.BucketIDStrCRC32(strconv.Itoa(req.CustomerID)) 119 | 120 | req.BucketId = bucketID 121 | 122 | resp, err := c.router.Call(ctx, bucketID, vshardrouter.CallModeRW, "customer_add", []interface{}{req}, vshardrouter.CallOpts{ 123 | Timeout: time.Minute, 124 | }) 125 | if err != nil { 126 | w.WriteHeader(http.StatusInternalServerError) 127 | return 128 | } 129 | 130 | fmt.Println(resp.Get()) 131 | } 132 | 133 | // customer lookup 134 | 135 | type CustomerLookupResponse struct { 136 | Accounts []struct { 137 | AccountId int `json:"account_id" msgpack:"account_id"` 138 | Balance int `json:"balance" msgpack:"balance"` 139 | Name string `json:"name" msgpack:"name"` 140 | } `json:"accounts" msgpack:"accounts"` 141 | CustomerId int `json:"customer_id" msgpack:"customer_id"` 142 | Name string `json:"name" msgpack:"name"` 143 | } 144 | 145 | // CustomerLookupHandler godoc 146 | // @Summary Todo 147 | // @Description Todo 148 | // @Produce json 149 | // @Param id query string false "id of customer" 150 | // @Success 200 {object} CustomerLookupResponse 151 | // @Router /customer_lookup [get] 152 | // @Tags Get 153 | func (c *controller) CustomerLookupHandler(w http.ResponseWriter, r *http.Request) { 154 | ctx := r.Context() 155 | customerID := r.URL.Query().Get("id") // get customer id 156 | if customerID == "" { 157 | http.Error(w, "no id query param", http.StatusInternalServerError) 158 | return 159 | } 160 | 161 | csID, err := strconv.Atoi(customerID) 162 | if err != nil { 163 | w.WriteHeader(http.StatusBadRequest) 164 | return 165 | } 166 | 167 | bucketID := c.router.BucketIDStrCRC32(customerID) 168 | resp, err := c.router.Call(ctx, bucketID, vshardrouter.CallModeBRO, "customer_lookup", []interface{}{csID}, vshardrouter.CallOpts{ 169 | Timeout: time.Minute, 170 | }) 171 | if err != nil { 172 | http.Error(w, err.Error(), http.StatusInternalServerError) 173 | return 174 | } 175 | 176 | log.Println(resp.Get()) 177 | 178 | rsp := &CustomerLookupResponse{} 179 | 180 | err = resp.GetTyped(rsp) 181 | if err != nil { 182 | http.Error(w, err.Error(), http.StatusInternalServerError) 183 | return 184 | } 185 | 186 | w.Header().Set("Content-Type", "application/json") 187 | w.WriteHeader(http.StatusOK) 188 | json.NewEncoder(w).Encode(resp) 189 | } 190 | 191 | // 192 | // --------- CONFIG ---------- 193 | // 194 | 195 | type Config struct { 196 | ListenerConfig ListenerConfig `yaml:"listener" mapstructure:"listener"` 197 | Routers RoutersConfig `yaml:"routers"` 198 | Storage StorageConfig `yaml:"storage" mapstructure:"storage"` 199 | } 200 | 201 | type ListenerConfig struct { 202 | Address string `yaml:"address" mapstructure:"address"` 203 | } 204 | 205 | type RoutersConfig struct { 206 | Addrs []string `yaml:"addrs" mapstructure:"addrs"` 207 | } 208 | 209 | type StorageConfig struct { 210 | TotalBucketCount uint64 `yaml:"total_bucket_count" mapstructure:"total_bucket_count"` 211 | SourceTopology *SourceTopologyConfig `yaml:"topology,omitempty" mapstructure:"topology,omitempty"` 212 | Topology map[vshardrouter.ReplicasetInfo][]vshardrouter.InstanceInfo `yaml:"-" mapstructure:"-"` 213 | } 214 | type ClusterInfo struct { 215 | ReplicasetUUID string `yaml:"replicaset_uuid" mapstructure:"replicaset_uuid"` 216 | } 217 | 218 | type InstanceInfo struct { 219 | Cluster string 220 | Box struct { 221 | Listen string 222 | InstanceUUID string `yaml:"instance_uuid" mapstructure:"instance_uuid"` 223 | } 224 | } 225 | type SourceTopologyConfig struct { 226 | Clusters map[string]ClusterInfo `json:"clusters,omitempty" yaml:"clusters" ` 227 | Instances map[string]InstanceInfo `json:"instances,omitempty" yaml:"instances"` 228 | } 229 | 230 | func readCfg(cfgPath string) Config { 231 | // read cfg 232 | viper.SetConfigType("yaml") 233 | viper.SetConfigFile(cfgPath) 234 | err := viper.ReadInConfig() 235 | if err != nil { 236 | log.Println("viper cant read in such config file ") 237 | os.Exit(2) 238 | } 239 | 240 | cfg := &Config{} 241 | err = viper.Unmarshal(cfg) 242 | if err != nil { 243 | log.Println(err) 244 | 245 | os.Exit(2) 246 | } 247 | 248 | // проверяем что если топология из конфига - то в конфиге она должна быть в наличии 249 | if cfg.Storage.SourceTopology == nil { // проверяем что из конфига эта топология спарсилась 250 | log.Println(fmt.Errorf("topology provider uses config source, but topology: is empty")) 251 | 252 | os.Exit(2) 253 | } 254 | 255 | // prepare vshard router config 256 | vshardRouterTopology := make(map[vshardrouter.ReplicasetInfo][]vshardrouter.InstanceInfo) 257 | 258 | for rsName, rs := range cfg.Storage.SourceTopology.Clusters { 259 | rsUUID, err := uuid.Parse(rs.ReplicasetUUID) 260 | if err != nil { 261 | log.Printf("cant parse replicaset uuid: %s", err) 262 | 263 | os.Exit(2) 264 | } 265 | 266 | rsInstances := make([]vshardrouter.InstanceInfo, 0) 267 | 268 | for _, instInfo := range cfg.Storage.SourceTopology.Instances { 269 | if instInfo.Cluster != rsName { 270 | continue 271 | } 272 | 273 | instUUID, err := uuid.Parse(instInfo.Box.InstanceUUID) 274 | if err != nil { 275 | log.Printf("cant parse replicaset uuid: %s", err) 276 | 277 | os.Exit(2) 278 | } 279 | 280 | rsInstances = append(rsInstances, vshardrouter.InstanceInfo{ 281 | Addr: instInfo.Box.Listen, 282 | UUID: instUUID, 283 | }) 284 | 285 | } 286 | 287 | vshardRouterTopology[vshardrouter.ReplicasetInfo{ 288 | Name: rsName, 289 | UUID: rsUUID, 290 | }] = rsInstances 291 | } 292 | cfg.Storage.Topology = vshardRouterTopology 293 | 294 | return *cfg 295 | } 296 | -------------------------------------------------------------------------------- /examples/customer/tarantool/.luarocks-config.lua: -------------------------------------------------------------------------------- 1 | rocks_servers = { 2 | "http://moonlibs.github.io/rocks", --moonlibs libs 3 | "http://rocks.tarantool.org/", --vshard and other tarantool libs 4 | "http://luarocks.org/repositories/rocks", -- luarocks 5 | } 6 | -------------------------------------------------------------------------------- /examples/customer/tarantool/.tarantoolctl: -------------------------------------------------------------------------------- 1 | local workdir = './data/' 2 | local fio = require('fio') 3 | if not fio.stat('./data') then 4 | fio.mkdir('./data') 5 | end 6 | 7 | default_cfg = { 8 | pid_file = workdir, 9 | wal_dir = workdir, 10 | snap_dir = workdir, 11 | vinyl_dir = workdir, 12 | logger = workdir, 13 | } 14 | 15 | instance_dir = "." 16 | 17 | -- vim: set ft=lua ts=4 sts=4 sw=4 et: 18 | -------------------------------------------------------------------------------- /examples/customer/tarantool/Makefile: -------------------------------------------------------------------------------- 1 | all: stop clean start enter 2 | 3 | libs: # устанавливает все либы в .rocks/lib и .rocks/share 4 | tarantoolctl rocks --tree .rocks install --only-deps customer-scm-1.rockspec 5 | 6 | start: libs 7 | tarantoolctl start storage_1_a 8 | tarantoolctl start storage_1_b 9 | tarantoolctl start storage_1_c 10 | tarantoolctl start storage_2_a 11 | tarantoolctl start storage_2_b 12 | tarantoolctl start router_1 13 | @echo "Waiting cluster to start" 14 | @sleep 1 15 | echo "vshard.router.bootstrap()" | tarantoolctl enter router_1 16 | 17 | stop: 18 | tarantoolctl stop storage_1_a 19 | tarantoolctl stop storage_1_b 20 | tarantoolctl stop storage_1_c 21 | tarantoolctl stop storage_2_a 22 | tarantoolctl stop storage_2_b 23 | tarantoolctl stop router_1 24 | 25 | .PHONY: enter 26 | router: 27 | tarantoolctl enter router_1 28 | storage: 29 | tarantoolctl enter storage_1_a 30 | 31 | logcat: 32 | tail -f data/*.log 33 | 34 | clean: 35 | rm -rf data/ 36 | 37 | .PHONY: console test deploy clean 38 | -------------------------------------------------------------------------------- /examples/customer/tarantool/customer-scm-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "customer" 2 | version = "scm-1" 3 | 4 | source = { 5 | url = '/dev/null', 6 | } 7 | 8 | dependencies = { 9 | "vshard >= 0.1.26", 10 | } 11 | 12 | build = { 13 | type = 'none'; 14 | } 15 | 16 | -------------------------------------------------------------------------------- /examples/customer/tarantool/localcfg.lua: -------------------------------------------------------------------------------- 1 | return { 2 | sharding = { 3 | ['cbf06940-0790-498b-948d-042b62cf3d29'] = { -- replicaset #1 4 | master = 'auto', 5 | replicas = { 6 | ['8a274925-a26d-47fc-9e1b-af88ce939412'] = { 7 | uri = 'storage:storage@127.0.0.1:3301', 8 | name = 'storage_1_a', 9 | }, 10 | ['3de2e3e1-9ebe-4d0d-abb1-26d301b84633'] = { 11 | uri = 'storage:storage@127.0.0.1:3302', 12 | name = 'storage_1_b' 13 | }, 14 | ['3de2e3e1-9ebe-4d0d-abb1-26d301b84635'] = { 15 | uri = 'storage:storage@127.0.0.1:3305', 16 | name = 'storage_1_c' 17 | } 18 | }, 19 | }, -- replicaset #1 20 | ['ac522f65-aa94-4134-9f64-51ee384f1a54'] = { -- replicaset #2 21 | master = 'auto', 22 | replicas = { 23 | ['1e02ae8a-afc0-4e91-ba34-843a356b8ed7'] = { 24 | uri = 'storage:storage@127.0.0.1:3303', 25 | name = 'storage_2_a', 26 | }, 27 | ['001688c3-66f8-4a31-8e19-036c17d489c2'] = { 28 | uri = 'storage:storage@127.0.0.1:3304', 29 | name = 'storage_2_b' 30 | } 31 | }, 32 | }, -- replicaset #2 33 | }, -- sharding 34 | replication_connect_quorum = 0, 35 | election_mode = "candidate" 36 | } -------------------------------------------------------------------------------- /examples/customer/tarantool/router.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | require('strict').on() 4 | local fiber = require('fiber') 5 | rawset(_G, 'fiber', fiber) -- set fiber as global 6 | 7 | -- Check if we are running under test-run 8 | if os.getenv('ADMIN') then 9 | test_run = require('test_run').new() 10 | require('console').listen(os.getenv('ADMIN')) 11 | end 12 | 13 | box.cfg{ 14 | listen = "127.0.0.1:12002" 15 | } 16 | 17 | box.once('access:v1', function() 18 | box.schema.user.grant('guest', 'read,write,execute', 'universe') 19 | end) 20 | 21 | replicasets = {'cbf06940-0790-498b-948d-042b62cf3d29', 22 | 'ac522f65-aa94-4134-9f64-51ee384f1a54'} 23 | 24 | -- Call a configuration provider 25 | cfg = dofile('localcfg.lua') 26 | if arg[1] == 'discovery_disable' then 27 | cfg.discovery_mode = 'off' 28 | end 29 | 30 | cfg.bucket_count = 10000 31 | 32 | -- Start the database with sharding 33 | local vshard = require 'vshard' 34 | rawset(_G, 'vshard', vshard) -- set vshard as global 35 | 36 | vshard.router.cfg(cfg) 37 | 38 | -------------------------------------------------------------------------------- /examples/customer/tarantool/router_1.lua: -------------------------------------------------------------------------------- 1 | router.lua -------------------------------------------------------------------------------- /examples/customer/tarantool/storage.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | require('strict').on() 4 | 5 | -- Get instance name 6 | local fio = require('fio') 7 | local NAME = fio.basename(arg[0], '.lua') 8 | local fiber = require('fiber') 9 | 10 | -- Check if we are running under test-run 11 | if os.getenv('ADMIN') then 12 | test_run = require('test_run').new() 13 | require('console').listen(os.getenv('ADMIN')) 14 | end 15 | 16 | -- Call a configuration provider 17 | cfg = dofile('localcfg.lua') 18 | -- Name to uuid map 19 | names = { 20 | ['storage_1_a'] = '8a274925-a26d-47fc-9e1b-af88ce939412', 21 | ['storage_1_b'] = '3de2e3e1-9ebe-4d0d-abb1-26d301b84633', 22 | ['storage_1_c'] = '3de2e3e1-9ebe-4d0d-abb1-26d301b84635', 23 | ['storage_2_a'] = '1e02ae8a-afc0-4e91-ba34-843a356b8ed7', 24 | ['storage_2_b'] = '001688c3-66f8-4a31-8e19-036c17d489c2', 25 | } 26 | 27 | replicasets = {'cbf06940-0790-498b-948d-042b62cf3d29', 28 | 'ac522f65-aa94-4134-9f64-51ee384f1a54'} 29 | 30 | -- Start the database with sharding 31 | local vshard = require 'vshard' 32 | rawset(_G, 'vshard', vshard) -- set as global variable 33 | 34 | vshard.storage.cfg(cfg, names[NAME]) 35 | 36 | box.once('access:v1', function() 37 | box.schema.user.grant('guest', 'read,write,execute', 'universe') 38 | end) 39 | 40 | box.once("testapp:schema:1", function() 41 | local customer = box.schema.space.create('customer') 42 | customer:format({ 43 | {'customer_id', 'unsigned'}, 44 | {'bucket_id', 'unsigned'}, 45 | {'name', 'string'}, 46 | }) 47 | customer:create_index('customer_id', {parts = {'customer_id'}}) 48 | customer:create_index('bucket_id', {parts = {'bucket_id'}, unique = false}) 49 | 50 | local account = box.schema.space.create('account') 51 | account:format({ 52 | {'account_id', 'unsigned'}, 53 | {'customer_id', 'unsigned'}, 54 | {'bucket_id', 'unsigned'}, 55 | {'balance', 'unsigned'}, 56 | {'name', 'string'}, 57 | }) 58 | account:create_index('account_id', {parts = {'account_id'}}) 59 | account:create_index('customer_id', {parts = {'customer_id'}, unique = false}) 60 | account:create_index('bucket_id', {parts = {'bucket_id'}, unique = false}) 61 | box.snapshot() 62 | 63 | box.schema.func.create('customer_lookup') 64 | box.schema.role.grant('public', 'execute', 'function', 'customer_lookup') 65 | box.schema.func.create('customer_add') 66 | box.schema.role.grant('public', 'execute', 'function', 'customer_add') 67 | box.schema.func.create('echo') 68 | box.schema.role.grant('public', 'execute', 'function', 'echo') 69 | box.schema.func.create('sleep') 70 | box.schema.role.grant('public', 'execute', 'function', 'sleep') 71 | box.schema.func.create('raise_luajit_error') 72 | box.schema.role.grant('public', 'execute', 'function', 'raise_luajit_error') 73 | box.schema.func.create('raise_client_error') 74 | box.schema.role.grant('public', 'execute', 'function', 'raise_client_error') 75 | end) 76 | 77 | function insert_customer(customer) 78 | box.space.customer:insert({customer.customer_id, customer.bucket_id, customer.name}) 79 | for _, account in ipairs(customer.accounts) do 80 | box.space.account:insert({ 81 | account.account_id, 82 | customer.customer_id, 83 | customer.bucket_id, 84 | 0, 85 | account.name 86 | }) 87 | end 88 | end 89 | 90 | function customer_add(customer) 91 | local ares = box.atomic(insert_customer, customer) 92 | 93 | return {res = ares } 94 | end 95 | 96 | function customer_lookup(customer_id) 97 | if type(customer_id) ~= 'number' then 98 | error('Usage: customer_lookup(customer_id)') 99 | end 100 | 101 | local customer = box.space.customer:get(customer_id) 102 | if customer == nil then 103 | return nil 104 | end 105 | customer = { 106 | customer_id = customer.customer_id; 107 | name = customer.name; 108 | } 109 | local accounts = {} 110 | for _, account in box.space.account.index.customer_id:pairs(customer_id) do 111 | table.insert(accounts, { 112 | account_id = account.account_id; 113 | name = account.name; 114 | balance = account.balance; 115 | }) 116 | end 117 | customer.accounts = accounts; 118 | return customer, {test=123} 119 | end 120 | 121 | function echo(...) 122 | return ... 123 | end 124 | 125 | function sleep(time) 126 | fiber.sleep(time) 127 | return true 128 | end 129 | 130 | function raise_luajit_error() 131 | assert(1 == 2) 132 | end 133 | 134 | function raise_client_error() 135 | box.error(box.error.UNKNOWN) 136 | end 137 | -------------------------------------------------------------------------------- /examples/customer/tarantool/storage_1_a.lua: -------------------------------------------------------------------------------- 1 | storage.lua -------------------------------------------------------------------------------- /examples/customer/tarantool/storage_1_b.lua: -------------------------------------------------------------------------------- 1 | storage.lua -------------------------------------------------------------------------------- /examples/customer/tarantool/storage_1_c.lua: -------------------------------------------------------------------------------- 1 | storage.lua -------------------------------------------------------------------------------- /examples/customer/tarantool/storage_2_a.lua: -------------------------------------------------------------------------------- 1 | storage.lua -------------------------------------------------------------------------------- /examples/customer/tarantool/storage_2_b.lua: -------------------------------------------------------------------------------- 1 | storage.lua -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tarantool/go-vshard-router/v2 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/google/uuid v1.6.0 7 | github.com/prometheus/client_golang v1.11.1 8 | github.com/snksoft/crc v1.1.0 9 | github.com/spf13/viper v1.19.0 10 | github.com/stretchr/testify v1.10.0 11 | github.com/tarantool/go-tarantool/v2 v2.3.1 12 | github.com/vmihailenco/msgpack/v5 v5.4.1 13 | go.etcd.io/etcd/client/v2 v2.305.17 14 | go.etcd.io/etcd/client/v3 v3.5.17 15 | go.etcd.io/etcd/server/v3 v3.5.17 16 | golang.org/x/sync v0.10.0 17 | ) 18 | 19 | require ( 20 | cloud.google.com/go v0.112.1 // indirect 21 | cloud.google.com/go/compute v1.24.0 // indirect 22 | cloud.google.com/go/compute/metadata v0.2.3 // indirect 23 | cloud.google.com/go/firestore v1.15.0 // indirect 24 | cloud.google.com/go/longrunning v0.5.5 // indirect 25 | github.com/armon/go-metrics v0.4.1 // indirect 26 | github.com/beorn7/perks v1.0.1 // indirect 27 | github.com/cenkalti/backoff/v4 v4.2.1 // indirect 28 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 29 | github.com/coreos/go-semver v0.3.0 // indirect 30 | github.com/coreos/go-systemd/v22 v22.3.2 // indirect 31 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 32 | github.com/dustin/go-humanize v1.0.0 // indirect 33 | github.com/fatih/color v1.14.1 // indirect 34 | github.com/felixge/httpsnoop v1.0.4 // indirect 35 | github.com/fsnotify/fsnotify v1.7.0 // indirect 36 | github.com/go-logr/logr v1.4.1 // indirect 37 | github.com/go-logr/stdr v1.2.2 // indirect 38 | github.com/gogo/protobuf v1.3.2 // indirect 39 | github.com/golang-jwt/jwt/v4 v4.4.2 // indirect 40 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 41 | github.com/golang/protobuf v1.5.4 // indirect 42 | github.com/google/btree v1.0.1 // indirect 43 | github.com/google/s2a-go v0.1.7 // indirect 44 | github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect 45 | github.com/googleapis/gax-go/v2 v2.12.3 // indirect 46 | github.com/gorilla/websocket v1.4.2 // indirect 47 | github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect 48 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect 49 | github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect 50 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect 51 | github.com/hashicorp/consul/api v1.28.2 // indirect 52 | github.com/hashicorp/errwrap v1.1.0 // indirect 53 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 54 | github.com/hashicorp/go-hclog v1.5.0 // indirect 55 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 56 | github.com/hashicorp/go-multierror v1.1.1 // indirect 57 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 58 | github.com/hashicorp/golang-lru v0.5.4 // indirect 59 | github.com/hashicorp/hcl v1.0.0 // indirect 60 | github.com/hashicorp/serf v0.10.1 // indirect 61 | github.com/jonboulle/clockwork v0.2.2 // indirect 62 | github.com/json-iterator/go v1.1.12 // indirect 63 | github.com/klauspost/compress v1.17.2 // indirect 64 | github.com/magiconair/properties v1.8.7 // indirect 65 | github.com/mattn/go-colorable v0.1.13 // indirect 66 | github.com/mattn/go-isatty v0.0.17 // indirect 67 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 68 | github.com/mitchellh/go-homedir v1.1.0 // indirect 69 | github.com/mitchellh/mapstructure v1.5.0 // indirect 70 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 71 | github.com/modern-go/reflect2 v1.0.2 // indirect 72 | github.com/nats-io/nats.go v1.34.0 // indirect 73 | github.com/nats-io/nkeys v0.4.7 // indirect 74 | github.com/nats-io/nuid v1.0.1 // indirect 75 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 76 | github.com/pkg/errors v0.9.1 // indirect 77 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 78 | github.com/prometheus/client_model v0.2.0 // indirect 79 | github.com/prometheus/common v0.26.0 // indirect 80 | github.com/prometheus/procfs v0.6.0 // indirect 81 | github.com/sagikazarmark/crypt v0.19.0 // indirect 82 | github.com/sagikazarmark/locafero v0.4.0 // indirect 83 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 84 | github.com/sirupsen/logrus v1.9.3 // indirect 85 | github.com/soheilhy/cmux v0.1.5 // indirect 86 | github.com/sourcegraph/conc v0.3.0 // indirect 87 | github.com/spf13/afero v1.11.0 // indirect 88 | github.com/spf13/cast v1.6.0 // indirect 89 | github.com/spf13/pflag v1.0.5 // indirect 90 | github.com/stretchr/objx v0.5.2 // indirect 91 | github.com/subosito/gotenv v1.6.0 // indirect 92 | github.com/tarantool/go-iproto v1.1.0 // indirect 93 | github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect 94 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 95 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect 96 | go.etcd.io/bbolt v1.3.11 // indirect 97 | go.etcd.io/etcd/api/v3 v3.5.17 // indirect 98 | go.etcd.io/etcd/client/pkg/v3 v3.5.17 // indirect 99 | go.etcd.io/etcd/pkg/v3 v3.5.17 // indirect 100 | go.etcd.io/etcd/raft/v3 v3.5.17 // indirect 101 | go.opencensus.io v0.24.0 // indirect 102 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect 103 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect 104 | go.opentelemetry.io/otel v1.24.0 // indirect 105 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 // indirect 106 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0 // indirect 107 | go.opentelemetry.io/otel/metric v1.24.0 // indirect 108 | go.opentelemetry.io/otel/sdk v1.22.0 // indirect 109 | go.opentelemetry.io/otel/trace v1.24.0 // indirect 110 | go.opentelemetry.io/proto/otlp v1.0.0 // indirect 111 | go.uber.org/atomic v1.9.0 // indirect 112 | go.uber.org/multierr v1.9.0 // indirect 113 | go.uber.org/zap v1.21.0 // indirect 114 | golang.org/x/crypto v0.31.0 // indirect 115 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect 116 | golang.org/x/net v0.33.0 // indirect 117 | golang.org/x/oauth2 v0.18.0 // indirect 118 | golang.org/x/sys v0.28.0 // indirect 119 | golang.org/x/text v0.21.0 // indirect 120 | golang.org/x/time v0.5.0 // indirect 121 | google.golang.org/api v0.171.0 // indirect 122 | google.golang.org/appengine v1.6.8 // indirect 123 | google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect 124 | google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2 // indirect 125 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c // indirect 126 | google.golang.org/grpc v1.62.1 // indirect 127 | google.golang.org/protobuf v1.33.0 // indirect 128 | gopkg.in/ini.v1 v1.67.0 // indirect 129 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect 130 | gopkg.in/yaml.v2 v2.4.0 // indirect 131 | gopkg.in/yaml.v3 v3.0.1 // indirect 132 | sigs.k8s.io/yaml v1.2.0 // indirect 133 | ) 134 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package vshard_router //nolint:revive 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/tarantool/go-tarantool/v2" 7 | ) 8 | 9 | // go-tarantool writes logs by default to stderr. Stderr might be not available, or user might use syslog or logging into file. 10 | // So we should implement logging interface and redirect go-tarantool logs to the user's logger. 11 | type tarantoolOptsLogger struct { 12 | loggerf LogfProvider 13 | ctx context.Context 14 | } 15 | 16 | // Does almost the same thing as defaultLogger in go-tarantool, but uses user provided logger instead of stdout logger. 17 | // https://github.com/tarantool/go-tarantool/blob/592db69eed8649b82ce432b930c27daeee98c52f/connection.go#L90 18 | func (l tarantoolOptsLogger) Report(event tarantool.ConnLogKind, conn *tarantool.Connection, v ...interface{}) { 19 | // We use safe type assertion (with ok check), because we don't rely on go-tarantools internal contract about "v...". 20 | // Otherwise, we could encounter an unexpected panic due to logging, if go-tarantool maintainers change contract about "v...". 21 | switch event { 22 | case tarantool.LogReconnectFailed: 23 | var reconnects uint 24 | var err error 25 | var reconnectsOk, errOk bool 26 | 27 | if len(v) >= 2 { 28 | reconnects, reconnectsOk = v[0].(uint) 29 | err, errOk = v[1].(error) 30 | } 31 | 32 | if reconnectsOk && errOk { 33 | l.loggerf.Errorf(l.ctx, "tarantool: reconnect (%d) to %s failed: %s", reconnects, conn.Addr(), err) 34 | } else { 35 | l.loggerf.Errorf(l.ctx, "tarantool: reconnect to %s failed (unexpected v... format): %+v", conn.Addr(), v) 36 | } 37 | case tarantool.LogLastReconnectFailed: 38 | var err error 39 | var errOk bool 40 | 41 | if len(v) >= 1 { 42 | err, errOk = v[0].(error) 43 | } 44 | 45 | if errOk { 46 | l.loggerf.Errorf(l.ctx, "tarantool: last reconnect to %s failed: %s, giving it up", conn.Addr(), err) 47 | } else { 48 | l.loggerf.Errorf(l.ctx, "tarantool: last reconnect to %s failed (unexpected v... format): %v+", conn.Addr(), v) 49 | } 50 | case tarantool.LogUnexpectedResultId: 51 | var header tarantool.Header 52 | var headerOk bool 53 | 54 | if len(v) >= 1 { 55 | header, headerOk = v[0].(tarantool.Header) 56 | } 57 | 58 | if headerOk { 59 | l.loggerf.Errorf(l.ctx, "tarantool: connection %s got unexpected resultId (%d) in response"+ 60 | "(probably canceled request)", 61 | conn.Addr(), header.RequestId) 62 | } else { 63 | l.loggerf.Errorf(l.ctx, "tarantool: connection %s got unexpected resultId in response"+ 64 | "(probably canceled request) (unexpected v... format): %+v", 65 | conn.Addr(), v) 66 | } 67 | case tarantool.LogWatchEventReadFailed: 68 | var err error 69 | var errOk bool 70 | 71 | if len(v) >= 1 { 72 | err, errOk = v[0].(error) 73 | } 74 | 75 | if errOk { 76 | l.loggerf.Errorf(l.ctx, "tarantool: unable to parse watch event: %s", err) 77 | } else { 78 | l.loggerf.Errorf(l.ctx, "tarantool: unable to parse watch event (unexpected v... format): %+v", v) 79 | } 80 | case tarantool.LogAppendPushFailed: 81 | var err error 82 | var errOk bool 83 | 84 | if len(v) >= 1 { 85 | err, errOk = v[0].(error) 86 | } 87 | 88 | if errOk { 89 | l.loggerf.Errorf(l.ctx, "tarantool: unable to append a push response: %s", err) 90 | } else { 91 | l.loggerf.Errorf(l.ctx, "tarantool: unable to append a push response (unexpected v... format): %+v", v) 92 | } 93 | default: 94 | l.loggerf.Errorf(l.ctx, "tarantool: unexpected event %d on conn %s, v...: %+v", event, conn, v) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /mocks/topology/topology_controller.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.51.1. DO NOT EDIT. 2 | 3 | package mocktopology 4 | 5 | import ( 6 | context "context" 7 | 8 | mock "github.com/stretchr/testify/mock" 9 | vshard_router "github.com/tarantool/go-vshard-router/v2" 10 | ) 11 | 12 | // TopologyController is an autogenerated mock type for the TopologyController type 13 | type TopologyController struct { 14 | mock.Mock 15 | } 16 | 17 | // AddInstance provides a mock function with given fields: ctx, rsName, info 18 | func (_m *TopologyController) AddInstance(ctx context.Context, rsName string, info vshard_router.InstanceInfo) error { 19 | ret := _m.Called(ctx, rsName, info) 20 | 21 | if len(ret) == 0 { 22 | panic("no return value specified for AddInstance") 23 | } 24 | 25 | var r0 error 26 | if rf, ok := ret.Get(0).(func(context.Context, string, vshard_router.InstanceInfo) error); ok { 27 | r0 = rf(ctx, rsName, info) 28 | } else { 29 | r0 = ret.Error(0) 30 | } 31 | 32 | return r0 33 | } 34 | 35 | // AddReplicaset provides a mock function with given fields: ctx, rsInfo, instances 36 | func (_m *TopologyController) AddReplicaset(ctx context.Context, rsInfo vshard_router.ReplicasetInfo, instances []vshard_router.InstanceInfo) error { 37 | ret := _m.Called(ctx, rsInfo, instances) 38 | 39 | if len(ret) == 0 { 40 | panic("no return value specified for AddReplicaset") 41 | } 42 | 43 | var r0 error 44 | if rf, ok := ret.Get(0).(func(context.Context, vshard_router.ReplicasetInfo, []vshard_router.InstanceInfo) error); ok { 45 | r0 = rf(ctx, rsInfo, instances) 46 | } else { 47 | r0 = ret.Error(0) 48 | } 49 | 50 | return r0 51 | } 52 | 53 | // AddReplicasets provides a mock function with given fields: ctx, replicasets 54 | func (_m *TopologyController) AddReplicasets(ctx context.Context, replicasets map[vshard_router.ReplicasetInfo][]vshard_router.InstanceInfo) error { 55 | ret := _m.Called(ctx, replicasets) 56 | 57 | if len(ret) == 0 { 58 | panic("no return value specified for AddReplicasets") 59 | } 60 | 61 | var r0 error 62 | if rf, ok := ret.Get(0).(func(context.Context, map[vshard_router.ReplicasetInfo][]vshard_router.InstanceInfo) error); ok { 63 | r0 = rf(ctx, replicasets) 64 | } else { 65 | r0 = ret.Error(0) 66 | } 67 | 68 | return r0 69 | } 70 | 71 | // RemoveInstance provides a mock function with given fields: ctx, rsName, instanceName 72 | func (_m *TopologyController) RemoveInstance(ctx context.Context, rsName string, instanceName string) error { 73 | ret := _m.Called(ctx, rsName, instanceName) 74 | 75 | if len(ret) == 0 { 76 | panic("no return value specified for RemoveInstance") 77 | } 78 | 79 | var r0 error 80 | if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { 81 | r0 = rf(ctx, rsName, instanceName) 82 | } else { 83 | r0 = ret.Error(0) 84 | } 85 | 86 | return r0 87 | } 88 | 89 | // RemoveReplicaset provides a mock function with given fields: ctx, rsName 90 | func (_m *TopologyController) RemoveReplicaset(ctx context.Context, rsName string) []error { 91 | ret := _m.Called(ctx, rsName) 92 | 93 | if len(ret) == 0 { 94 | panic("no return value specified for RemoveReplicaset") 95 | } 96 | 97 | var r0 []error 98 | if rf, ok := ret.Get(0).(func(context.Context, string) []error); ok { 99 | r0 = rf(ctx, rsName) 100 | } else { 101 | if ret.Get(0) != nil { 102 | r0 = ret.Get(0).([]error) 103 | } 104 | } 105 | 106 | return r0 107 | } 108 | 109 | // NewTopologyController creates a new instance of TopologyController. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 110 | // The first argument is typically a *testing.T value. 111 | func NewTopologyController(t interface { 112 | mock.TestingT 113 | Cleanup(func()) 114 | }) *TopologyController { 115 | mock := &TopologyController{} 116 | mock.Mock.Test(t) 117 | 118 | t.Cleanup(func() { mock.AssertExpectations(t) }) 119 | 120 | return mock 121 | } 122 | -------------------------------------------------------------------------------- /providers.go: -------------------------------------------------------------------------------- 1 | package vshard_router //nolint:revive 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "time" 7 | ) 8 | 9 | var ( 10 | emptyMetricsProvider MetricsProvider = (*EmptyMetrics)(nil) 11 | emptyLogfProvider LogfProvider = emptyLogger{} 12 | 13 | // Ensure StdoutLoggerf implements LogfProvider 14 | _ LogfProvider = StdoutLoggerf{} 15 | // Ensure SlogLoggerf implements LogfProvider 16 | ) 17 | 18 | // LogfProvider an interface to inject a custom logger. 19 | type LogfProvider interface { 20 | Debugf(ctx context.Context, format string, v ...any) 21 | Infof(ctx context.Context, format string, v ...any) 22 | Warnf(ctx context.Context, format string, v ...any) 23 | Errorf(ctx context.Context, format string, v ...any) 24 | } 25 | 26 | type emptyLogger struct{} 27 | 28 | func (e emptyLogger) Debugf(_ context.Context, _ string, _ ...any) {} 29 | func (e emptyLogger) Infof(_ context.Context, _ string, _ ...any) {} 30 | func (e emptyLogger) Warnf(_ context.Context, _ string, _ ...any) {} 31 | func (e emptyLogger) Errorf(_ context.Context, _ string, _ ...any) {} 32 | 33 | // StdoutLogLevel is a type to control log level for StdoutLoggerf. 34 | type StdoutLogLevel int 35 | 36 | const ( 37 | // StdoutLogDefault is equal to default value of StdoutLogLevel. Acts like StdoutLogInfo. 38 | StdoutLogDefault StdoutLogLevel = iota 39 | // StdoutLogDebug enables debug or higher level logs for StdoutLoggerf 40 | StdoutLogDebug 41 | // StdoutLogInfo enables only info or higher level logs for StdoutLoggerf 42 | StdoutLogInfo 43 | // StdoutLogWarn enables only warn or higher level logs for StdoutLoggerf 44 | StdoutLogWarn 45 | // StdoutLogError enables error level logs for StdoutLoggerf 46 | StdoutLogError 47 | ) 48 | 49 | // StdoutLoggerf a logger that prints into stderr 50 | type StdoutLoggerf struct { 51 | // LogLevel controls log level to print, see StdoutLogLevel constants for details. 52 | LogLevel StdoutLogLevel 53 | } 54 | 55 | func (s StdoutLoggerf) printLevel(level StdoutLogLevel, prefix string, format string, v ...any) { 56 | var currentLogLevel = s.LogLevel 57 | 58 | if currentLogLevel == StdoutLogDefault { 59 | currentLogLevel = StdoutLogInfo 60 | } 61 | 62 | if level >= currentLogLevel { 63 | log.Printf(prefix+format, v...) 64 | } 65 | } 66 | 67 | // Debugf implements Debugf method for LogfProvider interface 68 | func (s StdoutLoggerf) Debugf(_ context.Context, format string, v ...any) { 69 | s.printLevel(StdoutLogDebug, "[DEBUG] ", format, v...) 70 | } 71 | 72 | // Infof implements Infof method for LogfProvider interface 73 | func (s StdoutLoggerf) Infof(_ context.Context, format string, v ...any) { 74 | s.printLevel(StdoutLogInfo, "[INFO] ", format, v...) 75 | } 76 | 77 | // Warnf implements Warnf method for LogfProvider interface 78 | func (s StdoutLoggerf) Warnf(_ context.Context, format string, v ...any) { 79 | s.printLevel(StdoutLogWarn, "[WARN] ", format, v...) 80 | } 81 | 82 | // Errorf implements Errorf method for LogfProvider interface 83 | func (s StdoutLoggerf) Errorf(_ context.Context, format string, v ...any) { 84 | s.printLevel(StdoutLogError, "[ERROR] ", format, v...) 85 | } 86 | 87 | // Metrics 88 | 89 | // MetricsProvider is an interface for passing library metrics to your prometheus/graphite and other metrics. 90 | // This logic is experimental and may be changed in the release. 91 | type MetricsProvider interface { 92 | CronDiscoveryEvent(ok bool, duration time.Duration, reason string) 93 | RetryOnCall(reason string) 94 | RequestDuration(duration time.Duration, procedure string, ok, mapReduce bool) 95 | } 96 | 97 | // EmptyMetrics is default empty metrics provider 98 | // you can embed this type and realize just some metrics 99 | type EmptyMetrics struct{} 100 | 101 | func (e *EmptyMetrics) CronDiscoveryEvent(_ bool, _ time.Duration, _ string) {} 102 | func (e *EmptyMetrics) RetryOnCall(_ string) {} 103 | func (e *EmptyMetrics) RequestDuration(_ time.Duration, _ string, _, _ bool) {} 104 | 105 | // TopologyProvider is external module that can lookup current topology of cluster 106 | // it might be etcd/config/consul or smth else 107 | type TopologyProvider interface { 108 | // Init should create the current topology at the beginning 109 | // and change the state during the process of changing the point of receiving the cluster configuration 110 | Init(t TopologyController) error 111 | // Close closes all connections if the provider created them 112 | Close() 113 | } 114 | -------------------------------------------------------------------------------- /providers/etcd/README.md: -------------------------------------------------------------------------------- 1 | # Tarantool ETCD topology provider based on moonlibs/config 2 | -------------------------------------------------------------------------------- /providers/etcd/doc.go: -------------------------------------------------------------------------------- 1 | // Package etcd based on moonlibs config library 2 | // https://github.com/moonlibs/config?tab=readme-ov-file#multi-shard-topology-for-custom-sharding-etcdclustermaster 3 | package etcd 4 | -------------------------------------------------------------------------------- /providers/etcd/provider.go: -------------------------------------------------------------------------------- 1 | package etcd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "path/filepath" 7 | 8 | "github.com/google/uuid" 9 | vshardrouter "github.com/tarantool/go-vshard-router/v2" 10 | "go.etcd.io/etcd/client/v2" 11 | ) 12 | 13 | var ( 14 | ErrNodesError = fmt.Errorf("etcd nodes err") 15 | ErrClusterError = fmt.Errorf("etcd cluster err") 16 | ErrInstancesError = fmt.Errorf("etcd instances err") 17 | ErrInvalidUUID = fmt.Errorf("invalid uuid") 18 | ) 19 | 20 | // Check that provider implements TopologyProvider interface 21 | var _ vshardrouter.TopologyProvider = (*Provider)(nil) 22 | 23 | type Provider struct { 24 | // ctx is root ctx of application 25 | ctx context.Context 26 | 27 | kapi client.KeysAPI 28 | path string 29 | } 30 | 31 | type Config struct { 32 | EtcdConfig client.Config 33 | // Path for storages configuration in etcd for example /project/store/storage 34 | Path string 35 | } 36 | 37 | // NewProvider returns provider to etcd configuration 38 | // Set here path to etcd storages config and etcd config 39 | func NewProvider(ctx context.Context, cfg Config) (*Provider, error) { 40 | c, err := client.New(cfg.EtcdConfig) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | kapi := client.NewKeysAPI(c) 46 | 47 | return &Provider{ 48 | ctx: ctx, 49 | kapi: kapi, 50 | path: cfg.Path, 51 | }, nil 52 | } 53 | 54 | // mapCluster2Instances combines clusters with instances in map 55 | func mapCluster2Instances(replicasets []vshardrouter.ReplicasetInfo, 56 | instances map[string][]*vshardrouter.InstanceInfo) map[vshardrouter.ReplicasetInfo][]vshardrouter.InstanceInfo { 57 | 58 | currentTopology := map[vshardrouter.ReplicasetInfo][]vshardrouter.InstanceInfo{} 59 | 60 | for _, replicasetInfo := range replicasets { 61 | var resInst []vshardrouter.InstanceInfo 62 | 63 | for _, inst := range instances[replicasetInfo.Name] { 64 | resInst = append(resInst, *inst) 65 | } 66 | 67 | currentTopology[replicasetInfo] = resInst 68 | } 69 | 70 | return currentTopology 71 | } 72 | 73 | // nolint:contextcheck 74 | func (p *Provider) GetTopology() (map[vshardrouter.ReplicasetInfo][]vshardrouter.InstanceInfo, error) { 75 | resp, err := p.kapi.Get(context.TODO(), p.path, &client.GetOptions{Recursive: true}) 76 | if err != nil { 77 | return nil, err 78 | } 79 | nodes := resp.Node.Nodes 80 | 81 | if nodes.Len() < 2 { 82 | return nil, fmt.Errorf("%w: etcd path %s subnodes <2; minimum 2 (/clusters & /instances)", ErrNodesError, p.path) 83 | } 84 | 85 | var replicasets []vshardrouter.ReplicasetInfo 86 | instances := map[string][]*vshardrouter.InstanceInfo{} // cluster name to instance info 87 | 88 | for _, node := range nodes { 89 | var err error 90 | 91 | switch filepath.Base(node.Key) { 92 | case "clusters": 93 | if len(node.Nodes) < 1 { 94 | return nil, fmt.Errorf("%w: etcd path %s has no clusters", ErrClusterError, node.Key) 95 | } 96 | 97 | for _, rsNode := range node.Nodes { 98 | replicaset := vshardrouter.ReplicasetInfo{} 99 | 100 | replicaset.Name = filepath.Base(rsNode.Key) 101 | 102 | for _, rsInfoNode := range rsNode.Nodes { 103 | switch filepath.Base(rsInfoNode.Key) { 104 | case "replicaset_uuid": 105 | replicaset.UUID, err = uuid.Parse(rsInfoNode.Value) 106 | if err != nil { 107 | return nil, fmt.Errorf("cant parse replicaset %s uuid %s", replicaset.Name, rsInfoNode.Value) 108 | } 109 | case "master": 110 | // TODO: now we dont support non master auto implementation 111 | default: 112 | continue 113 | } 114 | } 115 | 116 | replicasets = append(replicasets, replicaset) 117 | } 118 | case "instances": 119 | if len(node.Nodes) < 1 { 120 | return nil, fmt.Errorf("%w: etcd path %s has no instances", ErrInstancesError, node.Key) 121 | } 122 | 123 | for _, instanceNode := range node.Nodes { 124 | instanceName := filepath.Base(instanceNode.Key) 125 | 126 | instance := &vshardrouter.InstanceInfo{ 127 | Name: instanceName, 128 | } 129 | 130 | for _, instanceInfoNode := range instanceNode.Nodes { 131 | switch filepath.Base(instanceInfoNode.Key) { 132 | case "cluster": 133 | instances[instanceInfoNode.Value] = append(instances[instanceInfoNode.Value], instance) 134 | case "box": 135 | for _, boxNode := range instanceInfoNode.Nodes { 136 | switch filepath.Base(boxNode.Key) { 137 | case "listen": 138 | instance.Addr = boxNode.Value 139 | case "instance_uuid": 140 | instance.UUID, err = uuid.Parse(boxNode.Value) 141 | if err != nil { 142 | return nil, fmt.Errorf("%w: cant parse for instance uuid %s", 143 | ErrInvalidUUID, boxNode.Value) 144 | } 145 | } 146 | } 147 | } 148 | } 149 | } 150 | default: 151 | continue 152 | } 153 | } 154 | 155 | if replicasets == nil { 156 | return nil, fmt.Errorf("empty replicasets") 157 | } 158 | 159 | currentTopology := mapCluster2Instances(replicasets, instances) 160 | 161 | return currentTopology, nil 162 | } 163 | 164 | func (p *Provider) Init(c vshardrouter.TopologyController) error { 165 | topology, err := p.GetTopology() 166 | if err != nil { 167 | return err 168 | } 169 | 170 | return c.AddReplicasets(p.ctx, topology) 171 | } 172 | 173 | // Close must close connection, but etcd v2 client has no interfaces for this 174 | func (p *Provider) Close() {} 175 | -------------------------------------------------------------------------------- /providers/etcd/provider_test.go: -------------------------------------------------------------------------------- 1 | package etcd 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "net/url" 9 | "os" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/require" 13 | vshardrouter "github.com/tarantool/go-vshard-router/v2" 14 | mocktopology "github.com/tarantool/go-vshard-router/v2/mocks/topology" 15 | "go.etcd.io/etcd/client/v2" 16 | "go.etcd.io/etcd/server/v3/embed" 17 | ) 18 | 19 | func parseEtcdUrls(strs []string) []url.URL { 20 | urls := make([]url.URL, 0, len(strs)) 21 | 22 | for _, str := range strs { 23 | u, err := url.Parse(str) 24 | if err != nil { 25 | log.Printf("Invalid url %s, error: %s", str, err.Error()) 26 | continue 27 | 28 | } 29 | urls = append(urls, *u) 30 | } 31 | 32 | return urls 33 | } 34 | 35 | func TestNewProvider(t *testing.T) { 36 | ctx := context.Background() 37 | 38 | t.Run("no etcd endpoints provider error", func(t *testing.T) { 39 | t.Parallel() 40 | 41 | p, err := NewProvider(ctx, Config{EtcdConfig: client.Config{}}) 42 | require.Error(t, err) 43 | require.Nil(t, p) 44 | }) 45 | 46 | clientConfig := client.Config{Endpoints: []string{"http://127.0.0.1:2379"}} 47 | 48 | c, err := client.New(clientConfig) 49 | require.NoError(t, err) 50 | 51 | kapi := client.NewKeysAPI(c) 52 | require.NotNil(t, kapi) 53 | 54 | t.Run("provider creates ok", func(t *testing.T) { 55 | t.Parallel() 56 | 57 | p, err := NewProvider(ctx, Config{EtcdConfig: clientConfig}) 58 | require.NoError(t, err) 59 | require.NotNil(t, p) 60 | }) 61 | 62 | t.Run("provider invalid config error", func(t *testing.T) { 63 | invalidClientConfig := client.Config{Endpoints: []string{"http://0.0.0.0:23803"}} 64 | 65 | p, err := NewProvider(ctx, Config{EtcdConfig: invalidClientConfig}) 66 | require.NoError(t, err) 67 | require.NotNil(t, p) 68 | 69 | _, err = p.GetTopology() 70 | require.Error(t, err) 71 | }) 72 | 73 | } 74 | 75 | func TestProvider_GetTopology(t *testing.T) { 76 | ctx := context.Background() 77 | 78 | clientConfig := client.Config{Endpoints: []string{"http://127.0.0.1:2379"}} 79 | 80 | c, err := client.New(clientConfig) 81 | require.NoError(t, err) 82 | 83 | kapi := client.NewKeysAPI(c) 84 | require.NotNil(t, kapi) 85 | 86 | t.Run("nodes error", func(t *testing.T) { 87 | nodesErrorPath := "/no-nodes" 88 | 89 | p, err := NewProvider(ctx, Config{EtcdConfig: clientConfig, Path: nodesErrorPath}) 90 | require.NoError(t, err) 91 | require.NotNil(t, p) 92 | 93 | _, err = kapi.Set(ctx, nodesErrorPath, "test", &client.SetOptions{}) 94 | require.NoError(t, err) 95 | 96 | _, err = p.GetTopology() 97 | require.ErrorIs(t, err, ErrNodesError) 98 | }) 99 | 100 | t.Run("no clusters or instances nodes error", func(t *testing.T) { 101 | nodesClusterErrPath := "/test-nodes-cluster-instances-error" 102 | 103 | p, err := NewProvider(ctx, Config{EtcdConfig: clientConfig, Path: nodesClusterErrPath}) 104 | require.NoError(t, err) 105 | require.NotNil(t, p) 106 | 107 | _, _ = kapi.Set(ctx, nodesClusterErrPath, "", &client.SetOptions{Dir: true}) 108 | 109 | _, err = kapi.Set(ctx, nodesClusterErrPath+"/clusters", "test", &client.SetOptions{}) 110 | 111 | require.NoError(t, err) 112 | 113 | _, err = kapi.Set(ctx, nodesClusterErrPath+"/instances", "test", &client.SetOptions{}) 114 | 115 | require.NoError(t, err) 116 | 117 | _, err = p.GetTopology() 118 | require.NotErrorIs(t, err, ErrNodesError) 119 | require.True(t, errors.Is(err, ErrClusterError) || errors.Is(err, ErrInstancesError)) 120 | }) 121 | 122 | t.Run("ok topology", func(t *testing.T) { 123 | /*tarantool: 124 | userdb: 125 | clusters: 126 | userdb: 127 | master: userdb_001 128 | replicaset_uuid: 045e12d8-0001-0000-0000-000000000000 129 | common: 130 | box: 131 | log_level: 5 132 | memtx_memory: 268435456 133 | instances: 134 | userdb_001: 135 | cluster: userdb 136 | box: 137 | instance_uuid: 045e12d8-0000-0001-0000-000000000000 138 | listen: 10.0.1.11:3301 139 | userdb_002: 140 | cluster: userdb 141 | box: 142 | instance_uuid: 045e12d8-0000-0002-0000-000000000000 143 | listen: 10.0.1.12:3302 144 | userdb_003: 145 | cluster: userdb 146 | box: 147 | instance_uuid: 045e12d8-0000-0003-0000-000000000000 148 | listen: 10.0.1.13:3303 149 | */ 150 | 151 | dbName := "userdb" 152 | _, _ = kapi.Set(ctx, dbName, "", &client.SetOptions{Dir: true}) 153 | 154 | p, err := NewProvider(ctx, Config{EtcdConfig: clientConfig, Path: dbName}) 155 | require.NoError(t, err) 156 | 157 | // set root paths 158 | _, _ = kapi.Set(ctx, fmt.Sprintf("%s/%s", dbName, "clusters"), "", &client.SetOptions{Dir: true}) 159 | _, _ = kapi.Set(ctx, fmt.Sprintf("%s/%s", dbName, "instances"), "", &client.SetOptions{Dir: true}) 160 | // set cluster 161 | _, _ = kapi.Set(ctx, fmt.Sprintf("%s/%s/%s", dbName, "clusters", "userdb"), "", &client.SetOptions{Dir: true}) 162 | 163 | _, _ = kapi.Set(ctx, fmt.Sprintf("%s/%s/%s", dbName, "instances", "userdb_001"), "", &client.SetOptions{Dir: true}) 164 | _, _ = kapi.Set(ctx, fmt.Sprintf("%s/%s/%s/%s", dbName, "instances", "userdb_001", "cluster"), "userdb", &client.SetOptions{Dir: false}) 165 | 166 | _, _ = kapi.Set(ctx, fmt.Sprintf("%s/%s/%s/%s", dbName, "instances", "userdb_001", "box"), "", &client.SetOptions{Dir: true}) 167 | _, _ = kapi.Set(ctx, fmt.Sprintf("%s/%s/%s/%s/%s", dbName, "instances", "userdb_001", "box", "listen"), "10.0.1.13:3303", &client.SetOptions{Dir: false}) 168 | 169 | topology, err := p.GetTopology() 170 | require.NoError(t, err) 171 | require.NotNil(t, topology) 172 | }) 173 | 174 | t.Run("mapCluster2Instances", func(t *testing.T) { 175 | t.Parallel() 176 | 177 | tCases := []struct { 178 | name string 179 | replicasets []vshardrouter.ReplicasetInfo 180 | instances map[string][]*vshardrouter.InstanceInfo 181 | result map[vshardrouter.ReplicasetInfo][]vshardrouter.InstanceInfo 182 | }{ 183 | // test case 1 184 | { 185 | name: "1 rs and 1 instance for this rs", 186 | replicasets: []vshardrouter.ReplicasetInfo{ 187 | { 188 | Name: "rs_1", 189 | }, 190 | }, 191 | instances: map[string][]*vshardrouter.InstanceInfo{ 192 | "rs_1": { 193 | { 194 | Name: "inst_1_1", 195 | }, 196 | }, 197 | }, 198 | result: map[vshardrouter.ReplicasetInfo][]vshardrouter.InstanceInfo{ 199 | { 200 | Name: "rs_1", 201 | }: { 202 | { 203 | Name: "inst_1_1", 204 | }, 205 | }, 206 | }, 207 | }, 208 | // test case 2 209 | { 210 | name: "1 rs and 2 instance for this rs", 211 | replicasets: []vshardrouter.ReplicasetInfo{ 212 | { 213 | Name: "rs_1", 214 | }, 215 | }, 216 | instances: map[string][]*vshardrouter.InstanceInfo{ 217 | "rs_1": { 218 | { 219 | Name: "inst_1_1", 220 | }, 221 | { 222 | Name: "inst_1_2", 223 | }, 224 | }, 225 | }, 226 | result: map[vshardrouter.ReplicasetInfo][]vshardrouter.InstanceInfo{ 227 | { 228 | Name: "rs_1", 229 | }: { 230 | { 231 | Name: "inst_1_1", 232 | }, 233 | { 234 | Name: "inst_1_2", 235 | }, 236 | }, 237 | }, 238 | }, 239 | } 240 | 241 | for _, tCase := range tCases { 242 | t.Run(tCase.name, func(t *testing.T) { 243 | require.EqualValues(t, mapCluster2Instances(tCase.replicasets, tCase.instances), tCase.result) 244 | }) 245 | } 246 | }) 247 | } 248 | 249 | func runTestMain(m *testing.M) int { 250 | config := embed.NewConfig() 251 | 252 | config.Name = "localhost" 253 | config.Dir = "/tmp/my-embedded-ectd-cluster" 254 | 255 | config.ListenPeerUrls = parseEtcdUrls([]string{"http://0.0.0.0:2380"}) 256 | config.ListenClientUrls = parseEtcdUrls([]string{"http://0.0.0.0:2379"}) 257 | config.AdvertisePeerUrls = parseEtcdUrls([]string{"http://localhost:2380"}) 258 | config.AdvertiseClientUrls = parseEtcdUrls([]string{"http://localhost:2379"}) 259 | config.InitialCluster = "localhost=http://localhost:2380" 260 | config.LogLevel = "panic" 261 | 262 | // enable v2 263 | config.EnableV2 = true 264 | 265 | etcd, err := embed.StartEtcd(config) 266 | if err != nil { 267 | panic(err) 268 | } 269 | 270 | defer etcd.Close() 271 | 272 | return m.Run() 273 | } 274 | 275 | func TestProvider_Init(t *testing.T) { 276 | ctx := context.Background() 277 | 278 | clientConfig := client.Config{Endpoints: []string{"http://127.0.0.1:2379"}} 279 | 280 | p, err := NewProvider(ctx, Config{EtcdConfig: clientConfig}) 281 | require.NoError(t, err) 282 | require.NotNil(t, p) 283 | 284 | t.Run("init with wrong provider returns topology error", func(t *testing.T) { 285 | tc := mocktopology.NewTopologyController(t) 286 | 287 | require.Error(t, p.Init(tc)) 288 | }) 289 | } 290 | 291 | func TestProvider_Close(t *testing.T) { 292 | ctx := context.Background() 293 | 294 | clientConfig := client.Config{Endpoints: []string{"http://127.0.0.1:2379"}} 295 | 296 | p, err := NewProvider(ctx, Config{EtcdConfig: clientConfig}) 297 | require.NoError(t, err) 298 | require.NotNil(t, p) 299 | 300 | require.NotPanics(t, func() { 301 | p.Close() 302 | }) 303 | } 304 | 305 | func TestMain(m *testing.M) { 306 | code := runTestMain(m) 307 | os.Exit(code) 308 | } 309 | -------------------------------------------------------------------------------- /providers/prometheus/prometheus.go: -------------------------------------------------------------------------------- 1 | package prometheus 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | 7 | "github.com/prometheus/client_golang/prometheus" 8 | vshardrouter "github.com/tarantool/go-vshard-router/v2" 9 | ) 10 | 11 | // Check that provider implements MetricsProvider interface 12 | var _ vshardrouter.MetricsProvider = (*Provider)(nil) 13 | 14 | // Check that provider implements Collector interface 15 | var _ prometheus.Collector = (*Provider)(nil) 16 | 17 | // Provider is a struct that implements collector and provider methods. 18 | // It gives users a simple way to use metrics for go-vshard-router. 19 | type Provider struct { 20 | // cronDiscoveryEvent - histogram for cron discovery events. 21 | cronDiscoveryEvent *prometheus.HistogramVec 22 | // retryOnCall - counter for retry calls. 23 | retryOnCall *prometheus.CounterVec 24 | // requestDuration - histogram for map reduce and single request durations. 25 | requestDuration *prometheus.HistogramVec 26 | } 27 | 28 | // Describe sends the descriptors of each metric to the provided channel. 29 | func (pp *Provider) Describe(ch chan<- *prometheus.Desc) { 30 | pp.cronDiscoveryEvent.Describe(ch) 31 | pp.retryOnCall.Describe(ch) 32 | pp.requestDuration.Describe(ch) 33 | } 34 | 35 | // Collect gathers the metrics and sends them to the provided channel. 36 | func (pp *Provider) Collect(ch chan<- prometheus.Metric) { 37 | pp.cronDiscoveryEvent.Collect(ch) 38 | pp.retryOnCall.Collect(ch) 39 | pp.requestDuration.Collect(ch) 40 | } 41 | 42 | // CronDiscoveryEvent records the duration of a cron discovery event with labels. 43 | func (pp *Provider) CronDiscoveryEvent(ok bool, duration time.Duration, reason string) { 44 | pp.cronDiscoveryEvent.With(prometheus.Labels{ 45 | "ok": strconv.FormatBool(ok), 46 | "reason": reason, 47 | }).Observe(float64(duration.Milliseconds())) 48 | } 49 | 50 | // RetryOnCall increments the retry counter for a specific reason. 51 | func (pp *Provider) RetryOnCall(reason string) { 52 | pp.retryOnCall.With(prometheus.Labels{ 53 | "reason": reason, 54 | }).Inc() 55 | } 56 | 57 | // RequestDuration records the duration of a request with labels for success and map-reduce usage. 58 | func (pp *Provider) RequestDuration(duration time.Duration, procedure string, ok, mapReduce bool) { 59 | pp.requestDuration.With(prometheus.Labels{ 60 | "ok": strconv.FormatBool(ok), 61 | "map_reduce": strconv.FormatBool(mapReduce), 62 | "procedure": procedure, 63 | }).Observe(float64(duration.Milliseconds())) 64 | } 65 | 66 | // NewPrometheusProvider - is an experimental function. 67 | // Prometheus Provider is one of the ready-to-use providers implemented 68 | // for go-vshard-router. It can be used to easily integrate metrics into 69 | // your service without worrying about buckets, metric names, or other 70 | // metric options. 71 | // 72 | // The provider implements both the interface required by vshard-router 73 | // and the Prometheus collector interface. 74 | // 75 | // To register it in your Prometheus instance, use: 76 | // registry.MustRegister(provider) 77 | // 78 | // Then, pass it to go-vshard-router so that it can manage the metrics: 79 | // 80 | // vshard_router.NewRouter(ctx, vshard_router.Config{ 81 | // Metrics: provider, 82 | // }) 83 | // 84 | // This approach simplifies the process of collecting and handling metrics, 85 | // freeing you from manually managing metric-specific configurations. 86 | func NewPrometheusProvider() *Provider { 87 | return &Provider{ 88 | cronDiscoveryEvent: prometheus.NewHistogramVec(prometheus.HistogramOpts{ 89 | Name: "cron_discovery_event", 90 | Namespace: "vshard", 91 | }, []string{"ok", "reason"}), // Histogram for tracking cron discovery events 92 | 93 | retryOnCall: prometheus.NewCounterVec(prometheus.CounterOpts{ 94 | Name: "retry_on_call", 95 | Namespace: "vshard", 96 | }, []string{"reason"}), // Counter for retry attempts 97 | 98 | requestDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{ 99 | Name: "request_duration", 100 | Namespace: "vshard", 101 | }, []string{"procedure", "ok", "map_reduce"}), // Histogram for request durations 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /providers/prometheus/prometheus_example_test.go: -------------------------------------------------------------------------------- 1 | package prometheus 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "net/http/httptest" 8 | "strings" 9 | "time" 10 | 11 | "github.com/prometheus/client_golang/prometheus" 12 | "github.com/prometheus/client_golang/prometheus/promhttp" 13 | ) 14 | 15 | func ExampleNewPrometheusProvider() { 16 | // Let's create new prometheus provider. 17 | provider := NewPrometheusProvider() 18 | 19 | // Create new prometheus registry. 20 | registry := prometheus.NewRegistry() 21 | // Register prometheus provider. 22 | registry.MustRegister(provider) 23 | 24 | // Create example http server. 25 | server := httptest.NewServer(promhttp.HandlerFor(registry, promhttp.HandlerOpts{})) 26 | defer server.Close() 27 | 28 | // Then we can register our provider in go vshard router. 29 | // It will use our provider to write prometheus metrics. 30 | /* 31 | vshard_router.NewRouter(ctx, vshard_router.Config{ 32 | Metrics: provider, 33 | }) 34 | */ 35 | 36 | provider.CronDiscoveryEvent(true, 150*time.Millisecond, "success") 37 | provider.RetryOnCall("timeout") 38 | provider.RequestDuration(200*time.Millisecond, "test", true, false) 39 | 40 | resp, err := http.Get(server.URL + "/metrics") 41 | if err != nil { 42 | panic(err) 43 | } 44 | 45 | defer resp.Body.Close() 46 | 47 | body, err := io.ReadAll(resp.Body) 48 | if err != nil { 49 | panic(err) 50 | } 51 | 52 | metricsOutput := string(body) 53 | 54 | if strings.Contains(metricsOutput, "vshard_request_duration_bucket") { 55 | fmt.Println("Metrics output contains vshard_request_duration_bucket") 56 | } 57 | if strings.Contains(metricsOutput, "vshard_cron_discovery_event_bucket") { 58 | fmt.Println("Metrics output contains vshard_cron_discovery_event_bucket") 59 | } 60 | 61 | if strings.Contains(metricsOutput, "vshard_retry_on_call") { 62 | fmt.Println("Metrics output contains vshard_retry_on_call") 63 | } 64 | // Output: Metrics output contains vshard_request_duration_bucket 65 | // Metrics output contains vshard_cron_discovery_event_bucket 66 | // Metrics output contains vshard_retry_on_call 67 | } 68 | -------------------------------------------------------------------------------- /providers/prometheus/prometheus_test.go: -------------------------------------------------------------------------------- 1 | package prometheus 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | "time" 9 | 10 | "github.com/prometheus/client_golang/prometheus" 11 | "github.com/prometheus/client_golang/prometheus/promhttp" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestPrometheusMetricsServer(t *testing.T) { 16 | provider := NewPrometheusProvider() 17 | 18 | registry := prometheus.NewRegistry() 19 | registry.MustRegister(provider) 20 | 21 | server := httptest.NewServer(promhttp.HandlerFor(registry, promhttp.HandlerOpts{})) 22 | defer server.Close() 23 | 24 | provider.CronDiscoveryEvent(true, 150*time.Millisecond, "success") 25 | provider.RetryOnCall("timeout") 26 | provider.RequestDuration(200*time.Millisecond, "test", true, false) 27 | 28 | resp, err := http.Get(server.URL + "/metrics") 29 | require.NoError(t, err) 30 | 31 | defer resp.Body.Close() 32 | 33 | body, err := io.ReadAll(resp.Body) 34 | require.NoError(t, err) 35 | metricsOutput := string(body) 36 | 37 | require.Contains(t, metricsOutput, "vshard_request_duration_bucket") 38 | require.Contains(t, metricsOutput, "vshard_cron_discovery_event_bucket") 39 | require.Contains(t, metricsOutput, "vshard_retry_on_call") 40 | } 41 | -------------------------------------------------------------------------------- /providers/slog/slog.go: -------------------------------------------------------------------------------- 1 | package slog 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log/slog" 7 | 8 | vshardrouter "github.com/tarantool/go-vshard-router/v2" 9 | ) 10 | 11 | // Check that provider implements LogfProvider interface. 12 | var _ vshardrouter.LogfProvider = (*SlogLoggerf)(nil) 13 | 14 | // NewSlogLogger wraps slog logger for go-vshard-router. 15 | func NewSlogLogger(logger *slog.Logger) *SlogLoggerf { 16 | return &SlogLoggerf{ 17 | Logger: logger, 18 | } 19 | } 20 | 21 | // SlogLoggerf is adapter for slog to Logger interface. 22 | type SlogLoggerf struct { 23 | Logger *slog.Logger 24 | } 25 | 26 | // Debugf implements Debugf method for LogfProvider interface 27 | func (s *SlogLoggerf) Debugf(ctx context.Context, format string, v ...any) { 28 | if !s.Logger.Enabled(ctx, slog.LevelDebug) { 29 | return 30 | } 31 | s.Logger.DebugContext(ctx, fmt.Sprintf(format, v...)) 32 | } 33 | 34 | // Infof implements Infof method for LogfProvider interface 35 | func (s *SlogLoggerf) Infof(ctx context.Context, format string, v ...any) { 36 | if !s.Logger.Enabled(ctx, slog.LevelInfo) { 37 | return 38 | } 39 | s.Logger.InfoContext(ctx, fmt.Sprintf(format, v...)) 40 | } 41 | 42 | // Warnf implements Warnf method for LogfProvider interface 43 | func (s *SlogLoggerf) Warnf(ctx context.Context, format string, v ...any) { 44 | if !s.Logger.Enabled(ctx, slog.LevelWarn) { 45 | return 46 | } 47 | s.Logger.WarnContext(ctx, fmt.Sprintf(format, v...)) 48 | } 49 | 50 | // Errorf implements Errorf method for LogfProvider interface 51 | func (s *SlogLoggerf) Errorf(ctx context.Context, format string, v ...any) { 52 | if !s.Logger.Enabled(ctx, slog.LevelError) { 53 | return 54 | } 55 | s.Logger.ErrorContext(ctx, fmt.Sprintf(format, v...)) 56 | } 57 | -------------------------------------------------------------------------------- /providers/slog/slog_test.go: -------------------------------------------------------------------------------- 1 | package slog 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "os" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | vshardrouter "github.com/tarantool/go-vshard-router/v2" 11 | ) 12 | 13 | func TestNewSlogLogger(t *testing.T) { 14 | var slogProvider vshardrouter.LogfProvider 15 | 16 | require.NotPanics(t, func() { 17 | slogProvider = NewSlogLogger(nil) 18 | }) 19 | 20 | require.Panics(t, func() { 21 | slogProvider.Warnf(context.TODO(), "") 22 | }) 23 | } 24 | 25 | func TestSlogProvider(t *testing.T) { 26 | ctx := context.Background() 27 | 28 | // create new logger handler 29 | handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ 30 | Level: slog.LevelError, 31 | }) 32 | // create new SLogger instance 33 | sLogger := slog.New(handler) 34 | 35 | logProvider := NewSlogLogger(sLogger) 36 | 37 | require.NotPanics(t, func() { 38 | logProvider.Infof(ctx, "test %s", "s") 39 | }) 40 | 41 | require.NotPanics(t, func() { 42 | logProvider.Warnf(ctx, "test %s", "s") 43 | }) 44 | 45 | require.NotPanics(t, func() { 46 | logProvider.Errorf(ctx, "test %s", "s") 47 | }) 48 | 49 | require.NotPanics(t, func() { 50 | logProvider.Debugf(ctx, "test %s", "s") 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /providers/static/provider.go: -------------------------------------------------------------------------------- 1 | package static 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | vshardrouter "github.com/tarantool/go-vshard-router/v2" 8 | ) 9 | 10 | // Check that provider implements TopologyProvider interface 11 | var _ vshardrouter.TopologyProvider = (*Provider)(nil) 12 | 13 | type Provider struct { 14 | rs map[vshardrouter.ReplicasetInfo][]vshardrouter.InstanceInfo 15 | } 16 | 17 | func NewProvider(rs map[vshardrouter.ReplicasetInfo][]vshardrouter.InstanceInfo) *Provider { 18 | if rs == nil { 19 | panic("rs must not be nil") 20 | } 21 | 22 | if len(rs) == 0 { 23 | panic("rs must not be empty") 24 | } 25 | 26 | return &Provider{rs: rs} 27 | } 28 | 29 | func (p *Provider) Validate() error { 30 | for rs := range p.rs { 31 | // check replicaset name 32 | if rs.Name == "" { 33 | return fmt.Errorf("one of replicaset name is empty") 34 | } 35 | } 36 | 37 | return nil 38 | } 39 | 40 | func (p *Provider) Init(c vshardrouter.TopologyController) error { 41 | return c.AddReplicasets(context.TODO(), p.rs) 42 | } 43 | 44 | func (p *Provider) Close() {} 45 | -------------------------------------------------------------------------------- /providers/static/provider_test.go: -------------------------------------------------------------------------------- 1 | package static 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/google/uuid" 8 | "github.com/stretchr/testify/require" 9 | 10 | vshardrouter "github.com/tarantool/go-vshard-router/v2" 11 | ) 12 | 13 | func TestNewProvider(t *testing.T) { 14 | tCases := []struct { 15 | Source map[vshardrouter.ReplicasetInfo][]vshardrouter.InstanceInfo 16 | }{ 17 | {nil}, 18 | {make(map[vshardrouter.ReplicasetInfo][]vshardrouter.InstanceInfo)}, 19 | {map[vshardrouter.ReplicasetInfo][]vshardrouter.InstanceInfo{ 20 | {}: { 21 | vshardrouter.InstanceInfo{}, 22 | vshardrouter.InstanceInfo{}, 23 | }, 24 | }}, 25 | } 26 | 27 | for _, tc := range tCases { 28 | tc := tc 29 | 30 | t.Run("provider", func(t *testing.T) { 31 | t.Parallel() 32 | if len(tc.Source) == 0 { 33 | 34 | require.Panics(t, func() { 35 | NewProvider(tc.Source) 36 | }) 37 | 38 | return 39 | } 40 | 41 | require.NotPanics(t, func() { 42 | provider := NewProvider(tc.Source) 43 | require.NotNil(t, provider) 44 | }) 45 | }) 46 | } 47 | } 48 | 49 | func TestProvider_Validate(t *testing.T) { 50 | tCases := []struct { 51 | Name string 52 | Source map[vshardrouter.ReplicasetInfo][]vshardrouter.InstanceInfo 53 | IsErr bool 54 | }{ 55 | { 56 | Name: "empty name", 57 | Source: map[vshardrouter.ReplicasetInfo][]vshardrouter.InstanceInfo{ 58 | {}: { 59 | vshardrouter.InstanceInfo{}, 60 | vshardrouter.InstanceInfo{}, 61 | }, 62 | }, 63 | IsErr: true, 64 | }, 65 | { 66 | Name: "no uuid", 67 | Source: map[vshardrouter.ReplicasetInfo][]vshardrouter.InstanceInfo{ 68 | {Name: "rs_1"}: { 69 | vshardrouter.InstanceInfo{}, 70 | vshardrouter.InstanceInfo{}, 71 | }, 72 | }, 73 | IsErr: false, // uuid is not required. tarantool3 have an only alias support. 74 | }, 75 | { 76 | Name: "valid", 77 | Source: map[vshardrouter.ReplicasetInfo][]vshardrouter.InstanceInfo{ 78 | {Name: "rs_1", UUID: uuid.New()}: { 79 | vshardrouter.InstanceInfo{}, 80 | vshardrouter.InstanceInfo{}, 81 | }, 82 | }, 83 | IsErr: false, 84 | }, 85 | } 86 | 87 | for _, tc := range tCases { 88 | tc := tc 89 | t.Run(fmt.Sprintf("is err: %v", tc.IsErr), func(t *testing.T) { 90 | t.Parallel() 91 | provider := NewProvider(tc.Source) 92 | if tc.IsErr { 93 | require.Error(t, provider.Validate()) 94 | return 95 | } 96 | 97 | require.NoError(t, provider.Validate()) 98 | }) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /providers/viper/README.md: -------------------------------------------------------------------------------- 1 | # Viper Topology Provider 2 | 3 | The Viper topology provider allows you to work with Tarantool configuration and retrieve topology from all sources, [which are supported by Viper in file formats](https://github.com/spf13/viper?tab=readme-ov-file#what-is-viper). 4 | 5 | There are 2 supported configuration formats: moonlibs and tarantool3. 6 | ## Examples 7 | 8 | ### ETCD v3 9 | ```go 10 | import ( 11 | "context" 12 | "github.com/spf13/viper" 13 | _ "github.com/spf13/viper/remote" // dont forget to import remote pkg for viper remote 14 | vprovider "github.com/tarantool/go-vshard-router/providers/viper" 15 | ) 16 | 17 | // ... 18 | ctx := context.TODO() 19 | key := "/myapp" 20 | etcdViper := viper.New() 21 | err = etcdViper.AddRemoteProvider("etcd3", "http://127.0.0.1:2379", key) 22 | if err != nil { 23 | panic(err) 24 | } 25 | 26 | etcdViper.SetConfigType("yaml") 27 | err = etcdViper.ReadRemoteConfig() 28 | if err != nil { 29 | panic(err) 30 | } 31 | 32 | provider := vprovider.NewProvider(ctx, etcdViper, vprovider.ConfigTypeTarantool3) 33 | 34 | /// ... 35 | ``` 36 | 37 | Check more config examples in test dir or inside provider_test.go -------------------------------------------------------------------------------- /providers/viper/moonlibs/config.go: -------------------------------------------------------------------------------- 1 | package moonlibs 2 | 3 | // ----- Moonlibs configuration ----- 4 | 5 | // Config is a representation of the topology configuration for tarantool version below 3. 6 | // based on https://github.com/moonlibs/config?tab=readme-ov-file#example-of-etcd-configuration-etcdclustermaster. 7 | type Config struct { 8 | Topology SourceTopologyConfig `json:"topology"` 9 | } 10 | 11 | type SourceTopologyConfig struct { 12 | Clusters map[string]ClusterInfo `json:"clusters,omitempty" yaml:"clusters" ` 13 | Instances map[string]InstanceInfo `json:"instances,omitempty" yaml:"instances"` 14 | } 15 | 16 | type ClusterInfo struct { 17 | ReplicasetUUID string `yaml:"replicaset_uuid" mapstructure:"replicaset_uuid"` 18 | } 19 | 20 | type InstanceInfo struct { 21 | Cluster string 22 | Box struct { 23 | Listen string `json:"listen,omitempty" yaml:"listen" mapstructure:"listen"` 24 | InstanceUUID string `yaml:"instance_uuid" mapstructure:"instance_uuid" json:"instanceUUID,omitempty"` 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /providers/viper/moonlibs/convert.go: -------------------------------------------------------------------------------- 1 | package moonlibs 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/google/uuid" 8 | vshardrouter "github.com/tarantool/go-vshard-router/v2" 9 | ) 10 | 11 | func (cfg *Config) Convert() (map[vshardrouter.ReplicasetInfo][]vshardrouter.InstanceInfo, error) { 12 | if cfg.Topology.Instances == nil { 13 | return nil, fmt.Errorf("no topology instances found") 14 | } 15 | 16 | if cfg.Topology.Clusters == nil { 17 | return nil, fmt.Errorf("no topology clusters found") 18 | } 19 | 20 | // prepare vshard router config 21 | vshardRouterTopology := make(map[vshardrouter.ReplicasetInfo][]vshardrouter.InstanceInfo) 22 | 23 | for rsName, rs := range cfg.Topology.Clusters { 24 | rsUUID, err := uuid.Parse(rs.ReplicasetUUID) 25 | if err != nil { 26 | return nil, fmt.Errorf("invalid topology replicaset UUID: %s", rs.ReplicasetUUID) 27 | } 28 | 29 | rsInstances := make([]vshardrouter.InstanceInfo, 0) 30 | 31 | for instName, instInfo := range cfg.Topology.Instances { 32 | if instInfo.Cluster != rsName { 33 | continue 34 | } 35 | 36 | instUUID, err := uuid.Parse(instInfo.Box.InstanceUUID) 37 | if err != nil { 38 | log.Printf("Can't parse replicaset uuid: %s", err) 39 | 40 | return nil, fmt.Errorf("invalid topology instance UUID: %s", instInfo.Box.InstanceUUID) 41 | } 42 | 43 | rsInstances = append(rsInstances, vshardrouter.InstanceInfo{ 44 | Name: instName, 45 | Addr: instInfo.Box.Listen, 46 | UUID: instUUID, 47 | }) 48 | } 49 | 50 | vshardRouterTopology[vshardrouter.ReplicasetInfo{ 51 | Name: rsName, 52 | UUID: rsUUID, 53 | }] = rsInstances 54 | } 55 | 56 | return vshardRouterTopology, nil 57 | } 58 | -------------------------------------------------------------------------------- /providers/viper/moonlibs/convert_test.go: -------------------------------------------------------------------------------- 1 | package moonlibs 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/uuid" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestConfig_Convert(t *testing.T) { 11 | t.Parallel() 12 | 13 | t.Run("name not empty", func(t *testing.T) { 14 | t.Parallel() 15 | cfg := Config{Topology: SourceTopologyConfig{ 16 | Clusters: map[string]ClusterInfo{ 17 | "cluster_1": { 18 | ReplicasetUUID: uuid.New().String(), 19 | }, 20 | }, 21 | Instances: map[string]InstanceInfo{ 22 | "instance_1": { 23 | Cluster: "cluster_1", 24 | Box: struct { 25 | Listen string `json:"listen,omitempty" yaml:"listen" mapstructure:"listen"` 26 | InstanceUUID string `yaml:"instance_uuid" mapstructure:"instance_uuid" json:"instanceUUID,omitempty"` 27 | }(struct { 28 | Listen string 29 | InstanceUUID string 30 | }{Listen: "0.0.0.0:1111", InstanceUUID: uuid.New().String()}), 31 | }, 32 | }, 33 | }} 34 | 35 | m, err := cfg.Convert() 36 | require.NoError(t, err) 37 | 38 | for _, instances := range m { 39 | require.Len(t, instances, 1) 40 | require.NotEmpty(t, instances[0].Name) 41 | } 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /providers/viper/provider.go: -------------------------------------------------------------------------------- 1 | package viper 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/google/uuid" 8 | srcviper "github.com/spf13/viper" 9 | vshardrouter "github.com/tarantool/go-vshard-router/v2" 10 | "github.com/tarantool/go-vshard-router/v2/providers/viper/moonlibs" 11 | "github.com/tarantool/go-vshard-router/v2/providers/viper/tarantool3" 12 | ) 13 | 14 | // Check that provider implements TopologyProvider interface 15 | var _ vshardrouter.TopologyProvider = (*Provider)(nil) 16 | 17 | type Provider struct { 18 | ctx context.Context 19 | 20 | v *srcviper.Viper 21 | rs map[vshardrouter.ReplicasetInfo][]vshardrouter.InstanceInfo 22 | } 23 | 24 | type ConfigType int 25 | 26 | const ( 27 | ConfigTypeMoonlibs ConfigType = iota 28 | ConfigTypeTarantool3 29 | ) 30 | 31 | type Convertable interface { 32 | Convert() (map[vshardrouter.ReplicasetInfo][]vshardrouter.InstanceInfo, error) 33 | } 34 | 35 | func NewProvider(ctx context.Context, v *srcviper.Viper, cfgType ConfigType) *Provider { 36 | if v == nil { 37 | panic("viper entity is nil") 38 | } 39 | 40 | var cfg Convertable 41 | 42 | switch cfgType { 43 | case ConfigTypeMoonlibs: 44 | cfg = &moonlibs.Config{} 45 | case ConfigTypeTarantool3: 46 | cfg = &tarantool3.Config{} 47 | default: 48 | panic("unknown config type") 49 | } 50 | 51 | err := v.Unmarshal(cfg) 52 | if err != nil { 53 | panic(err) 54 | } 55 | 56 | resultMap, err := cfg.Convert() 57 | if err != nil { 58 | panic(err) 59 | } 60 | 61 | return &Provider{ctx: ctx, v: v, rs: resultMap} 62 | } 63 | 64 | func (p *Provider) WatchChanges() *Provider { 65 | // todo 66 | return p 67 | } 68 | 69 | func (p *Provider) Topology() map[vshardrouter.ReplicasetInfo][]vshardrouter.InstanceInfo { 70 | return p.rs 71 | } 72 | 73 | func (p *Provider) Validate() error { 74 | if len(p.rs) < 1 { 75 | return fmt.Errorf("replicasets are empty") 76 | } 77 | 78 | for rs := range p.rs { 79 | // check replicaset name 80 | if rs.Name == "" { 81 | return fmt.Errorf("one of replicaset name is empty") 82 | } 83 | 84 | // check replicaset uuid 85 | if rs.UUID == uuid.Nil { 86 | return fmt.Errorf("one of replicaset uuid is empty") 87 | } 88 | } 89 | 90 | return nil 91 | } 92 | 93 | func (p *Provider) Init(c vshardrouter.TopologyController) error { 94 | return c.AddReplicasets(p.ctx, p.rs) 95 | } 96 | 97 | func (p *Provider) Close() {} 98 | -------------------------------------------------------------------------------- /providers/viper/provider_test.go: -------------------------------------------------------------------------------- 1 | package viper_test 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net/url" 7 | "os" 8 | "testing" 9 | 10 | "github.com/spf13/viper" 11 | _ "github.com/spf13/viper/remote" 12 | "github.com/stretchr/testify/require" 13 | vprovider "github.com/tarantool/go-vshard-router/v2/providers/viper" 14 | clientv3 "go.etcd.io/etcd/client/v3" 15 | "go.etcd.io/etcd/server/v3/embed" 16 | ) 17 | 18 | func TestProvider_Close(t *testing.T) { 19 | require.NotPanics(t, (&vprovider.Provider{}).Close) 20 | } 21 | 22 | func TestNewProviderNilPanic(t *testing.T) { 23 | require.Panics(t, func() { 24 | vprovider.NewProvider(context.TODO(), nil, vprovider.ConfigTypeMoonlibs) 25 | }) 26 | } 27 | 28 | func TestNewProviderDirect(t *testing.T) { 29 | ctx := context.TODO() 30 | v := viper.New() 31 | 32 | v.AddConfigPath("testdata/") 33 | v.SetConfigName("config-direct") 34 | v.SetConfigType("yaml") 35 | 36 | err := v.ReadInConfig() 37 | require.NoError(t, err) 38 | 39 | provider := vprovider.NewProvider(ctx, v, vprovider.ConfigTypeMoonlibs) 40 | 41 | anyProviderValidation(t, provider) 42 | } 43 | 44 | func TestNewProviderSub(t *testing.T) { 45 | ctx := context.TODO() 46 | v := viper.New() 47 | 48 | v.AddConfigPath("testdata/") 49 | v.SetConfigName("config-sub") 50 | v.SetConfigType("yaml") 51 | 52 | err := v.ReadInConfig() 53 | require.NoError(t, err) 54 | 55 | v = v.Sub("supbpath") 56 | 57 | provider := vprovider.NewProvider(ctx, v, vprovider.ConfigTypeMoonlibs) 58 | 59 | anyProviderValidation(t, provider) 60 | } 61 | 62 | func TestNewProvider_Tarantool3(t *testing.T) { 63 | ctx := context.TODO() 64 | v := viper.New() 65 | 66 | v.AddConfigPath("testdata/") 67 | v.SetConfigName("config-tarantool3") 68 | v.SetConfigType("yaml") 69 | 70 | err := v.ReadInConfig() 71 | require.NoError(t, err) 72 | 73 | provider := vprovider.NewProvider(ctx, v, vprovider.ConfigTypeTarantool3) 74 | 75 | anyProviderValidation(t, provider) 76 | } 77 | 78 | func parseEtcdUrls(strs []string) []url.URL { 79 | urls := make([]url.URL, 0, len(strs)) 80 | 81 | for _, str := range strs { 82 | u, err := url.Parse(str) 83 | if err != nil { 84 | log.Printf("Invalid url %s, error: %s", str, err.Error()) 85 | continue 86 | 87 | } 88 | urls = append(urls, *u) 89 | } 90 | 91 | return urls 92 | 93 | } 94 | 95 | func TestNewProvider_ETCD3(t *testing.T) { 96 | ctx := context.TODO() 97 | 98 | config := embed.NewConfig() 99 | 100 | config.Name = "localhost" 101 | config.Dir = "/tmp/my-embedded-ectd-cluster" 102 | 103 | config.ListenPeerUrls = parseEtcdUrls([]string{"http://0.0.0.0:2480"}) 104 | config.ListenClientUrls = parseEtcdUrls([]string{"http://0.0.0.0:2479"}) 105 | config.AdvertisePeerUrls = parseEtcdUrls([]string{"http://localhost:2480"}) 106 | config.AdvertiseClientUrls = parseEtcdUrls([]string{"http://localhost:2479"}) 107 | config.InitialCluster = "localhost=http://localhost:2480" 108 | config.LogLevel = "panic" 109 | 110 | etcd, err := embed.StartEtcd(config) 111 | require.NoError(t, err) 112 | 113 | defer etcd.Close() 114 | 115 | client, err := clientv3.New(clientv3.Config{ 116 | Endpoints: []string{"localhost:2479"}, 117 | }) 118 | require.NoError(t, err) 119 | 120 | defer client.Close() 121 | 122 | kv := clientv3.NewKV(client) 123 | key := "/myapp" 124 | 125 | cfgBts, err := os.ReadFile("testdata/config-tarantool3.yaml") 126 | require.NoError(t, err) 127 | 128 | _, err = kv.Put(ctx, key, string(cfgBts)) 129 | require.NoError(t, err) 130 | 131 | getResponse, err := kv.Get(ctx, key) 132 | require.NoError(t, err) 133 | require.NotEmpty(t, getResponse.Kvs[0].Value) 134 | 135 | t.Run("ok reads config", func(t *testing.T) { 136 | etcdViper := viper.New() 137 | err = etcdViper.AddRemoteProvider("etcd3", "http://127.0.0.1:2479", key) 138 | require.NoError(t, err) 139 | 140 | etcdViper.SetConfigType("yaml") 141 | err = etcdViper.ReadRemoteConfig() 142 | require.NoError(t, err) 143 | 144 | provider := vprovider.NewProvider(ctx, etcdViper, vprovider.ConfigTypeTarantool3) 145 | anyProviderValidation(t, provider) 146 | }) 147 | 148 | t.Run("invalid path", func(t *testing.T) { 149 | etcdViper := viper.New() 150 | 151 | err = etcdViper.AddRemoteProvider("etcd3", "http://127.0.0.1:2479", "/some-invalid-path") 152 | require.NoError(t, err) 153 | 154 | etcdViper.SetConfigType("yaml") 155 | 156 | err = etcdViper.ReadRemoteConfig() 157 | require.Error(t, err, "path not found error") 158 | }) 159 | 160 | t.Run("invalid config panics", func(t *testing.T) { 161 | etcdViper := viper.New() 162 | 163 | emptyPath := "/some-empty-path" 164 | 165 | _, err := kv.Put(ctx, emptyPath, "") 166 | require.NoError(t, err) 167 | 168 | err = etcdViper.AddRemoteProvider("etcd3", "http://127.0.0.1:2479", "/some-empty-path") 169 | require.NoError(t, err) 170 | 171 | etcdViper.SetConfigType("yaml") 172 | err = etcdViper.ReadRemoteConfig() 173 | require.NoError(t, err) 174 | 175 | require.Panics(t, func() { 176 | vprovider.NewProvider(ctx, etcdViper, vprovider.ConfigTypeTarantool3) 177 | }) 178 | }) 179 | } 180 | 181 | func anyProviderValidation(t testing.TB, provider *vprovider.Provider) { 182 | // not empty 183 | require.NotNil(t, provider) 184 | 185 | topology := provider.Topology() 186 | // topology not empty 187 | require.NotNil(t, topology) 188 | // topology more than 0 189 | require.True(t, len(topology) > 0) 190 | 191 | // there are no empty replicates 192 | for _, instances := range topology { 193 | require.NotEmpty(t, instances) 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /providers/viper/tarantool3/config.go: -------------------------------------------------------------------------------- 1 | package tarantool3 2 | 3 | // ----- Tarantool 3 configuration ----- 4 | 5 | // Config - configuration for all groups 6 | // based on https://www.tarantool.io/en/doc/latest/getting_started/vshard_quick/#step-4-defining-the-cluster-topology. 7 | type Config struct { 8 | Groups Group `yaml:"groups"` 9 | } 10 | 11 | // Group is a structure for each group configuration 12 | type Group struct { 13 | Storages *Storages `yaml:"storages,omitempty"` 14 | } 15 | 16 | // Storages configuration 17 | type Storages struct { 18 | App App `yaml:"app"` 19 | Sharding Sharding `yaml:"sharding"` 20 | Replication Replication `yaml:"replication"` 21 | Replicasets map[string]Replicaset `yaml:"replicasets"` 22 | } 23 | 24 | // App - general information about the module 25 | type App struct { 26 | Module string `yaml:"module"` 27 | } 28 | 29 | // Sharding configuration 30 | type Sharding struct { 31 | Roles []string `yaml:"roles"` 32 | } 33 | 34 | // Replication configuration 35 | type Replication struct { 36 | Failover string `yaml:"failover"` 37 | } 38 | 39 | // Replicaset configuration 40 | type Replicaset struct { 41 | Leader string `yaml:"leader"` 42 | Instances map[string]Instance `yaml:"instances"` 43 | } 44 | 45 | // Instance in the Replicaset 46 | type Instance struct { 47 | IProto IProto `yaml:"iproto"` 48 | } 49 | 50 | // IProto configuration 51 | type IProto struct { 52 | Listen []Listen `yaml:"listen"` 53 | } 54 | 55 | // Listen configuration (URI for connection) 56 | type Listen struct { 57 | URI string `yaml:"uri"` 58 | } 59 | -------------------------------------------------------------------------------- /providers/viper/tarantool3/convert.go: -------------------------------------------------------------------------------- 1 | package tarantool3 2 | 3 | import ( 4 | "fmt" 5 | 6 | vshardrouter "github.com/tarantool/go-vshard-router/v2" 7 | ) 8 | 9 | func (cfg *Config) Convert() (map[vshardrouter.ReplicasetInfo][]vshardrouter.InstanceInfo, error) { 10 | if cfg.Groups.Storages == nil { 11 | return nil, fmt.Errorf("cant get groups storage from etcd") 12 | } 13 | 14 | if cfg.Groups.Storages.Replicasets == nil { 15 | return nil, fmt.Errorf("cant get storage replicasets from etcd") 16 | } 17 | 18 | m := make(map[vshardrouter.ReplicasetInfo][]vshardrouter.InstanceInfo) 19 | 20 | for rsName, rs := range cfg.Groups.Storages.Replicasets { 21 | rsInfo := vshardrouter.ReplicasetInfo{Name: rsName} 22 | instances := make([]vshardrouter.InstanceInfo, 0, len(rs.Instances)) 23 | 24 | for instanceName, instance := range rs.Instances { 25 | instances = append(instances, vshardrouter.InstanceInfo{ 26 | Name: instanceName, 27 | Addr: instance.IProto.Listen[0].URI, 28 | }) 29 | } 30 | 31 | m[rsInfo] = instances 32 | } 33 | 34 | return m, nil 35 | } 36 | -------------------------------------------------------------------------------- /providers/viper/testdata/config-direct.yaml: -------------------------------------------------------------------------------- 1 | topology: 2 | clusters: 3 | storage_1: 4 | replicaset_uuid: cbf06940-0790-498b-948d-042b62cf3d29 5 | storage_2: 6 | replicaset_uuid: ac522f65-aa94-4134-9f64-51ee384f1a54 7 | instances: 8 | storage_1_a: 9 | cluster: storage_1 10 | box: 11 | listen: '127.0.0.1:3301' 12 | instance_uuid: '6E35AC64-1241-0001-0001-000000000000' 13 | storage_1_b: 14 | cluster: storage_1 15 | box: 16 | listen: '127.0.0.1:3302' 17 | instance_uuid: '6E35AC64-1241-0001-0002-000000000000' 18 | storage_2_a: 19 | cluster: storage_2 20 | box: 21 | listen: '127.0.0.1:3303' 22 | instance_uuid: '6E35AC64-1241-0002-0001-000000000000' 23 | storage_2_b: 24 | cluster: storage_2 25 | box: 26 | listen: '127.0.0.1:3304' 27 | instance_uuid: '6E35AC64-1241-0002-0002-000000000000' -------------------------------------------------------------------------------- /providers/viper/testdata/config-sub.yaml: -------------------------------------------------------------------------------- 1 | supbpath: 2 | topology: 3 | clusters: 4 | storage_1: 5 | replicaset_uuid: cbf06940-0790-498b-948d-042b62cf3d29 6 | storage_2: 7 | replicaset_uuid: ac522f65-aa94-4134-9f64-51ee384f1a54 8 | instances: 9 | storage_1_a: 10 | cluster: storage_1 11 | box: 12 | listen: '127.0.0.1:3301' 13 | instance_uuid: '6E35AC64-1241-0001-0001-000000000000' 14 | storage_1_b: 15 | cluster: storage_1 16 | box: 17 | listen: '127.0.0.1:3302' 18 | instance_uuid: '6E35AC64-1241-0001-0002-000000000000' 19 | storage_2_a: 20 | cluster: storage_2 21 | box: 22 | listen: '127.0.0.1:3303' 23 | instance_uuid: '6E35AC64-1241-0002-0001-000000000000' 24 | storage_2_b: 25 | cluster: storage_2 26 | box: 27 | listen: '127.0.0.1:3304' 28 | instance_uuid: '6E35AC64-1241-0002-0002-000000000000' -------------------------------------------------------------------------------- /providers/viper/testdata/config-tarantool3.yaml: -------------------------------------------------------------------------------- 1 | credentials: 2 | users: 3 | replicator: 4 | password: 'topsecret' 5 | roles: [replication] 6 | storage: 7 | password: 'secret' 8 | roles: [sharding] 9 | 10 | iproto: 11 | advertise: 12 | peer: 13 | login: replicator 14 | sharding: 15 | login: storage 16 | 17 | sharding: 18 | bucket_count: 1000 19 | 20 | groups: 21 | storages: 22 | app: 23 | module: storage 24 | sharding: 25 | roles: [storage] 26 | replication: 27 | failover: manual 28 | replicasets: 29 | storage-a: 30 | leader: storage-a-001 31 | instances: 32 | storage-a-001: 33 | iproto: 34 | listen: 35 | - uri: '127.0.0.1:3302' 36 | storage-a-002: 37 | iproto: 38 | listen: 39 | - uri: '127.0.0.1:3303' 40 | storage-b: 41 | leader: storage-b-001 42 | instances: 43 | storage-b-001: 44 | iproto: 45 | listen: 46 | - uri: '127.0.0.1:3304' 47 | storage-b-002: 48 | iproto: 49 | listen: 50 | - uri: '127.0.0.1:3305' 51 | routers: 52 | app: 53 | module: router 54 | sharding: 55 | roles: [router] 56 | replicasets: 57 | router-a: 58 | instances: 59 | router-a-001: 60 | iproto: 61 | listen: 62 | - uri: '127.0.0.1:3301' 63 | -------------------------------------------------------------------------------- /providers_test.go: -------------------------------------------------------------------------------- 1 | package vshard_router_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/require" 9 | 10 | vshardrouter "github.com/tarantool/go-vshard-router/v2" 11 | ) 12 | 13 | var ( 14 | emptyMetrics = vshardrouter.EmptyMetrics{} 15 | stdoutLogger = vshardrouter.StdoutLoggerf{} 16 | ) 17 | 18 | func TestEmptyMetrics_RetryOnCall(t *testing.T) { 19 | require.NotPanics(t, func() { 20 | emptyMetrics.RetryOnCall("") 21 | }) 22 | } 23 | 24 | func TestEmptyMetrics_RequestDuration(t *testing.T) { 25 | require.NotPanics(t, func() { 26 | emptyMetrics.RequestDuration(time.Second, "test", false, false) 27 | }) 28 | } 29 | 30 | func TestEmptyMetrics_CronDiscoveryEvent(t *testing.T) { 31 | require.NotPanics(t, func() { 32 | emptyMetrics.CronDiscoveryEvent(false, time.Second, "") 33 | }) 34 | } 35 | 36 | func TestStdoutLogger(t *testing.T) { 37 | ctx := context.TODO() 38 | 39 | require.NotPanics(t, func() { 40 | stdoutLogger.Errorf(ctx, "") 41 | }) 42 | require.NotPanics(t, func() { 43 | stdoutLogger.Infof(ctx, "") 44 | }) 45 | require.NotPanics(t, func() { 46 | stdoutLogger.Warnf(ctx, "") 47 | }) 48 | require.NotPanics(t, func() { 49 | stdoutLogger.Debugf(ctx, "") 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /replicaset.go: -------------------------------------------------------------------------------- 1 | package vshard_router //nolint:revive 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math" 7 | "time" 8 | 9 | "github.com/google/uuid" 10 | "github.com/tarantool/go-tarantool/v2" 11 | "github.com/tarantool/go-tarantool/v2/pool" 12 | "github.com/vmihailenco/msgpack/v5" 13 | "github.com/vmihailenco/msgpack/v5/msgpcode" 14 | ) 15 | 16 | // ReplicasetInfo represents information about a replicaset, including its name, unique identifier, weight, and state. 17 | type ReplicasetInfo struct { 18 | // Name — the name of the replicaset. 19 | // This string is required and is used to identify the replicaset. 20 | Name string 21 | // UUID — the unique identifier of the replica. 22 | // This is an optional value that can be used to uniquely distinguish each replicaset. 23 | UUID uuid.UUID 24 | // Weight — the weight of the replicaset. 25 | // This floating-point number may be used to determine the importance or priority of the replicaset. 26 | Weight float64 27 | // PinnedCount — the number of pinned items. 28 | // This value indicates how many items or tasks are associated with the replicaset. 29 | PinnedCount uint64 30 | // IgnoreDisbalance — a flag indicating whether to ignore load imbalance when distributing tasks. 31 | // If true, the replicaset will be excluded from imbalance checks. 32 | IgnoreDisbalance bool 33 | } 34 | 35 | func (ri ReplicasetInfo) Validate() error { 36 | if ri.Name == "" { 37 | return fmt.Errorf("%w: rsInfo.Name is empty", ErrInvalidReplicasetInfo) 38 | } 39 | 40 | return nil 41 | } 42 | 43 | func (ri ReplicasetInfo) String() string { 44 | return fmt.Sprintf("{name: %s, uuid: %s}", ri.Name, ri.UUID) 45 | } 46 | 47 | type ReplicasetCallOpts struct { 48 | PoolMode pool.Mode 49 | Timeout time.Duration 50 | } 51 | 52 | // Pooler is an interface for the tarantool.Pool wrapper, 53 | // which is necessary for the correct operation of the library. 54 | type Pooler interface { 55 | pool.Pooler 56 | // GetInfo is an addition to the standard interface that allows you 57 | // to get the current state of the connection pool. 58 | // This is necessary for proper operation with topology providers 59 | // for adding or removing instances. 60 | GetInfo() map[string]pool.ConnectionInfo 61 | } 62 | 63 | type Replicaset struct { 64 | conn Pooler 65 | info ReplicasetInfo 66 | EtalonBucketCount uint64 67 | } 68 | 69 | func (rs *Replicaset) Pooler() pool.Pooler { 70 | return rs.conn 71 | } 72 | 73 | func (rs *Replicaset) String() string { 74 | return rs.info.String() 75 | } 76 | 77 | func (rs *Replicaset) BucketStat(ctx context.Context, bucketID uint64) (BucketStatInfo, error) { 78 | future := rs.bucketStatAsync(ctx, bucketID) 79 | 80 | return bucketStatWait(future) 81 | } 82 | 83 | func (rs *Replicaset) bucketStatAsync(ctx context.Context, bucketID uint64) *tarantool.Future { 84 | const bucketStatFnc = "vshard.storage.bucket_stat" 85 | 86 | return rs.CallAsync(ctx, ReplicasetCallOpts{PoolMode: pool.RO}, bucketStatFnc, []interface{}{bucketID}) 87 | } 88 | 89 | type vshardStorageBucketStatResponseProto struct { 90 | ok bool 91 | info BucketStatInfo 92 | err StorageCallVShardError 93 | } 94 | 95 | func (r *vshardStorageBucketStatResponseProto) DecodeMsgpack(d *msgpack.Decoder) error { 96 | // bucket_stat returns pair: stat, err 97 | // https://github.com/tarantool/vshard/blob/e1c806e1d3d2ce8a4e6b4d498c09051bf34ab92a/vshard/storage/init.lua#L1413 98 | 99 | respArrayLen, err := d.DecodeArrayLen() 100 | if err != nil { 101 | return err 102 | } 103 | 104 | if respArrayLen <= 0 { 105 | return fmt.Errorf("protocol violation bucketStatWait: respArrayLen=%d", respArrayLen) 106 | } 107 | 108 | code, err := d.PeekCode() 109 | if err != nil { 110 | return err 111 | } 112 | 113 | if code == msgpcode.Nil { 114 | err = d.DecodeNil() 115 | if err != nil { 116 | return err 117 | } 118 | 119 | if respArrayLen != 2 { 120 | return fmt.Errorf("protocol violation bucketStatWait: length is %d on vshard error case", respArrayLen) 121 | } 122 | 123 | err = d.Decode(&r.err) 124 | if err != nil { 125 | return fmt.Errorf("failed to decode storage vshard error: %w", err) 126 | } 127 | 128 | return nil 129 | } 130 | 131 | err = d.Decode(&r.info) 132 | if err != nil { 133 | return fmt.Errorf("failed to decode bucket stat info: %w", err) 134 | } 135 | 136 | r.ok = true 137 | 138 | return nil 139 | } 140 | 141 | func bucketStatWait(future *tarantool.Future) (BucketStatInfo, error) { 142 | var bucketStatResponse vshardStorageBucketStatResponseProto 143 | 144 | err := future.GetTyped(&bucketStatResponse) 145 | if err != nil { 146 | return BucketStatInfo{}, err 147 | } 148 | 149 | if !bucketStatResponse.ok { 150 | return BucketStatInfo{}, bucketStatResponse.err 151 | } 152 | 153 | return bucketStatResponse.info, nil 154 | } 155 | 156 | // CallAsync sends async request to remote storage 157 | func (rs *Replicaset) CallAsync(ctx context.Context, opts ReplicasetCallOpts, fnc string, args interface{}) *tarantool.Future { 158 | if opts.Timeout > 0 { 159 | // Don't set any timeout by default, parent context timeout would be inherited in this case. 160 | // Don't call cancel in defer, because this we send request asynchronously, 161 | // and wait for result outside from this function. 162 | // suppress linter warning: lostcancel: the cancel function returned by context.WithTimeout should be called, not discarded, to avoid a context leak (govet) 163 | //nolint:govet 164 | ctx, _ = context.WithTimeout(ctx, opts.Timeout) 165 | } 166 | 167 | req := tarantool.NewCallRequest(fnc). 168 | Context(ctx). 169 | Args(args) 170 | 171 | return rs.conn.Do(req, opts.PoolMode) 172 | } 173 | 174 | func (rs *Replicaset) bucketsDiscoveryAsync(ctx context.Context, from uint64) *tarantool.Future { 175 | const bucketsDiscoveryFnc = "vshard.storage.buckets_discovery" 176 | 177 | var bucketsDiscoveryPaginationRequest = struct { 178 | From uint64 `msgpack:"from"` 179 | }{From: from} 180 | 181 | return rs.CallAsync(ctx, ReplicasetCallOpts{PoolMode: pool.PreferRO}, bucketsDiscoveryFnc, 182 | []interface{}{bucketsDiscoveryPaginationRequest}) 183 | } 184 | 185 | type bucketsDiscoveryResp struct { 186 | Buckets []uint64 `msgpack:"buckets"` 187 | NextFrom uint64 `msgpack:"next_from"` 188 | } 189 | 190 | func bucketsDiscoveryWait(future *tarantool.Future) (bucketsDiscoveryResp, error) { 191 | // We intentionally don't support old vshard storages that mentioned here: 192 | // https://github.com/tarantool/vshard/blob/8d299bfecff8bc656056658350ad48c829f9ad3f/vshard/router/init.lua#L343 193 | var resp bucketsDiscoveryResp 194 | 195 | err := future.GetTyped(&[]interface{}{&resp}) 196 | if err != nil { 197 | return resp, fmt.Errorf("future.GetTyped() failed: %v", err) 198 | } 199 | 200 | return resp, nil 201 | } 202 | 203 | func (rs *Replicaset) bucketsDiscovery(ctx context.Context, from uint64) (bucketsDiscoveryResp, error) { 204 | future := rs.bucketsDiscoveryAsync(ctx, from) 205 | 206 | return bucketsDiscoveryWait(future) 207 | } 208 | 209 | // CalculateEtalonBalance computes the ideal bucket count for each replicaset. 210 | // This iterative algorithm seeks the optimal balance within a cluster by 211 | // calculating the ideal bucket count for each replicaset at every step. 212 | // If the ideal count cannot be achieved due to pinned buckets, the algorithm 213 | // makes a best effort to approximate balance by ignoring the replicaset with 214 | // pinned buckets and its associated pinned count. After each iteration, a new 215 | // balance is recalculated. However, this can lead to scenarios where the 216 | // conditions are still unmet; ignoring pinned buckets in overloaded 217 | // replicasets can reduce the ideal bucket count in others, potentially 218 | // causing new values to fall below their pinned count. 219 | // 220 | // At each iteration, the algorithm either concludes or disregards at least 221 | // one new overloaded replicaset. Therefore, its time complexity is O(N^2), 222 | // where N is the number of replicasets. 223 | // based on https://github.com/tarantool/vshard/blob/99ceaee014ea3a67424c2026545838e08d69b90c/vshard/replicaset.lua#L1358 224 | func CalculateEtalonBalance(replicasets []Replicaset, bucketCount uint64) error { 225 | isBalanceFound := false 226 | weightSum := 0.0 227 | stepCount := 0 228 | replicasetCount := len(replicasets) 229 | 230 | // Calculate total weight 231 | for _, replicaset := range replicasets { 232 | weightSum += replicaset.info.Weight 233 | } 234 | 235 | // Balance calculation loop 236 | for !isBalanceFound { 237 | stepCount++ 238 | if weightSum <= 0 { 239 | return fmt.Errorf("weightSum should be greater than 0") 240 | } 241 | 242 | bucketPerWeight := float64(bucketCount) / weightSum 243 | bucketsCalculated := uint64(0) 244 | 245 | // Calculate etalon bucket count for each replicaset 246 | for i := range replicasets { 247 | if !replicasets[i].info.IgnoreDisbalance { 248 | replicasets[i].EtalonBucketCount = uint64(math.Ceil(replicasets[i].info.Weight * bucketPerWeight)) 249 | bucketsCalculated += replicasets[i].EtalonBucketCount 250 | } 251 | } 252 | 253 | bucketsRest := bucketsCalculated - bucketCount 254 | isBalanceFound = true 255 | 256 | // Spread disbalance and check for pinned buckets 257 | for i := range replicasets { 258 | if !replicasets[i].info.IgnoreDisbalance { 259 | if bucketsRest > 0 { 260 | n := replicasets[i].info.Weight * bucketPerWeight 261 | ceil := math.Ceil(n) 262 | floor := math.Floor(n) 263 | if replicasets[i].EtalonBucketCount > 0 && ceil != floor { 264 | replicasets[i].EtalonBucketCount-- 265 | bucketsRest-- 266 | } 267 | } 268 | 269 | // Handle pinned buckets 270 | pinned := replicasets[i].info.PinnedCount 271 | if pinned > 0 && replicasets[i].EtalonBucketCount < pinned { 272 | isBalanceFound = false 273 | bucketCount -= pinned 274 | replicasets[i].EtalonBucketCount = pinned 275 | replicasets[i].info.IgnoreDisbalance = true 276 | weightSum -= replicasets[i].info.Weight 277 | } 278 | } 279 | } 280 | 281 | if bucketsRest != 0 { 282 | return fmt.Errorf("bucketsRest should be 0") 283 | } 284 | 285 | // Safety check to prevent infinite loops 286 | if stepCount > replicasetCount { 287 | return fmt.Errorf("[PANIC]: the rebalancer is broken") 288 | } 289 | } 290 | 291 | return nil 292 | } 293 | 294 | func (rs *Replicaset) BucketsCount(ctx context.Context) (uint64, error) { 295 | const bucketCountFnc = "vshard.storage.buckets_count" 296 | 297 | var bucketCount uint64 298 | 299 | fut := rs.CallAsync(ctx, ReplicasetCallOpts{PoolMode: pool.ANY}, bucketCountFnc, nil) 300 | err := fut.GetTyped(&[]interface{}{&bucketCount}) 301 | 302 | return bucketCount, err 303 | } 304 | 305 | func (rs *Replicaset) BucketForceCreate(ctx context.Context, firstBucketID, count uint64) error { 306 | const bucketForceCreateFnc = "vshard.storage.bucket_force_create" 307 | 308 | fut := rs.CallAsync(ctx, ReplicasetCallOpts{PoolMode: pool.RW}, bucketForceCreateFnc, []interface{}{firstBucketID, count}) 309 | _, err := fut.GetResponse() 310 | 311 | return err 312 | } 313 | -------------------------------------------------------------------------------- /replicaset_test.go: -------------------------------------------------------------------------------- 1 | package vshard_router //nolint:revive 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "testing" 8 | 9 | "github.com/google/uuid" 10 | "github.com/stretchr/testify/mock" 11 | "github.com/stretchr/testify/require" 12 | "github.com/tarantool/go-tarantool/v2" 13 | "github.com/vmihailenco/msgpack/v5" 14 | 15 | mockpool "github.com/tarantool/go-vshard-router/v2/mocks/pool" 16 | ) 17 | 18 | func TestReplicasetInfo_String(t *testing.T) { 19 | rsUUID := uuid.New() 20 | rsInfo := ReplicasetInfo{ 21 | Name: "test", 22 | UUID: rsUUID, 23 | } 24 | 25 | rs := Replicaset{ 26 | info: rsInfo, 27 | } 28 | 29 | require.Equal(t, rsInfo.String(), rs.String()) 30 | require.Contains(t, rsInfo.String(), "test") 31 | require.Contains(t, rsInfo.String(), rsUUID.String()) 32 | } 33 | 34 | func TestReplicaset_BucketStat(t *testing.T) { 35 | ctx := context.TODO() 36 | rsUUID := uuid.New() 37 | rsInfo := ReplicasetInfo{ 38 | Name: "test", 39 | UUID: rsUUID, 40 | } 41 | 42 | rs := Replicaset{ 43 | info: rsInfo, 44 | } 45 | 46 | t.Run("pool do error", func(t *testing.T) { 47 | futureError := fmt.Errorf("testErr") 48 | 49 | errFuture := tarantool.NewFuture(tarantool.NewCallRequest("test")) 50 | errFuture.SetError(futureError) 51 | 52 | mPool := mockpool.NewPooler(t) 53 | mPool.On("Do", mock.Anything, mock.Anything).Return(errFuture) 54 | rs.conn = mPool 55 | 56 | _, err := rs.BucketStat(ctx, 123) 57 | require.Equal(t, futureError, err) 58 | }) 59 | 60 | t.Run("unsupported or broken proto resp", func(t *testing.T) { 61 | f := tarantool.NewFuture(tarantool.NewCallRequest("vshard.storage.bucket_stat")) 62 | 63 | bts, _ := msgpack.Marshal([]interface{}{1}) 64 | 65 | err := f.SetResponse(tarantool.Header{}, bytes.NewReader(bts)) 66 | require.NoError(t, err) 67 | 68 | mPool := mockpool.NewPooler(t) 69 | mPool.On("Do", mock.Anything, mock.Anything).Return(f) 70 | rs.conn = mPool 71 | 72 | // todo: add real tests 73 | 74 | statInfo, err := rs.BucketStat(ctx, 123) 75 | require.Error(t, err) 76 | require.Equal(t, statInfo, BucketStatInfo{BucketID: 0, Status: ""}) 77 | }) 78 | 79 | /* 80 | TODO: add test for wrong bucket response 81 | 82 | unix/:./data/storage_1_a.control> vshard.storage.bucket_stat(1000) 83 | --- 84 | - null 85 | - bucket_id: 1000 86 | reason: Not found 87 | code: 1 88 | type: ShardingError 89 | message: 'Cannot perform action with bucket 1000, reason: Not found' 90 | name: WRONG_BUCKET 91 | ... 92 | */ 93 | } 94 | 95 | func TestCalculateEtalonBalance(t *testing.T) { 96 | tests := []struct { 97 | name string 98 | replicasets []Replicaset 99 | bucketCount uint64 100 | expectedCounts []uint64 101 | expectError bool 102 | }{ 103 | { 104 | name: "FullBalance", 105 | replicasets: []Replicaset{ 106 | {info: ReplicasetInfo{Weight: 1, PinnedCount: 0, IgnoreDisbalance: false}}, 107 | {info: ReplicasetInfo{Weight: 1, PinnedCount: 0, IgnoreDisbalance: false}}, 108 | {info: ReplicasetInfo{Weight: 1, PinnedCount: 0, IgnoreDisbalance: false}}, 109 | }, 110 | bucketCount: 9, 111 | expectedCounts: []uint64{3, 3, 3}, 112 | expectError: false, 113 | }, 114 | { 115 | name: "PinnedMoreThanWeight", 116 | replicasets: []Replicaset{ 117 | {info: ReplicasetInfo{Weight: 1, PinnedCount: 60, IgnoreDisbalance: false}}, 118 | {info: ReplicasetInfo{Weight: 1, PinnedCount: 0, IgnoreDisbalance: false}}, 119 | }, 120 | bucketCount: 100, 121 | expectedCounts: []uint64{60, 40}, 122 | expectError: false, 123 | }, 124 | { 125 | name: "ZeroWeight", 126 | replicasets: []Replicaset{ 127 | {info: ReplicasetInfo{Weight: 0, PinnedCount: 0, IgnoreDisbalance: false}}, 128 | {info: ReplicasetInfo{Weight: 1, PinnedCount: 0, IgnoreDisbalance: false}}, 129 | }, 130 | bucketCount: 10, 131 | expectError: false, 132 | expectedCounts: []uint64{0, 10}, 133 | }, 134 | { 135 | name: "ZeroAllWeights", 136 | replicasets: []Replicaset{ 137 | {info: ReplicasetInfo{Weight: 0, PinnedCount: 0, IgnoreDisbalance: false}}, 138 | {info: ReplicasetInfo{Weight: 0, PinnedCount: 0, IgnoreDisbalance: false}}, 139 | }, 140 | bucketCount: 10, 141 | expectError: true, 142 | }, 143 | { 144 | name: "UnevenDistribution", 145 | replicasets: []Replicaset{ 146 | {info: ReplicasetInfo{Weight: 1, PinnedCount: 0, IgnoreDisbalance: false}}, 147 | {info: ReplicasetInfo{Weight: 2, PinnedCount: 0, IgnoreDisbalance: false}}, 148 | }, 149 | bucketCount: 7, 150 | expectError: false, 151 | expectedCounts: []uint64{2, 5}, 152 | }, 153 | } 154 | 155 | for _, tt := range tests { 156 | t.Run(tt.name, func(t *testing.T) { 157 | err := CalculateEtalonBalance(tt.replicasets, tt.bucketCount) 158 | 159 | if tt.expectError { 160 | require.Error(t, err) 161 | } else { 162 | require.NoError(t, err) 163 | for i, expectedCount := range tt.expectedCounts { 164 | require.Equal(t, expectedCount, tt.replicasets[i].EtalonBucketCount) 165 | } 166 | } 167 | }) 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /sugar.go: -------------------------------------------------------------------------------- 1 | package vshard_router //nolint:revive 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/tarantool/go-tarantool/v2/pool" 8 | ) 9 | 10 | // CallRequest helps you to create a call request object for execution 11 | // by a Connection. 12 | type CallRequest struct { 13 | ctx context.Context 14 | fnc string 15 | args interface{} 16 | bucketID uint64 17 | } 18 | 19 | // CallResponse is a backwards-compatible structure with go-tarantool for easier replacement. 20 | type CallResponse struct { 21 | resp VshardRouterCallResp 22 | err error 23 | } 24 | 25 | // NewCallRequest returns a new empty CallRequest. 26 | func NewCallRequest(function string) *CallRequest { 27 | req := new(CallRequest) 28 | req.fnc = function 29 | return req 30 | } 31 | 32 | // Do perform a request synchronously on the connection. 33 | // It is important that the logic of this method is different from go-tarantool. 34 | func (r *Router) Do(req *CallRequest, userMode pool.Mode) *CallResponse { 35 | ctx := req.ctx 36 | bucketID := req.bucketID 37 | resp := new(CallResponse) 38 | 39 | if req.fnc == "" { 40 | resp.err = fmt.Errorf("func name is empty") 41 | return resp 42 | } 43 | 44 | if req.args == nil { 45 | resp.err = fmt.Errorf("no request args") 46 | return resp 47 | } 48 | 49 | if req.bucketID == 0 { 50 | if r.cfg.BucketGetter == nil { 51 | resp.err = fmt.Errorf("bucket id for request is not set") 52 | return resp 53 | } 54 | 55 | bucketID = r.cfg.BucketGetter(ctx) 56 | } 57 | 58 | vshardMode := CallModeBRO 59 | 60 | // If the user says he prefers to do it on the master, 61 | // then he agrees that it will go to the replica, which means he will not write. 62 | if userMode == pool.RW { 63 | vshardMode = CallModeRW 64 | } 65 | 66 | resp.resp, resp.err = r.Call(ctx, bucketID, vshardMode, req.fnc, req.args, CallOpts{ 67 | Timeout: r.cfg.RequestTimeout, 68 | }) 69 | 70 | return resp 71 | } 72 | 73 | // Args sets the args for the eval request. 74 | // Note: default value is empty. 75 | func (req *CallRequest) Args(args interface{}) *CallRequest { 76 | req.args = args 77 | return req 78 | } 79 | 80 | // Context sets a passed context to the request. 81 | func (req *CallRequest) Context(ctx context.Context) *CallRequest { 82 | req.ctx = ctx 83 | return req 84 | } 85 | 86 | // BucketID method that sets the bucketID for your request. 87 | // You can ignore this parameter if you have a bucketGetter. 88 | // However, this method has a higher priority. 89 | func (req *CallRequest) BucketID(bucketID uint64) *CallRequest { 90 | req.bucketID = bucketID 91 | return req 92 | } 93 | 94 | // GetTyped waits synchronously for response and calls msgpack.Decoder.Decode(result) if no error happens. 95 | func (resp *CallResponse) GetTyped(result interface{}) error { 96 | if resp.err != nil { 97 | return resp.err 98 | } 99 | 100 | return resp.resp.GetTyped(result) 101 | } 102 | 103 | // Get implementation now works synchronously for response. 104 | // The interface was created purely for convenient migration to go-vshard-router from go-tarantool. 105 | func (resp *CallResponse) Get() ([]interface{}, error) { 106 | if resp.err != nil { 107 | return nil, resp.err 108 | } 109 | 110 | return resp.resp.Get() 111 | } 112 | -------------------------------------------------------------------------------- /test_helper/helper.go: -------------------------------------------------------------------------------- 1 | // nolint:revive 2 | package test_helper 3 | 4 | import ( 5 | "context" 6 | 7 | ttnt "github.com/tarantool/go-tarantool/v2/test_helpers" 8 | "golang.org/x/sync/errgroup" 9 | ) 10 | 11 | func StartTarantoolInstances(instsOpts []ttnt.StartOpts) ([]*ttnt.TarantoolInstance, error) { 12 | ctx := context.Background() 13 | instances := make([]*ttnt.TarantoolInstance, len(instsOpts)) 14 | errGr, _ := errgroup.WithContext(ctx) 15 | 16 | for i, opts := range instsOpts { 17 | opts := opts 18 | i := i 19 | errGr.Go(func() error { 20 | instance, err := ttnt.StartTarantool(opts) 21 | instances[i] = instance 22 | 23 | return err 24 | }) 25 | 26 | } 27 | err := errGr.Wait() 28 | 29 | return instances, err 30 | } 31 | -------------------------------------------------------------------------------- /topology.go: -------------------------------------------------------------------------------- 1 | package vshard_router //nolint:revive 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/tarantool/go-tarantool/v2" 8 | "github.com/tarantool/go-tarantool/v2/pool" 9 | ) 10 | 11 | var ( 12 | ErrReplicasetExists = fmt.Errorf("replicaset already exists") 13 | ErrReplicasetNotExists = fmt.Errorf("replicaset not exists") 14 | 15 | ErrConcurrentTopologyChangeDetected = fmt.Errorf("concurrent topology change detected") 16 | ) 17 | 18 | // TopologyController is an entity that allows you to interact with the topology. 19 | // TopologyController is not concurrent safe. 20 | // This decision is made intentionally because there is no point in providing concurrence safety for this case. 21 | // In any case, a caller can use his own external synchronization primitive to handle concurrent access. 22 | type TopologyController interface { 23 | AddInstance(ctx context.Context, rsName string, info InstanceInfo) error 24 | RemoveReplicaset(ctx context.Context, rsName string) []error 25 | RemoveInstance(ctx context.Context, rsName, instanceName string) error 26 | AddReplicaset(ctx context.Context, rsInfo ReplicasetInfo, instances []InstanceInfo) error 27 | AddReplicasets(ctx context.Context, replicasets map[ReplicasetInfo][]InstanceInfo) error 28 | } 29 | 30 | func copyMap[K comparable, V any](m map[K]V) map[K]V { 31 | copy := make(map[K]V) 32 | for k, v := range m { 33 | copy[k] = v 34 | } 35 | return copy 36 | } 37 | 38 | func (r *Router) setEmptyNameToReplicaset() { 39 | var nameToReplicasetRef map[string]*Replicaset 40 | _ = r.swapNameToReplicaset(nil, &nameToReplicasetRef) 41 | } 42 | 43 | func (r *Router) swapNameToReplicaset(old, new *map[string]*Replicaset) error { 44 | if swapped := r.nameToReplicaset.CompareAndSwap(old, new); !swapped { 45 | return ErrConcurrentTopologyChangeDetected 46 | } 47 | return nil 48 | } 49 | 50 | func (r *Router) getNameToReplicaset() map[string]*Replicaset { 51 | ptr := r.nameToReplicaset.Load() 52 | return *ptr 53 | } 54 | 55 | func (r *Router) Topology() TopologyController { 56 | return r 57 | } 58 | 59 | func (r *Router) AddInstance(ctx context.Context, rsName string, info InstanceInfo) error { 60 | r.log().Debugf(ctx, "Trying to add instance %s to router topology in rs %s", info, rsName) 61 | 62 | err := info.Validate() 63 | if err != nil { 64 | return err 65 | } 66 | 67 | dialer := info.Dialer 68 | if dialer == nil { 69 | dialer = tarantool.NetDialer{ 70 | Address: info.Addr, 71 | User: r.cfg.User, 72 | Password: r.cfg.Password, 73 | } 74 | } 75 | 76 | instance := pool.Instance{ 77 | Name: info.Name, 78 | Dialer: dialer, 79 | Opts: r.cfg.PoolOpts, 80 | } 81 | 82 | nameToReplicasetRef := r.getNameToReplicaset() 83 | 84 | rs := nameToReplicasetRef[rsName] 85 | if rs == nil { 86 | return ErrReplicasetNotExists 87 | } 88 | 89 | return rs.conn.Add(ctx, instance) 90 | } 91 | 92 | // RemoveInstance removes a specific instance from the router topology within a replicaset. 93 | // It takes a context, the replicaset name (rsName), and the instance name (instanceName) as inputs. 94 | // If the replicaset name is empty, it searches through all replica sets to locate the instance. 95 | // Returns an error if the specified replicaset does not exist or if any issue occurs during removal. 96 | func (r *Router) RemoveInstance(ctx context.Context, rsName, instanceName string) error { 97 | r.log().Debugf(ctx, "Trying to remove instance %s from router topology in rs %s", instanceName, rsName) 98 | 99 | nameToReplicasetRef := r.getNameToReplicaset() 100 | 101 | var rs *Replicaset 102 | 103 | if rsName == "" { 104 | r.log().Debugf(ctx, "Replicaset name is not provided for instance %s, attempting to find it", 105 | instanceName) 106 | 107 | for _, trs := range nameToReplicasetRef { 108 | _, exists := trs.conn.GetInfo()[instanceName] 109 | if exists { 110 | r.log().Debugf(ctx, "Replicaset found for instance %s, removing it", instanceName) 111 | 112 | rs = trs 113 | } 114 | } 115 | } else { 116 | rs = nameToReplicasetRef[rsName] 117 | } 118 | 119 | if rs == nil { 120 | return ErrReplicasetNotExists 121 | } 122 | 123 | return rs.conn.Remove(instanceName) 124 | } 125 | 126 | func (r *Router) AddReplicaset(ctx context.Context, rsInfo ReplicasetInfo, instances []InstanceInfo) error { 127 | r.log().Debugf(ctx, "Trying to add replicaset %s to router topology", rsInfo) 128 | 129 | err := rsInfo.Validate() 130 | if err != nil { 131 | return err 132 | } 133 | 134 | nameToReplicasetOldPtr := r.nameToReplicaset.Load() 135 | if _, ok := (*nameToReplicasetOldPtr)[rsInfo.Name]; ok { 136 | return ErrReplicasetExists 137 | } 138 | 139 | rsInstances := make([]pool.Instance, 0, len(instances)) 140 | for _, instance := range instances { 141 | dialer := instance.Dialer 142 | if dialer == nil { 143 | dialer = tarantool.NetDialer{ 144 | Address: instance.Addr, 145 | User: r.cfg.User, 146 | Password: r.cfg.Password, 147 | } 148 | } 149 | 150 | rsInstances = append(rsInstances, pool.Instance{ 151 | Name: instance.Name, 152 | Dialer: dialer, 153 | Opts: r.cfg.PoolOpts, 154 | }) 155 | } 156 | 157 | conn, err := pool.Connect(ctx, rsInstances) 158 | if err != nil { 159 | return err 160 | } 161 | 162 | poolInfo := conn.GetInfo() 163 | for instName, instConnInfo := range poolInfo { 164 | connectStatus := "connected now" 165 | if !instConnInfo.ConnectedNow { 166 | connectStatus = "not connected" 167 | } 168 | 169 | r.log().Infof(ctx, "[replicaset %s ] instance %s %s in role %s", rsInfo, instName, connectStatus, instConnInfo.ConnRole) 170 | } 171 | 172 | switch isConnected, err := conn.ConnectedNow(pool.RW); { 173 | case err != nil: 174 | r.log().Errorf(ctx, "cant check rs pool conntected rw now with error: %v", err) 175 | case !isConnected: 176 | r.log().Errorf(ctx, "got connected now as false to pool.RW") 177 | } 178 | 179 | replicaset := &Replicaset{ 180 | info: rsInfo, 181 | conn: conn, 182 | } 183 | 184 | // Create an entirely new map object 185 | nameToReplicasetNew := copyMap(*nameToReplicasetOldPtr) 186 | nameToReplicasetNew[rsInfo.Name] = replicaset // add when conn is ready 187 | 188 | if err = r.swapNameToReplicaset(nameToReplicasetOldPtr, &nameToReplicasetNew); err != nil { 189 | // replicaset has not added, so just close it 190 | _ = replicaset.conn.Close() 191 | return err 192 | } 193 | 194 | return nil 195 | } 196 | 197 | func (r *Router) AddReplicasets(ctx context.Context, replicasets map[ReplicasetInfo][]InstanceInfo) error { 198 | for rsInfo, rsInstances := range replicasets { 199 | // We assume that AddReplicasets is called only once during initialization. 200 | // We also expect that cluster configuration changes very rarely, 201 | // so we prefer more simple code rather than the efficiency of this part of logic. 202 | // Even if there are 1000 replicasets, it is still cheap. 203 | err := r.AddReplicaset(ctx, rsInfo, rsInstances) 204 | if err != nil { 205 | return err 206 | } 207 | } 208 | 209 | return nil 210 | } 211 | 212 | func (r *Router) RemoveReplicaset(ctx context.Context, rsName string) []error { 213 | r.log().Debugf(ctx, "Trying to remove replicaset %s from router topology", rsName) 214 | 215 | nameToReplicasetOldPtr := r.nameToReplicaset.Load() 216 | rs := (*nameToReplicasetOldPtr)[rsName] 217 | if rs == nil { 218 | return []error{ErrReplicasetNotExists} 219 | } 220 | 221 | // Create an entirely new map object 222 | nameToReplicasetNew := copyMap(*nameToReplicasetOldPtr) 223 | delete(nameToReplicasetNew, rsName) 224 | 225 | if err := r.swapNameToReplicaset(nameToReplicasetOldPtr, &nameToReplicasetNew); err != nil { 226 | return []error{err} 227 | } 228 | 229 | return rs.conn.CloseGraceful() 230 | } 231 | -------------------------------------------------------------------------------- /topology_test.go: -------------------------------------------------------------------------------- 1 | package vshard_router //nolint:revive 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/google/uuid" 9 | "github.com/stretchr/testify/mock" 10 | "github.com/stretchr/testify/require" 11 | "github.com/tarantool/go-tarantool/v2/pool" 12 | mockpool "github.com/tarantool/go-vshard-router/v2/mocks/pool" 13 | ) 14 | 15 | func TestRouter_Topology(t *testing.T) { 16 | router := Router{} 17 | 18 | require.NotNil(t, router.Topology()) 19 | } 20 | 21 | func TestController_AddInstance(t *testing.T) { 22 | ctx := context.Background() 23 | 24 | t.Run("no such replicaset", func(t *testing.T) { 25 | router := Router{ 26 | cfg: Config{ 27 | Loggerf: emptyLogfProvider, 28 | }, 29 | } 30 | router.setEmptyNameToReplicaset() 31 | 32 | err := router.Topology().AddInstance(ctx, uuid.New().String(), InstanceInfo{ 33 | Addr: "127.0.0.1:8060", 34 | Name: "instance_001", 35 | }) 36 | require.True(t, errors.Is(err, ErrReplicasetNotExists)) 37 | }) 38 | 39 | t.Run("invalid instance info", func(t *testing.T) { 40 | router := Router{ 41 | cfg: Config{ 42 | Loggerf: emptyLogfProvider, 43 | }, 44 | } 45 | router.setEmptyNameToReplicaset() 46 | 47 | err := router.Topology().AddInstance(ctx, uuid.New().String(), InstanceInfo{}) 48 | require.True(t, errors.Is(err, ErrInvalidInstanceInfo)) 49 | }) 50 | } 51 | 52 | func TestController_RemoveInstance_NoSuchReplicaset(t *testing.T) { 53 | t.Parallel() 54 | ctx := context.Background() 55 | 56 | router := Router{ 57 | cfg: Config{ 58 | Loggerf: emptyLogfProvider, 59 | }, 60 | } 61 | router.setEmptyNameToReplicaset() 62 | 63 | err := router.Topology().RemoveInstance(ctx, uuid.New().String(), "") 64 | require.True(t, errors.Is(err, ErrReplicasetNotExists)) 65 | 66 | } 67 | 68 | func TestController_RemoveInstance_NoReplicasetNameProvided(t *testing.T) { 69 | t.Parallel() 70 | ctx := context.Background() 71 | 72 | instanceName := "instance_001" 73 | 74 | mp := mockpool.NewPooler(t) 75 | mp.On("GetInfo").Return(map[string]pool.ConnectionInfo{ 76 | instanceName: { 77 | ConnectedNow: true, 78 | }, 79 | }) 80 | 81 | mp.On("Remove", mock.Anything).Return(nil) 82 | 83 | router := Router{ 84 | cfg: Config{ 85 | Loggerf: emptyLogfProvider, 86 | }, 87 | } 88 | _ = router.swapNameToReplicaset(nil, &map[string]*Replicaset{ 89 | "replicaset_1": { 90 | conn: mp, 91 | }, 92 | }) 93 | 94 | err := router.Topology().RemoveInstance(ctx, "", instanceName) 95 | require.NoError(t, err) 96 | 97 | } 98 | 99 | func TestController_RemoveReplicaset(t *testing.T) { 100 | t.Parallel() 101 | 102 | ctx := context.Background() 103 | 104 | uuidToRemove := uuid.New() 105 | mPool := mockpool.NewPooler(t) 106 | mPool.On("CloseGraceful").Return(nil) 107 | 108 | router := Router{ 109 | cfg: Config{ 110 | Loggerf: emptyLogfProvider, 111 | }, 112 | } 113 | _ = router.swapNameToReplicaset(nil, &map[string]*Replicaset{ 114 | uuidToRemove.String(): {conn: mPool}, 115 | }) 116 | 117 | t.Run("no such replicaset", func(t *testing.T) { 118 | t.Parallel() 119 | errs := router.Topology().RemoveReplicaset(ctx, uuid.New().String()) 120 | require.True(t, errors.Is(errs[0], ErrReplicasetNotExists)) 121 | }) 122 | t.Run("successfully remove", func(t *testing.T) { 123 | t.Parallel() 124 | errs := router.Topology().RemoveReplicaset(ctx, uuidToRemove.String()) 125 | require.Empty(t, errs) 126 | }) 127 | } 128 | 129 | func TestRouter_AddReplicaset_AlreadyExists(t *testing.T) { 130 | ctx := context.TODO() 131 | 132 | alreadyExistingRsName := uuid.New().String() 133 | 134 | router := Router{ 135 | cfg: Config{ 136 | Loggerf: emptyLogfProvider, 137 | }, 138 | } 139 | _ = router.swapNameToReplicaset(nil, &map[string]*Replicaset{ 140 | alreadyExistingRsName: {conn: nil}, 141 | }) 142 | 143 | // Test that such replicaset already exists 144 | err := router.AddReplicaset(ctx, ReplicasetInfo{Name: alreadyExistingRsName}, []InstanceInfo{}) 145 | require.Equalf(t, ErrReplicasetExists, err, "such replicaset must already exists") 146 | } 147 | 148 | func TestRouter_AddReplicaset_InvalidReplicaset(t *testing.T) { 149 | t.Parallel() 150 | 151 | ctx := context.TODO() 152 | 153 | alreadyExistingRsName := uuid.New().String() 154 | 155 | router := Router{ 156 | cfg: Config{ 157 | Loggerf: emptyLogfProvider, 158 | }, 159 | } 160 | _ = router.swapNameToReplicaset(nil, &map[string]*Replicaset{ 161 | alreadyExistingRsName: {conn: nil}, 162 | }) 163 | 164 | // Test that such replicaset already exists 165 | rsInfo := ReplicasetInfo{} 166 | 167 | err := router.AddReplicaset(ctx, rsInfo, []InstanceInfo{}) 168 | require.Error(t, err) 169 | require.Equal(t, rsInfo.Validate().Error(), err.Error()) 170 | } 171 | -------------------------------------------------------------------------------- /vshard_shadow_test.go: -------------------------------------------------------------------------------- 1 | package vshard_router_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/google/uuid" 9 | "github.com/stretchr/testify/require" 10 | 11 | vshardrouter "github.com/tarantool/go-vshard-router/v2" 12 | "github.com/tarantool/go-vshard-router/v2/providers/static" 13 | ) 14 | 15 | type errorTopologyProvider struct{} 16 | 17 | func (e *errorTopologyProvider) Init(_ vshardrouter.TopologyController) error { 18 | return fmt.Errorf("test error") 19 | } 20 | func (e *errorTopologyProvider) Close() {} 21 | 22 | func TestNewRouter_ProviderError(t *testing.T) { 23 | ctx := context.TODO() 24 | _, err := vshardrouter.NewRouter(ctx, vshardrouter.Config{ 25 | TotalBucketCount: 256000, 26 | TopologyProvider: &errorTopologyProvider{}, 27 | }) 28 | 29 | require.ErrorIs(t, err, vshardrouter.ErrTopologyProvider) 30 | } 31 | 32 | func TestNewRouter_EmptyReplicasets(t *testing.T) { 33 | ctx := context.TODO() 34 | 35 | router, err := vshardrouter.NewRouter(ctx, vshardrouter.Config{}) 36 | require.Error(t, err) 37 | require.Nil(t, router) 38 | } 39 | 40 | func TestNewRouter_InvalidReplicasetUUID(t *testing.T) { 41 | ctx := context.TODO() 42 | 43 | router, err := vshardrouter.NewRouter(ctx, vshardrouter.Config{ 44 | TopologyProvider: static.NewProvider(map[vshardrouter.ReplicasetInfo][]vshardrouter.InstanceInfo{ 45 | { 46 | Name: "123", 47 | }: { 48 | {Addr: "first.internal:1212"}, 49 | }, 50 | }), 51 | }) 52 | 53 | require.Error(t, err) 54 | require.Nil(t, router) 55 | } 56 | 57 | func TestNewRouter_InstanceAddr(t *testing.T) { 58 | ctx := context.TODO() 59 | 60 | router, err := vshardrouter.NewRouter(ctx, vshardrouter.Config{ 61 | TopologyProvider: static.NewProvider(map[vshardrouter.ReplicasetInfo][]vshardrouter.InstanceInfo{ 62 | { 63 | Name: "123", 64 | UUID: uuid.New(), 65 | }: { 66 | {Addr: "first.internal:1212"}, 67 | }, 68 | }), 69 | }) 70 | 71 | require.Error(t, err) 72 | require.Nil(t, router) 73 | } 74 | 75 | func TestRouterBucketIDStrCRC32(t *testing.T) { 76 | // required values from tarantool example 77 | require.Equal(t, uint64(103202), vshardrouter.BucketIDStrCRC32("2707623829", uint64(256000))) 78 | require.Equal(t, uint64(35415), vshardrouter.BucketIDStrCRC32("2706201716", uint64(256000))) 79 | } 80 | 81 | func BenchmarkRouterBucketIDStrCRC32(b *testing.B) { 82 | for i := 0; i < b.N; i++ { 83 | vshardrouter.BucketIDStrCRC32("test_bench_key", uint64(256000)) 84 | } 85 | b.ReportAllocs() 86 | } 87 | 88 | func TestInstanceInfo_Validate(t *testing.T) { 89 | tCases := []struct { 90 | Name string 91 | II vshardrouter.InstanceInfo 92 | Valid bool 93 | }{ 94 | { 95 | Name: "no info", 96 | II: vshardrouter.InstanceInfo{}, 97 | Valid: false, 98 | }, 99 | { 100 | Name: "no uuid - ok", 101 | II: vshardrouter.InstanceInfo{Addr: "first.internal:1212", Name: "instance_123"}, 102 | Valid: true, 103 | }, 104 | { 105 | Name: "no addr", 106 | II: vshardrouter.InstanceInfo{UUID: uuid.New()}, 107 | Valid: false, 108 | }, 109 | { 110 | Name: "no instance name", 111 | II: vshardrouter.InstanceInfo{UUID: uuid.New(), Addr: "first.internal:1212"}, 112 | Valid: false, 113 | }, 114 | } 115 | 116 | for _, tc := range tCases { 117 | t.Run(tc.Name, func(t *testing.T) { 118 | err := tc.II.Validate() 119 | if tc.Valid { 120 | require.NoError(t, err) 121 | } else { 122 | require.Error(t, err) 123 | } 124 | }) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /vshard_test.go: -------------------------------------------------------------------------------- 1 | package vshard_router //nolint:revive 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestRouter_RouterBucketIDStrCRC32(t *testing.T) { 10 | r := Router{ 11 | cfg: Config{TotalBucketCount: uint64(256000)}, 12 | } 13 | 14 | t.Run("new logic with current hash sum", func(t *testing.T) { 15 | require.Equal(t, uint64(103202), r.BucketIDStrCRC32("2707623829")) 16 | }) 17 | } 18 | 19 | func TestRouter_RouterBucketCount(t *testing.T) { 20 | bucketCount := uint64(123) 21 | 22 | r := Router{ 23 | cfg: Config{TotalBucketCount: bucketCount}, 24 | } 25 | 26 | require.Equal(t, bucketCount, r.BucketCount()) 27 | } 28 | 29 | func TestRouter_RouteMapClean(t *testing.T) { 30 | r := Router{ 31 | cfg: Config{TotalBucketCount: 10}, 32 | } 33 | 34 | require.NotPanics(t, func() { 35 | r.RouteMapClean() 36 | }) 37 | } 38 | --------------------------------------------------------------------------------