drop uuid and use modified nanoid

Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
This commit is contained in:
2021-08-20 22:40:48 +03:00
parent 4509323cae
commit a45b672c98
21 changed files with 310 additions and 49 deletions

View File

@@ -4,9 +4,9 @@ import (
"context"
"time"
"github.com/google/uuid"
"github.com/unistack-org/micro/v3/auth"
"github.com/unistack-org/micro/v3/logger"
"github.com/unistack-org/micro/v3/util/id"
)
// Verify the auth credentials and refresh the auth token periodically
@@ -22,7 +22,11 @@ func Verify(a auth.Auth) error {
auth.WithScopes("service"),
}
acc, err := a.Generate(uuid.New().String(), opts...)
id, err := id.New()
if err != nil {
return err
}
acc, err := a.Generate(id, opts...)
if err != nil {
return err
}

22
util/id/LICENSE Normal file
View File

@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2018-2021 Matous Dzivjak <matousdzivjak@gmail.com>
Copyright (c) 2021 Unistack LLC <v.tolstov@unistack.org>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

108
util/id/id.go Normal file
View File

@@ -0,0 +1,108 @@
package id
import (
"crypto/rand"
"errors"
"math"
)
// DefaultAlphabet is the alphabet used for ID characters by default
var DefaultAlphabet = []rune("6789BCDFGHJKLMNPQRTWbcdfghjkmnpqrtwz")
// DefaultSize is the size used for ID by default
var DefaultSize = 16
// getMask generates bit mask used to obtain bits from the random bytes that are used to get index of random character
// from the alphabet. Example: if the alphabet has 6 = (110)_2 characters it is sufficient to use mask 7 = (111)_2
func getMask(alphabetSize int) int {
for i := 1; i <= 8; i++ {
mask := (2 << uint(i)) - 1
if mask >= alphabetSize-1 {
return mask
}
}
return 0
}
// New returns new id or error
func New(opts ...Option) (string, error) {
options := NewOptions(opts...)
if len(options.Alphabet) == 0 || len(options.Alphabet) > 255 {
return "", errors.New("alphabet must not be empty and contain no more than 255 chars")
}
if options.Size <= 0 {
return "", errors.New("size must be positive integer")
}
chars := options.Alphabet
mask := getMask(len(chars))
// estimate how many random bytes we will need for the ID, we might actually need more but this is tradeoff
// between average case and worst case
ceilArg := 1.6 * float64(mask*options.Size) / float64(len(options.Alphabet))
step := int(math.Ceil(ceilArg))
id := make([]rune, options.Size)
bytes := make([]byte, step)
for j := 0; ; {
_, err := rand.Read(bytes)
if err != nil {
return "", err
}
for i := 0; i < step; i++ {
currByte := bytes[i] & byte(mask)
if currByte < byte(len(chars)) {
id[j] = chars[currByte]
j++
if j == options.Size {
return string(id[:options.Size]), nil
}
}
}
}
}
// Must is the same as New but panics on error
func Must(opts ...Option) string {
id, err := New(opts...)
if err != nil {
panic(err)
}
return id
}
// Options contains id deneration options
type Options struct {
Alphabet []rune
Size int
}
// Option func signature
type Option func(*Options)
// Alphabet specifies alphabet to use
func Alphabet(alphabet string) Option {
return func(o *Options) {
o.Alphabet = []rune(alphabet)
}
}
// Size specifies id size
func Size(size int) Option {
return func(o *Options) {
o.Size = size
}
}
// NewOptions returns new Options struct filled by opts
func NewOptions(opts ...Option) Options {
options := Options{
Alphabet: DefaultAlphabet,
Size: DefaultSize,
}
for _, o := range opts {
o(&options)
}
return options
}

View File

@@ -0,0 +1,41 @@
package id
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestHasNoCollisions(t *testing.T) {
tries := 100_000
used := make(map[string]bool, tries)
for i := 0; i < tries; i++ {
id := Must()
require.False(t, used[id], "shouldn't return colliding IDs")
used[id] = true
}
}
func TestFlatDistribution(t *testing.T) {
tries := 100_000
alphabet := "abcdefghij"
size := 10
chars := make(map[rune]int)
for i := 0; i < tries; i++ {
id := Must(Alphabet(alphabet), Size(size))
for _, r := range id {
chars[r]++
}
}
for _, count := range chars {
require.InEpsilon(t, size*tries/len(alphabet), count, .01, "should have flat distribution")
}
}
// Benchmark id generator
func BenchmarkNanoid(b *testing.B) {
for n := 0; n < b.N; n++ {
_, _ = New()
}
}

