Skip to content

Commit 8299ab9

Browse files
authored
Pool, Secure, and Parse Any (#2)
1 parent b44c431 commit 8299ab9

File tree

6 files changed

+319
-43
lines changed

6 files changed

+319
-43
lines changed

README.md

+105-29
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,69 @@ the documentation for details.
8181

8282
## CLI Tool
8383

84-
Coming Soon!
84+
The CLI tool helps debug and generate ULIDs for your development workflow. Install the CLI using `go` as follows:
85+
86+
```shell
87+
go install go.rtnl.ai/ulid/cmd/ulid@latest
88+
```
89+
90+
Usage:
91+
92+
```shell
93+
Rotational ULID debugging utility
94+
Usage: generate or inspect a ULID
95+
96+
Generate:
97+
98+
ulid [options]
99+
100+
-n INT, --num INT number of ULIDs to generate
101+
-q, --quick use quick entropy (not cryptographic)
102+
-m, --mono use monotonic entropy (for more than one ULID)
103+
-z, --zero use zero entropy
104+
105+
Inspect:
106+
107+
ulid [options] ULID [ULID ...]
108+
109+
-f, --format string time format (default, rfc3339, unix, ms)
110+
-l, --local use local time instead of UTC
111+
-p, --path assumes argument is a path with a ULID filename (strips directory and extension)
112+
113+
Options:
114+
115+
-h, --help display this help and exit
116+
```
117+
118+
Examples:
119+
120+
```
121+
$ ulid
122+
01JKEHMRSH3HXYCYYZ1HZR2JBS
123+
```
124+
125+
```
126+
$ ulid -n 3 -mono
127+
01JKEHNQPA0END3NHMFKB2Y6SE
128+
01JKEHNQPA0END3NHMFNPBB9WE
129+
01JKEHNQPA0END3NHMFRMCX384
130+
```
131+
132+
```
133+
$ ulid 01JKEHNQPA0END3NHMFKB2Y6SE
134+
Thu Feb 06 21:11:53.29 UTC 2025
135+
```
136+
137+
```
138+
$ ulid -f rfc3339 --local 01JKEHNQPA0END3NHMFKB2Y6SE 01JKEHNQPA0END3NHMFNPBB9WE
139+
2025-02-06T15:11:53.290-06:00
140+
2025-02-06T15:11:53.290-06:00
141+
```
142+
143+
```
144+
$ ulid --path path/to/01JKEHNQPA0END3NHMFKB2Y6SE.json
145+
Thu Feb 06 21:11:53.29 UTC 2025
146+
```
85147

86148
## Background
87149

@@ -173,41 +235,55 @@ goos: darwin
173235
goarch: arm64
174236
pkg: go.rtnl.ai/ulid
175237
cpu: Apple M1 Max
176-
BenchmarkNew/WithCrypoEntropy-10 9962818 109.0 ns/op 16 B/op 1 allocs/op
177-
BenchmarkNew/WithEntropy-10 39486076 33.55 ns/op 16 B/op 1 allocs/op
178-
BenchmarkNew/WithMonotonicEntropy_SameTimestamp_Inc0-10 39891241 29.24 ns/op 16 B/op 1 allocs/op
179-
BenchmarkNew/WithMonotonicEntropy_DifferentTimestamp_Inc0-10 32685003 36.05 ns/op 16 B/op 1 allocs/op
180-
BenchmarkNew/WithMonotonicEntropy_SameTimestamp_Inc1-10 45091450 25.16 ns/op 16 B/op 1 allocs/op
181-
BenchmarkNew/WithMonotonicEntropy_DifferentTimestamp_Inc1-10 34196192 35.31 ns/op 16 B/op 1 allocs/op
182-
BenchmarkNew/WithCryptoMonotonicEntropy_SameTimestamp_Inc1-10 47389621 25.40 ns/op 16 B/op 1 allocs/op
238+
BenchmarkNew/WithCrypoEntropy-10 9962818 109.0 ns/op 16 B/op 1 allocs/op
239+
BenchmarkNew/WithEntropy-10 39486076 33.55 ns/op 16 B/op 1 allocs/op
240+
BenchmarkNew/WithoutEntropy-10 72576985 16.62 ns/op 16 B/op 1 allocs/op
241+
BenchmarkMustNew/WithCrypoEntropy-10 11441258 107.4 ns/op 16 B/op 1 allocs/op
242+
BenchmarkMustNew/WithEntropy-10 37700085 31.30 ns/op 16 B/op 1 allocs/op
243+
BenchmarkMustNew/WithoutEntropy-10 70010307 18.37 ns/op 16 B/op 1 allocs/op
244+
```
245+
246+
```
247+
goos: darwin
248+
goarch: arm64
249+
pkg: go.rtnl.ai/ulid
250+
cpu: Apple M1 Max
251+
BenchmarkParse-10 100000000 10.65 ns/op 2441.46 MB/s 0 B/op 0 allocs/op
252+
BenchmarkParseStrict-10 73864335 15.97 ns/op 1627.67 MB/s 0 B/op 0 allocs/op
253+
BenchmarkMustParse-10 95626101 12.61 ns/op 2061.36 MB/s 0 B/op 0 allocs/op
254+
BenchmarkString-10 86481555 13.67 ns/op 1170.76 MB/s 0 B/op 0 allocs/op
255+
BenchmarkMarshal/Text-10 94831988 12.63 ns/op 1266.42 MB/s 0 B/op 0 allocs/op
256+
BenchmarkMarshal/TextTo-10 100000000 10.98 ns/op 1456.62 MB/s 0 B/op 0 allocs/op
257+
BenchmarkMarshal/Binary-10 455631534 2.760 ns/op 5797.31 MB/s 0 B/op 0 allocs/op
258+
BenchmarkMarshal/BinaryTo-10 1000000000 1.111 ns/op 14402.78 MB/s 0 B/op 0 allocs/op
259+
BenchmarkUnmarshal/Text-10 100000000 10.43 ns/op 2492.96 MB/s 0 B/op 0 allocs/op
260+
BenchmarkUnmarshal/Binary-10 569854686 2.135 ns/op 7493.13 MB/s 0 B/op 0 allocs/op
261+
BenchmarkNow-10 31315614 38.77 ns/op 206.35 MB/s 0 B/op 0 allocs/op
262+
BenchmarkTimestamp-10 1000000000 0.7833 ns/op 10212.69 MB/s 0 B/op 0 allocs/op
263+
BenchmarkTime-10 1000000000 0.8018 ns/op 9977.95 MB/s 0 B/op 0 allocs/op
264+
BenchmarkSetTime-10 950735085 1.262 ns/op 6338.36 MB/s 0 B/op 0 allocs/op
265+
BenchmarkEntropy-10 574565655 2.042 ns/op 4896.79 MB/s 0 B/op 0 allocs/op
266+
BenchmarkSetEntropy-10 1000000000 0.9536 ns/op 10486.70 MB/s 0 B/op 0 allocs/op
267+
BenchmarkCompare-10 522322389 2.254 ns/op 14197.26 MB/s 0 B/op 0 allocs/op
268+
```
269+
270+
```
271+
goos: darwin
272+
goarch: arm64
273+
pkg: go.rtnl.ai/ulid
274+
cpu: Apple M1 Max
275+
BenchmarkNew/WithMonotonicEntropy_SameTimestamp_Inc0-10 39891241 29.24 ns/op 16 B/op 1 allocs/op
276+
BenchmarkNew/WithMonotonicEntropy_DifferentTimestamp_Inc0-10 32685003 36.05 ns/op 16 B/op 1 allocs/op
277+
BenchmarkNew/WithMonotonicEntropy_SameTimestamp_Inc1-10 45091450 25.16 ns/op 16 B/op 1 allocs/op
278+
BenchmarkNew/WithMonotonicEntropy_DifferentTimestamp_Inc1-10 34196192 35.31 ns/op 16 B/op 1 allocs/op
279+
BenchmarkNew/WithCryptoMonotonicEntropy_SameTimestamp_Inc1-10 47389621 25.40 ns/op 16 B/op 1 allocs/op
183280
BenchmarkNew/WithCryptoMonotonicEntropy_DifferentTimestamp_Inc1-10 39461244 30.50 ns/op 16 B/op 1 allocs/op
184-
BenchmarkNew/WithoutEntropy-10 72576985 16.62 ns/op 16 B/op 1 allocs/op
185-
BenchmarkMustNew/WithCrypoEntropy-10 11441258 107.4 ns/op 16 B/op 1 allocs/op
186-
BenchmarkMustNew/WithEntropy-10 37700085 31.30 ns/op 16 B/op 1 allocs/op
187281
BenchmarkMustNew/WithMonotonicEntropy_SameTimestamp_Inc0-10 41440399 29.14 ns/op 16 B/op 1 allocs/op
188282
BenchmarkMustNew/WithMonotonicEntropy_DifferentTimestamp_Inc0-10 32740442 36.39 ns/op 16 B/op 1 allocs/op
189283
BenchmarkMustNew/WithMonotonicEntropy_SameTimestamp_Inc1-10 46801796 26.14 ns/op 16 B/op 1 allocs/op
190284
BenchmarkMustNew/WithMonotonicEntropy_DifferentTimestamp_Inc1-10 32244736 37.13 ns/op 16 B/op 1 allocs/op
191285
BenchmarkMustNew/WithCryptoMonotonicEntropy_SameTimestamp_Inc1-10 45454687 26.91 ns/op 16 B/op 1 allocs/op
192286
BenchmarkMustNew/WithCryptoMonotonicEntropy_DifferentTimestamp_Inc1-10 36388584 33.81 ns/op 16 B/op 1 allocs/op
193-
BenchmarkMustNew/WithoutEntropy-10 70010307 18.37 ns/op 16 B/op 1 allocs/op
194-
BenchmarkParse-10 100000000 10.65 ns/op 2441.46 MB/s 0 B/op 0 allocs/op
195-
BenchmarkParseStrict-10 73864335 15.97 ns/op 1627.67 MB/s 0 B/op 0 allocs/op
196-
BenchmarkMustParse-10 95626101 12.61 ns/op 2061.36 MB/s 0 B/op 0 allocs/op
197-
BenchmarkString-10 86481555 13.67 ns/op 1170.76 MB/s 0 B/op 0 allocs/op
198-
BenchmarkMarshal/Text-10 94831988 12.63 ns/op 1266.42 MB/s 0 B/op 0 allocs/op
199-
BenchmarkMarshal/TextTo-10 100000000 10.98 ns/op 1456.62 MB/s 0 B/op 0 allocs/op
200-
BenchmarkMarshal/Binary-10 455631534 2.760 ns/op 5797.31 MB/s 0 B/op 0 allocs/op
201-
BenchmarkMarshal/BinaryTo-10 1000000000 1.111 ns/op 14402.78 MB/s 0 B/op 0 allocs/op
202-
BenchmarkUnmarshal/Text-10 100000000 10.43 ns/op 2492.96 MB/s 0 B/op 0 allocs/op
203-
BenchmarkUnmarshal/Binary-10 569854686 2.135 ns/op 7493.13 MB/s 0 B/op 0 allocs/op
204-
BenchmarkNow-10 31315614 38.77 ns/op 206.35 MB/s 0 B/op 0 allocs/op
205-
BenchmarkTimestamp-10 1000000000 0.7833 ns/op 10212.69 MB/s 0 B/op 0 allocs/op
206-
BenchmarkTime-10 1000000000 0.8018 ns/op 9977.95 MB/s 0 B/op 0 allocs/op
207-
BenchmarkSetTime-10 950735085 1.262 ns/op 6338.36 MB/s 0 B/op 0 allocs/op
208-
BenchmarkEntropy-10 574565655 2.042 ns/op 4896.79 MB/s 0 B/op 0 allocs/op
209-
BenchmarkSetEntropy-10 1000000000 0.9536 ns/op 10486.70 MB/s 0 B/op 0 allocs/op
210-
BenchmarkCompare-10 522322389 2.254 ns/op 14197.26 MB/s 0 B/op 0 allocs/op
211287
```
212288

213289
## References

entropy.go

+61
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package ulid
22

33
import (
44
"bufio"
5+
crand "crypto/rand"
56
"encoding/binary"
67
"io"
78
"math"
@@ -11,6 +12,10 @@ import (
1112
"time"
1213
)
1314

15+
//===========================================================================
16+
// Default Entropy
17+
//===========================================================================
18+
1419
var defaultEntropy = func() io.Reader {
1520
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
1621
return &LockedMonotonicReader{MonotonicReader: Monotonic(rng, 0)}
@@ -22,6 +27,62 @@ func DefaultEntropy() io.Reader {
2227
return defaultEntropy
2328
}
2429

30+
//===========================================================================
31+
// Secure Entropy
32+
//===========================================================================
33+
34+
var secureEntropy = func() io.Reader {
35+
return Pool(func() io.Reader { return crand.Reader })
36+
}()
37+
38+
// SecureEntropy returns a thread-safe per process monotonically increasing
39+
// entropy source that uses cryptographically random generation and a sync.Pool
40+
func SecureEntropy() io.Reader {
41+
return secureEntropy
42+
}
43+
44+
//===========================================================================
45+
// Pool Entropy
46+
//===========================================================================
47+
48+
// Provides a thread-safe source of entropy to assist with fast, concurrent access
49+
// to random data generation. Specify the type of entropy to use
50+
51+
type PoolEntropy struct {
52+
sync.Pool
53+
}
54+
55+
type MakeEntropy func() io.Reader
56+
57+
var _ io.Reader = &PoolEntropy{}
58+
59+
func Pool(entropy MakeEntropy) *PoolEntropy {
60+
return &PoolEntropy{
61+
Pool: sync.Pool{
62+
New: func() any { return entropy() },
63+
},
64+
}
65+
}
66+
67+
func (e *PoolEntropy) Read(p []byte) (n int, err error) {
68+
r := e.Pool.Get().(io.Reader)
69+
n, err = r.Read(p)
70+
e.Pool.Put(r)
71+
return n, err
72+
}
73+
74+
func (e *PoolEntropy) Get() io.Reader {
75+
return e.Pool.Get().(io.Reader)
76+
}
77+
78+
func (e *PoolEntropy) Put(r io.Reader) {
79+
e.Pool.Put(r)
80+
}
81+
82+
//===========================================================================
83+
// Monotonic Readers
84+
//===========================================================================
85+
2586
// MonotonicReader is an interface that should yield monotonically increasing
2687
// entropy into the provided slice for all calls with the same ms parameter. If
2788
// a MonotonicReader is provided to the New constructor, its MonotonicRead

entropy_test.go

+25
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,37 @@ import (
77
"io"
88
"math"
99
"math/rand"
10+
"sync"
1011
"testing"
1112
"time"
1213

1314
"go.rtnl.ai/ulid"
1415
)
1516

17+
func TestPoolEntropy(t *testing.T) {
18+
wg := sync.WaitGroup{}
19+
entropy := ulid.Pool(func() io.Reader { return rand.New(rand.NewSource(time.Now().UnixNano())) })
20+
21+
for i := 0; i < 8; i++ {
22+
wg.Add(1)
23+
go func() {
24+
defer wg.Done()
25+
for i := 0; i < 128; i++ {
26+
uu, err := ulid.New(ulid.Now(), entropy)
27+
if err != nil {
28+
t.Errorf("could not create ulid: %s", err)
29+
}
30+
31+
if uu.IsZero() {
32+
t.Error("expected ulid to not be null")
33+
}
34+
}
35+
}()
36+
}
37+
38+
wg.Wait()
39+
}
40+
1641
func TestMonotonic(t *testing.T) {
1742
now := ulid.Now()
1843
for _, e := range []struct {

errors.go

+3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ package ulid
33
import "errors"
44

55
var (
6+
// Occurs when parsing or unmarshaling ULIDs that aren't strings or []byte.
7+
ErrUnknownType = errors.New("ulid: cannot parse unknown type")
8+
69
// Occurs when parsing or unmarshaling ULIDs with the wrong number of bytes.
710
ErrDataSize = errors.New("ulid: bad data size when unmarshaling")
811

ulid.go

+52-12
Original file line numberDiff line numberDiff line change
@@ -101,15 +101,30 @@ func MustNewDefault(t time.Time) ULID {
101101
return MustNew(Timestamp(t), defaultEntropy)
102102
}
103103

104+
// MustNewSecure is a convenience function equivalent to MustNew with
105+
// SecureEntropy as the entropy. It may panic if the given time.Time is too
106+
// large or too small.
107+
func MustNewSecure(t time.Time) ULID {
108+
return MustNew(Timestamp(t), secureEntropy)
109+
}
110+
104111
// Make returns a ULID with the current time in Unix milliseconds and
105112
// monotonically increasing entropy for the same millisecond.
106-
// It is safe for concurrent use, leveraging a sync.Pool underneath for minimal
107-
// contention.
113+
// It is safe for concurrent use, using a sync.Mutex to protect entropy access.
108114
func Make() (id ULID) {
109115
// NOTE: MustNew can't panic since DefaultEntropy never returns an error.
110116
return MustNew(Now(), defaultEntropy)
111117
}
112118

119+
// MakeSecure returns a ULID with the current time in Unix milliseconds and a
120+
// cryptographically secure, monotonically increasing entropy for the same
121+
// millisecond. It is safe for concurrent use, leveraging a sync.Pool underneath
122+
// for minimal contention.
123+
func MakeSecure() (id ULID) {
124+
// NOTE: MustNew can't panic since SecureEntropy never returns an error.
125+
return MustNew(Now(), secureEntropy)
126+
}
127+
113128
//===========================================================================
114129
// Parsing
115130
//===========================================================================
@@ -119,8 +134,22 @@ func Make() (id ULID) {
119134
// ErrDataSize is returned if the len(ulid) is different from an encoded
120135
// ULID's length. Invalid encodings produce undefined ULIDs. For a version that
121136
// returns an error instead, see ParseStrict.
122-
func Parse(ulid string) (id ULID, err error) {
123-
return id, parse([]byte(ulid), false, &id)
137+
func Parse(ulid any) (id ULID, err error) {
138+
switch t := ulid.(type) {
139+
case ULID:
140+
return t, nil
141+
case string:
142+
if t == "" {
143+
return Zero, nil
144+
}
145+
return id, parse([]byte(t), false, &id)
146+
case []byte:
147+
return id, id.UnmarshalBinary(t)
148+
case [16]byte:
149+
return ULID(t), nil
150+
default:
151+
return Zero, ErrUnknownType
152+
}
124153
}
125154

126155
// ParseStrict parses an encoded ULID, returning an error in case of failure.
@@ -130,8 +159,19 @@ func Parse(ulid string) (id ULID, err error) {
130159
//
131160
// ErrDataSize is returned if the len(ulid) is different from an encoded
132161
// ULID's length. Invalid encodings return ErrInvalidCharacters.
133-
func ParseStrict(ulid string) (id ULID, err error) {
134-
return id, parse([]byte(ulid), true, &id)
162+
func ParseStrict(ulid any) (id ULID, err error) {
163+
switch t := ulid.(type) {
164+
case ULID:
165+
return t, nil
166+
case string:
167+
return id, parse([]byte(t), true, &id)
168+
case []byte:
169+
return id, id.UnmarshalBinary(t)
170+
case [16]byte:
171+
return id, id.UnmarshalBinary(t[:])
172+
default:
173+
return Zero, ErrUnknownType
174+
}
135175
}
136176

137177
func parse(v []byte, strict bool, id *ULID) error {
@@ -209,19 +249,19 @@ func parse(v []byte, strict bool, id *ULID) error {
209249

210250
// MustParse is a convenience function equivalent to Parse that panics on failure
211251
// instead of returning an error.
212-
func MustParse(ulid string) ULID {
213-
id, err := Parse(ulid)
214-
if err != nil {
252+
func MustParse(ulid any) (id ULID) {
253+
var err error
254+
if id, err = Parse(ulid); err != nil {
215255
panic(err)
216256
}
217257
return id
218258
}
219259

220260
// MustParseStrict is a convenience function equivalent to ParseStrict that
221261
// panics on failure instead of returning an error.
222-
func MustParseStrict(ulid string) ULID {
223-
id, err := ParseStrict(ulid)
224-
if err != nil {
262+
func MustParseStrict(ulid any) (id ULID) {
263+
var err error
264+
if id, err = ParseStrict(ulid); err != nil {
225265
panic(err)
226266
}
227267
return id

0 commit comments

Comments
 (0)