|
| 1 | +// Package xid is a globally unique id generator suited for web scale |
| 2 | +// |
| 3 | +// Xid is using Mongo Object ID algorithm to generate globally unique ids: |
| 4 | +// https://docs.mongodb.org/manual/reference/object-id/ |
| 5 | +// |
| 6 | +// - 4-byte value representing the seconds since the Unix epoch, |
| 7 | +// - 3-byte machine identifier, |
| 8 | +// - 2-byte process id, and |
| 9 | +// - 3-byte counter, starting with a random value. |
| 10 | +// |
| 11 | +// The binary representation of the id is compatible with Mongo 12 bytes Object IDs. |
| 12 | +// The string representation is using URL safe base64 for better space efficiency when |
| 13 | +// stored in that form (16 bytes). |
| 14 | +// |
| 15 | +// UUID is 16 bytes (128 bits), snowflake is 8 bytes (64 bits), xid stands in between |
| 16 | +// with 12 bytes with a more compact string representation ready for the web and no |
| 17 | +// required configuration or central generation server. |
| 18 | +// |
| 19 | +// Features: |
| 20 | +// |
| 21 | +// - Size: 12 bytes (96 bits), smaller than UUID, larger than snowflake |
| 22 | +// - Base64 URL safe encoded by default (16 bytes storage when transported as printable string) |
| 23 | +// - Non configured, you don't need set a unique machine and/or data center id |
| 24 | +// - K-ordered |
| 25 | +// - Embedded time with 1 second precision |
| 26 | +// - Unicity guaranted for 16,777,216 (24 bits) unique ids per second and per host/process |
| 27 | +// |
| 28 | +// Best used with xlog's RequestIDHandler (https://godoc.org/github.com/rs/xlog#RequestIDHandler). |
| 29 | +// |
| 30 | +// References: |
| 31 | +// |
| 32 | +// - http://www.slideshare.net/davegardnerisme/unique-id-generation-in-distributed-systems |
| 33 | +// - https://en.wikipedia.org/wiki/Universally_unique_identifier |
| 34 | +// - https://blog.twitter.com/2010/announcing-snowflake |
| 35 | +package xid |
| 36 | + |
| 37 | +import ( |
| 38 | + "crypto/md5" |
| 39 | + "crypto/rand" |
| 40 | + "encoding/base64" |
| 41 | + "encoding/binary" |
| 42 | + "errors" |
| 43 | + "fmt" |
| 44 | + "io" |
| 45 | + "os" |
| 46 | + "sync/atomic" |
| 47 | + "time" |
| 48 | +) |
| 49 | + |
| 50 | +// Code inspired from mgo/bson ObjectId |
| 51 | + |
| 52 | +// ID represents a unique request id |
| 53 | +type ID [rawLen]byte |
| 54 | + |
| 55 | +const ( |
| 56 | + encodedLen = 16 |
| 57 | + rawLen = 12 |
| 58 | +) |
| 59 | + |
| 60 | +// ErrInvalidID is returned when trying to unmarshal an invalid ID |
| 61 | +var ErrInvalidID = errors.New("invalid ID") |
| 62 | + |
| 63 | +// objectIDCounter is atomically incremented when generating a new ObjectId |
| 64 | +// using NewObjectId() function. It's used as a counter part of an id. |
| 65 | +var objectIDCounter uint32 |
| 66 | + |
| 67 | +// machineId stores machine id generated once and used in subsequent calls |
| 68 | +// to NewObjectId function. |
| 69 | +var machineID = readMachineID() |
| 70 | + |
| 71 | +// readMachineId generates machine id and puts it into the machineId global |
| 72 | +// variable. If this function fails to get the hostname, it will cause |
| 73 | +// a runtime error. |
| 74 | +func readMachineID() []byte { |
| 75 | + id := make([]byte, 3) |
| 76 | + hostname, err1 := os.Hostname() |
| 77 | + if err1 != nil { |
| 78 | + // Fallback to rand number if machine id can't be gathered |
| 79 | + _, err2 := io.ReadFull(rand.Reader, id) |
| 80 | + if err2 != nil { |
| 81 | + panic(fmt.Errorf("cannot get hostname: %v; %v", err1, err2)) |
| 82 | + } |
| 83 | + return id |
| 84 | + } |
| 85 | + hw := md5.New() |
| 86 | + hw.Write([]byte(hostname)) |
| 87 | + copy(id, hw.Sum(nil)) |
| 88 | + return id |
| 89 | +} |
| 90 | + |
| 91 | +// New generates a globaly unique ID |
| 92 | +func New() ID { |
| 93 | + var id ID |
| 94 | + // Timestamp, 4 bytes, big endian |
| 95 | + binary.BigEndian.PutUint32(id[:], uint32(time.Now().Unix())) |
| 96 | + // Machine, first 3 bytes of md5(hostname) |
| 97 | + id[4] = machineID[0] |
| 98 | + id[5] = machineID[1] |
| 99 | + id[6] = machineID[2] |
| 100 | + // Pid, 2 bytes, specs don't specify endianness, but we use big endian. |
| 101 | + pid := os.Getpid() |
| 102 | + id[7] = byte(pid >> 8) |
| 103 | + id[8] = byte(pid) |
| 104 | + // Increment, 3 bytes, big endian |
| 105 | + i := atomic.AddUint32(&objectIDCounter, 1) |
| 106 | + id[9] = byte(i >> 16) |
| 107 | + id[10] = byte(i >> 8) |
| 108 | + id[11] = byte(i) |
| 109 | + return id |
| 110 | +} |
| 111 | + |
| 112 | +// String returns a base64 URL safe representation of the id |
| 113 | +func (id ID) String() string { |
| 114 | + return base64.URLEncoding.EncodeToString(id[:]) |
| 115 | +} |
| 116 | + |
| 117 | +// MarshalText implements encoding/text TextMarshaler interface |
| 118 | +func (id ID) MarshalText() (text []byte, err error) { |
| 119 | + text = make([]byte, encodedLen) |
| 120 | + base64.URLEncoding.Encode(text, id[:]) |
| 121 | + return |
| 122 | +} |
| 123 | + |
| 124 | +// UnmarshalText implements encoding/text TextUnmarshaler interface |
| 125 | +func (id *ID) UnmarshalText(text []byte) error { |
| 126 | + if len(text) != encodedLen { |
| 127 | + return ErrInvalidID |
| 128 | + } |
| 129 | + b := make([]byte, rawLen) |
| 130 | + _, err := base64.URLEncoding.Decode(b, text) |
| 131 | + for i, c := range b { |
| 132 | + id[i] = c |
| 133 | + } |
| 134 | + return err |
| 135 | +} |
| 136 | + |
| 137 | +// Time returns the timestamp part of the id. |
| 138 | +// It's a runtime error to call this method with an invalid id. |
| 139 | +func (id ID) Time() time.Time { |
| 140 | + // First 4 bytes of ObjectId is 32-bit big-endian seconds from epoch. |
| 141 | + secs := int64(binary.BigEndian.Uint32(id[0:4])) |
| 142 | + return time.Unix(secs, 0) |
| 143 | +} |
| 144 | + |
| 145 | +// Machine returns the 3-byte machine id part of the id. |
| 146 | +// It's a runtime error to call this method with an invalid id. |
| 147 | +func (id ID) Machine() []byte { |
| 148 | + return id[4:7] |
| 149 | +} |
| 150 | + |
| 151 | +// Pid returns the process id part of the id. |
| 152 | +// It's a runtime error to call this method with an invalid id. |
| 153 | +func (id ID) Pid() uint16 { |
| 154 | + return binary.BigEndian.Uint16(id[7:9]) |
| 155 | +} |
| 156 | + |
| 157 | +// Counter returns the incrementing value part of the id. |
| 158 | +// It's a runtime error to call this method with an invalid id. |
| 159 | +func (id ID) Counter() int32 { |
| 160 | + b := id[9:12] |
| 161 | + // Counter is stored as big-endian 3-byte value |
| 162 | + return int32(uint32(b[0])<<16 | uint32(b[1])<<8 | uint32(b[2])) |
| 163 | +} |
0 commit comments