Repository: edwingeng/wuid Branch: master Commit: 29d94c466647 Files: 39 Total size: 77.2 KB Directory structure: gitextract_35st_wr2/ ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── callback/ │ └── wuid/ │ ├── coverage.sh │ ├── vet.sh │ ├── wuid.go │ └── wuid_test.go ├── check.sh ├── go.mod ├── go.sum ├── internal/ │ ├── coverage.sh │ ├── vet.sh │ ├── wuid.go │ └── wuid_test.go ├── mongo/ │ ├── docker-mongo-client.sh │ ├── docker-mongo-server.sh │ └── wuid/ │ ├── coverage.sh │ ├── vet.sh │ ├── wuid.go │ └── wuid_test.go ├── mysql/ │ ├── db.sql │ ├── docker-mysql-client.sh │ ├── docker-mysql-server.sh │ └── wuid/ │ ├── coverage.sh │ ├── vet.sh │ ├── wuid.go │ └── wuid_test.go ├── redis/ │ ├── docker-redis-client.sh │ ├── docker-redis-server.sh │ ├── v8/ │ │ └── wuid/ │ │ ├── coverage.sh │ │ ├── vet.sh │ │ ├── wuid.go │ │ └── wuid_test.go │ └── wuid/ │ ├── coverage.sh │ ├── vet.sh │ ├── wuid.go │ └── wuid_test.go └── wuid.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ * text=auto eol=lf ================================================ FILE: .gitignore ================================================ # Binaries for programs and plugins *.exe *.dll *.so *.dylib # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 .glide/ # Misc /.idea /vendor ================================================ FILE: LICENSE ================================================ BSD 3-Clause License Copyright (c) 2018-2022, Edwin Geng All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: README.md ================================================ # Overview - `WUID` is a universal unique identifier generator. - `WUID` is much faster than traditional UUID. Each `WUID` instance can even generate 100M unique identifiers in a single second. - In the nutshell, `WUID` generates 64-bit integers in sequence. The high 28 bits are loaded from a data source. By now, Redis, MySQL, MongoDB and Callback are supported. - The uniqueness is guaranteed as long as all `WUID` instances share a same data source or each group of them has a different section ID. - `WUID` automatically renews the high 28 bits when the low 36 bits are about to run out. - `WUID` is thread-safe, and lock free. - Obfuscation is supported. # Benchmarks ``` BenchmarkWUID 159393580 7.661 ns/op 0 B/op 0 allocs/op BenchmarkRand 100000000 14.95 ns/op 0 B/op 0 allocs/op BenchmarkTimestamp 164224915 7.359 ns/op 0 B/op 0 allocs/op BenchmarkUUID_V1 23629536 43.42 ns/op 0 B/op 0 allocs/op BenchmarkUUID_V2 29351550 43.96 ns/op 0 B/op 0 allocs/op BenchmarkUUID_V3 4703044 254.2 ns/op 144 B/op 4 allocs/op BenchmarkUUID_V4 5796310 210.0 ns/op 16 B/op 1 allocs/op BenchmarkUUID_V5 4051291 310.7 ns/op 168 B/op 4 allocs/op BenchmarkRedis 2996 38725 ns/op 160 B/op 5 allocs/op BenchmarkSnowflake 1000000 2092 ns/op 0 B/op 0 allocs/op BenchmarkULID 5660170 207.7 ns/op 16 B/op 1 allocs/op BenchmarkXID 49639082 26.21 ns/op 0 B/op 0 allocs/op BenchmarkShortID 1312386 922.2 ns/op 320 B/op 11 allocs/op BenchmarkKsuid 19717675 59.79 ns/op 0 B/op 0 allocs/op ``` # Getting Started ``` bash go get -u github.com/edwingeng/wuid ``` # Usages ### Redis ``` go import "github.com/edwingeng/wuid/redis/v8/wuid" newClient := func() (redis.UniversalClient, bool, error) { var client redis.UniversalClient // ... return client, true, nil } // Setup w := NewWUID("alpha", nil) err := w.LoadH28FromRedis(newClient, "wuid") if err != nil { panic(err) } // Generate for i := 0; i < 10; i++ { fmt.Printf("%#016x\n", w.Next()) } ``` ### MySQL ``` go import "github.com/edwingeng/wuid/mysql/wuid" openDB := func() (*sql.DB, bool, error) { var db *sql.DB // ... return db, true, nil } // Setup w := NewWUID("alpha", nil) err := w.LoadH28FromMysql(openDB, "wuid") if err != nil { panic(err) } // Generate for i := 0; i < 10; i++ { fmt.Printf("%#016x\n", w.Next()) } ``` ### MongoDB ``` go import "github.com/edwingeng/wuid/mongo/wuid" newClient := func() (*mongo.Client, bool, error) { var client *mongo.Client // ... return client, true, nil } // Setup w := NewWUID("alpha", nil) err := w.LoadH28FromMongo(newClient, "test", "wuid", "default") if err != nil { panic(err) } // Generate for i := 0; i < 10; i++ { fmt.Printf("%#016x\n", w.Next()) } ``` ### Callback ``` go import "github.com/edwingeng/wuid/callback/wuid" callback := func() (int64, func(), error) { var h28 int64 // ... return h28, nil, nil } // Setup w := NewWUID("alpha", nil) err := w.LoadH28WithCallback(callback) if err != nil { panic(err) } // Generate for i := 0; i < 10; i++ { fmt.Printf("%#016x\n", w.Next()) } ``` # Mysql Table Creation ``` sql CREATE TABLE IF NOT EXISTS `wuid` ( `h` int(10) NOT NULL AUTO_INCREMENT, `x` tinyint(4) NOT NULL DEFAULT '0', PRIMARY KEY (`x`), UNIQUE KEY `h` (`h`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1; ``` # Options - `WithSection` brands a section ID on each generated number. A section ID must be in between [0, 7]. - `WithStep` sets the step and the floor for each generated number. - `WithObfuscation` enables number obfuscation. # Attentions It is highly recommended to pass a logger to `wuid.NewWUID` and keep an eye on the warnings that include "renew failed". It indicates that the low 36 bits are about to run out in hours to hundreds of hours, and the renewal program failed for some reason. `WUID` will make many renewal attempts until succeeded. # Special thanks - [dustinfog](https://github.com/dustinfog) # Ports - swift - https://github.com/ekscrypto/SwiftWUID ================================================ FILE: callback/wuid/coverage.sh ================================================ #!/usr/bin/env bash [[ "$TRACE" ]] && set -x pushd `dirname "$0"` > /dev/null trap __EXIT EXIT colorful=false tput setaf 7 > /dev/null 2>&1 if [[ $? -eq 0 ]]; then colorful=true fi function __EXIT() { popd > /dev/null } function printError() { $colorful && tput setaf 1 >&2 echo "Error: $@" $colorful && tput setaf 7 } function printImportantMessage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } function printUsage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } go test -cover -coverprofile=c.out -v "$@" && go tool cover -html=c.out ================================================ FILE: callback/wuid/vet.sh ================================================ #!/usr/bin/env bash [[ "$TRACE" ]] && set -x pushd `dirname "$0"` > /dev/null trap __EXIT EXIT colorful=false tput setaf 7 > /dev/null 2>&1 if [[ $? -eq 0 ]]; then colorful=true fi function __EXIT() { popd > /dev/null } function printError() { $colorful && tput setaf 1 >&2 echo "Error: $@" $colorful && tput setaf 7 } function printImportantMessage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } function printUsage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } printImportantMessage "====== gofmt" gofmt -w . printImportantMessage "====== go vet" go vet ./... printImportantMessage "====== gocyclo" gocyclo -over 15 . printImportantMessage "====== ineffassign" ineffassign ./... printImportantMessage "====== misspell" misspell * ================================================ FILE: callback/wuid/wuid.go ================================================ package wuid import ( "errors" "github.com/edwingeng/slog" "github.com/edwingeng/wuid/internal" ) // WUID is an extremely fast universal unique identifier generator. type WUID struct { w internal.WUID } // NewWUID creates a new WUID instance. func NewWUID(name string, logger slog.Logger, opts ...Option) *WUID { return &WUID{w: *internal.NewWUID(name, logger, opts...)} } // Next returns a unique identifier. func (w *WUID) Next() int64 { return w.w.Next() } type H28Callback func() (h28 int64, cleanUp func(), err error) // LoadH28WithCallback invokes cb to acquire a number. The number is used as the high 28 bits // of all generated numbers. In addition, cb is saved for future renewal. func (w *WUID) LoadH28WithCallback(cb H28Callback) error { if cb == nil { return errors.New("cb cannot be nil") } h28, cleanUp, err := cb() if err != nil { return err } if cleanUp != nil { defer cleanUp() } if err = w.w.VerifyH28(h28); err != nil { return err } w.w.Reset(h28 << 36) w.w.Infof(" new h28: %d. name: %s", h28, w.w.Name) w.w.Lock() defer w.w.Unlock() if w.w.Renew != nil { return nil } w.w.Renew = func() error { return w.LoadH28WithCallback(cb) } return nil } // RenewNow reacquires the high 28 bits immediately. func (w *WUID) RenewNow() error { return w.w.RenewNow() } type Option = internal.Option // WithH28Verifier adds an extra verifier for the high 28 bits. func WithH28Verifier(cb func(h28 int64) error) Option { return internal.WithH28Verifier(cb) } // WithSection brands a section ID on each generated number. A section ID must be in between [0, 7]. func WithSection(section int8) Option { return internal.WithSection(section) } // WithStep sets the step and the floor for each generated number. func WithStep(step int64, floor int64) Option { return internal.WithStep(step, floor) } // WithObfuscation enables number obfuscation. func WithObfuscation(seed int) Option { return internal.WithObfuscation(seed) } ================================================ FILE: callback/wuid/wuid_test.go ================================================ package wuid import ( "errors" "fmt" "github.com/edwingeng/slog" "github.com/edwingeng/wuid/internal" "math/rand" "strings" "sync/atomic" "testing" "time" ) var ( dumb = slog.NewDumbLogger() ) func TestWUID_LoadH28WithCallback_Error(t *testing.T) { w := NewWUID("alpha", dumb) err1 := w.LoadH28WithCallback(nil) if err1 == nil { t.Fatal("LoadH28WithCallback should fail when cb is nil") } err2 := w.LoadH28WithCallback(func() (int64, func(), error) { return 0, nil, errors.New("foo") }) if err2 == nil { t.Fatal("LoadH28WithCallback should fail when cb returns an error") } err3 := w.LoadH28WithCallback(func() (int64, func(), error) { return 0, nil, nil }) if err3 == nil { t.Fatal("LoadH28WithCallback should fail when cb returns an invalid h28") } } func TestWUID_LoadH28WithCallback(t *testing.T) { var h28, counter int64 done := func() { counter++ } cb := func() (int64, func(), error) { return atomic.AddInt64(&h28, 1), done, nil } w := NewWUID("alpha", dumb) err := w.LoadH28WithCallback(cb) if err != nil { t.Fatal(err) } for i := 1; i < 1000; i++ { if err := w.RenewNow(); err != nil { t.Fatal(err) } v := (int64(i) + 1) << 36 if atomic.LoadInt64(&w.w.N) != v { t.Fatalf("w.w.N is %d, while it should be %d. i: %d", atomic.LoadInt64(&w.w.N), v, i) } n := rand.Intn(10) for j := 0; j < n; j++ { w.Next() } } if counter != 1000 { t.Fatalf("the callback done do not work as expected. counter: %d", counter) } } func TestWUID_LoadH28WithCallback_Section(t *testing.T) { var h28 int64 cb := func() (int64, func(), error) { return atomic.AddInt64(&h28, 1), nil, nil } w := NewWUID("alpha", dumb, WithSection(1)) for i := 0; i < 1000; i++ { err := w.LoadH28WithCallback(cb) if err != nil { t.Fatal(err) } v := (int64(i) + 1 + 0x1000000) << 36 if atomic.LoadInt64(&w.w.N) != v { t.Fatalf("w.w.N is %d, while it should be %d. i: %d", atomic.LoadInt64(&w.w.N), v, i) } n := rand.Intn(10) for j := 0; j < n; j++ { w.Next() } } } func TestWUID_LoadH28WithCallback_Same(t *testing.T) { cb := func() (int64, func(), error) { return 100, nil, nil } w1 := NewWUID("alpha", dumb) _ = w1.LoadH28WithCallback(cb) if err := w1.LoadH28WithCallback(cb); err == nil { t.Fatal("LoadH28WithCallback should return an error") } w2 := NewWUID("alpha", dumb, WithSection(1)) _ = w2.LoadH28WithCallback(cb) if err := w2.LoadH28WithCallback(cb); err == nil { t.Fatal("LoadH28WithCallback should return an error") } } func waitUntilNumRenewedReaches(t *testing.T, w *WUID, expected int64) { t.Helper() startTime := time.Now() for time.Since(startTime) < time.Second { if atomic.LoadInt64(&w.w.Stats.NumRenewed) == expected { return } time.Sleep(time.Millisecond * 10) } t.Fatal("timeout") } func TestWUID_Renew(t *testing.T) { w := NewWUID("alpha", slog.NewScavenger()) err := w.LoadH28WithCallback(func() (h28 int64, clean func(), err error) { return (atomic.LoadInt64(&w.w.N) >> 36) + 1, nil, nil }) if err != nil { t.Fatal(err) } h28 := atomic.LoadInt64(&w.w.N) >> 36 atomic.StoreInt64(&w.w.N, (h28<<36)|internal.Bye) n1a := w.Next() if n1a>>36 != h28 { t.Fatal(`n1a>>36 != h28`) } waitUntilNumRenewedReaches(t, w, 1) n1b := w.Next() if n1b != (h28+1)<<36+1 { t.Fatal(`n1b != (h28+1)<<36+1`) } atomic.StoreInt64(&w.w.N, ((h28+1)<<36)|internal.Bye) n2a := w.Next() if n2a>>36 != h28+1 { t.Fatal(`n2a>>36 != h28+1`) } waitUntilNumRenewedReaches(t, w, 2) n2b := w.Next() if n2b != (h28+2)<<36+1 { t.Fatal(`n2b != (h28+2)<<36+1`) } atomic.StoreInt64(&w.w.N, ((h28+2)<<36)|internal.Bye) n3a := w.Next() if n3a>>36 != h28+2 { t.Fatal(`n3a>>36 != h28+2`) } waitUntilNumRenewedReaches(t, w, 3) n3b := w.Next() if n3b != (h28+3)<<36+1 { t.Fatal(`n3b != (h28+3)<<36+1`) } atomic.StoreInt64(&w.w.N, ((h28+2)<<36)+internal.Bye+1) for i := 0; i < 100; i++ { w.Next() } if atomic.LoadInt64(&w.w.Stats.NumRenewAttempts) != 3 { t.Fatal(`atomic.LoadInt64(&w.w.Stats.NumRenewAttempts) != 3`) } var num int sc := w.w.Logger.(*slog.Scavenger) sc.Filter(func(level, msg string) bool { if level == slog.LevelInfo && strings.Contains(msg, "renew succeeded") { num++ } return true }) if num != 3 { t.Fatal(`num != 3`) } } func Example() { callback := func() (int64, func(), error) { var h28 int64 // ... return h28, nil, nil } // Setup w := NewWUID("alpha", nil) err := w.LoadH28WithCallback(callback) if err != nil { panic(err) } // Generate for i := 0; i < 10; i++ { fmt.Printf("%#016x\n", w.Next()) } } ================================================ FILE: check.sh ================================================ #!/usr/bin/env bash [[ "$TRACE" ]] && set -x pushd `dirname "$0"` > /dev/null trap __EXIT EXIT colorful=false tput setaf 7 > /dev/null 2>&1 if [[ $? -eq 0 ]]; then colorful=true fi function __EXIT() { popd > /dev/null } function printError() { $colorful && tput setaf 1 >&2 echo "Error: $@" $colorful && tput setaf 7 } function printImportantMessage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } function printUsage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } find . -name coverage.sh | xargs -n 1 bash -c \ 'cmp --silent internal/coverage.sh "$0" || echo "files are different. file: $0"' find . -name vet.sh | xargs -n 1 bash -c \ 'cmp --silent internal/vet.sh "$0" || echo "files are different. file: $0"' ================================================ FILE: go.mod ================================================ module github.com/edwingeng/wuid go 1.18 require ( github.com/edwingeng/slog v0.0.0-20221027170832-482f0dfb6247 github.com/go-redis/redis v6.15.9+incompatible github.com/go-redis/redis/v8 v8.11.5 github.com/go-sql-driver/mysql v1.6.0 go.mongodb.org/mongo-driver v1.10.2 ) require ( github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/golang/snappy v0.0.1 // indirect github.com/klauspost/compress v1.13.6 // indirect github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect github.com/pkg/errors v0.9.1 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.1 // indirect github.com/xdg-go/stringprep v1.0.3 // indirect github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.8.0 // indirect go.uber.org/zap v1.23.0 // indirect golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/net v0.7.0 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/sys v0.5.0 // indirect golang.org/x/text v0.7.0 // indirect ) ================================================ FILE: go.sum ================================================ github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/edwingeng/slog v0.0.0-20221027170832-482f0dfb6247 h1:1Vb/cbeFfMh9q+CxEMLSJlHciX9x3JHdaNyHlvhGxhk= github.com/edwingeng/slog v0.0.0-20221027170832-482f0dfb6247/go.mod h1:mfngKiTrPWlUpIkQjkVzlumITH8chNhxsAiMklqH4Bo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.1 h1:VOMT+81stJgXW3CpHyqHN3AXDYIMsx56mEFrB37Mb/E= github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= github.com/xdg-go/stringprep v1.0.3 h1:kdwGpVNwPFtjs98xCGkHjQtGKh86rDcRZN17QEMCOIs= github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= go.mongodb.org/mongo-driver v1.10.2 h1:4Wk3cnqOrQCn0P92L3/mmurMxzdvWWs5J9jinAVKD+k= go.mongodb.org/mongo-driver v1.10.2/go.mod h1:z4XpeoU6w+9Vht+jAFyLgVrD+jGSQQe0+CBWFHNiHt8= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: internal/coverage.sh ================================================ #!/usr/bin/env bash [[ "$TRACE" ]] && set -x pushd `dirname "$0"` > /dev/null trap __EXIT EXIT colorful=false tput setaf 7 > /dev/null 2>&1 if [[ $? -eq 0 ]]; then colorful=true fi function __EXIT() { popd > /dev/null } function printError() { $colorful && tput setaf 1 >&2 echo "Error: $@" $colorful && tput setaf 7 } function printImportantMessage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } function printUsage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } go test -cover -coverprofile=c.out -v "$@" && go tool cover -html=c.out ================================================ FILE: internal/vet.sh ================================================ #!/usr/bin/env bash [[ "$TRACE" ]] && set -x pushd `dirname "$0"` > /dev/null trap __EXIT EXIT colorful=false tput setaf 7 > /dev/null 2>&1 if [[ $? -eq 0 ]]; then colorful=true fi function __EXIT() { popd > /dev/null } function printError() { $colorful && tput setaf 1 >&2 echo "Error: $@" $colorful && tput setaf 7 } function printImportantMessage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } function printUsage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } printImportantMessage "====== gofmt" gofmt -w . printImportantMessage "====== go vet" go vet ./... printImportantMessage "====== gocyclo" gocyclo -over 15 . printImportantMessage "====== ineffassign" ineffassign ./... printImportantMessage "====== misspell" misspell * ================================================ FILE: internal/wuid.go ================================================ package internal import ( "errors" "fmt" "github.com/edwingeng/slog" "sync" "sync/atomic" ) const ( // PanicValue indicates when Next starts to panic. PanicValue int64 = ((1 << 36) * 96 / 100) & ^1023 // CriticalValue indicates when to renew the high 28 bits. CriticalValue int64 = ((1 << 36) * 80 / 100) & ^1023 // RenewIntervalMask indicates the 'time' between two renewal attempts. RenewIntervalMask int64 = 0x20000000 - 1 ) const ( Bye = ((CriticalValue + RenewIntervalMask) & ^RenewIntervalMask) - 1 ) const ( H28Mask = 0x07FFFFFF << 36 L36Mask = 0x0FFFFFFFFF ) type WUID struct { N int64 Step int64 Floor int64 Flags int8 Obfuscation bool Monolithic bool ObfuscationMask int64 Section int64 slog.Logger Name string H28Verifier func(h28 int64) error sync.Mutex Renew func() error Stats struct { NumRenewAttempts int64 NumRenewed int64 } } func NewWUID(name string, logger slog.Logger, opts ...Option) (w *WUID) { w = &WUID{Step: 1, Name: name, Monolithic: true} if logger != nil { w.Logger = logger } else { w.Logger = slog.NewDevelopmentConfig().MustBuild() } for _, opt := range opts { opt(w) } if !w.Obfuscation || w.Floor == 0 { return } ones := w.Step - 1 w.ObfuscationMask |= ones return } func (w *WUID) Next() int64 { v1 := atomic.AddInt64(&w.N, w.Step) v2 := v1 & L36Mask if v2 >= PanicValue { panicValue := v1&H28Mask | PanicValue atomic.CompareAndSwapInt64(&w.N, v1, panicValue) panic(fmt.Errorf("the low 36 bits are about to run out")) } if v2 >= CriticalValue && v2&RenewIntervalMask == 0 { go renewImpl(w) } switch w.Flags { case 0: return v1 case 1: x := v1 ^ w.ObfuscationMask r := v1&H28Mask | x&L36Mask return r case 2: r := v1 / w.Floor * w.Floor return r case 3: x := v1 ^ w.ObfuscationMask q := v1&H28Mask | x&L36Mask r := q / w.Floor * w.Floor return r default: panic("impossible") } } func renewImpl(w *WUID) { defer func() { atomic.AddInt64(&w.Stats.NumRenewAttempts, 1) }() defer func() { if r := recover(); r != nil { w.Warnf(" panic, renew failed. name: %s, reason: %+v", w.Name, r) } }() err := w.RenewNow() if err != nil { w.Warnf(" renew failed. name: %s, reason: %+v", w.Name, err) } else { w.Infof(" renew succeeded. name: %s", w.Name) atomic.AddInt64(&w.Stats.NumRenewed, 1) } } func (w *WUID) RenewNow() error { w.Lock() f := w.Renew w.Unlock() return f() } func (w *WUID) Reset(n int64) { if n < 0 { panic("n cannot be negative") } if n&L36Mask >= PanicValue { panic("n is too old") } if w.Monolithic { // Empty } else { const L60Mask = 0x0FFFFFFFFFFFFFFF n = n&L60Mask | w.Section } if w.Floor > 1 { if n&(w.Step-1) == 0 { atomic.StoreInt64(&w.N, n) } else { atomic.StoreInt64(&w.N, n&^(w.Step-1)+w.Step) } } else { atomic.StoreInt64(&w.N, n) } } func (w *WUID) VerifyH28(h28 int64) error { if h28 <= 0 { return errors.New("h28 must be positive") } if w.Monolithic { if h28 > 0x07FFFFFF { return errors.New("h28 should not exceed 0x07FFFFFF") } } else { if h28 > 0x00FFFFFF { return errors.New("h28 should not exceed 0x00FFFFFF") } } current := atomic.LoadInt64(&w.N) >> 36 if w.Monolithic { if h28 == current { return fmt.Errorf("h28 should be a different value other than %d", h28) } } else { if h28 == current&0x00FFFFFF { return fmt.Errorf("h28 should be a different value other than %d", h28) } } if w.H28Verifier != nil { if err := w.H28Verifier(h28); err != nil { return err } } return nil } type Option func(w *WUID) func WithH28Verifier(cb func(h28 int64) error) Option { return func(w *WUID) { w.H28Verifier = cb } } func WithSection(section int8) Option { if section < 0 || section > 7 { panic("section must be in between [0, 7]") } return func(w *WUID) { w.Monolithic = false w.Section = int64(section) << 60 } } func WithStep(step int64, floor int64) Option { switch step { case 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024: default: panic("the step must be one of these values: 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024") } if floor != 0 && (floor < 0 || floor >= step) { panic(fmt.Errorf("floor must be in between [0, %d)", step)) } return func(w *WUID) { if w.Step != 1 { panic("a second WithStep detected") } w.Step = step if floor >= 2 { w.Floor = floor w.Flags |= 2 } } } func WithObfuscation(seed int) Option { if seed == 0 { panic("seed cannot be zero") } return func(w *WUID) { w.Obfuscation = true x := uint64(seed) x = (x ^ (x >> 30)) * uint64(0xbf58476d1ce4e5b9) x = (x ^ (x >> 27)) * uint64(0x94d049bb133111eb) x = (x ^ (x >> 31)) & 0x7FFFFFFFFFFFFFFF w.ObfuscationMask = int64(x) w.Flags |= 1 } } ================================================ FILE: internal/wuid_test.go ================================================ package internal import ( "errors" "github.com/edwingeng/slog" "math/rand" "sort" "strings" "sync" "sync/atomic" "testing" "time" ) func (w *WUID) Scavenger() *slog.Scavenger { return w.Logger.(*slog.Scavenger) } func TestWUID_Next(t *testing.T) { for i := 0; i < 100; i++ { w := NewWUID("alpha", nil) w.Reset(int64(i+1) << 36) v := atomic.LoadInt64(&w.N) for j := 0; j < 100; j++ { v++ if id := w.Next(); id != v { t.Fatalf("the id is %d, while it should be %d", id, v) } } } } func TestWUID_Next_Concurrent(t *testing.T) { w := NewWUID("alpha", nil) var mu sync.Mutex const N1 = 100 const N2 = 100 a := make([]int64, 0, N1*N2) var wg sync.WaitGroup for i := 0; i < N1; i++ { wg.Add(1) go func() { defer wg.Done() for j := 0; j < N2; j++ { id := w.Next() mu.Lock() a = append(a, id) mu.Unlock() } }() } wg.Wait() sort.Slice(a, func(i, j int) bool { return a[i] < a[j] }) for i := 0; i < N1*N2-1; i++ { if a[i] == a[i+1] { t.Fatalf("duplication detected") } } } func TestWUID_Next_Panic(t *testing.T) { const total = 100 w := NewWUID("alpha", nil) atomic.StoreInt64(&w.N, PanicValue) ch := make(chan int64, total) for i := 0; i < total; i++ { go func() { defer func() { if r := recover(); r != nil { ch <- 0 } }() ch <- w.Next() }() } for i := 0; i < total; i++ { v := <-ch if v != 0 { t.Fatal("something is wrong with Next()") } } } func waitUntilNumRenewAttemptsReaches(t *testing.T, w *WUID, expected int64) { t.Helper() startTime := time.Now() for time.Since(startTime) < time.Second { if atomic.LoadInt64(&w.Stats.NumRenewAttempts) == expected { return } time.Sleep(time.Millisecond * 10) } t.Fatal("timeout") } func waitUntilNumRenewedReaches(t *testing.T, w *WUID, expected int64) { t.Helper() startTime := time.Now() for time.Since(startTime) < time.Second { if atomic.LoadInt64(&w.Stats.NumRenewed) == expected { return } time.Sleep(time.Millisecond * 10) } t.Fatal("timeout") } func TestWUID_Renew(t *testing.T) { w := NewWUID("alpha", slog.NewScavenger()) w.Renew = func() error { w.Reset(((atomic.LoadInt64(&w.N) >> 36) + 1) << 36) return nil } w.Reset(Bye) n1a := w.Next() if n1a>>36 != 0 { t.Fatal(`n1a>>36 != 0`) } waitUntilNumRenewedReaches(t, w, 1) n1b := w.Next() if n1b != 1<<36+1 { t.Fatal(`n1b != 1<<36+1`) } w.Reset(1<<36 | Bye) n2a := w.Next() if n2a>>36 != 1 { t.Fatal(`n2a>>36 != 1`) } waitUntilNumRenewedReaches(t, w, 2) n2b := w.Next() if n2b != 2<<36+1 { t.Fatal(`n2b != 2<<36+1`) } w.Reset(2<<36 | Bye + RenewIntervalMask + 1) n3a := w.Next() if n3a>>36 != 2 { t.Fatal(`n3a>>36 != 2`) } waitUntilNumRenewedReaches(t, w, 3) n3b := w.Next() if n3b != 3<<36+1 { t.Fatal(`n3b != 3<<36+1`) } w.Reset(Bye + 1) for i := 0; i < 100; i++ { w.Next() } if atomic.LoadInt64(&w.Stats.NumRenewAttempts) != 3 { t.Fatal(`atomic.LoadInt64(&w.Stats.NumRenewAttempts) != 3`) } var num int w.Scavenger().Filter(func(level, msg string) bool { if level == slog.LevelInfo && strings.Contains(msg, "renew succeeded") { num++ } return true }) if num != 3 { t.Fatal(`num != 3`) } } func TestWUID_Renew_Error(t *testing.T) { w := NewWUID("alpha", slog.NewScavenger()) w.Renew = func() error { return errors.New("foo") } w.Reset((1 >> 36 << 36) | Bye) w.Next() waitUntilNumRenewAttemptsReaches(t, w, 1) w.Next() w.Reset((2 >> 36 << 36) | Bye) w.Next() waitUntilNumRenewAttemptsReaches(t, w, 2) for i := 0; i < 100; i++ { w.Next() } if atomic.LoadInt64(&w.Stats.NumRenewAttempts) != 2 { t.Fatal(`atomic.LoadInt64(&w.Stats.NumRenewAttempts) != 2`) } if atomic.LoadInt64(&w.Stats.NumRenewed) != 0 { t.Fatal(`atomic.LoadInt64(&w.Stats.NumRenewed) != 0`) } var num int w.Scavenger().Filter(func(level, msg string) bool { if level == slog.LevelWarn && strings.Contains(msg, "renew failed") && strings.Contains(msg, "foo") { num++ } return true }) if num != 2 { t.Fatal(`num != 2`) } } func TestWUID_Renew_Panic(t *testing.T) { w := NewWUID("alpha", slog.NewScavenger()) w.Renew = func() error { panic("foo") } w.Reset((1 >> 36 << 36) | Bye) w.Next() waitUntilNumRenewAttemptsReaches(t, w, 1) w.Next() w.Reset((2 >> 36 << 36) | Bye) w.Next() waitUntilNumRenewAttemptsReaches(t, w, 2) for i := 0; i < 100; i++ { w.Next() } if atomic.LoadInt64(&w.Stats.NumRenewAttempts) != 2 { t.Fatal(`atomic.LoadInt64(&w.Stats.NumRenewAttempts) != 2`) } if atomic.LoadInt64(&w.Stats.NumRenewed) != 0 { t.Fatal(`atomic.LoadInt64(&w.Stats.NumRenewed) != 0`) } var num int w.Scavenger().Filter(func(level, msg string) bool { if level == slog.LevelWarn && strings.Contains(msg, "renew failed") && strings.Contains(msg, "foo") { num++ } return true }) if num != 2 { t.Fatal(`num != 2`) } } func TestWUID_Step(t *testing.T) { const step = 16 w := NewWUID("alpha", slog.NewScavenger(), WithStep(step, 0)) w.Reset(17 << 36) w.Renew = func() error { w.Reset(((atomic.LoadInt64(&w.N) >> 36) + 1) << 36) return nil } for i := int64(1); i < 100; i++ { if w.Next()&L36Mask != step*i { t.Fatal("w.Next()&L36Mask != step*i") } } n1 := w.Next() w.Reset(((n1 >> 36 << 36) | Bye) & ^(step - 1)) w.Next() waitUntilNumRenewedReaches(t, w, 1) n2 := w.Next() w.Reset(((n2 >> 36 << 36) | Bye) & ^(step - 1)) w.Next() waitUntilNumRenewedReaches(t, w, 2) n3 := w.Next() if n2>>36-n1>>36 != 1 || n3>>36-n2>>36 != 1 { t.Fatalf("the renew mechanism does not work as expected: %x, %x, %x", n1>>36, n2>>36, n3>>36) } var num int w.Scavenger().Filter(func(level, msg string) bool { if level == slog.LevelInfo && strings.Contains(msg, "renew succeeded") { num++ } return true }) if num != 2 { t.Fatal(`num != 2`) } func() { defer func() { _ = recover() }() NewWUID("alpha", nil, WithStep(5, 0)) t.Fatal("WithStep should have panicked") }() } func TestWUID_Floor(t *testing.T) { r := rand.New(rand.NewSource(time.Now().Unix())) allSteps := []int64{1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024} for loop := 0; loop < 10000; loop++ { step := allSteps[r.Intn(len(allSteps))] var floor = r.Int63n(step) w := NewWUID("alpha", slog.NewScavenger(), WithStep(step, floor)) if floor < 2 { if w.Flags != 0 { t.Fatal(`w.Flags != 0`) } } else { if w.Flags != 2 { t.Fatal(`w.Flags != 2`) } } w.Reset(r.Int63n(100) << 36) baseValue := atomic.LoadInt64(&w.N) for i := int64(1); i < 100; i++ { x := w.Next() if floor != 0 { if reminder := x % floor; reminder != 0 { t.Fatal("reminder != 0") } } if x <= baseValue+i*step-step || x > baseValue+i*step { t.Fatal("x <= baseValue+i*step-step || x > baseValue+i*step") } } } func() { defer func() { _ = recover() }() NewWUID("alpha", nil, WithStep(1024, 2000)) t.Fatal("WithStep should have panicked") }() func() { defer func() { _ = recover() }() NewWUID("alpha", nil, WithStep(1024, 0), WithStep(128, 0)) t.Fatal("WithStep should have panicked") }() } func TestWUID_VerifyH28(t *testing.T) { w1 := NewWUID("alpha", nil) w1.Reset(H28Mask) if err := w1.VerifyH28(100); err != nil { t.Fatalf("VerifyH28 does not work as expected. n: 100, error: %s", err) } if err := w1.VerifyH28(0); err == nil { t.Fatalf("VerifyH28 does not work as expected. n: 0") } if err := w1.VerifyH28(0x08000000); err == nil { t.Fatalf("VerifyH28 does not work as expected. n: 0x08000000") } if err := w1.VerifyH28(0x07FFFFFF); err == nil { t.Fatalf("VerifyH28 does not work as expected. n: 0x07FFFFFF") } w2 := NewWUID("alpha", nil, WithSection(1)) w2.Reset(H28Mask) if err := w2.VerifyH28(100); err != nil { t.Fatalf("VerifyH28 does not work as expected. section: 1, n: 100, error: %s", err) } if err := w2.VerifyH28(0); err == nil { t.Fatalf("VerifyH28 does not work as expected. section: 1, n: 0") } if err := w2.VerifyH28(0x01000000); err == nil { t.Fatalf("VerifyH28 does not work as expected. section: 1, n: 0x01000000") } if err := w2.VerifyH28(0x00FFFFFF); err == nil { t.Fatalf("VerifyH28 does not work as expected. section: 1, n: 0x00FFFFFF") } } func TestWithSection_Panic(t *testing.T) { for i := -100; i <= 100; i++ { func(j int8) { defer func() { _ = recover() }() WithSection(j) if j >= 8 { t.Fatalf("WithSection should only accept the values in [0, 7]. j: %d", j) } }(int8(i)) } } func TestWithSection_Reset(t *testing.T) { for i := 0; i < 28; i++ { n := int64(1) << (uint(i) + 36) func() { defer func() { if r := recover(); r != nil { if i != 27 { t.Fatal(r) } } }() for j := int8(1); j < 8; j++ { w := NewWUID("alpha", nil, WithSection(j)) w.Reset(n) v := atomic.LoadInt64(&w.N) if v>>60 != int64(j) { t.Fatalf("w.Section does not work as expected. w.N: %x, n: %x, i: %d, j: %d", v, n, i, j) } } }() } func() { defer func() { _ = recover() }() w := NewWUID("alpha", nil) w.Reset((1 << 36) | PanicValue) t.Fatal("Reset should have panicked") }() } func TestWithH28Verifier(t *testing.T) { w := NewWUID("alpha", nil, WithH28Verifier(func(h28 int64) error { if h28 >= 20 { return errors.New("bomb") } return nil })) if err := w.VerifyH28(10); err != nil { t.Fatal("the H28Verifier should not return error") } if err := w.VerifyH28(20); err == nil || err.Error() != "bomb" { t.Fatal("the H28Verifier was not called") } } //gocyclo:ignore func TestWithObfuscation(t *testing.T) { w1 := NewWUID("alpha", nil, WithObfuscation(1)) if w1.Flags != 1 { t.Fatal(`w1.Flags != 1`) } if w1.ObfuscationMask == 0 { t.Fatal(`w1.ObfuscationMask == 0`) } w1.Reset(1 << 36) for i := 1; i < 100; i++ { v := w1.Next() if v&H28Mask != 1<<36 { t.Fatal(`v&H28Mask != 1<<36`) } tmp := v ^ w1.ObfuscationMask if tmp&L36Mask != int64(i) { t.Fatal(`tmp&L36Mask != int64(i)`) } } w2 := NewWUID("alpha", nil, WithObfuscation(1), WithStep(128, 100)) if w2.Flags != 3 { t.Fatal(`w2.Flags != 3`) } if w2.ObfuscationMask == 0 { t.Fatal(`w2.ObfuscationMask == 0`) } w2.Reset(1 << 36) for i := 1; i < 100; i++ { v := w2.Next() if v%w2.Floor != 0 { t.Fatal(`v%w2.Floor != 0`) } if v&H28Mask != 1<<36 { t.Fatal(`v&H28Mask != 1<<36`) } tmp := v ^ w2.ObfuscationMask if tmp&L36Mask&^(w2.Step-1) != w2.Step*int64(i) { t.Fatal(`tmp&L36Mask&^(w2.Step-1) != w2.Step*int64(i)`) } } w3 := NewWUID("alpha", nil, WithObfuscation(1), WithStep(1024, 659)) if w3.Flags != 3 { t.Fatal(`w3.Flags != 3`) } if w3.ObfuscationMask == 0 { t.Fatal(`w3.ObfuscationMask == 0`) } w3.Reset(1<<36 + 1) for i := 1; i < 100; i++ { v := w3.Next() if v%w3.Floor != 0 { t.Fatal(`v%w3.Floor != 0`) } if v&H28Mask != 1<<36 { t.Fatal(`v&H28Mask != 1<<36`) } tmp := v ^ w3.ObfuscationMask if tmp&L36Mask&^(w3.Step-1) != w3.Step*int64(i+1) { t.Fatal(`tmp&L36Mask&^(w3.Step-1) != w3.Step*int64(i+1)`) } } func() { defer func() { _ = recover() }() NewWUID("alpha", nil, WithObfuscation(0)) t.Fatal("WithObfuscation should have panicked") }() } ================================================ FILE: mongo/docker-mongo-client.sh ================================================ #!/usr/bin/env bash [[ "$TRACE" ]] && set -x pushd `dirname "$0"` > /dev/null trap __EXIT EXIT colorful=false tput setaf 7 > /dev/null 2>&1 if [[ $? -eq 0 ]]; then colorful=true fi function __EXIT() { popd > /dev/null } function printError() { $colorful && tput setaf 1 >&2 echo "Error: $@" $colorful && tput setaf 7 } function printImportantMessage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } function printUsage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } docker run -it --rm mongo mongosh host.docker.internal ================================================ FILE: mongo/docker-mongo-server.sh ================================================ #!/usr/bin/env bash [[ "$TRACE" ]] && set -x pushd `dirname "$0"` > /dev/null trap __EXIT EXIT colorful=false tput setaf 7 > /dev/null 2>&1 if [[ $? -eq 0 ]]; then colorful=true fi function __EXIT() { popd > /dev/null } function printError() { $colorful && tput setaf 1 >&2 echo "Error: $@" $colorful && tput setaf 7 } function printImportantMessage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } function printUsage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } docker run --name mongo-server -p 27017:27017 -d mongo ================================================ FILE: mongo/wuid/coverage.sh ================================================ #!/usr/bin/env bash [[ "$TRACE" ]] && set -x pushd `dirname "$0"` > /dev/null trap __EXIT EXIT colorful=false tput setaf 7 > /dev/null 2>&1 if [[ $? -eq 0 ]]; then colorful=true fi function __EXIT() { popd > /dev/null } function printError() { $colorful && tput setaf 1 >&2 echo "Error: $@" $colorful && tput setaf 7 } function printImportantMessage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } function printUsage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } go test -cover -coverprofile=c.out -v "$@" && go tool cover -html=c.out ================================================ FILE: mongo/wuid/vet.sh ================================================ #!/usr/bin/env bash [[ "$TRACE" ]] && set -x pushd `dirname "$0"` > /dev/null trap __EXIT EXIT colorful=false tput setaf 7 > /dev/null 2>&1 if [[ $? -eq 0 ]]; then colorful=true fi function __EXIT() { popd > /dev/null } function printError() { $colorful && tput setaf 1 >&2 echo "Error: $@" $colorful && tput setaf 7 } function printImportantMessage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } function printUsage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } printImportantMessage "====== gofmt" gofmt -w . printImportantMessage "====== go vet" go vet ./... printImportantMessage "====== gocyclo" gocyclo -over 15 . printImportantMessage "====== ineffassign" ineffassign ./... printImportantMessage "====== misspell" misspell * ================================================ FILE: mongo/wuid/wuid.go ================================================ package wuid import ( "context" "errors" "github.com/edwingeng/slog" "github.com/edwingeng/wuid/internal" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "go.mongodb.org/mongo-driver/mongo/readconcern" "go.mongodb.org/mongo-driver/mongo/readpref" "go.mongodb.org/mongo-driver/mongo/writeconcern" "time" ) // WUID is an extremely fast universal unique identifier generator. type WUID struct { w *internal.WUID } // NewWUID creates a new WUID instance. func NewWUID(name string, logger slog.Logger, opts ...Option) *WUID { return &WUID{w: internal.NewWUID(name, logger, opts...)} } // Next returns a unique identifier. func (w *WUID) Next() int64 { return w.w.Next() } type NewClient func() (client *mongo.Client, autoDisconnect bool, err error) // LoadH28FromMongo adds 1 to a specific number in MongoDB and fetches its new value. // The new value is used as the high 28 bits of all generated numbers. In addition, all the // arguments passed in are saved for future renewal. func (w *WUID) LoadH28FromMongo(newClient NewClient, dbName, coll, docID string) error { if len(dbName) == 0 { return errors.New("dbName cannot be empty") } if len(coll) == 0 { return errors.New("coll cannot be empty") } if len(docID) == 0 { return errors.New("docID cannot be empty") } client, autoDisconnect, err := newClient() if err != nil { return err } defer func() { if autoDisconnect { ctx2, cancel2 := context.WithTimeout(context.Background(), time.Second*5) defer cancel2() _ = client.Disconnect(ctx2) } }() ctx1, cancel1 := context.WithTimeout(context.Background(), time.Second*5) defer cancel1() if err := client.Ping(ctx1, readpref.Primary()); err != nil { return err } collOpts := &options.CollectionOptions{ ReadConcern: readconcern.Majority(), WriteConcern: writeconcern.New(writeconcern.WMajority()), ReadPreference: readpref.Primary(), } var doc struct { N int32 } filter := bson.D{ {Key: "_id", Value: docID}, } update := bson.D{ { Key: "$inc", Value: bson.D{ {Key: "n", Value: int32(1)}, }, }, } var findOneAndUpdateOptions options.FindOneAndUpdateOptions findOneAndUpdateOptions.SetUpsert(true).SetReturnDocument(options.After) c := client.Database(dbName).Collection(coll, collOpts) err = c.FindOneAndUpdate(ctx1, filter, update, &findOneAndUpdateOptions).Decode(&doc) if err != nil { return err } h28 := int64(doc.N) if err = w.w.VerifyH28(h28); err != nil { return err } w.w.Reset(h28 << 36) w.w.Logger.Infof(" new h28: %d. name: %s", h28, w.w.Name) w.w.Lock() defer w.w.Unlock() if w.w.Renew != nil { return nil } w.w.Renew = func() error { return w.LoadH28FromMongo(newClient, dbName, coll, docID) } return nil } // RenewNow reacquires the high 28 bits immediately. func (w *WUID) RenewNow() error { return w.w.RenewNow() } type Option = internal.Option // WithH28Verifier adds an extra verifier for the high 28 bits. func WithH28Verifier(cb func(h28 int64) error) Option { return internal.WithH28Verifier(cb) } // WithSection brands a section ID on each generated number. A section ID must be in between [0, 7]. func WithSection(section int8) Option { return internal.WithSection(section) } // WithStep sets the step and the floor for each generated number. func WithStep(step int64, floor int64) Option { return internal.WithStep(step, floor) } // WithObfuscation enables number obfuscation. func WithObfuscation(seed int) Option { return internal.WithObfuscation(seed) } ================================================ FILE: mongo/wuid/wuid_test.go ================================================ package wuid import ( "context" "errors" "fmt" "github.com/edwingeng/slog" "github.com/edwingeng/wuid/internal" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "math/rand" "strings" "sync/atomic" "testing" "time" ) var ( dumb = slog.NewDumbLogger() ) var ( cfg struct { addr string dbName string coll string docID string } ) func init() { cfg.addr = "127.0.0.1:27017" cfg.dbName = "test" cfg.coll = "wuid" cfg.docID = "default" } func connectMongodb() (*mongo.Client, error) { ctx1, cancel1 := context.WithTimeout(context.Background(), time.Second*3) defer cancel1() uri := fmt.Sprintf("mongodb://%s", cfg.addr) return mongo.Connect(ctx1, options.Client().ApplyURI(uri)) } func TestWUID_LoadH28FromMongo(t *testing.T) { newClient := func() (*mongo.Client, bool, error) { client, err := connectMongodb() return client, true, err } w := NewWUID(cfg.docID, dumb) err := w.LoadH28FromMongo(newClient, cfg.dbName, cfg.coll, cfg.docID) if err != nil { t.Fatal(err) } initial := atomic.LoadInt64(&w.w.N) for i := 1; i < 100; i++ { if err := w.RenewNow(); err != nil { t.Fatal(err) } expected := ((initial >> 36) + int64(i)) << 36 if atomic.LoadInt64(&w.w.N) != expected { t.Fatalf("w.w.N is %d, while it should be %d. i: %d", atomic.LoadInt64(&w.w.N), expected, i) } n := rand.Intn(10) for j := 0; j < n; j++ { w.Next() } } } func TestWUID_LoadH28FromMongo_Error(t *testing.T) { w := NewWUID(cfg.docID, dumb) if w.LoadH28FromMongo(nil, "", cfg.coll, cfg.docID) == nil { t.Fatal("dbName is not properly checked") } if w.LoadH28FromMongo(nil, cfg.dbName, "", cfg.docID) == nil { t.Fatal("coll is not properly checked") } if w.LoadH28FromMongo(nil, cfg.dbName, cfg.coll, "") == nil { t.Fatal("docID is not properly checked") } newErrorClient := func() (*mongo.Client, bool, error) { return nil, true, errors.New("beta") } if w.LoadH28FromMongo(newErrorClient, cfg.dbName, cfg.coll, cfg.docID) == nil { t.Fatal(`w.LoadH28FromMongo(newErrorClient, cfg.dbName, cfg.coll, cfg.docID) == nil`) } } func waitUntilNumRenewedReaches(t *testing.T, w *WUID, expected int64) { t.Helper() startTime := time.Now() for time.Since(startTime) < time.Second*3 { if atomic.LoadInt64(&w.w.Stats.NumRenewed) == expected { return } time.Sleep(time.Millisecond * 10) } t.Fatal("timeout") } func TestWUID_Renew(t *testing.T) { client, err := connectMongodb() if err != nil { t.Fatal(err) } newClient := func() (*mongo.Client, bool, error) { return client, false, err } w := NewWUID(cfg.docID, slog.NewScavenger()) err = w.LoadH28FromMongo(newClient, cfg.dbName, cfg.coll, cfg.docID) if err != nil { t.Fatal(err) } h28 := atomic.LoadInt64(&w.w.N) >> 36 atomic.StoreInt64(&w.w.N, (h28<<36)|internal.Bye) n1a := w.Next() if n1a>>36 != h28 { t.Fatal(`n1a>>36 != h28`) } waitUntilNumRenewedReaches(t, w, 1) n1b := w.Next() if n1b != (h28+1)<<36+1 { t.Fatal(`n1b != (h28+1)<<36+1`) } atomic.StoreInt64(&w.w.N, ((h28+1)<<36)|internal.Bye) n2a := w.Next() if n2a>>36 != h28+1 { t.Fatal(`n2a>>36 != h28+1`) } waitUntilNumRenewedReaches(t, w, 2) n2b := w.Next() if n2b != (h28+2)<<36+1 { t.Fatal(`n2b != (h28+2)<<36+1`) } atomic.StoreInt64(&w.w.N, ((h28+2)<<36)|internal.Bye) n3a := w.Next() if n3a>>36 != h28+2 { t.Fatal(`n3a>>36 != h28+2`) } waitUntilNumRenewedReaches(t, w, 3) n3b := w.Next() if n3b != (h28+3)<<36+1 { t.Fatal(`n3b != (h28+3)<<36+1`) } atomic.StoreInt64(&w.w.N, ((h28+2)<<36)+internal.Bye+1) for i := 0; i < 100; i++ { w.Next() } if atomic.LoadInt64(&w.w.Stats.NumRenewAttempts) != 3 { t.Fatal(`atomic.LoadInt64(&w.w.Stats.NumRenewAttempts) != 3`) } var num int sc := w.w.Logger.(*slog.Scavenger) sc.Filter(func(level, msg string) bool { if level == slog.LevelInfo && strings.Contains(msg, "renew succeeded") { num++ } return true }) if num != 3 { t.Fatal(`num != 3`) } } func Example() { newClient := func() (*mongo.Client, bool, error) { var client *mongo.Client // ... return client, true, nil } // Setup w := NewWUID("alpha", nil) err := w.LoadH28FromMongo(newClient, "test", "wuid", "default") if err != nil { panic(err) } // Generate for i := 0; i < 10; i++ { fmt.Printf("%#016x\n", w.Next()) } } ================================================ FILE: mysql/db.sql ================================================ CREATE DATABASE IF NOT EXISTS test; use test; CREATE TABLE IF NOT EXISTS `wuid` ( `h` int(10) NOT NULL AUTO_INCREMENT, `x` tinyint(4) NOT NULL DEFAULT '0', PRIMARY KEY (`x`), UNIQUE KEY `h` (`h`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1; ================================================ FILE: mysql/docker-mysql-client.sh ================================================ #!/usr/bin/env bash [[ "$TRACE" ]] && set -x pushd `dirname "$0"` > /dev/null trap __EXIT EXIT colorful=false tput setaf 7 > /dev/null 2>&1 if [[ $? -eq 0 ]]; then colorful=true fi function __EXIT() { popd > /dev/null } function printError() { $colorful && tput setaf 1 >&2 echo "Error: $@" $colorful && tput setaf 7 } function printImportantMessage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } function printUsage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } docker run -it --rm mysql mysql -h host.docker.internal -u root -phello test ================================================ FILE: mysql/docker-mysql-server.sh ================================================ #!/usr/bin/env bash [[ "$TRACE" ]] && set -x pushd `dirname "$0"` > /dev/null trap __EXIT EXIT colorful=false tput setaf 7 > /dev/null 2>&1 if [[ $? -eq 0 ]]; then colorful=true fi function __EXIT() { popd > /dev/null } function printError() { $colorful && tput setaf 1 >&2 echo "Error: $@" $colorful && tput setaf 7 } function printImportantMessage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } function printUsage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } docker run --name mysql-server -p 3306:3306 -e MYSQL_ROOT_PASSWORD=hello -d mysql [[ $? -ne 0 ]] && exit 1 printImportantMessage "It may take quite a few seconds to get ready." for ((i=0;i<1000;i++)); do docker run -it --rm mysql mysqladmin ping -h host.docker.internal --silent [[ $? -eq 0 ]] && echo "Ready." && break echo "Waiting $((i+1))..." sleep 1 done sleep 1 docker run -v `pwd`/db.sql:/tmp/db.sql -it --rm mysql /bin/bash -c 'cat /tmp/db.sql | mysql -h host.docker.internal -u root -phello' [[ $? -ne 0 ]] && exit 1 echo "Job done." ================================================ FILE: mysql/wuid/coverage.sh ================================================ #!/usr/bin/env bash [[ "$TRACE" ]] && set -x pushd `dirname "$0"` > /dev/null trap __EXIT EXIT colorful=false tput setaf 7 > /dev/null 2>&1 if [[ $? -eq 0 ]]; then colorful=true fi function __EXIT() { popd > /dev/null } function printError() { $colorful && tput setaf 1 >&2 echo "Error: $@" $colorful && tput setaf 7 } function printImportantMessage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } function printUsage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } go test -cover -coverprofile=c.out -v "$@" && go tool cover -html=c.out ================================================ FILE: mysql/wuid/vet.sh ================================================ #!/usr/bin/env bash [[ "$TRACE" ]] && set -x pushd `dirname "$0"` > /dev/null trap __EXIT EXIT colorful=false tput setaf 7 > /dev/null 2>&1 if [[ $? -eq 0 ]]; then colorful=true fi function __EXIT() { popd > /dev/null } function printError() { $colorful && tput setaf 1 >&2 echo "Error: $@" $colorful && tput setaf 7 } function printImportantMessage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } function printUsage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } printImportantMessage "====== gofmt" gofmt -w . printImportantMessage "====== go vet" go vet ./... printImportantMessage "====== gocyclo" gocyclo -over 15 . printImportantMessage "====== ineffassign" ineffassign ./... printImportantMessage "====== misspell" misspell * ================================================ FILE: mysql/wuid/wuid.go ================================================ package wuid import ( "database/sql" "errors" "fmt" "github.com/edwingeng/slog" "github.com/edwingeng/wuid/internal" _ "github.com/go-sql-driver/mysql" ) // WUID is an extremely fast universal unique identifier generator. type WUID struct { w *internal.WUID } // NewWUID creates a new WUID instance. func NewWUID(name string, logger slog.Logger, opts ...Option) *WUID { return &WUID{w: internal.NewWUID(name, logger, opts...)} } // Next returns a unique identifier. func (w *WUID) Next() int64 { return w.w.Next() } type OpenDB func() (client *sql.DB, autoClose bool, err error) // LoadH28FromMysql adds 1 to a specific number in MySQL and fetches its new value. // The new value is used as the high 28 bits of all generated numbers. In addition, all the // arguments passed in are saved for future renewal. func (w *WUID) LoadH28FromMysql(openDB OpenDB, table string) error { if len(table) == 0 { return errors.New("table cannot be empty") } db, autoClose, err := openDB() if err != nil { return err } defer func() { if autoClose { _ = db.Close() } }() result, err := db.Exec(fmt.Sprintf("REPLACE INTO %s (x) VALUES (0)", table)) if err != nil { return err } h28, err := result.LastInsertId() if err != nil { return err } if err = w.w.VerifyH28(h28); err != nil { return err } w.w.Reset(h28 << 36) w.w.Logger.Infof(" new h28: %d. name: %s", h28, w.w.Name) w.w.Lock() defer w.w.Unlock() if w.w.Renew != nil { return nil } w.w.Renew = func() error { return w.LoadH28FromMysql(openDB, table) } return nil } // RenewNow reacquires the high 28 bits immediately. func (w *WUID) RenewNow() error { return w.w.RenewNow() } type Option = internal.Option // WithH28Verifier adds an extra verifier for the high 28 bits. func WithH28Verifier(cb func(h28 int64) error) Option { return internal.WithH28Verifier(cb) } // WithSection brands a section ID on each generated number. A section ID must be in between [0, 7]. func WithSection(section int8) Option { return internal.WithSection(section) } // WithStep sets the step and the floor for each generated number. func WithStep(step int64, floor int64) Option { return internal.WithStep(step, floor) } // WithObfuscation enables number obfuscation. func WithObfuscation(seed int) Option { return internal.WithObfuscation(seed) } ================================================ FILE: mysql/wuid/wuid_test.go ================================================ package wuid import ( "database/sql" "errors" "fmt" "github.com/edwingeng/slog" "github.com/edwingeng/wuid/internal" _ "github.com/go-sql-driver/mysql" "math/rand" "strings" "sync/atomic" "testing" "time" ) var ( dumb = slog.NewDumbLogger() ) var ( cfg struct { addr string user string pass string dbName string table string } ) func init() { cfg.addr = "127.0.0.1:3306" cfg.user = "root" cfg.pass = "hello" cfg.dbName = "test" cfg.table = "wuid" } func connect() (*sql.DB, error) { dsn := cfg.user if len(cfg.pass) > 0 { dsn += ":" + cfg.pass } dsn += "@tcp(" + cfg.addr + ")/" + cfg.dbName return sql.Open("mysql", dsn) } func TestWUID_LoadH28FromMysql(t *testing.T) { openDB := func() (*sql.DB, bool, error) { db, err := connect() return db, true, err } w := NewWUID("alpha", dumb) err := w.LoadH28FromMysql(openDB, cfg.table) if err != nil { t.Fatal(err) } initial := atomic.LoadInt64(&w.w.N) for i := 1; i < 100; i++ { if err := w.RenewNow(); err != nil { t.Fatal(err) } expected := ((initial >> 36) + int64(i)) << 36 if atomic.LoadInt64(&w.w.N) != expected { t.Fatalf("w.w.N is %d, while it should be %d. i: %d", atomic.LoadInt64(&w.w.N), expected, i) } n := rand.Intn(10) for j := 0; j < n; j++ { w.Next() } } } func TestWUID_LoadH28FromMysql_Error(t *testing.T) { w := NewWUID("alpha", dumb) if w.LoadH28FromMysql(nil, "") == nil { t.Fatal("table is not properly checked") } newErrorDB := func() (client *sql.DB, autoClose bool, err error) { return nil, true, errors.New("beta") } if w.LoadH28FromMysql(newErrorDB, "beta") == nil { t.Fatal(`w.LoadH28FromMysql(newErrorDB, "beta") == nil`) } } func waitUntilNumRenewedReaches(t *testing.T, w *WUID, expected int64) { t.Helper() startTime := time.Now() for time.Since(startTime) < time.Second*3 { if atomic.LoadInt64(&w.w.Stats.NumRenewed) == expected { return } time.Sleep(time.Millisecond * 10) } t.Fatal("timeout") } func TestWUID_Next_Renew(t *testing.T) { db, err := connect() if err != nil { t.Fatal(err) } openDB := func() (*sql.DB, bool, error) { return db, false, err } w := NewWUID("alpha", slog.NewScavenger()) err = w.LoadH28FromMysql(openDB, cfg.table) if err != nil { t.Fatal(err) } h28 := atomic.LoadInt64(&w.w.N) >> 36 atomic.StoreInt64(&w.w.N, (h28<<36)|internal.Bye) n1a := w.Next() if n1a>>36 != h28 { t.Fatal(`n1a>>36 != h28`) } waitUntilNumRenewedReaches(t, w, 1) n1b := w.Next() if n1b != (h28+1)<<36+1 { t.Fatal(`n1b != (h28+1)<<36+1`) } atomic.StoreInt64(&w.w.N, ((h28+1)<<36)|internal.Bye) n2a := w.Next() if n2a>>36 != h28+1 { t.Fatal(`n2a>>36 != h28+1`) } waitUntilNumRenewedReaches(t, w, 2) n2b := w.Next() if n2b != (h28+2)<<36+1 { t.Fatal(`n2b != (h28+2)<<36+1`) } atomic.StoreInt64(&w.w.N, ((h28+2)<<36)|internal.Bye) n3a := w.Next() if n3a>>36 != h28+2 { t.Fatal(`n3a>>36 != h28+2`) } waitUntilNumRenewedReaches(t, w, 3) n3b := w.Next() if n3b != (h28+3)<<36+1 { t.Fatal(`n3b != (h28+3)<<36+1`) } atomic.StoreInt64(&w.w.N, ((h28+2)<<36)+internal.Bye+1) for i := 0; i < 100; i++ { w.Next() } if atomic.LoadInt64(&w.w.Stats.NumRenewAttempts) != 3 { t.Fatal(`atomic.LoadInt64(&w.w.Stats.NumRenewAttempts) != 3`) } var num int sc := w.w.Logger.(*slog.Scavenger) sc.Filter(func(level, msg string) bool { if level == slog.LevelInfo && strings.Contains(msg, "renew succeeded") { num++ } return true }) if num != 3 { t.Fatal(`num != 3`) } } func Example() { openDB := func() (*sql.DB, bool, error) { var db *sql.DB // ... return db, true, nil } // Setup w := NewWUID("alpha", nil) err := w.LoadH28FromMysql(openDB, "wuid") if err != nil { panic(err) } // Generate for i := 0; i < 10; i++ { fmt.Printf("%#016x\n", w.Next()) } } ================================================ FILE: redis/docker-redis-client.sh ================================================ #!/usr/bin/env bash [[ "$TRACE" ]] && set -x pushd `dirname "$0"` > /dev/null trap __EXIT EXIT colorful=false tput setaf 7 > /dev/null 2>&1 if [[ $? -eq 0 ]]; then colorful=true fi function __EXIT() { popd > /dev/null } function printError() { $colorful && tput setaf 1 >&2 echo "Error: $@" $colorful && tput setaf 7 } function printImportantMessage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } function printUsage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } docker run -it --rm redis redis-cli -h host.docker.internal ================================================ FILE: redis/docker-redis-server.sh ================================================ #!/usr/bin/env bash [[ "$TRACE" ]] && set -x pushd `dirname "$0"` > /dev/null trap __EXIT EXIT colorful=false tput setaf 7 > /dev/null 2>&1 if [[ $? -eq 0 ]]; then colorful=true fi function __EXIT() { popd > /dev/null } function printError() { $colorful && tput setaf 1 >&2 echo "Error: $@" $colorful && tput setaf 7 } function printImportantMessage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } function printUsage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } docker run --name redis-server -d -p 6379-6383:6379 redis ================================================ FILE: redis/v8/wuid/coverage.sh ================================================ #!/usr/bin/env bash [[ "$TRACE" ]] && set -x pushd `dirname "$0"` > /dev/null trap __EXIT EXIT colorful=false tput setaf 7 > /dev/null 2>&1 if [[ $? -eq 0 ]]; then colorful=true fi function __EXIT() { popd > /dev/null } function printError() { $colorful && tput setaf 1 >&2 echo "Error: $@" $colorful && tput setaf 7 } function printImportantMessage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } function printUsage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } go test -cover -coverprofile=c.out -v "$@" && go tool cover -html=c.out ================================================ FILE: redis/v8/wuid/vet.sh ================================================ #!/usr/bin/env bash [[ "$TRACE" ]] && set -x pushd `dirname "$0"` > /dev/null trap __EXIT EXIT colorful=false tput setaf 7 > /dev/null 2>&1 if [[ $? -eq 0 ]]; then colorful=true fi function __EXIT() { popd > /dev/null } function printError() { $colorful && tput setaf 1 >&2 echo "Error: $@" $colorful && tput setaf 7 } function printImportantMessage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } function printUsage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } printImportantMessage "====== gofmt" gofmt -w . printImportantMessage "====== go vet" go vet ./... printImportantMessage "====== gocyclo" gocyclo -over 15 . printImportantMessage "====== ineffassign" ineffassign ./... printImportantMessage "====== misspell" misspell * ================================================ FILE: redis/v8/wuid/wuid.go ================================================ package wuid import ( "context" "errors" "github.com/edwingeng/slog" "github.com/edwingeng/wuid/internal" "github.com/go-redis/redis/v8" "time" ) // WUID is an extremely fast universal unique identifier generator. type WUID struct { w *internal.WUID } // NewWUID creates a new WUID instance. func NewWUID(name string, logger slog.Logger, opts ...Option) *WUID { return &WUID{w: internal.NewWUID(name, logger, opts...)} } // Next returns a unique identifier. func (w *WUID) Next() int64 { return w.w.Next() } type NewClient func() (client redis.UniversalClient, autoClose bool, err error) // LoadH28FromRedis adds 1 to a specific number in Redis and fetches its new value. // The new value is used as the high 28 bits of all generated numbers. In addition, all the // arguments passed in are saved for future renewal. func (w *WUID) LoadH28FromRedis(newClient NewClient, key string) error { if len(key) == 0 { return errors.New("key cannot be empty") } client, autoClose, err := newClient() if err != nil { return err } defer func() { if autoClose { _ = client.Close() } }() ctx1, cancel1 := context.WithTimeout(context.Background(), time.Second*5) defer cancel1() h28, err := client.Incr(ctx1, key).Result() if err != nil { return err } if err = w.w.VerifyH28(h28); err != nil { return err } w.w.Reset(h28 << 36) w.w.Logger.Infof(" new h28: %d. name: %s", h28, w.w.Name) w.w.Lock() defer w.w.Unlock() if w.w.Renew != nil { return nil } w.w.Renew = func() error { return w.LoadH28FromRedis(newClient, key) } return nil } // RenewNow reacquires the high 28 bits immediately. func (w *WUID) RenewNow() error { return w.w.RenewNow() } type Option = internal.Option // WithH28Verifier adds an extra verifier for the high 28 bits. func WithH28Verifier(cb func(h28 int64) error) Option { return internal.WithH28Verifier(cb) } // WithSection brands a section ID on each generated number. A section ID must be in between [0, 7]. func WithSection(section int8) Option { return internal.WithSection(section) } // WithStep sets the step and the floor for each generated number. func WithStep(step int64, floor int64) Option { return internal.WithStep(step, floor) } // WithObfuscation enables number obfuscation. func WithObfuscation(seed int) Option { return internal.WithObfuscation(seed) } ================================================ FILE: redis/v8/wuid/wuid_test.go ================================================ package wuid import ( "errors" "flag" "fmt" "github.com/edwingeng/slog" "github.com/edwingeng/wuid/internal" "github.com/go-redis/redis/v8" "math/rand" "strings" "sync/atomic" "testing" "time" ) var redisCluster = flag.Bool("cluster", false, "") var ( dumb = slog.NewDumbLogger() ) var ( cfg struct { addrs []string password string key string } ) func init() { cfg.addrs = []string{"127.0.0.1:6379", "127.0.0.1:6380", "127.0.0.1:6381"} cfg.key = "v8:wuid" } func connect() redis.UniversalClient { if *redisCluster { return redis.NewClusterClient(&redis.ClusterOptions{ Addrs: cfg.addrs, Password: cfg.password, }) } else { return redis.NewClient(&redis.Options{ Addr: cfg.addrs[0], Password: cfg.password, }) } } func TestWUID_LoadH28FromRedis(t *testing.T) { newClient := func() (redis.UniversalClient, bool, error) { return connect(), true, nil } w := NewWUID("alpha", dumb) err := w.LoadH28FromRedis(newClient, cfg.key) if err != nil { t.Fatal(err) } initial := atomic.LoadInt64(&w.w.N) for i := 1; i < 100; i++ { if err := w.RenewNow(); err != nil { t.Fatal(err) } expected := ((initial >> 36) + int64(i)) << 36 if atomic.LoadInt64(&w.w.N) != expected { t.Fatalf("w.w.N is %d, while it should be %d. i: %d", atomic.LoadInt64(&w.w.N), expected, i) } n := rand.Intn(10) for j := 0; j < n; j++ { w.Next() } } } func TestWUID_LoadH28FromRedis_Error(t *testing.T) { w := NewWUID("alpha", dumb) if w.LoadH28FromRedis(nil, "") == nil { t.Fatal("key is not properly checked") } newErrorClient := func() (redis.UniversalClient, bool, error) { return nil, true, errors.New("beta") } if w.LoadH28FromRedis(newErrorClient, "beta") == nil { t.Fatal(`w.LoadH28FromRedis(newErrorClient, "beta") == nil`) } } func waitUntilNumRenewedReaches(t *testing.T, w *WUID, expected int64) { t.Helper() startTime := time.Now() for time.Since(startTime) < time.Second*3 { if atomic.LoadInt64(&w.w.Stats.NumRenewed) == expected { return } time.Sleep(time.Millisecond * 10) } t.Fatal("timeout") } func TestWUID_Next_Renew(t *testing.T) { client := connect() newClient := func() (redis.UniversalClient, bool, error) { return client, false, nil } w := NewWUID("alpha", slog.NewScavenger()) err := w.LoadH28FromRedis(newClient, cfg.key) if err != nil { t.Fatal(err) } h28 := atomic.LoadInt64(&w.w.N) >> 36 atomic.StoreInt64(&w.w.N, (h28<<36)|internal.Bye) n1a := w.Next() if n1a>>36 != h28 { t.Fatal(`n1a>>36 != h28`) } waitUntilNumRenewedReaches(t, w, 1) n1b := w.Next() if n1b != (h28+1)<<36+1 { t.Fatal(`n1b != (h28+1)<<36+1`) } atomic.StoreInt64(&w.w.N, ((h28+1)<<36)|internal.Bye) n2a := w.Next() if n2a>>36 != h28+1 { t.Fatal(`n2a>>36 != h28+1`) } waitUntilNumRenewedReaches(t, w, 2) n2b := w.Next() if n2b != (h28+2)<<36+1 { t.Fatal(`n2b != (h28+2)<<36+1`) } atomic.StoreInt64(&w.w.N, ((h28+2)<<36)|internal.Bye) n3a := w.Next() if n3a>>36 != h28+2 { t.Fatal(`n3a>>36 != h28+2`) } waitUntilNumRenewedReaches(t, w, 3) n3b := w.Next() if n3b != (h28+3)<<36+1 { t.Fatal(`n3b != (h28+3)<<36+1`) } atomic.StoreInt64(&w.w.N, ((h28+2)<<36)+internal.Bye+1) for i := 0; i < 100; i++ { w.Next() } if atomic.LoadInt64(&w.w.Stats.NumRenewAttempts) != 3 { t.Fatal(`atomic.LoadInt64(&w.w.Stats.NumRenewAttempts) != 3`) } var num int sc := w.w.Logger.(*slog.Scavenger) sc.Filter(func(level, msg string) bool { if level == slog.LevelInfo && strings.Contains(msg, "renew succeeded") { num++ } return true }) if num != 3 { t.Fatal(`num != 3`) } } func Example() { newClient := func() (redis.UniversalClient, bool, error) { var client redis.UniversalClient // ... return client, true, nil } // Setup w := NewWUID("alpha", nil) err := w.LoadH28FromRedis(newClient, "wuid") if err != nil { panic(err) } // Generate for i := 0; i < 10; i++ { fmt.Printf("%#016x\n", w.Next()) } } ================================================ FILE: redis/wuid/coverage.sh ================================================ #!/usr/bin/env bash [[ "$TRACE" ]] && set -x pushd `dirname "$0"` > /dev/null trap __EXIT EXIT colorful=false tput setaf 7 > /dev/null 2>&1 if [[ $? -eq 0 ]]; then colorful=true fi function __EXIT() { popd > /dev/null } function printError() { $colorful && tput setaf 1 >&2 echo "Error: $@" $colorful && tput setaf 7 } function printImportantMessage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } function printUsage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } go test -cover -coverprofile=c.out -v "$@" && go tool cover -html=c.out ================================================ FILE: redis/wuid/vet.sh ================================================ #!/usr/bin/env bash [[ "$TRACE" ]] && set -x pushd `dirname "$0"` > /dev/null trap __EXIT EXIT colorful=false tput setaf 7 > /dev/null 2>&1 if [[ $? -eq 0 ]]; then colorful=true fi function __EXIT() { popd > /dev/null } function printError() { $colorful && tput setaf 1 >&2 echo "Error: $@" $colorful && tput setaf 7 } function printImportantMessage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } function printUsage() { $colorful && tput setaf 3 >&2 echo "$@" $colorful && tput setaf 7 } printImportantMessage "====== gofmt" gofmt -w . printImportantMessage "====== go vet" go vet ./... printImportantMessage "====== gocyclo" gocyclo -over 15 . printImportantMessage "====== ineffassign" ineffassign ./... printImportantMessage "====== misspell" misspell * ================================================ FILE: redis/wuid/wuid.go ================================================ package wuid import ( "errors" "github.com/edwingeng/slog" "github.com/edwingeng/wuid/internal" "github.com/go-redis/redis" ) // WUID is an extremely fast universal unique identifier generator. type WUID struct { w *internal.WUID } // NewWUID creates a new WUID instance. func NewWUID(name string, logger slog.Logger, opts ...Option) *WUID { return &WUID{w: internal.NewWUID(name, logger, opts...)} } // Next returns a unique identifier. func (w *WUID) Next() int64 { return w.w.Next() } type NewClient func() (client redis.UniversalClient, autoClose bool, err error) // LoadH28FromRedis adds 1 to a specific number in Redis and fetches its new value. // The new value is used as the high 28 bits of all generated numbers. In addition, all the // arguments passed in are saved for future renewal. func (w *WUID) LoadH28FromRedis(newClient NewClient, key string) error { if len(key) == 0 { return errors.New("key cannot be empty") } client, autoClose, err := newClient() if err != nil { return err } defer func() { if autoClose { _ = client.Close() } }() h28, err := client.Incr(key).Result() if err != nil { return err } if err = w.w.VerifyH28(h28); err != nil { return err } w.w.Reset(h28 << 36) w.w.Logger.Infof(" new h28: %d. name: %s", h28, w.w.Name) w.w.Lock() defer w.w.Unlock() if w.w.Renew != nil { return nil } w.w.Renew = func() error { return w.LoadH28FromRedis(newClient, key) } return nil } // RenewNow reacquires the high 28 bits immediately. func (w *WUID) RenewNow() error { return w.w.RenewNow() } type Option = internal.Option // WithH28Verifier adds an extra verifier for the high 28 bits. func WithH28Verifier(cb func(h28 int64) error) Option { return internal.WithH28Verifier(cb) } // WithSection brands a section ID on each generated number. A section ID must be in between [0, 7]. func WithSection(section int8) Option { return internal.WithSection(section) } // WithStep sets the step and the floor for each generated number. func WithStep(step int64, floor int64) Option { return internal.WithStep(step, floor) } // WithObfuscation enables number obfuscation. func WithObfuscation(seed int) Option { return internal.WithObfuscation(seed) } ================================================ FILE: redis/wuid/wuid_test.go ================================================ package wuid import ( "errors" "flag" "fmt" "github.com/edwingeng/slog" "github.com/edwingeng/wuid/internal" "github.com/go-redis/redis" "math/rand" "strings" "sync/atomic" "testing" "time" ) var redisCluster = flag.Bool("cluster", false, "") var ( dumb = slog.NewDumbLogger() ) var ( cfg struct { addrs []string password string key string } ) func init() { cfg.addrs = []string{"127.0.0.1:6379", "127.0.0.1:6380", "127.0.0.1:6381"} cfg.key = "wuid" } func connect() redis.UniversalClient { if *redisCluster { return redis.NewClusterClient(&redis.ClusterOptions{ Addrs: cfg.addrs, Password: cfg.password, }) } else { return redis.NewClient(&redis.Options{ Addr: cfg.addrs[0], Password: cfg.password, }) } } func TestWUID_LoadH28FromRedis(t *testing.T) { newClient := func() (redis.UniversalClient, bool, error) { return connect(), true, nil } w := NewWUID("alpha", dumb) err := w.LoadH28FromRedis(newClient, cfg.key) if err != nil { t.Fatal(err) } initial := atomic.LoadInt64(&w.w.N) for i := 1; i < 100; i++ { if err := w.RenewNow(); err != nil { t.Fatal(err) } expected := ((initial >> 36) + int64(i)) << 36 if atomic.LoadInt64(&w.w.N) != expected { t.Fatalf("w.w.N is %d, while it should be %d. i: %d", atomic.LoadInt64(&w.w.N), expected, i) } n := rand.Intn(10) for j := 0; j < n; j++ { w.Next() } } } func TestWUID_LoadH28FromRedis_Error(t *testing.T) { w := NewWUID("alpha", dumb) if w.LoadH28FromRedis(nil, "") == nil { t.Fatal("key is not properly checked") } newErrorClient := func() (redis.UniversalClient, bool, error) { return nil, true, errors.New("beta") } if w.LoadH28FromRedis(newErrorClient, "beta") == nil { t.Fatal(`w.LoadH28FromRedis(newErrorClient, "beta") == nil`) } } func waitUntilNumRenewedReaches(t *testing.T, w *WUID, expected int64) { t.Helper() startTime := time.Now() for time.Since(startTime) < time.Second*3 { if atomic.LoadInt64(&w.w.Stats.NumRenewed) == expected { return } time.Sleep(time.Millisecond * 10) } t.Fatal("timeout") } func TestWUID_Next_Renew(t *testing.T) { client := connect() newClient := func() (redis.UniversalClient, bool, error) { return client, false, nil } w := NewWUID("alpha", slog.NewScavenger()) err := w.LoadH28FromRedis(newClient, cfg.key) if err != nil { t.Fatal(err) } h28 := atomic.LoadInt64(&w.w.N) >> 36 atomic.StoreInt64(&w.w.N, (h28<<36)|internal.Bye) n1a := w.Next() if n1a>>36 != h28 { t.Fatal(`n1a>>36 != h28`) } waitUntilNumRenewedReaches(t, w, 1) n1b := w.Next() if n1b != (h28+1)<<36+1 { t.Fatal(`n1b != (h28+1)<<36+1`) } atomic.StoreInt64(&w.w.N, ((h28+1)<<36)|internal.Bye) n2a := w.Next() if n2a>>36 != h28+1 { t.Fatal(`n2a>>36 != h28+1`) } waitUntilNumRenewedReaches(t, w, 2) n2b := w.Next() if n2b != (h28+2)<<36+1 { t.Fatal(`n2b != (h28+2)<<36+1`) } atomic.StoreInt64(&w.w.N, ((h28+2)<<36)|internal.Bye) n3a := w.Next() if n3a>>36 != h28+2 { t.Fatal(`n3a>>36 != h28+2`) } waitUntilNumRenewedReaches(t, w, 3) n3b := w.Next() if n3b != (h28+3)<<36+1 { t.Fatal(`n3b != (h28+3)<<36+1`) } atomic.StoreInt64(&w.w.N, ((h28+2)<<36)+internal.Bye+1) for i := 0; i < 100; i++ { w.Next() } if atomic.LoadInt64(&w.w.Stats.NumRenewAttempts) != 3 { t.Fatal(`atomic.LoadInt64(&w.w.Stats.NumRenewAttempts) != 3`) } var num int sc := w.w.Logger.(*slog.Scavenger) sc.Filter(func(level, msg string) bool { if level == slog.LevelInfo && strings.Contains(msg, "renew succeeded") { num++ } return true }) if num != 3 { t.Fatal(`num != 3`) } } func Example() { newClient := func() (redis.UniversalClient, bool, error) { var client redis.UniversalClient // ... return client, true, nil } // Setup w := NewWUID("alpha", nil) err := w.LoadH28FromRedis(newClient, "wuid") if err != nil { panic(err) } // Generate for i := 0; i < 10; i++ { fmt.Printf("%#016x\n", w.Next()) } } ================================================ FILE: wuid.go ================================================ package wuid type WUID interface { Next() int64 }