Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ulid: add DefaultEntropy() and Make() #81

Merged
merged 6 commits into from
Jun 22, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,25 @@ go get github.com/oklog/ulid/v2
An ULID is constructed with a `time.Time` and an `io.Reader` entropy source.
This design allows for greater flexibility in choosing your trade-offs.

Please note that `rand.Rand` from the `math` package is *not* safe for concurrent use.
Instantiate one per long living go-routine or use a `sync.Pool` if you want to avoid the potential contention of a locked `rand.Source` as its been frequently observed in the package level functions.
If you want sane defaults, just use `ulid.Make()` which will produce a per process monotonically increasing ULID based on the current time that is safe for concurrent use.

```go
func ExampleULID() {
func ExampleMake() {
fmt.Println(ulid.Make())
// Output: 0000XSNJG0MQJHBF4QX1EFD6Y3
}
```

Otherwise, you'll need to provide your own entropy source. Since `rand.Rand` from the `math` package is [*not* safe for concurrent use](https://github.com/golang/go/issues/3611), consider the following guidance:

- Instantiate one `rand.Rand` per long living go-routine if your concurrency model permits — this option will result in no lock contention.

- For usage in short lived go-routines (e.g. HTTP handlers), use [`golang.org/x/exp/rand.Rand`](https://pkg.go.dev/golang.org/x/exp/rand#example-LockedSource) which is safe for concurrent use, but can result in a high amount of lock contention if many go-routines concurrently call `Read` on it.

- When lock contention is too high with the previous approach, use a `sync.Pool` of `rand.Rand` instances. This won't provide any benefits if you need to per process monotonic ULIDs, since using `LockedMonotonicReader` will synchronize all reads anyways.

peterbourgon marked this conversation as resolved.
Show resolved Hide resolved
```go
func ExampleMustNew() {
t := time.Unix(1000000, 0)
entropy := ulid.Monotonic(rand.New(rand.NewSource(t.UnixNano())), 0)
fmt.Println(ulid.MustNew(ulid.Timestamp(t), entropy))
Expand Down
47 changes: 45 additions & 2 deletions ulid.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"math"
"math/bits"
"math/rand"
"sync"
"time"
)

Expand Down Expand Up @@ -121,6 +122,32 @@ func MustNew(ms uint64, entropy io.Reader) ULID {
return id
}

var (
entropy io.Reader
entropyOnce sync.Once
)

// DefaultEntropy returns a thread-safe per process monotonically increasing
// entropy source.
peterbourgon marked this conversation as resolved.
Show resolved Hide resolved
func DefaultEntropy() io.Reader {
entropyOnce.Do(func() {
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
entropy = &LockedMonotonicReader{
MonotonicReader: Monotonic(rng, 0),
}
})
return entropy
}

// Make returns an ULID with the current time in Unix milliseconds and
// monotonically increasing entropy for the same millisecond.
// It is safe for concurrent use, leveraging a sync.Pool underneath for minimal
// contention.
func Make() (id ULID) {
// NOTE: MustNew can't panic since DefaultEntropy never returns an error.
return MustNew(Now(), DefaultEntropy())
}

// Parse parses an encoded ULID, returning an error in case of failure.
//
// ErrDataSize is returned if the len(ulid) is different from an encoded
Expand Down Expand Up @@ -531,21 +558,37 @@ func Monotonic(entropy io.Reader, inc uint64) *MonotonicEntropy {
m.inc = math.MaxUint32
}

if rng, ok := entropy.(*rand.Rand); ok {
if rng, ok := entropy.(rng); ok {
m.rng = rng
}

return &m
}

type rng interface{ Int63n(n int64) int64 }

// LockedMonotonicReader wraps a MonotonicReader with a sync.Mutex for
// safe concurrent use.
type LockedMonotonicReader struct {
mu sync.Mutex
MonotonicReader
}

func (r *LockedMonotonicReader) MonotonicRead(ms uint64, p []byte) (err error) {
r.mu.Lock()
err = r.MonotonicReader.MonotonicRead(ms, p)
r.mu.Unlock()
return err
}
peterbourgon marked this conversation as resolved.
Show resolved Hide resolved
peterbourgon marked this conversation as resolved.
Show resolved Hide resolved

// MonotonicEntropy is an opaque type that provides monotonic entropy.
type MonotonicEntropy struct {
io.Reader
ms uint64
inc uint64
entropy uint80
rand [8]byte
rng *rand.Rand
rng rng
}

// MonotonicRead implements the MonotonicReader interface.
Expand Down
39 changes: 14 additions & 25 deletions ulid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
"math"
"math/rand"
"strings"
"sync"
"testing"
"testing/iotest"
"testing/quick"
Expand Down Expand Up @@ -61,6 +60,11 @@ func TestNew(t *testing.T) {
})
}

func TestMake(t *testing.T) {
t.Parallel()
t.Log(ulid.Make())
peterbourgon marked this conversation as resolved.
Show resolved Hide resolved
peterbourgon marked this conversation as resolved.
Show resolved Hide resolved
}

func TestMustNew(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -142,7 +146,7 @@ func TestRoundTrips(t *testing.T) {
id == ulid.MustParseStrict(id.String())
}

err := quick.Check(prop, &quick.Config{MaxCount: 1E5})
err := quick.Check(prop, &quick.Config{MaxCount: 1e5})
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -229,7 +233,7 @@ func TestEncoding(t *testing.T) {
return true
}

if err := quick.Check(prop, &quick.Config{MaxCount: 1E5}); err != nil {
if err := quick.Check(prop, &quick.Config{MaxCount: 1e5}); err != nil {
t.Fatal(err)
}
}
Expand Down Expand Up @@ -258,7 +262,7 @@ func TestLexicographicalOrder(t *testing.T) {
top = next
}

if err := quick.Check(prop, &quick.Config{MaxCount: 1E6}); err != nil {
if err := quick.Check(prop, &quick.Config{MaxCount: 1e6}); err != nil {
t.Fatal(err)
}
}
Expand Down Expand Up @@ -316,7 +320,7 @@ func TestParseRobustness(t *testing.T) {
return err == nil
}

err := quick.Check(prop, &quick.Config{MaxCount: 1E4})
err := quick.Check(prop, &quick.Config{MaxCount: 1e4})
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -368,7 +372,7 @@ func TestTimestampRoundTrips(t *testing.T) {
return ts == ulid.Timestamp(ulid.Time(ts))
}

err := quick.Check(prop, &quick.Config{MaxCount: 1E5})
err := quick.Check(prop, &quick.Config{MaxCount: 1e5})
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -447,7 +451,7 @@ func TestEntropyRead(t *testing.T) {
return eq
}

if err := quick.Check(prop, &quick.Config{MaxCount: 1E4}); err != nil {
if err := quick.Check(prop, &quick.Config{MaxCount: 1e4}); err != nil {
t.Fatal(err)
}
}
Expand All @@ -463,7 +467,7 @@ func TestCompare(t *testing.T) {
return a.Compare(b)
}

err := quick.CheckEqual(a, b, &quick.Config{MaxCount: 1E5})
err := quick.CheckEqual(a, b, &quick.Config{MaxCount: 1e5})
if err != nil {
t.Error(err)
}
Expand Down Expand Up @@ -586,11 +590,8 @@ func TestMonotonicSafe(t *testing.T) {
t.Parallel()

var (
src = rand.NewSource(time.Now().UnixNano())
entropy = rand.New(src)
monotonic = ulid.Monotonic(entropy, 0)
safe = &safeMonotonicReader{MonotonicReader: monotonic}
t0 = ulid.Timestamp(time.Now())
safe = ulid.DefaultEntropy()
t0 = ulid.Timestamp(time.Now())
)

errs := make(chan error, 100)
Expand Down Expand Up @@ -630,18 +631,6 @@ func TestULID_Bytes(t *testing.T) {
}
}

type safeMonotonicReader struct {
mtx sync.Mutex
ulid.MonotonicReader
}

func (r *safeMonotonicReader) MonotonicRead(ms uint64, p []byte) (err error) {
r.mtx.Lock()
err = r.MonotonicReader.MonotonicRead(ms, p)
r.mtx.Unlock()
return err
}

func BenchmarkNew(b *testing.B) {
benchmarkMakeULID(b, func(timestamp uint64, entropy io.Reader) {
_, _ = ulid.New(timestamp, entropy)
Expand Down