68
util/id/id_test.go Normal file
View File

@@ -0,0 +1,68 @@
package id_test
import (
"strings"
"testing"
"unicode/utf8"
"github.com/stretchr/testify/assert"
id "github.com/unistack-org/micro/v3/util/id"
)
func TestGenerate(t *testing.T) {
t.Run("short alphabet", func(t *testing.T) {
alphabet := ""
_, err := id.New(id.Alphabet(alphabet), id.Size(32))
assert.Error(t, err, "should return error if the alphabet is too small")
})
t.Run("long alphabet", func(t *testing.T) {
alphabet := strings.Repeat("a", 256)
_, err := id.New(id.Alphabet(alphabet), id.Size(32))
assert.Error(t, err, "should return error if the alphabet is too long")
})
t.Run("negative ID length", func(t *testing.T) {
_, err := id.New(id.Alphabet("abcdef"), id.Size(-1))
assert.Error(t, err, "should return error if the requested ID length is invalid")
})
t.Run("happy path", func(t *testing.T) {
alphabet := "abcdef"
id, err := id.New(id.Alphabet(alphabet), id.Size(6))
assert.NoError(t, err, "shouldn't return error")
assert.Len(t, id, 6, "should return ID of requested length")
for _, r := range id {
assert.True(t, strings.ContainsRune(alphabet, r), "should use given alphabet")
}
})
t.Run("works with unicode", func(t *testing.T) {
alphabet := "🚀💩🦄🤖"
id, err := id.New(id.Alphabet(alphabet), id.Size(6))
assert.NoError(t, err, "shouldn't return error")
assert.Equal(t, utf8.RuneCountInString(id), 6, "should return ID of requested length")
for _, r := range id {
assert.True(t, strings.ContainsRune(alphabet, r), "should use given alphabet")
}
})
}
func TestNew(t *testing.T) {
t.Run("negative ID length", func(t *testing.T) {
_, err := id.New(id.Size(-1))
assert.Error(t, err, "should return error if the requested ID length is invalid")
})
t.Run("happy path", func(t *testing.T) {
nid, err := id.New()
assert.NoError(t, err, "shouldn't return error")
assert.Len(t, nid, id.DefaultSize, "should return ID of default length")
})
t.Run("custom length", func(t *testing.T) {
id, err := id.New(id.Size(6))
assert.NoError(t, err, "shouldn't return error")
assert.Len(t, id, 6, "should return ID of requested length")
})
}

View File

@@ -5,8 +5,8 @@ import (
"sync"
"time"
"github.com/google/uuid"
"github.com/unistack-org/micro/v3/network/transport"
"github.com/unistack-org/micro/v3/util/id"
)
type pool struct {
@@ -87,9 +87,13 @@ func (p *pool) Get(ctx context.Context, addr string, opts ...transport.DialOptio
if err != nil {
return nil, err
}
id, err := id.New()
if err != nil {
return nil, err
}
return &poolConn{
Client: c,
id: uuid.New().String(),
id: id,
created: time.Now(),
}, nil
}

View File

@@ -5,7 +5,7 @@ import (
"sync"
"time"
"github.com/google/uuid"
"github.com/unistack-org/micro/v3/util/id"
)
// Buffer is ring buffer
@@ -112,7 +112,7 @@ func (b *Buffer) Stream() (<-chan *Entry, chan bool) {
defer b.Unlock()
entries := make(chan *Entry, 128)
id := uuid.New().String()
id := id.Must()
stop := make(chan bool)
b.streams[id] = &Stream{

View File

@@ -6,9 +6,9 @@ import (
"fmt"
"time"
"github.com/google/uuid"
"github.com/unistack-org/micro/v3/auth"
"github.com/unistack-org/micro/v3/store"
"github.com/unistack-org/micro/v3/util/id"
"github.com/unistack-org/micro/v3/util/token"
)
@@ -44,7 +44,11 @@ func (b *Basic) Generate(acc *auth.Account, opts ...token.GenerateOption) (*toke
}
// write to the store
key := uuid.New().String()
key, err := id.New()
if err !=nil {
return nil, err
}
err = b.store.Write(context.Background(), fmt.Sprintf("%v%v", StorePrefix, key), bytes, store.WriteTTL(options.Expiry))
if err != nil {
return nil, err