Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
26 changes: 17 additions & 9 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,20 @@ jobs:
steps:
- name: Check out code
uses: actions/checkout@v3

- name: Install Go
uses: actions/setup-go@v3
with:
go-version: 1.x

- name: fmt
run: test -z $(gofmt -l .)

- name: vet
run: go vet ./...

- name: staticcheck
uses: dominikh/staticcheck-action@v1.2.0
uses: dominikh/staticcheck-action@v1.3.0
with:
install-go: false

Expand All @@ -24,11 +30,13 @@ jobs:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Install Go
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go-version }}
- name: Check out code
uses: actions/checkout@v2
- name: Test
run: go test -race ./...
- name: Install Go
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go-version }}

- name: Check out code
uses: actions/checkout@v2

- name: Test
run: go test -race ./...
51 changes: 24 additions & 27 deletions ulid.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,24 +126,18 @@ func MustNew(ms uint64, entropy io.Reader) ULID {
// DefaultEntropy as the entropy. It may panic if the given time.Time is too
// large or too small.
func MustNewDefault(t time.Time) ULID {
return MustNew(Timestamp(t), DefaultEntropy())
return MustNew(Timestamp(t), defaultEntropy)
}

var (
entropy io.Reader
entropyOnce sync.Once
)
var defaultEntropy = func() io.Reader {
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
return &LockedMonotonicReader{MonotonicReader: Monotonic(rng, 0)}
}()

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

// Make returns a ULID with the current time in Unix milliseconds and
Expand All @@ -152,7 +146,7 @@ func DefaultEntropy() io.Reader {
// contention.
func Make() (id ULID) {
// NOTE: MustNew can't panic since DefaultEntropy never returns an error.
return MustNew(Now(), DefaultEntropy())
return MustNew(Now(), defaultEntropy)
}

// Parse parses an encoded ULID, returning an error in case of failure.
Expand Down Expand Up @@ -539,20 +533,23 @@ func (id ULID) Value() (driver.Value, error) {
return id.MarshalBinary()
}

// Monotonic returns an entropy source that is guaranteed to yield
// strictly increasing entropy bytes for the same ULID timestamp.
// On conflicts, the previous ULID entropy is incremented with a
// random number between 1 and `inc` (inclusive).
// Monotonic returns a source of entropy that yields strictly increasing entropy
// bytes, to a limit governeed by the `inc` parameter.
//
// Specifically, calls to MonotonicRead within the same ULID timestamp return
// entropy incremented by a random number between 1 and `inc` inclusive. If an
// increment results in entropy that would overflow available space,
// MonotonicRead returns ErrMonotonicOverflow.
//
// The provided entropy source must actually yield random bytes or else
// monotonic reads are not guaranteed to terminate, since there isn't
// enough randomness to compute an increment number.
// Passing `inc == 0` results in the reasonable default `math.MaxUint32`. Lower
// values of `inc` provide more monotonic entropy in a single millisecond, at
// the cost of easier "guessability" of generated ULIDs. If your code depends on
// ULIDs having secure entropy bytes, then it's recommended to use the secure
// default value of `inc == 0`, unless you know what you're doing.
//
// When `inc == 0`, it'll be set to a secure default of `math.MaxUint32`.
// The lower the value of `inc`, the easier the next ULID within the
// same millisecond is to guess. If your code depends on ULIDs having
// secure entropy bytes, then don't go under this default unless you know
// what you're doing.
// The provided entropy source must actually yield random bytes. Otherwise,
// monotonic reads are not guaranteed to terminate, since there isn't enough
// randomness to compute an increment number.
//
// The returned type isn't safe for concurrent use.
func Monotonic(entropy io.Reader, inc uint64) *MonotonicEntropy {
Expand All @@ -574,8 +571,8 @@ func Monotonic(entropy io.Reader, inc uint64) *MonotonicEntropy {

type rng interface{ Int63n(n int64) int64 }

// LockedMonotonicReader wraps a MonotonicReader with a sync.Mutex for
// safe concurrent use.
// LockedMonotonicReader wraps a MonotonicReader with a sync.Mutex for safe
// concurrent use.
type LockedMonotonicReader struct {
mu sync.Mutex
MonotonicReader
Expand Down
4 changes: 3 additions & 1 deletion ulid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -621,7 +621,8 @@ func TestMonotonicSafe(t *testing.T) {
t.Parallel()

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

Expand All @@ -644,6 +645,7 @@ func TestMonotonicSafe(t *testing.T) {
errs <- nil
}()
}

for i := 0; i < cap(errs); i++ {
if err := <-errs; err != nil {
t.Fatal(err)
Expand Down