├── .golangci.yml ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── bench_test.go ├── cluster.go ├── cluster_test.go ├── example_cluster_test.go ├── export_test.go ├── go.mod ├── go.sum ├── idgen.go ├── idgen_test.go ├── renovate.json ├── uuid.go └── uuid_test.go /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | tests: false 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | language: go 3 | 4 | addons: 5 | postgresql: "9.6" 6 | 7 | go: 8 | - 1.14.x 9 | - 1.15.x 10 | - tip 11 | 12 | matrix: 13 | allow_failures: 14 | - go: tip 15 | 16 | go_import_path: github.com/go-pg/sharding 17 | 18 | before_install: 19 | - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.21.0 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 github.com/go-pg/sharding Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 15 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 16 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 17 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 18 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 19 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 20 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | go test ./... 3 | go test ./... -short -race 4 | go test ./... -run=NONE -bench=. 5 | env GOOS=linux GOARCH=386 go test ./... 6 | go vet ./... 7 | go get github.com/gordonklaus/ineffassign 8 | ineffassign . 9 | golangci-lint run 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PostgreSQL sharding for go-pg and Golang 2 | 3 | [![Build Status](https://travis-ci.org/go-pg/sharding.svg)](https://travis-ci.org/go-pg/sharding) 4 | 5 | > :heart: [**Uptrace.dev** - distributed traces, logs, and errors in one place](https://uptrace.dev) 6 | 7 | This package uses a [go-pg PostgreSQL client](https://github.com/go-pg/pg) to help sharding your data across a set of PostgreSQL servers as described in [Sharding & IDs at Instagram](http://instagram-engineering.tumblr.com/post/10853187575/sharding-ids-at-instagram). In 2 words it maps many (2048-8192) logical shards implemented using PostgreSQL schemas to far fewer physical PostgreSQL servers. 8 | 9 | API docs: http://godoc.org/github.com/go-pg/sharding. 10 | Examples: http://godoc.org/github.com/go-pg/sharding#pkg-examples. 11 | 12 | ## Installation 13 | 14 | This package requires [Go modules](https://github.com/golang/go/wiki/Modules) support: 15 | 16 | go get github.com/go-pg/sharding/v8 17 | 18 | ## Quickstart 19 | 20 | ```go 21 | package sharding_test 22 | 23 | import ( 24 | "fmt" 25 | 26 | "github.com/go-pg/sharding/v8" 27 | "github.com/go-pg/pg/v10" 28 | ) 29 | 30 | // Users are sharded by AccountId, i.e. users with same account id are 31 | // placed on the same shard. 32 | type User struct { 33 | tableName string `pg:"?SHARD.users"` 34 | 35 | Id int64 36 | AccountId int64 37 | Name string 38 | Emails []string 39 | } 40 | 41 | func (u User) String() string { 42 | return u.Name 43 | } 44 | 45 | // CreateUser picks shard by account id and creates user in the shard. 46 | func CreateUser(cluster *sharding.Cluster, user *User) error { 47 | return cluster.Shard(user.AccountId).Insert(user) 48 | } 49 | 50 | // GetUser splits shard from user id and fetches user from the shard. 51 | func GetUser(cluster *sharding.Cluster, id int64) (*User, error) { 52 | var user User 53 | err := cluster.SplitShard(id).Model(&user).Where("id = ?", id).Select() 54 | return &user, err 55 | } 56 | 57 | // GetUsers picks shard by account id and fetches users from the shard. 58 | func GetUsers(cluster *sharding.Cluster, accountId int64) ([]User, error) { 59 | var users []User 60 | err := cluster.Shard(accountId).Model(&users).Where("account_id = ?", accountId).Select() 61 | return users, err 62 | } 63 | 64 | // createShard creates database schema for a given shard. 65 | func createShard(shard *pg.DB) error { 66 | queries := []string{ 67 | `DROP SCHEMA IF EXISTS ?SHARD CASCADE`, 68 | `CREATE SCHEMA ?SHARD`, 69 | sqlFuncs, 70 | `CREATE TABLE ?SHARD.users (id bigint DEFAULT ?SHARD.next_id(), account_id int, name text, emails jsonb)`, 71 | } 72 | 73 | for _, q := range queries { 74 | _, err := shard.Exec(q) 75 | if err != nil { 76 | return err 77 | } 78 | } 79 | 80 | return nil 81 | } 82 | 83 | func ExampleCluster() { 84 | db := pg.Connect(&pg.Options{ 85 | User: "postgres", 86 | }) 87 | 88 | dbs := []*pg.DB{db} // list of physical PostgreSQL servers 89 | nshards := 2 // 2 logical shards 90 | // Create cluster with 1 physical server and 2 logical shards. 91 | cluster := sharding.NewCluster(dbs, nshards) 92 | 93 | // Create database schema for our logical shards. 94 | for i := 0; i < nshards; i++ { 95 | if err := createShard(cluster.Shard(int64(i))); err != nil { 96 | panic(err) 97 | } 98 | } 99 | 100 | // user1 will be created in shard1 because AccountId % nshards = shard1. 101 | user1 := &User{ 102 | Name: "user1", 103 | AccountId: 1, 104 | Emails: []string{"user1@domain"}, 105 | } 106 | err := CreateUser(cluster, user1) 107 | if err != nil { 108 | panic(err) 109 | } 110 | 111 | // user2 will be created in shard1 too because AccountId is the same. 112 | user2 := &User{ 113 | Name: "user2", 114 | AccountId: 1, 115 | Emails: []string{"user2@domain"}, 116 | } 117 | err = CreateUser(cluster, user2) 118 | if err != nil { 119 | panic(err) 120 | } 121 | 122 | // user3 will be created in shard0 because AccountId % nshards = shard0. 123 | user3 := &User{ 124 | Name: "user3", 125 | AccountId: 2, 126 | Emails: []string{"user3@domain"}, 127 | } 128 | err = CreateUser(cluster, user3) 129 | if err != nil { 130 | panic(err) 131 | } 132 | 133 | user, err := GetUser(cluster, user1.Id) 134 | if err != nil { 135 | panic(err) 136 | } 137 | 138 | users, err := GetUsers(cluster, 1) 139 | if err != nil { 140 | panic(err) 141 | } 142 | 143 | fmt.Println(user) 144 | fmt.Println(users[0], users[1]) 145 | // Output: user1 146 | // user1 user2 147 | } 148 | 149 | const sqlFuncs = ` 150 | CREATE OR REPLACE FUNCTION public.make_id(tm timestamptz, seq_id bigint, shard_id int) 151 | RETURNS bigint AS $$ 152 | DECLARE 153 | max_shard_id CONSTANT bigint := 2048; 154 | max_seq_id CONSTANT bigint := 4096; 155 | id bigint; 156 | BEGIN 157 | shard_id := shard_id % max_shard_id; 158 | seq_id := seq_id % max_seq_id; 159 | id := (floor(extract(epoch FROM tm) * 1000)::bigint - ?EPOCH) << 23; 160 | id := id | (shard_id << 12); 161 | id := id | seq_id; 162 | RETURN id; 163 | END; 164 | $$ 165 | LANGUAGE plpgsql IMMUTABLE; 166 | 167 | CREATE FUNCTION ?SHARD.make_id(tm timestamptz, seq_id bigint) 168 | RETURNS bigint AS $$ 169 | BEGIN 170 | RETURN public.make_id(tm, seq_id, ?SHARD_ID); 171 | END; 172 | $$ 173 | LANGUAGE plpgsql IMMUTABLE; 174 | 175 | CREATE SEQUENCE ?SHARD.id_seq; 176 | ` 177 | ``` 178 | 179 | ## Howto 180 | 181 | Please use [Golang PostgreSQL client](https://github.com/go-pg/pg) docs to get the idea how to use this package. 182 | -------------------------------------------------------------------------------- /bench_test.go: -------------------------------------------------------------------------------- 1 | package sharding_test 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | "time" 7 | 8 | "github.com/go-pg/sharding/v8" 9 | 10 | "github.com/go-pg/pg/v10" 11 | ) 12 | 13 | func benchmarkDB() *pg.DB { 14 | return pg.Connect(&pg.Options{ 15 | User: "postgres", 16 | Database: "postgres", 17 | DialTimeout: 30 * time.Second, 18 | ReadTimeout: 10 * time.Second, 19 | WriteTimeout: 10 * time.Second, 20 | PoolSize: 10, 21 | PoolTimeout: 30 * time.Second, 22 | }) 23 | } 24 | 25 | func BenchmarkGopg(b *testing.B) { 26 | db := benchmarkDB() 27 | defer db.Close() 28 | 29 | b.ResetTimer() 30 | 31 | b.RunParallel(func(pb *testing.PB) { 32 | for pb.Next() { 33 | _, err := db.Exec("SELECT 1") 34 | if err != nil { 35 | b.Fatal(err) 36 | } 37 | } 38 | }) 39 | } 40 | 41 | func BenchmarkCluster(b *testing.B) { 42 | db := benchmarkDB() 43 | defer db.Close() 44 | 45 | cluster := sharding.NewCluster([]*pg.DB{db}, 1) 46 | defer cluster.Close() 47 | 48 | b.ResetTimer() 49 | 50 | b.RunParallel(func(pb *testing.PB) { 51 | for pb.Next() { 52 | _, err := cluster.Shard(0).Exec("SELECT 1") 53 | if err != nil { 54 | b.Fatal(err) 55 | } 56 | } 57 | }) 58 | } 59 | 60 | var sink *sharding.SubCluster 61 | 62 | func BenchmarkSubCluster(b *testing.B) { 63 | db := benchmarkDB() 64 | defer db.Close() 65 | 66 | cluster := sharding.NewCluster([]*pg.DB{db}, 1) 67 | defer cluster.Close() 68 | 69 | b.RunParallel(func(pb *testing.PB) { 70 | n := rand.Int63() 71 | for pb.Next() { 72 | sink = cluster.SubCluster(n, 32) 73 | } 74 | }) 75 | } 76 | 77 | func BenchmarkNewUUID(b *testing.B) { 78 | tm := time.Now() 79 | 80 | b.RunParallel(func(pb *testing.PB) { 81 | for pb.Next() { 82 | _ = sharding.NewUUID(0, tm) 83 | } 84 | }) 85 | } 86 | -------------------------------------------------------------------------------- /cluster.go: -------------------------------------------------------------------------------- 1 | package sharding 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "sync" 7 | 8 | "github.com/go-pg/pg/v10" 9 | ) 10 | 11 | type shardInfo struct { 12 | id int 13 | shard *pg.DB 14 | dbInd int 15 | } 16 | 17 | // Cluster maps many (up to 2048) logical database shards implemented 18 | // using PostgreSQL schemas to far fewer physical PostgreSQL servers. 19 | type Cluster struct { 20 | gen *IDGen 21 | 22 | dbs []*pg.DB 23 | servers []*pg.DB // unique dbs 24 | 25 | shards []shardInfo 26 | shardList []*pg.DB 27 | } 28 | 29 | // NewClusterWithGen returns new PostgreSQL cluster consisting of physical 30 | // dbs and running nshards logical shards. 31 | func NewClusterWithGen(dbs []*pg.DB, nshards int, gen *IDGen) *Cluster { 32 | if gen == nil { 33 | gen = DefaultIDGen 34 | } 35 | if len(dbs) == 0 { 36 | panic("at least one db is required") 37 | } 38 | if nshards == 0 { 39 | panic("at least one shard is required") 40 | } 41 | if len(dbs) > gen.NumShards() || nshards > gen.NumShards() { 42 | panic(fmt.Sprintf("too many shards")) 43 | } 44 | if nshards < len(dbs) { 45 | panic("number of shards must be greater or equal number of dbs") 46 | } 47 | if nshards%len(dbs) != 0 { 48 | panic("number of shards must be divideable by number of dbs") 49 | } 50 | 51 | cl := &Cluster{ 52 | gen: gen, 53 | dbs: dbs, 54 | shards: make([]shardInfo, nshards), 55 | shardList: make([]*pg.DB, nshards), 56 | } 57 | cl.init() 58 | 59 | return cl 60 | } 61 | 62 | func NewCluster(dbs []*pg.DB, nshards int) *Cluster { 63 | return NewClusterWithGen(dbs, nshards, nil) 64 | } 65 | 66 | func (cl *Cluster) init() { 67 | dbSet := make(map[*pg.DB]struct{}) 68 | for _, db := range cl.dbs { 69 | if _, ok := dbSet[db]; ok { 70 | continue 71 | } 72 | dbSet[db] = struct{}{} 73 | cl.servers = append(cl.servers, db) 74 | } 75 | 76 | for i := 0; i < len(cl.shards); i++ { 77 | dbInd := i % len(cl.dbs) 78 | shard := cl.newShard(cl.dbs[dbInd], int64(i)) 79 | cl.shards[i] = shardInfo{ 80 | id: i, 81 | shard: shard, 82 | dbInd: dbInd, 83 | } 84 | cl.shardList[i] = shard 85 | } 86 | } 87 | 88 | func (cl *Cluster) IDGen() *IDGen { 89 | return cl.gen 90 | } 91 | 92 | func (cl *Cluster) newShard(db *pg.DB, id int64) *pg.DB { 93 | name := "shard" + strconv.FormatInt(id, 10) 94 | return db. 95 | WithParam("shard_id", id). 96 | WithParam("shard", pg.Safe(name)). 97 | WithParam("epoch", cl.gen.epoch). 98 | WithParam("SHARD_ID", id). 99 | WithParam("SHARD", pg.Safe(name)). 100 | WithParam("EPOCH", cl.gen.epoch) 101 | } 102 | 103 | func (cl *Cluster) Close() error { 104 | var firstErr error 105 | for _, db := range cl.servers { 106 | if err := db.Close(); err != nil && firstErr == nil { 107 | firstErr = err 108 | } 109 | } 110 | return firstErr 111 | } 112 | 113 | // DBs returns list of database servers in the cluster. 114 | func (cl *Cluster) DBs() []*pg.DB { 115 | return cl.dbs 116 | } 117 | 118 | // DB returns db id and db for the number. 119 | func (cl *Cluster) DB(number int64) (int, *pg.DB) { 120 | idx := uint64(number) 121 | idx %= uint64(len(cl.shards)) 122 | dbInd := cl.shards[idx].dbInd 123 | return dbInd, cl.dbs[dbInd] 124 | } 125 | 126 | // Shards returns list of shards running in the db. If db is nil all 127 | // shards are returned. 128 | func (cl *Cluster) Shards(db *pg.DB) []*pg.DB { 129 | if db == nil { 130 | return cl.shardList 131 | } 132 | 133 | var shards []*pg.DB 134 | for i := range cl.shards { 135 | shard := &cl.shards[i] 136 | if cl.dbs[shard.dbInd] == db { 137 | shards = append(shards, shard.shard) 138 | } 139 | } 140 | return shards 141 | } 142 | 143 | // Shard maps the number to the corresponding shard in the cluster. 144 | func (cl *Cluster) Shard(number int64) *pg.DB { 145 | idx := uint64(number) % uint64(len(cl.shards)) 146 | return cl.shards[idx].shard 147 | } 148 | 149 | // SplitShard uses SplitID to extract shard id from the id and then 150 | // returns corresponding Shard in the cluster. 151 | func (cl *Cluster) SplitShard(id int64) *pg.DB { 152 | _, shardID, _ := cl.gen.SplitID(id) 153 | return cl.Shard(shardID) 154 | } 155 | 156 | // ForEachDB concurrently calls the fn on each database in the cluster. 157 | func (cl *Cluster) ForEachDB(fn func(db *pg.DB) error) error { 158 | errCh := make(chan error, 1) 159 | var wg sync.WaitGroup 160 | wg.Add(len(cl.servers)) 161 | for _, db := range cl.servers { 162 | go func(db *pg.DB) { 163 | defer wg.Done() 164 | if err := fn(db); err != nil { 165 | select { 166 | case errCh <- err: 167 | default: 168 | } 169 | } 170 | }(db) 171 | } 172 | wg.Wait() 173 | 174 | select { 175 | case err := <-errCh: 176 | return err 177 | default: 178 | return nil 179 | } 180 | } 181 | 182 | // ForEachShard concurrently calls the fn on each shard in the cluster. 183 | // It is the same as ForEachNShards(1, fn). 184 | func (cl *Cluster) ForEachShard(fn func(shard *pg.DB) error) error { 185 | return cl.ForEachDB(func(db *pg.DB) error { 186 | var firstErr error 187 | for i := range cl.shards { 188 | shard := cl.shards[i].shard 189 | 190 | if shard.Options() != db.Options() { 191 | continue 192 | } 193 | 194 | if err := fn(shard); err != nil && firstErr == nil { 195 | firstErr = err 196 | } 197 | } 198 | return firstErr 199 | }) 200 | } 201 | 202 | // ForEachNShards concurrently calls the fn on each N shards in the cluster. 203 | func (cl *Cluster) ForEachNShards(n int, fn func(shard *pg.DB) error) error { 204 | return cl.ForEachDB(func(db *pg.DB) error { 205 | var wg sync.WaitGroup 206 | errCh := make(chan error, 1) 207 | limit := make(chan struct{}, n) 208 | 209 | for i := range cl.shards { 210 | shard := cl.shards[i].shard 211 | 212 | if shard.Options() != db.Options() { 213 | continue 214 | } 215 | 216 | limit <- struct{}{} 217 | wg.Add(1) 218 | go func(shard *pg.DB) { 219 | defer func() { 220 | <-limit 221 | wg.Done() 222 | }() 223 | if err := fn(shard); err != nil { 224 | select { 225 | case errCh <- err: 226 | default: 227 | } 228 | } 229 | }(shard) 230 | } 231 | 232 | wg.Wait() 233 | 234 | select { 235 | case err := <-errCh: 236 | return err 237 | default: 238 | return nil 239 | } 240 | }) 241 | } 242 | 243 | // SubCluster is a subset of the cluster. 244 | type SubCluster struct { 245 | cl *Cluster 246 | shards []*shardInfo 247 | } 248 | 249 | // SubCluster returns a subset of the cluster of the given size. 250 | func (cl *Cluster) SubCluster(number int64, size int) *SubCluster { 251 | if size > len(cl.shards) { 252 | size = len(cl.shards) 253 | } 254 | step := len(cl.shards) / size 255 | clusterId := int(uint64(number)%uint64(step)) * size 256 | shards := make([]*shardInfo, size) 257 | for i := 0; i < size; i++ { 258 | shards[i] = &cl.shards[clusterId+i] 259 | } 260 | 261 | return &SubCluster{ 262 | cl: cl, 263 | shards: shards, 264 | } 265 | } 266 | 267 | // SplitShard uses SplitID to extract shard id from the id and then 268 | // returns corresponding Shard in the subcluster. 269 | func (cl *SubCluster) SplitShard(id int64) *pg.DB { 270 | _, shardID, _ := cl.cl.gen.SplitID(id) 271 | return cl.Shard(shardID) 272 | } 273 | 274 | // Shard maps the number to the corresponding shard in the subscluster. 275 | func (cl *SubCluster) Shard(number int64) *pg.DB { 276 | idx := uint64(number) % uint64(len(cl.shards)) 277 | return cl.shards[idx].shard 278 | } 279 | 280 | // ForEachShard concurrently calls the fn on each shard in the subcluster. 281 | // It is the same as ForEachNShards(1, fn). 282 | func (cl *SubCluster) ForEachShard(fn func(shard *pg.DB) error) error { 283 | return cl.cl.ForEachDB(func(db *pg.DB) error { 284 | var firstErr error 285 | for i := range cl.shards { 286 | shard := cl.shards[i].shard 287 | 288 | if shard.Options() != db.Options() { 289 | continue 290 | } 291 | 292 | if err := fn(shard); err != nil && firstErr == nil { 293 | firstErr = err 294 | } 295 | } 296 | return firstErr 297 | }) 298 | } 299 | 300 | // ForEachNShards concurrently calls the fn on each N shards in the subcluster. 301 | func (cl *SubCluster) ForEachNShards(n int, fn func(shard *pg.DB) error) error { 302 | return cl.cl.ForEachDB(func(db *pg.DB) error { 303 | var wg sync.WaitGroup 304 | errCh := make(chan error, 1) 305 | limit := make(chan struct{}, n) 306 | 307 | for i := range cl.shards { 308 | shard := cl.shards[i].shard 309 | 310 | if shard.Options() != db.Options() { 311 | continue 312 | } 313 | 314 | limit <- struct{}{} 315 | wg.Add(1) 316 | go func(shard *pg.DB) { 317 | defer func() { 318 | <-limit 319 | wg.Done() 320 | }() 321 | if err := fn(shard); err != nil { 322 | select { 323 | case errCh <- err: 324 | default: 325 | } 326 | } 327 | }(shard) 328 | } 329 | 330 | wg.Wait() 331 | 332 | select { 333 | case err := <-errCh: 334 | return err 335 | default: 336 | return nil 337 | } 338 | }) 339 | } 340 | -------------------------------------------------------------------------------- /cluster_test.go: -------------------------------------------------------------------------------- 1 | package sharding_test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "math" 7 | "sort" 8 | "sync" 9 | "testing" 10 | "time" 11 | 12 | "github.com/go-pg/sharding/v8" 13 | 14 | "github.com/go-pg/pg/v10" 15 | . "github.com/onsi/ginkgo" 16 | . "github.com/onsi/gomega" 17 | ) 18 | 19 | func TestGinkgo(t *testing.T) { 20 | RegisterFailHandler(Fail) 21 | RunSpecs(t, "sharding") 22 | } 23 | 24 | var _ = Describe("named params", func() { 25 | var cluster *sharding.Cluster 26 | 27 | BeforeEach(func() { 28 | db := pg.Connect(&pg.Options{ 29 | User: "postgres", 30 | }) 31 | cluster = sharding.NewCluster([]*pg.DB{db}, 4) 32 | }) 33 | 34 | It("supports ?SHARD", func() { 35 | var shardName, hello string 36 | _, err := cluster.Shard(3).QueryOne( 37 | pg.Scan(&shardName, &hello), `SELECT '?SHARD', ?`, "hello") 38 | Expect(err).NotTo(HaveOccurred()) 39 | Expect(shardName).To(Equal("shard3")) 40 | Expect(hello).To(Equal("hello")) 41 | }) 42 | 43 | It("supports ?SHARD_ID", func() { 44 | var shardID int 45 | _, err := cluster.Shard(3).QueryOne(pg.Scan(&shardID), "SELECT ?SHARD_ID") 46 | Expect(err).NotTo(HaveOccurred()) 47 | Expect(shardID).To(Equal(3)) 48 | }) 49 | 50 | It("supports ?EPOCH", func() { 51 | var epoch int64 52 | _, err := cluster.Shard(0).QueryOne(pg.Scan(&epoch), "SELECT ?EPOCH") 53 | Expect(err).NotTo(HaveOccurred()) 54 | Expect(epoch).To(Equal(int64(1262304000000))) 55 | }) 56 | 57 | It("supports UUID", func() { 58 | src := sharding.NewUUID(1234, time.Unix(math.MaxInt64, 0)) 59 | var dst sharding.UUID 60 | _, err := cluster.Shard(3).QueryOne(pg.Scan(&dst), `SELECT ?`, src) 61 | Expect(err).NotTo(HaveOccurred()) 62 | Expect(dst).To(Equal(src)) 63 | }) 64 | }) 65 | 66 | var _ = Describe("Cluster", func() { 67 | var db1, db2 *pg.DB 68 | var cluster *sharding.Cluster 69 | 70 | BeforeEach(func() { 71 | db1 = pg.Connect(&pg.Options{ 72 | Addr: "db1", 73 | }) 74 | db2 = pg.Connect(&pg.Options{ 75 | Addr: "db2", 76 | }) 77 | Expect(db1).NotTo(Equal(db2)) 78 | cluster = sharding.NewCluster([]*pg.DB{db1, db2, db1, db2}, 4) 79 | }) 80 | 81 | AfterEach(func() { 82 | Expect(cluster.Close()).NotTo(HaveOccurred()) 83 | }) 84 | 85 | It("distributes projects on different servers", func() { 86 | tests := []struct { 87 | projectId int64 88 | wanted *pg.DB 89 | }{ 90 | {0, db1}, 91 | {1, db2}, 92 | {2, db1}, 93 | {3, db2}, 94 | {4, db1}, 95 | } 96 | 97 | for _, test := range tests { 98 | shard := cluster.Shard(test.projectId) 99 | Expect(shard.Options()).To(Equal(test.wanted.Options())) 100 | } 101 | }) 102 | 103 | Describe("ForEachDB", func() { 104 | It("fn is called once for every database", func() { 105 | var dbs []*pg.DB 106 | var mu sync.Mutex 107 | err := cluster.ForEachDB(func(db *pg.DB) error { 108 | defer GinkgoRecover() 109 | mu.Lock() 110 | Expect(dbs).NotTo(ContainElement(db)) 111 | dbs = append(dbs, db) 112 | mu.Unlock() 113 | return nil 114 | }) 115 | Expect(err).NotTo(HaveOccurred()) 116 | Expect(dbs).To(HaveLen(2)) 117 | }) 118 | 119 | It("returns an error if fn fails", func() { 120 | err := cluster.ForEachDB(func(db *pg.DB) error { 121 | if db == db2 { 122 | return errors.New("fake error") 123 | } 124 | return nil 125 | }) 126 | Expect(err).To(MatchError("fake error")) 127 | }) 128 | }) 129 | 130 | Describe("ForEachShard", func() { 131 | It("fn is called once for every shard", func() { 132 | var shards []int64 133 | var mu sync.Mutex 134 | err := cluster.ForEachShard(func(shard *pg.DB) error { 135 | defer GinkgoRecover() 136 | 137 | mu.Lock() 138 | Expect(shards).NotTo(ContainElement(shardID)) 139 | shards = append(shards, shardID(shard)) 140 | mu.Unlock() 141 | return nil 142 | }) 143 | Expect(err).NotTo(HaveOccurred()) 144 | Expect(shards).To(HaveLen(4)) 145 | for shardID := int64(0); shardID < 4; shardID++ { 146 | Expect(shards).To(ContainElement(shardID)) 147 | } 148 | }) 149 | 150 | It("returns an error if fn fails", func() { 151 | err := cluster.ForEachShard(func(shard *pg.DB) error { 152 | if shardID(shard) == 3 { 153 | return errors.New("fake error") 154 | } 155 | return nil 156 | }) 157 | Expect(err).To(MatchError("fake error")) 158 | }) 159 | }) 160 | 161 | Describe("ForEachNShards", func() { 162 | It("fn is called once for every shard", func() { 163 | var shards []int64 164 | var mu sync.Mutex 165 | err := cluster.ForEachNShards(2, func(shard *pg.DB) error { 166 | defer GinkgoRecover() 167 | 168 | shardID := shardID(shard) 169 | mu.Lock() 170 | Expect(shards).NotTo(ContainElement(shardID)) 171 | shards = append(shards, shardID) 172 | mu.Unlock() 173 | return nil 174 | }) 175 | Expect(err).NotTo(HaveOccurred()) 176 | Expect(shards).To(HaveLen(4)) 177 | for shardID := int64(0); shardID < 4; shardID++ { 178 | Expect(shards).To(ContainElement(shardID)) 179 | } 180 | }) 181 | 182 | It("returns an error if fn fails", func() { 183 | err := cluster.ForEachNShards(2, func(shard *pg.DB) error { 184 | if shardID(shard) == 3 { 185 | return errors.New("fake error") 186 | } 187 | return nil 188 | }) 189 | Expect(err).To(MatchError("fake error")) 190 | }) 191 | }) 192 | 193 | Describe("SubCluster", func() { 194 | var alldbs []*pg.DB 195 | 196 | BeforeEach(func() { 197 | alldbs = alldbs[:0] 198 | for i := 0; i < 4; i++ { 199 | alldbs = append(alldbs, pg.Connect(&pg.Options{ 200 | Addr: "db1", 201 | })) 202 | } 203 | cluster = sharding.NewCluster(alldbs, 8) 204 | }) 205 | 206 | It("creates sub-cluster", func() { 207 | tests := []struct { 208 | subcl *sharding.SubCluster 209 | shardIDs []int64 210 | }{ 211 | {cluster.SubCluster(0, 2), []int64{0, 1}}, 212 | {cluster.SubCluster(1, 2), []int64{2, 3}}, 213 | {cluster.SubCluster(2, 2), []int64{4, 5}}, 214 | {cluster.SubCluster(3, 2), []int64{6, 7}}, 215 | {cluster.SubCluster(4, 2), []int64{0, 1}}, 216 | 217 | {cluster.SubCluster(0, 4), []int64{0, 1, 2, 3}}, 218 | {cluster.SubCluster(1, 4), []int64{4, 5, 6, 7}}, 219 | {cluster.SubCluster(2, 4), []int64{0, 1, 2, 3}}, 220 | 221 | {cluster.SubCluster(0, 8), []int64{0, 1, 2, 3, 4, 5, 6, 7}}, 222 | {cluster.SubCluster(1, 8), []int64{0, 1, 2, 3, 4, 5, 6, 7}}, 223 | 224 | {cluster.SubCluster(0, 16), []int64{0, 1, 2, 3, 4, 5, 6, 7}}, 225 | {cluster.SubCluster(1, 16), []int64{0, 1, 2, 3, 4, 5, 6, 7}}, 226 | } 227 | for _, test := range tests { 228 | var mu sync.Mutex 229 | var shardIDs []int64 230 | dbs := make(map[*pg.Options]struct{}) 231 | err := test.subcl.ForEachShard(func(shard *pg.DB) error { 232 | mu.Lock() 233 | shardIDs = append(shardIDs, shardID(shard)) 234 | dbs[shard.Options()] = struct{}{} 235 | mu.Unlock() 236 | return nil 237 | }) 238 | Expect(err).NotTo(HaveOccurred()) 239 | sort.Slice(shardIDs, func(i, j int) bool { 240 | return shardIDs[i] < shardIDs[j] 241 | }) 242 | Expect(shardIDs).To(Equal(test.shardIDs)) 243 | Expect(len(dbs)).To(Equal(min(len(alldbs), len(shardIDs)))) 244 | 245 | shardIDs = shardIDs[:0] 246 | add := func(shardID int64) { 247 | for _, id := range shardIDs { 248 | if id == shardID { 249 | return 250 | } 251 | } 252 | shardIDs = append(shardIDs, shardID) 253 | } 254 | 255 | for i := 0; i < 16; i++ { 256 | shard := test.subcl.Shard(int64(i)) 257 | shardID := shardID(shard) 258 | Expect(test.shardIDs).To(ContainElement(shardID), "number=%d", i) 259 | add(shardID) 260 | } 261 | Expect(shardIDs).To(Equal(test.shardIDs)) 262 | } 263 | }) 264 | }) 265 | }) 266 | 267 | var _ = Describe("Cluster shards", func() { 268 | It("are distributed across the servers", func() { 269 | var dbs []*pg.DB 270 | for i := 0; i < 16; i++ { 271 | db := pg.Connect(&pg.Options{ 272 | Addr: fmt.Sprintf("db%d", i), 273 | }) 274 | dbs = append(dbs, db) 275 | } 276 | 277 | tests := []struct { 278 | dbs []int 279 | db int 280 | shards []int64 281 | }{ 282 | {[]int{0}, 0, []int64{0, 1, 2, 3, 4, 5, 6, 7}}, 283 | 284 | {[]int{0, 1}, 0, []int64{0, 2, 4, 6}}, 285 | {[]int{0, 1}, 1, []int64{1, 3, 5, 7}}, 286 | 287 | {[]int{0, 1, 2, 1}, 0, []int64{0, 4}}, 288 | {[]int{0, 1, 2, 1}, 1, []int64{1, 3, 5, 7}}, 289 | {[]int{0, 1, 2, 1}, 2, []int64{2, 6}}, 290 | 291 | {[]int{0, 1, 2, 3}, 0, []int64{0, 4}}, 292 | {[]int{0, 1, 2, 3}, 1, []int64{1, 5}}, 293 | {[]int{0, 1, 2, 3}, 2, []int64{2, 6}}, 294 | {[]int{0, 1, 2, 3}, 3, []int64{3, 7}}, 295 | } 296 | for _, test := range tests { 297 | var cldbs []*pg.DB 298 | for _, ind := range test.dbs { 299 | cldbs = append(cldbs, dbs[ind]) 300 | } 301 | cluster := sharding.NewCluster(cldbs, 8) 302 | 303 | var shardIDs []int64 304 | for _, shard := range cluster.Shards(dbs[test.db]) { 305 | shardIDs = append(shardIDs, shardID(shard)) 306 | } 307 | Expect(shardIDs).To(Equal(test.shards)) 308 | } 309 | }) 310 | }) 311 | 312 | func shardID(shard *pg.DB) int64 { 313 | return shard.Param("SHARD_ID").(int64) 314 | } 315 | 316 | func min(a, b int) int { 317 | if a <= b { 318 | return a 319 | } 320 | return b 321 | } 322 | -------------------------------------------------------------------------------- /example_cluster_test.go: -------------------------------------------------------------------------------- 1 | package sharding_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/go-pg/sharding/v8" 7 | 8 | "github.com/go-pg/pg/v10" 9 | ) 10 | 11 | // Users are sharded by AccountId, i.e. users with same account id are 12 | // placed on same shard. 13 | type User struct { 14 | tableName string `pg:"?SHARD.users"` 15 | 16 | Id int64 17 | AccountId int64 18 | Name string 19 | Emails []string 20 | } 21 | 22 | func (u User) String() string { 23 | return u.Name 24 | } 25 | 26 | // CreateUser picks shard by account id and creates user in the shard. 27 | func CreateUser(cluster *sharding.Cluster, user *User) error { 28 | _, err := cluster.Shard(user.AccountId).Model(user).Insert() 29 | return err 30 | } 31 | 32 | // GetUser splits shard from user id and fetches user from the shard. 33 | func GetUser(cluster *sharding.Cluster, id int64) (*User, error) { 34 | var user User 35 | err := cluster.SplitShard(id).Model(&user).Where("id = ?", id).Select() 36 | return &user, err 37 | } 38 | 39 | // GetUsers picks shard by account id and fetches users from the shard. 40 | func GetUsers(cluster *sharding.Cluster, accountId int64) ([]User, error) { 41 | var users []User 42 | err := cluster.Shard(accountId).Model(&users).Where("account_id = ?", accountId).Select() 43 | return users, err 44 | } 45 | 46 | // createShard creates database schema for a given shard. 47 | func createShard(shard *pg.DB) error { 48 | queries := []string{ 49 | `DROP SCHEMA IF EXISTS ?SHARD CASCADE`, 50 | `CREATE SCHEMA ?SHARD`, 51 | sqlFuncs, 52 | `CREATE TABLE ?SHARD.users (id bigint DEFAULT ?SHARD.next_id(), account_id int, name text, emails jsonb)`, 53 | } 54 | 55 | for _, q := range queries { 56 | _, err := shard.Exec(q) 57 | if err != nil { 58 | return err 59 | } 60 | } 61 | 62 | return nil 63 | } 64 | 65 | func ExampleCluster() { 66 | db := pg.Connect(&pg.Options{ 67 | User: "postgres", 68 | }) 69 | 70 | dbs := []*pg.DB{db} // list of physical PostgreSQL servers 71 | nshards := 2 // 2 logical shards 72 | // Create cluster with 1 physical server and 2 logical shards. 73 | cluster := sharding.NewCluster(dbs, nshards) 74 | 75 | // Create database schema for our logical shards. 76 | for i := 0; i < nshards; i++ { 77 | if err := createShard(cluster.Shard(int64(i))); err != nil { 78 | panic(err) 79 | } 80 | } 81 | 82 | // user1 will be created in shard1 because AccountId % nshards = shard1. 83 | user1 := &User{ 84 | Name: "user1", 85 | AccountId: 1, 86 | Emails: []string{"user1@domain"}, 87 | } 88 | err := CreateUser(cluster, user1) 89 | if err != nil { 90 | panic(err) 91 | } 92 | 93 | // user2 will be created in shard1 too AccountId is the same. 94 | user2 := &User{ 95 | Name: "user2", 96 | AccountId: 1, 97 | Emails: []string{"user2@domain"}, 98 | } 99 | err = CreateUser(cluster, user2) 100 | if err != nil { 101 | panic(err) 102 | } 103 | 104 | // user3 will be created in shard0 because AccountId % nshards = shard0. 105 | user3 := &User{ 106 | Name: "user3", 107 | AccountId: 2, 108 | Emails: []string{"user3@domain"}, 109 | } 110 | err = CreateUser(cluster, user3) 111 | if err != nil { 112 | panic(err) 113 | } 114 | 115 | user, err := GetUser(cluster, user1.Id) 116 | if err != nil { 117 | panic(err) 118 | } 119 | 120 | users, err := GetUsers(cluster, 1) 121 | if err != nil { 122 | panic(err) 123 | } 124 | 125 | fmt.Println(user) 126 | fmt.Println(users[0], users[1]) 127 | // Output: user1 128 | // user1 user2 129 | } 130 | 131 | const sqlFuncs = ` 132 | CREATE OR REPLACE FUNCTION public.make_id(tm timestamptz, seq_id bigint, shard_id int) 133 | RETURNS bigint AS $$ 134 | DECLARE 135 | max_shard_id CONSTANT bigint := 2048; 136 | max_seq_id CONSTANT bigint := 4096; 137 | id bigint; 138 | BEGIN 139 | shard_id := shard_id % max_shard_id; 140 | seq_id := seq_id % max_seq_id; 141 | id := (floor(extract(epoch FROM tm) * 1000)::bigint - ?EPOCH) << 23; 142 | id := id | (shard_id << 12); 143 | id := id | seq_id; 144 | RETURN id; 145 | END; 146 | $$ 147 | LANGUAGE plpgsql IMMUTABLE; 148 | 149 | CREATE FUNCTION ?SHARD.make_id(tm timestamptz, seq_id bigint) 150 | RETURNS bigint AS $$ 151 | BEGIN 152 | RETURN public.make_id(tm, seq_id, ?SHARD_ID); 153 | END; 154 | $$ 155 | LANGUAGE plpgsql IMMUTABLE; 156 | 157 | CREATE FUNCTION ?SHARD.next_id() 158 | RETURNS bigint AS $$ 159 | BEGIN 160 | RETURN ?SHARD.make_id(clock_timestamp(), nextval('?SHARD.id_seq')); 161 | END; 162 | $$ 163 | LANGUAGE plpgsql; 164 | 165 | CREATE SEQUENCE ?SHARD.id_seq; 166 | ` 167 | -------------------------------------------------------------------------------- /export_test.go: -------------------------------------------------------------------------------- 1 | package sharding 2 | 3 | import "math/rand" 4 | 5 | func SetUUIDRand(r *rand.Rand) { 6 | uuidRand = r 7 | } 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-pg/sharding/v8 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/go-pg/pg/v10 v10.3.0 7 | github.com/onsi/ginkgo v1.14.1 8 | github.com/onsi/gomega v1.10.2 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 5 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 6 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 11 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 12 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 13 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 14 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 15 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 16 | github.com/go-pg/pg/v10 v10.0.0 h1:2XP/r9XdRfiC+LKWrIwqi2qqc+bhvW7/UpUVnwkT7wk= 17 | github.com/go-pg/pg/v10 v10.0.0/go.mod h1:XHU1AkQW534GFuUdSiQ46+Xw6Ah+9+b8DlT4YwhiXL8= 18 | github.com/go-pg/pg/v10 v10.0.1 h1:YQAC4FcmBuAc08qZ0ZgLQVc9AkLtx6L1Yc5BFCaManI= 19 | github.com/go-pg/pg/v10 v10.0.1/go.mod h1:rfIarQ3lIylPqz362IR6XvexOG++pGxSilwkKFuwnvw= 20 | github.com/go-pg/pg/v10 v10.0.3 h1:w5CUqzvjEd41hyPph0Bh5XJD94cG1VtW5155ReBQ+Ho= 21 | github.com/go-pg/pg/v10 v10.0.3/go.mod h1:8mKn2zmanO72i4drlNaM4tnAxAbfm3HdYcwhrh+44y8= 22 | github.com/go-pg/pg/v10 v10.0.5 h1:0Y4tyrpfjIIkUyvGtw+1MRNHqkzjOL8zRm3JjKWAl5E= 23 | github.com/go-pg/pg/v10 v10.0.5/go.mod h1:sZ4iLl8yeQY+URTi7qcfE88J4kRGKQ8rJN1lN3OkKn4= 24 | github.com/go-pg/pg/v10 v10.0.6 h1:CbkDNNAT91glfzMqcVTeaxbWbgKf/VWveHcvHloY8k8= 25 | github.com/go-pg/pg/v10 v10.0.6/go.mod h1:sZ4iLl8yeQY+URTi7qcfE88J4kRGKQ8rJN1lN3OkKn4= 26 | github.com/go-pg/pg/v10 v10.0.7 h1:w7NvD+/XpMX1By7C5HKznWucF7bIr+SZ5JCeLS50zTQ= 27 | github.com/go-pg/pg/v10 v10.0.7/go.mod h1:drlDPrx8oNW+0F6t9JHTp2BhR5iir8jaSKkcAzNT8ow= 28 | github.com/go-pg/pg/v10 v10.1.1 h1:zSSDCKu2V7uYG6p44XRu6EdigdangNp4IVC/5HhAmLM= 29 | github.com/go-pg/pg/v10 v10.1.1/go.mod h1:eRA9aF/r771VBbghXZ1gJV2QLs2m26fQG5w1tvnt3Yw= 30 | github.com/go-pg/pg/v10 v10.3.0 h1:w3GJbaRnWJdtdSmY9xr7i3C+OxTdRE6q+x0vatLxTzE= 31 | github.com/go-pg/pg/v10 v10.3.0/go.mod h1:wY0cRYyO1JfUXBF2XjkbnNvhce3WyunFDhEvZXnHbP0= 32 | github.com/go-pg/zerochecker v0.2.0 h1:pp7f72c3DobMWOb2ErtZsnrPaSvHd2W4o9//8HtF4mU= 33 | github.com/go-pg/zerochecker v0.2.0/go.mod h1:NJZ4wKL0NmTtz0GKCoJ8kym6Xn/EQzXRl2OnAe7MmDo= 34 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 35 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 36 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 37 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 38 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 39 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 40 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 41 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 42 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 43 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 44 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 45 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 46 | github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= 47 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 48 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 49 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 50 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 51 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 52 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 53 | github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= 54 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 55 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 56 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 57 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 58 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 59 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 60 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 61 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 62 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 63 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 64 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 65 | github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= 66 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 67 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 68 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 69 | github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= 70 | github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 71 | github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 72 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 73 | github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= 74 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 75 | github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 76 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 77 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 78 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 79 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 80 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 81 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 82 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 83 | github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= 84 | github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= 85 | github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94= 86 | github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ= 87 | github.com/vmihailenco/msgpack/v4 v4.3.11/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= 88 | github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1 h1:d71/KA0LhvkrJ/Ok+Wx9qK7bU8meKA1Hk0jpVI5kJjk= 89 | github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1/go.mod h1:xlngVLeyQ/Qi05oQxhQ+oTuqa03RjMwMfk/7/TCs+QI= 90 | github.com/vmihailenco/tagparser v0.1.1 h1:quXMXlA39OCbd2wAdTsGDlK9RkOk6Wuw+x37wVyIuWY= 91 | github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= 92 | go.opentelemetry.io/otel v0.11.0 h1:IN2tzQa9Gc4ZVKnTaMbPVcHjvzOdg5n9QfnmlqiET7E= 93 | go.opentelemetry.io/otel v0.11.0/go.mod h1:G8UCk+KooF2HLkgo8RHX9epABH/aRGYET7gQOqBVdB0= 94 | go.opentelemetry.io/otel v0.12.0 h1:bwWaPd/h2q+U6KdKaAiOS5GLwOMd1LDt9iNaeyIoAI8= 95 | go.opentelemetry.io/otel v0.12.0/go.mod h1:dlSNewoRYikTkotEnxdmuBHgzT+k/idJSfDv/FxEnOY= 96 | golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 97 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 98 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 99 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 100 | golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM= 101 | golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 102 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 103 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 104 | golang.org/x/exp v0.0.0-20200901203048-c4f52b2c50aa h1:i1+omYRtqpxiCaQJB4MQhUToKvMPFqUUJKvRiRp0gtE= 105 | golang.org/x/exp v0.0.0-20200901203048-c4f52b2c50aa/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 106 | golang.org/x/exp v0.0.0-20200908183739-ae8ad444f925 h1:5XVKs2rlCg8EFyRcvO8/XFwYxh1oKJO1Q3X5vttIf9c= 107 | golang.org/x/exp v0.0.0-20200908183739-ae8ad444f925/go.mod h1:1phAWC201xIgDyaFpmDeZkgf70Q4Pd/CNqfRtVPtxNw= 108 | golang.org/x/exp v0.0.0-20200915172826-20d5ce0eab31 h1:T9e40qzKv+3l0j95+dAJ9LeE67fHjhVknTJQtC2Rna8= 109 | golang.org/x/exp v0.0.0-20200915172826-20d5ce0eab31/go.mod h1:1phAWC201xIgDyaFpmDeZkgf70Q4Pd/CNqfRtVPtxNw= 110 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 111 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 112 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 113 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 114 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 115 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 116 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 117 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 118 | golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 119 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 120 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 121 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 122 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 123 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 124 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 125 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 126 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 127 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 128 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 129 | golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA= 130 | golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 131 | golang.org/x/net v0.0.0-20200925080053-05aa5d4ee321/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 132 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 133 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 134 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 135 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 136 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 137 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 138 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 139 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 140 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 141 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 142 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 143 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 144 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 145 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 146 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 147 | golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f h1:Fqb3ao1hUmOR3GkUOg/Y+BadLwykBIzs5q8Ez2SbHyc= 148 | golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 149 | golang.org/x/sys v0.0.0-20200908134130-d2e65c121b96 h1:gJciq3lOg0eS9fSZJcoHfv7q1BfC6cJfnmSSKL1yu3Q= 150 | golang.org/x/sys v0.0.0-20200908134130-d2e65c121b96/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 151 | golang.org/x/sys v0.0.0-20200918174421-af09f7315aff h1:1CPUrky56AcgSpxz/KfgzQWzfG09u5YOL8MvPYBlrL8= 152 | golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 153 | golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 154 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 155 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 156 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 157 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 158 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 159 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 160 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 161 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 162 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 163 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 164 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 165 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 166 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 167 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 168 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 169 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 170 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 171 | google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= 172 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 173 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 174 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 175 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 176 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 177 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 178 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 179 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 180 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 181 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 182 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 183 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 184 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 185 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 186 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 187 | google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= 188 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 189 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 190 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 191 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 192 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 193 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 194 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 195 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 196 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 197 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 198 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 199 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 200 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 201 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 202 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 203 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 204 | mellium.im/sasl v0.2.1 h1:nspKSRg7/SyO0cRGY71OkfHab8tf9kCts6a6oTDut0w= 205 | mellium.im/sasl v0.2.1/go.mod h1:ROaEDLQNuf9vjKqE1SrAfnsobm2YKXT1gnN1uDp1PjQ= 206 | -------------------------------------------------------------------------------- /idgen.go: -------------------------------------------------------------------------------- 1 | package sharding 2 | 3 | import ( 4 | "math" 5 | "sync/atomic" 6 | "time" 7 | ) 8 | 9 | var ( 10 | _epoch = time.Date(2010, time.January, 01, 00, 0, 0, 0, time.UTC) 11 | DefaultIDGen = NewIDGen(41, 11, 12, _epoch) 12 | ) 13 | 14 | type IDGen struct { 15 | shardBits uint 16 | seqBits uint 17 | epoch int64 // in milliseconds 18 | minTime time.Time 19 | shardMask int64 20 | seqMask int64 21 | } 22 | 23 | func NewIDGen(timeBits, shardBits, seqBits uint, epoch time.Time) *IDGen { 24 | if timeBits+shardBits+seqBits != 64 { 25 | panic("timeBits + shardBits + seqBits != 64") 26 | } 27 | 28 | dur := time.Duration(1) << (timeBits - 1) * time.Millisecond 29 | return &IDGen{ 30 | shardBits: shardBits, 31 | seqBits: seqBits, 32 | epoch: epoch.UnixNano() / int64(time.Millisecond), 33 | minTime: epoch.Add(-dur), 34 | shardMask: int64(1)<>(g.shardBits+g.seqBits) + g.epoch 70 | sec := ms / 1000 71 | tm = time.Unix(sec, (ms-sec*1000)*int64(time.Millisecond)) 72 | shardID = (id >> g.seqBits) & g.shardMask 73 | seqID = id & g.seqMask 74 | return 75 | } 76 | 77 | //------------------------------------------------------------------------------ 78 | 79 | // IDGen generates sortable unique int64 numbers that consist of: 80 | // - 41 bits for time in milliseconds. 81 | // - 11 bits for shard id. 82 | // - 12 bits for auto-incrementing sequence. 83 | // 84 | // As a result we can generate 4096 ids per millisecond for each of 2048 shards. 85 | // Minimum supported time is 1975-02-28, maximum is 2044-12-31. 86 | type ShardIDGen struct { 87 | shard int64 88 | seq int64 89 | gen *IDGen 90 | } 91 | 92 | // NewShardIDGen returns id generator for the shard. 93 | func NewShardIDGen(shard int64, gen *IDGen) *ShardIDGen { 94 | if gen == nil { 95 | gen = DefaultIDGen 96 | } 97 | return &ShardIDGen{ 98 | shard: shard % int64(gen.NumShards()), 99 | gen: gen, 100 | } 101 | } 102 | 103 | // NextID returns incremental id for the time. Note that you can only 104 | // generate 4096 unique numbers per millisecond. 105 | func (g *ShardIDGen) NextID(tm time.Time) int64 { 106 | seq := atomic.AddInt64(&g.seq, 1) - 1 107 | return g.gen.MakeID(tm, g.shard, seq) 108 | } 109 | 110 | // MinId returns min id for the time. 111 | func (g *ShardIDGen) MinID(tm time.Time) int64 { 112 | return g.gen.MakeID(tm, g.shard, 0) 113 | } 114 | 115 | // MaxId returns max id for the time. 116 | func (g *ShardIDGen) MaxID(tm time.Time) int64 { 117 | return g.gen.MakeID(tm, g.shard, g.gen.seqMask) 118 | } 119 | 120 | // SplitID splits id into time, shard id, and sequence id. 121 | func (g *ShardIDGen) SplitID(id int64) (tm time.Time, shardID int64, seqID int64) { 122 | return g.gen.SplitID(id) 123 | } 124 | -------------------------------------------------------------------------------- /idgen_test.go: -------------------------------------------------------------------------------- 1 | package sharding_test 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | "time" 7 | 8 | "github.com/go-pg/sharding/v8" 9 | ) 10 | 11 | func TestMinIdMaxId(t *testing.T) { 12 | tm := time.Unix(1262304000, 0) 13 | 14 | minID := sharding.DefaultIDGen.MinID(tm) 15 | const wantedMinID = 0 16 | if minID != wantedMinID { 17 | t.Errorf("got %d, wanted %d", minID, wantedMinID) 18 | } 19 | 20 | maxID := sharding.DefaultIDGen.MaxID(tm) 21 | const wantedMaxID = 8388607 22 | if maxID != wantedMaxID { 23 | t.Errorf("got %d, wanted %d", maxID, wantedMaxID) 24 | } 25 | } 26 | 27 | func TestNextTime(t *testing.T) { 28 | minTime := time.Date(1975, time.February, 28, 4, 6, 12, 224000000, time.UTC) 29 | 30 | tests := []struct { 31 | tm time.Time 32 | wantedId int64 33 | }{ 34 | { 35 | tm: minTime.Add(-2 * time.Millisecond), 36 | wantedId: math.MinInt64, 37 | }, 38 | { 39 | tm: minTime.Add(-time.Millisecond), 40 | wantedId: math.MinInt64, 41 | }, 42 | { 43 | tm: minTime, 44 | wantedId: math.MinInt64, 45 | }, 46 | { 47 | tm: minTime.Add(time.Millisecond), 48 | wantedId: math.MinInt64 + 1<<23, 49 | }, 50 | { 51 | tm: minTime.Add(2 * time.Millisecond), 52 | wantedId: math.MinInt64 + 2<<23, 53 | }, 54 | { 55 | tm: minTime.Add(time.Hour), 56 | wantedId: (math.MinInt64 + 3600000<<23), 57 | }, 58 | } 59 | 60 | for _, test := range tests { 61 | minId := sharding.NewShardIDGen(0, nil).NextID(test.tm) 62 | if minId != test.wantedId { 63 | t.Errorf("got %d, wanted %d", minId, test.wantedId) 64 | } 65 | } 66 | } 67 | 68 | func TestNextTimeBounds(t *testing.T) { 69 | gen := sharding.NewShardIDGen(2049, nil) 70 | prev := int64(math.MinInt64) 71 | for i := 1976; i <= 2044; i++ { 72 | tm := time.Date(i, time.January, 0o1, 0, 0, 0, 0, time.UTC) 73 | next := gen.NextID(tm) 74 | if next <= prev { 75 | t.Errorf("%s: next=%d, prev=%d", tm, next, prev) 76 | } 77 | prev = next 78 | } 79 | } 80 | 81 | func TestShard(t *testing.T) { 82 | tm := time.Now() 83 | for shard := int64(0); shard < 2048; shard++ { 84 | gen := sharding.NewShardIDGen(shard, nil) 85 | id := gen.NextID(tm) 86 | gotTm, gotShard, gotSeq := gen.SplitID(id) 87 | if gotTm.Unix() != tm.Unix() { 88 | t.Errorf("got %s, expected %s", gotTm, tm) 89 | } 90 | if gotShard != shard { 91 | t.Errorf("got %d, expected %d", gotShard, shard) 92 | } 93 | if gotSeq != 0 { 94 | t.Errorf("got %d, expected 1", gotSeq) 95 | } 96 | } 97 | } 98 | 99 | func TestSequence(t *testing.T) { 100 | gen := sharding.NewShardIDGen(0, nil) 101 | tm := time.Now() 102 | max := gen.MaxID(tm) 103 | 104 | var prev int64 105 | for i := 0; i < 4096; i++ { 106 | next := gen.NextID(tm) 107 | if next <= prev { 108 | t.Errorf("iter %d: next=%d prev=%d", i, next, prev) 109 | } 110 | if next > max { 111 | t.Errorf("iter %d: next=%d max=%d", i, next, max) 112 | } 113 | prev = next 114 | } 115 | } 116 | 117 | func TestCollision(t *testing.T) { 118 | const n = 4096 119 | 120 | tm := time.Now() 121 | m := make(map[int64]struct{}, 2*n) 122 | 123 | gen := sharding.NewShardIDGen(0, nil) 124 | for i := 0; i < n; i++ { 125 | id := gen.NextID(tm) 126 | _, ok := m[id] 127 | if ok { 128 | t.Fatalf("collision for %d", id) 129 | } 130 | m[id] = struct{}{} 131 | } 132 | 133 | gen = sharding.NewShardIDGen(1, nil) 134 | for i := 0; i < n; i++ { 135 | id := gen.NextID(tm) 136 | _, ok := m[id] 137 | if ok { 138 | t.Fatalf("collision for %d", id) 139 | } 140 | m[id] = struct{}{} 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /uuid.go: -------------------------------------------------------------------------------- 1 | package sharding 2 | 3 | import ( 4 | "database/sql" 5 | "database/sql/driver" 6 | "encoding" 7 | "encoding/binary" 8 | "encoding/hex" 9 | "encoding/json" 10 | "fmt" 11 | "math/rand" 12 | "sync" 13 | "time" 14 | 15 | "github.com/go-pg/pg/v10/types" 16 | ) 17 | 18 | const ( 19 | uuidLen = 16 20 | uuidHexLen = 36 21 | ) 22 | 23 | var ( 24 | uuidRandMu sync.Mutex 25 | uuidRand = rand.New(rand.NewSource(time.Now().UnixNano())) 26 | ) 27 | 28 | type UUID [uuidLen]byte 29 | 30 | func NewUUID(shardID int64, tm time.Time) UUID { 31 | shardID = shardID % int64(DefaultIDGen.NumShards()) 32 | 33 | var u UUID 34 | binary.BigEndian.PutUint64(u[:8], uint64(unixMicrosecond(tm))) 35 | uuidRandMu.Lock() 36 | uuidRand.Read(u[8:]) 37 | uuidRandMu.Unlock() 38 | u[8] = (u[8] &^ 0x7) | byte(shardID>>8) 39 | u[9] = byte(shardID) 40 | return u 41 | } 42 | 43 | func ParseUUID(b []byte) (UUID, error) { 44 | var u UUID 45 | err := u.UnmarshalText(b) 46 | return u, err 47 | } 48 | 49 | func (u *UUID) IsZero() bool { 50 | if u == nil { 51 | return true 52 | } 53 | for _, c := range u { 54 | if c != 0 { 55 | return false 56 | } 57 | } 58 | return true 59 | } 60 | 61 | func (u *UUID) Split() (shardID int64, tm time.Time) { 62 | tm = fromUnixMicrosecond(int64(binary.BigEndian.Uint64(u[:8]))) 63 | shardID |= (int64(u[8]) & 0x7) << 8 64 | shardID |= int64(u[9]) 65 | return 66 | } 67 | 68 | func (u *UUID) ShardID() int64 { 69 | shardID, _ := u.Split() 70 | return shardID 71 | } 72 | 73 | func (u *UUID) Time() time.Time { 74 | _, tm := u.Split() 75 | return tm 76 | } 77 | 78 | func (u UUID) String() string { 79 | b := appendHex(nil, u[:]) 80 | return string(b) 81 | } 82 | 83 | var _ types.ValueAppender = (*UUID)(nil) 84 | 85 | func (u UUID) AppendValue(b []byte, quote int) ([]byte, error) { 86 | if u.IsZero() { 87 | return types.AppendNull(b, quote), nil 88 | } 89 | 90 | if quote == 2 { 91 | b = append(b, '"') 92 | } else if quote == 1 { 93 | b = append(b, '\'') 94 | } 95 | 96 | b = appendHex(b, u[:]) 97 | 98 | if quote == 2 { 99 | b = append(b, '"') 100 | } else if quote == 1 { 101 | b = append(b, '\'') 102 | } 103 | 104 | return b, nil 105 | } 106 | 107 | var _ driver.Valuer = (*UUID)(nil) 108 | 109 | func (u UUID) Value() (driver.Value, error) { 110 | return u.String(), nil 111 | } 112 | 113 | var _ sql.Scanner = (*UUID)(nil) 114 | 115 | func (u *UUID) Scan(b interface{}) error { 116 | if b == nil { 117 | for i := range u { 118 | u[i] = 0 119 | } 120 | return nil 121 | } 122 | 123 | uuid, err := ParseUUID(b.([]byte)) 124 | if err != nil { 125 | return err 126 | } 127 | 128 | for i, c := range uuid { 129 | u[i] = c 130 | } 131 | 132 | return nil 133 | } 134 | 135 | var _ encoding.BinaryMarshaler = (*UUID)(nil) 136 | 137 | func (u UUID) MarshalBinary() ([]byte, error) { 138 | return u[:], nil 139 | } 140 | 141 | var _ encoding.BinaryUnmarshaler = (*UUID)(nil) 142 | 143 | func (u *UUID) UnmarshalBinary(b []byte) error { 144 | switch len(b) { 145 | case uuidLen: 146 | copy(u[:], b) 147 | return nil 148 | case uuidHexLen - 4: 149 | _, err := hex.Decode(u[:], b) 150 | return err 151 | } 152 | return fmt.Errorf("sharding: invalid UUID: %q", b) 153 | } 154 | 155 | var _ encoding.TextMarshaler = (*UUID)(nil) 156 | 157 | func (u UUID) MarshalText() ([]byte, error) { 158 | return appendHex(nil, u[:]), nil 159 | } 160 | 161 | var _ encoding.TextUnmarshaler = (*UUID)(nil) 162 | 163 | func (u *UUID) UnmarshalText(b []byte) error { 164 | if len(b) == uuidHexLen-4 { 165 | _, err := hex.Decode(u[:], b) 166 | return err 167 | } 168 | 169 | if len(b) != uuidHexLen { 170 | return fmt.Errorf("sharding: invalid UUID: %q", b) 171 | } 172 | _, err := hex.Decode(u[:4], b[:8]) 173 | if err != nil { 174 | return err 175 | } 176 | _, err = hex.Decode(u[4:6], b[9:13]) 177 | if err != nil { 178 | return err 179 | } 180 | _, err = hex.Decode(u[6:8], b[14:18]) 181 | if err != nil { 182 | return err 183 | } 184 | _, err = hex.Decode(u[8:10], b[19:23]) 185 | if err != nil { 186 | return err 187 | } 188 | _, err = hex.Decode(u[10:], b[24:]) 189 | if err != nil { 190 | return err 191 | } 192 | return nil 193 | } 194 | 195 | var _ json.Marshaler = (*UUID)(nil) 196 | 197 | func (u UUID) MarshalJSON() ([]byte, error) { 198 | if u.IsZero() { 199 | return []byte("null"), nil 200 | } 201 | 202 | b := make([]byte, 0, uuidHexLen+2) 203 | b = append(b, '"') 204 | b = appendHex(b, u[:]) 205 | b = append(b, '"') 206 | return b, nil 207 | } 208 | 209 | var _ json.Unmarshaler = (*UUID)(nil) 210 | 211 | func (u *UUID) UnmarshalJSON(b []byte) error { 212 | if len(b) >= 2 { 213 | b = b[1 : len(b)-1] 214 | } 215 | return u.UnmarshalText(b) 216 | } 217 | 218 | func unixMicrosecond(tm time.Time) int64 { 219 | return tm.Unix()*1e6 + int64(tm.Nanosecond())/1e3 220 | } 221 | 222 | func fromUnixMicrosecond(n int64) time.Time { 223 | secs := n / 1e6 224 | return time.Unix(secs, (n-secs*1e6)*1e3) 225 | } 226 | 227 | func appendHex(b []byte, u []byte) []byte { 228 | b = append(b, make([]byte, uuidHexLen)...) 229 | bb := b[len(b)-uuidHexLen:] 230 | hex.Encode(bb[:8], u[:4]) 231 | bb[8] = '-' 232 | hex.Encode(bb[9:13], u[4:6]) 233 | bb[13] = '-' 234 | hex.Encode(bb[14:18], u[6:8]) 235 | bb[18] = '-' 236 | hex.Encode(bb[19:23], u[8:10]) 237 | bb[23] = '-' 238 | hex.Encode(bb[24:], u[10:]) 239 | return b 240 | } 241 | -------------------------------------------------------------------------------- /uuid_test.go: -------------------------------------------------------------------------------- 1 | package sharding_test 2 | 3 | import ( 4 | "bytes" 5 | "math/rand" 6 | "testing" 7 | "time" 8 | 9 | "github.com/go-pg/sharding/v8" 10 | ) 11 | 12 | func TestUUIDParse(t *testing.T) { 13 | sharding.SetUUIDRand(rand.New(rand.NewSource(0))) 14 | 15 | tm := time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC) 16 | uuid := sharding.NewUUID(0, tm) 17 | got := uuid.String() 18 | wanted := "00035d01-3b37-e000-0000-fdc2fa2ffcc0" 19 | if got != wanted { 20 | t.Fatalf("got %q, wanted %q", got, wanted) 21 | } 22 | 23 | parsed, err := sharding.ParseUUID([]byte(got)) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | if !bytes.Equal(parsed[:], uuid[:]) { 28 | t.Fatalf("got %x, wanted %x", parsed, uuid) 29 | } 30 | } 31 | 32 | func TestUUIDTime(t *testing.T) { 33 | shard := int64(2047) 34 | for i := 0; i < 100000; i++ { 35 | tm := time.Date(i, time.January, 1, 0, 0, 0, 0, time.UTC) 36 | uuid := sharding.NewUUID(shard, tm) 37 | gotShard, gotTm := uuid.Split() 38 | if tm.Unix() != gotTm.Unix() { 39 | t.Fatalf("got time %s, wanted %s", tm, gotTm) 40 | } 41 | if gotShard != shard { 42 | t.Fatalf("got shard %d, wanted %d", gotShard, shard) 43 | } 44 | } 45 | } 46 | 47 | func TestUUIDShard(t *testing.T) { 48 | tm := time.Now() 49 | for shard := int64(0); shard < 2048; shard++ { 50 | uuid := sharding.NewUUID(shard, tm) 51 | gotShard, gotTm := uuid.Split() 52 | if tm.Unix() != gotTm.Unix() { 53 | t.Fatalf("got time %s, wanted %s", tm, gotTm) 54 | } 55 | if gotShard != shard { 56 | t.Fatalf("got shard %d, wanted %d", gotShard, shard) 57 | } 58 | } 59 | } 60 | 61 | func TestUUIDCollision(t *testing.T) { 62 | tm := time.Now() 63 | shard := int64(2047) 64 | m := map[[16]byte]struct{}{} 65 | for i := 0; i < 1e6; i++ { 66 | uuid := sharding.NewUUID(shard, tm) 67 | _, ok := m[uuid] 68 | if ok { 69 | t.Fatalf("collision for %s", uuid) 70 | } 71 | m[uuid] = struct{}{} 72 | } 73 | } 74 | --------------------------------------------------------------------------------