aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrey Pechkurov <37772591+puzpuzpuz@users.noreply.github.com>2021-07-08 18:05:48 +0300
committerGitHub <noreply@github.com>2021-07-08 10:05:48 -0500
commit655bf50db9d265813a26b0fe2a5d1e80fb9e3c6b (patch)
tree13bb3c888dcf5628774b0580c5d2be85d438486d
parent512b657a42880af87e9f0d863aa6dccf3540d4ba (diff)
downloadgoogle-uuid-655bf50db9d265813a26b0fe2a5d1e80fb9e3c6b.tar.gz
Add randomness pool mode for V4 UUID (#80)
* Add randomness pool mode for V4 UUID Adds an optional randomness pool mode for Random (Version 4) UUID generation. The pool contains random bytes read from the random number generator on demand in batches. Enabling the pool may improve the UUID generation throughput significantly. Since the pool is stored on the Go heap, this feature may be a bad fit for security sensitive applications. That's why it's implemented as an opt-in feature. * fixup! document thread-safety aspects
-rw-r--r--uuid.go39
-rw-r--r--uuid_test.go59
-rw-r--r--version4.go27
3 files changed, 123 insertions, 2 deletions
diff --git a/uuid.go b/uuid.go
index d732ae8..a57207a 100644
--- a/uuid.go
+++ b/uuid.go
@@ -12,6 +12,7 @@ import (
"fmt"
"io"
"strings"
+ "sync"
)
// A UUID is a 128 bit (16 byte) Universal Unique IDentifier as defined in RFC
@@ -33,7 +34,15 @@ const (
Future // Reserved for future definition.
)
-var rander = rand.Reader // random function
+const randPoolSize = 16 * 16
+
+var (
+ rander = rand.Reader // random function
+ poolEnabled = false
+ poolMu sync.Mutex
+ poolPos = randPoolSize // protected with poolMu
+ pool [randPoolSize]byte // protected with poolMu
+)
type invalidLengthError struct{ len int }
@@ -255,3 +264,31 @@ func SetRand(r io.Reader) {
}
rander = r
}
+
+// EnableRandPool enables internal randomness pool used for Random
+// (Version 4) UUID generation. The pool contains random bytes read from
+// the random number generator on demand in batches. Enabling the pool
+// may improve the UUID generation throughput significantly.
+//
+// Since the pool is stored on the Go heap, this feature may be a bad fit
+// for security sensitive applications.
+//
+// Both EnableRandPool and DisableRandPool are not thread-safe and should
+// only be called when there is no possibility that New or any other
+// UUID Version 4 generation function will be called concurrently.
+func EnableRandPool() {
+ poolEnabled = true
+}
+
+// DisableRandPool disables the randomness pool if it was previously
+// enabled with EnableRandPool.
+//
+// Both EnableRandPool and DisableRandPool are not thread-safe and should
+// only be called when there is no possibility that New or any other
+// UUID Version 4 generation function will be called concurrently.
+func DisableRandPool() {
+ poolEnabled = false
+ defer poolMu.Unlock()
+ poolMu.Lock()
+ poolPos = randPoolSize
+}
diff --git a/uuid_test.go b/uuid_test.go
index 99f0bed..e98d0fe 100644
--- a/uuid_test.go
+++ b/uuid_test.go
@@ -179,6 +179,26 @@ func TestRandomUUID(t *testing.T) {
}
}
+func TestRandomUUID_Pooled(t *testing.T) {
+ defer DisableRandPool()
+ EnableRandPool()
+ m := make(map[string]bool)
+ for x := 1; x < 128; x++ {
+ uuid := New()
+ s := uuid.String()
+ if m[s] {
+ t.Errorf("NewRandom returned duplicated UUID %s", s)
+ }
+ m[s] = true
+ if v := uuid.Version(); v != 4 {
+ t.Errorf("Random UUID of version %s", v)
+ }
+ if uuid.Variant() != RFC4122 {
+ t.Errorf("Random UUID is variant %d", uuid.Variant())
+ }
+ }
+}
+
func TestNew(t *testing.T) {
m := make(map[UUID]bool)
for x := 1; x < 32; x++ {
@@ -517,6 +537,22 @@ func TestRandomFromReader(t *testing.T) {
}
}
+func TestRandPool(t *testing.T) {
+ myString := "8059ddhdle77cb52"
+ EnableRandPool()
+ SetRand(strings.NewReader(myString))
+ _, err := NewRandom()
+ if err == nil {
+ t.Errorf("expecting an error as reader has no more bytes")
+ }
+ DisableRandPool()
+ SetRand(strings.NewReader(myString))
+ _, err = NewRandom()
+ if err != nil {
+ t.Errorf("failed generating UUID from a reader")
+ }
+}
+
func TestWrongLength(t *testing.T) {
_, err := Parse("12345")
if err == nil {
@@ -641,3 +677,26 @@ func BenchmarkParseLen36Corrupted(b *testing.B) {
}
}
}
+
+func BenchmarkUUID_New(b *testing.B) {
+ b.RunParallel(func(pb *testing.PB) {
+ for pb.Next() {
+ _, err := NewRandom()
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+}
+
+func BenchmarkUUID_NewPooled(b *testing.B) {
+ EnableRandPool()
+ b.RunParallel(func(pb *testing.PB) {
+ for pb.Next() {
+ _, err := NewRandom()
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+}
diff --git a/version4.go b/version4.go
index 86160fb..7697802 100644
--- a/version4.go
+++ b/version4.go
@@ -27,6 +27,8 @@ func NewString() string {
// The strength of the UUIDs is based on the strength of the crypto/rand
// package.
//
+// Uses the randomness pool if it was enabled with EnableRandPool.
+//
// A note about uniqueness derived from the UUID Wikipedia entry:
//
// Randomly generated UUIDs have 122 random bits. One's annual risk of being
@@ -35,7 +37,10 @@ func NewString() string {
// equivalent to the odds of creating a few tens of trillions of UUIDs in a
// year and having one duplicate.
func NewRandom() (UUID, error) {
- return NewRandomFromReader(rander)
+ if !poolEnabled {
+ return NewRandomFromReader(rander)
+ }
+ return newRandomFromPool()
}
// NewRandomFromReader returns a UUID based on bytes read from a given io.Reader.
@@ -49,3 +54,23 @@ func NewRandomFromReader(r io.Reader) (UUID, error) {
uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10
return uuid, nil
}
+
+func newRandomFromPool() (UUID, error) {
+ var uuid UUID
+ poolMu.Lock()
+ if poolPos == randPoolSize {
+ _, err := io.ReadFull(rander, pool[:])
+ if err != nil {
+ poolMu.Unlock()
+ return Nil, err
+ }
+ poolPos = 0
+ }
+ copy(uuid[:], pool[poolPos:(poolPos+16)])
+ poolPos += 16
+ poolMu.Unlock()
+
+ uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4
+ uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10
+ return uuid, nil
+}