├── .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 |
4 |
5 | [](https://pkg.go.dev/github.com/tarantool/go-vshard-router)
6 | [![Actions Status][actions-badge]][actions-url]
7 | [](https://goreportcard.com/report/github.com/tarantool/go-vshard-router)
8 | [![Code Coverage][coverage-badge]][coverage-url]
9 | [](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 |
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 | 
234 | - tarantool-router: (80% cpu, heavy rps kills proxy at 100% cpu)
235 | 
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 |
4 |
5 | [](https://pkg.go.dev/github.com/tarantool/go-vshard-router)
6 | [![Actions Status][actions-badge]][actions-url]
7 | [](https://goreportcard.com/report/github.com/tarantool/go-vshard-router)
8 | [![Code Coverage][coverage-badge]][coverage-url]
9 | [](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 |
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 | 
253 | - tarantool-router: (80% cpu, heavy rps kills proxy at 100% cpu)
254 | 
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 |
--------------------------------------------------------------------------------