Skip to content

Commit 166207c

Browse files
committed
go-aah/aah#21 guid generater added
1 parent 44a3b2d commit 166207c

File tree

2 files changed

+224
-0
lines changed

2 files changed

+224
-0
lines changed

guid.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Copyright (c) Jeevanandam M. (https://github.com/jeevatkm)
2+
// go-aah/essentials source code and usage is governed by a MIT style
3+
// license that can be found in the LICENSE file.
4+
5+
package ess
6+
7+
import (
8+
"crypto/md5"
9+
"crypto/rand"
10+
"encoding/binary"
11+
"encoding/hex"
12+
"errors"
13+
"io"
14+
"os"
15+
"sync/atomic"
16+
"time"
17+
)
18+
19+
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
20+
// GUID generation
21+
// Code inspired from mgo/bson ObjectId
22+
//___________________________________
23+
24+
var (
25+
// guidCounter is atomically incremented when generating a new GUID
26+
// using UniqueID() function. It's used as a counter part of an id.
27+
guidCounter = readRandomUint32()
28+
29+
// machineID stores machine id generated once and used in subsequent calls
30+
// to UniqueId function.
31+
machineID = readMachineID()
32+
33+
// processID is current Process Id
34+
processID = os.Getpid()
35+
)
36+
37+
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
38+
// Global methods
39+
//___________________________________
40+
41+
// NewGUID method returns a new Globally Unique identifier (GUID).
42+
//
43+
// The 12-byte `UniqueId` consists of-
44+
// - 4-byte value representing the seconds since the Unix epoch,
45+
// - 3-byte machine identifier,
46+
// - 2-byte process id, and
47+
// - 3-byte counter, starting with a random value.
48+
//
49+
// NewGUID generation using Mongo Object ID algorithm to generate globally
50+
// unique ids - https://docs.mongodb.com/manual/reference/method/ObjectId/
51+
func NewGUID() string {
52+
var b [12]byte
53+
// Timestamp, 4 bytes, big endian
54+
binary.BigEndian.PutUint32(b[:], uint32(time.Now().Unix()))
55+
56+
// Machine, first 3 bytes of md5(hostname)
57+
b[4], b[5], b[6] = machineID[0], machineID[1], machineID[2]
58+
59+
// Pid, 2 bytes, specs don't specify endianness, but we use big endian.
60+
b[7], b[8] = byte(processID>>8), byte(processID)
61+
62+
// Increment, 3 bytes, big endian
63+
i := atomic.AddUint32(&guidCounter, 1)
64+
b[9], b[10], b[11] = byte(i>>16), byte(i>>8), byte(i)
65+
66+
return hex.EncodeToString(b[:])
67+
}
68+
69+
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
70+
// Unexported methods
71+
//___________________________________
72+
73+
// readRandomUint32 returns a random guidCounter.
74+
func readRandomUint32() uint32 {
75+
var b [4]byte
76+
if _, err := io.ReadFull(rand.Reader, b[:]); err == nil {
77+
return (uint32(b[0]) << 0) | (uint32(b[1]) << 8) | (uint32(b[2]) << 16) | (uint32(b[3]) << 24)
78+
}
79+
80+
panic(errors.New("ess - guid: unable to generate random object id"))
81+
}
82+
83+
// readMachineID generates and returns a machine id.
84+
// If this function fails to get the hostname it will cause a runtime error.
85+
func readMachineID() []byte {
86+
var sum [3]byte
87+
id := sum[:]
88+
89+
if hostname, err := os.Hostname(); err == nil {
90+
hw := md5.New()
91+
_, _ = hw.Write([]byte(hostname))
92+
copy(id, hw.Sum(nil))
93+
return id
94+
}
95+
96+
if _, err := io.ReadFull(rand.Reader, id); err == nil {
97+
return id
98+
}
99+
100+
// return nil, errors.New("guid: unable to get hostname and random bytes")
101+
panic(errors.New("ess - guid: unable to get hostname and random bytes"))
102+
}

guid_test.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// Copyright (c) Jeevanandam M. (https://github.com/jeevatkm)
2+
// go-aah/essentials source code and usage is governed by a MIT style
3+
// license that can be found in the LICENSE file.
4+
5+
package ess
6+
7+
import (
8+
"encoding/binary"
9+
"encoding/hex"
10+
"testing"
11+
"time"
12+
13+
"aahframework.org/test.v0/assert"
14+
)
15+
16+
func TestGUIDNew(t *testing.T) {
17+
// Generate 10 ids
18+
ids := make([]string, 10)
19+
for i := 0; i < 10; i++ {
20+
ids[i] = NewGUID()
21+
}
22+
for i := 1; i < 10; i++ {
23+
prevID := ids[i-1]
24+
id := ids[i]
25+
// Test for uniqueness among all other 9 generated ids
26+
for j, tid := range ids {
27+
if j != i {
28+
assert.NotEqualf(t, id, tid, "Generated ID is not unique")
29+
}
30+
}
31+
32+
// Check that timestamp was incremented and is within 30 seconds of the previous one
33+
secs := getTime(id).Sub(getTime(prevID)).Seconds()
34+
assert.Equalf(t, (secs >= 0 && secs <= 30), true, "wrong timestamp in generated Id")
35+
36+
// Check that machine ids are the same
37+
assert.Equal(t, getMachine(id), getMachine(prevID))
38+
39+
// Check that pids are the same
40+
assert.Equal(t, getPid(id), getPid(prevID))
41+
42+
// Test for proper increment
43+
delta := int(getCounter(id) - getCounter(prevID))
44+
assert.Equalf(t, delta, 1, "wrong increment in generated Id")
45+
}
46+
}
47+
48+
type guidParts struct {
49+
id string
50+
timestamp int64
51+
machine []byte
52+
pid uint16
53+
counter int32
54+
}
55+
56+
var uniqueIds = []guidParts{
57+
guidParts{
58+
"4d88e15b60f486e428412dc9",
59+
1300816219,
60+
[]byte{0x60, 0xf4, 0x86},
61+
0xe428,
62+
4271561,
63+
},
64+
guidParts{
65+
"000000000000000000000000",
66+
0,
67+
[]byte{0x00, 0x00, 0x00},
68+
0x0000,
69+
0,
70+
},
71+
guidParts{
72+
"00000000aabbccddee000001",
73+
0,
74+
[]byte{0xaa, 0xbb, 0xcc},
75+
0xddee,
76+
1,
77+
},
78+
}
79+
80+
func TestGUIDPartsExtraction(t *testing.T) {
81+
for i, v := range uniqueIds {
82+
assert.Equalf(t, getTime(v.id), time.Unix(v.timestamp, 0), "#%d timestamp", i)
83+
assert.Equalf(t, getMachine(v.id), v.machine, "#%d machine", i)
84+
assert.Equalf(t, getPid(v.id), v.pid, "#%d pid", i)
85+
assert.Equalf(t, getCounter(v.id), v.counter, "#%d counter", i)
86+
}
87+
}
88+
89+
func BenchmarkNewGUID(b *testing.B) {
90+
b.RunParallel(func(pb *testing.PB) {
91+
for pb.Next() {
92+
_ = NewGUID()
93+
}
94+
})
95+
}
96+
97+
func getMachine(id string) []byte {
98+
return byteSlice(id, 4, 7)
99+
}
100+
101+
func getTime(id string) time.Time {
102+
secs := int64(binary.BigEndian.Uint32(byteSlice(id, 0, 4)))
103+
return time.Unix(secs, 0)
104+
}
105+
106+
func getPid(id string) uint16 {
107+
return binary.BigEndian.Uint16(byteSlice(id, 7, 9))
108+
}
109+
110+
func getCounter(id string) int32 {
111+
b := byteSlice(id, 9, 12)
112+
// Counter is stored as big-endian 3-byte value
113+
return int32(uint32(b[0])<<16 | uint32(b[1])<<8 | uint32(b[2]))
114+
}
115+
116+
func byteSlice(id string, s, e int) []byte {
117+
if len(id) == 24 {
118+
b, _ := hex.DecodeString(id)
119+
return b[s:e]
120+
}
121+
return nil
122+
}

0 commit comments

Comments
 (0)