diff --git a/README.md b/README.md index 9d837e6..3fddeaa 100644 --- a/README.md +++ b/README.md @@ -29,13 +29,13 @@ generator servers. xid stands in between with 12 bytes (96 bits) and a more comp URL-safe string representation (20 chars). No configuration or central generator server is required so it can be used directly in server's code. -| Name | Binary Size | String Size | Features -|-------------|-------------|----------------|---------------- -| [UUID] | 16 bytes | 36 chars | configuration free, not sortable -| [shortuuid] | 16 bytes | 22 chars | configuration free, not sortable -| [Snowflake] | 8 bytes | up to 20 chars | needs machine/DC configuration, needs central server, sortable -| [MongoID] | 12 bytes | 24 chars | configuration free, sortable -| xid | 12 bytes | 20 chars | configuration free, sortable +| Name | Binary Size | String Size | Features | +| ----------- | ----------- | -------------- | -------------------------------------------------------------- | +| [UUID] | 16 bytes | 36 chars | configuration free, not sortable | +| [shortuuid] | 16 bytes | 22 chars | configuration free, not sortable | +| [Snowflake] | 8 bytes | up to 20 chars | needs machine/DC configuration, needs central server, sortable | +| [MongoID] | 12 bytes | 24 chars | configuration free, sortable | +| xid | 12 bytes | 20 chars | configuration free, sortable | [UUID]: https://en.wikipedia.org/wiki/Universally_unique_identifier [shortuuid]: https://github.com/stochastic-technologies/shortuuid @@ -46,7 +46,7 @@ Features: - Size: 12 bytes (96 bits), smaller than UUID, larger than snowflake - Base32 hex encoded by default (20 chars when transported as printable string, still sortable) -- Non configured, you don't need set a unique machine and/or data center id +- Non configured, you don't need set a unique machine and/or data center id (configurable if needed) - K-ordered - Embedded time with 1 second precision - Unicity guaranteed for 16,777,216 (24 bits) unique ids per second and per host/process @@ -58,6 +58,7 @@ Best used with [zerolog](https://github.com/rs/zerolog)'s Notes: - Xid is dependent on the system time, a monotonic counter and so is not cryptographically secure. If unpredictability of IDs is important, you should not use Xids. It is worth noting that most other UUID-like implementations are also not cryptographically secure. You should use libraries that rely on cryptographically secure sources (like /dev/urandom on unix, crypto/rand in golang), if you want a truly random ID generator. +- MachineID can be set by the environmental variable `XID_MACHINE_ID` to allow fine tune control over the generation. References: diff --git a/id.go b/id.go index 46f4bb7..a659282 100644 --- a/id.go +++ b/id.go @@ -51,6 +51,7 @@ import ( "hash/crc32" "os" "sort" + "strconv" "sync/atomic" "time" ) @@ -107,6 +108,11 @@ func init() { // value, or else the machine's hostname, or else a randomly-generated number. // It panics if all of these methods fail. func readMachineID() []byte { + // Allow env overrides for the machine id + if id := readMachineIDFromEnv(); len(id) == 3 { + return id + } + id := make([]byte, 3) hid, err := readPlatformMachineID() if err != nil || len(hid) == 0 { @@ -125,6 +131,25 @@ func readMachineID() []byte { return id } +func readMachineIDFromEnv() []byte { + envMachineID := os.Getenv("XID_MACHINE_ID") + if envMachineID == "" { + return nil + } + + num, err := strconv.Atoi(envMachineID) + if err != nil { + panic("XID_MACHINE_ID value is set to not a number") + } + + if num < 0 || num > 0xFFFFFF { + panic("XID_MACHINE_ID out of range for 3 bytes") + } + + // Encode the number into big endian. + return []byte{byte(num >> 16), byte(num >> 8), byte(num)} +} + // randInt generates a random uint32 func randInt() uint32 { b := make([]byte, 3) diff --git a/id_test.go b/id_test.go index 06ccdfc..d18fe51 100644 --- a/id_test.go +++ b/id_test.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "math/rand" + "os" "reflect" "testing" "testing/quick" @@ -329,6 +330,61 @@ func TestFromStringQuickInvalidChars(t *testing.T) { } } +func TestMachineFromEnv(t *testing.T) { + for name, test := range map[string]struct { + value string + expect int + shouldPanic string + }{ + "basic": { + value: "123", + expect: 123, + }, + "basic large": { + value: "16777214", + expect: 16777214, + }, + "bad input nan": { + value: "abcd", + shouldPanic: `XID_MACHINE_ID value is set to not a number`, + }, + "bad input negative": { + value: "-1", + shouldPanic: `XID_MACHINE_ID out of range for 3 bytes`, + }, + "bad input large": { + value: "16777216", + shouldPanic: `XID_MACHINE_ID out of range for 3 bytes`, + }, + } { + t.Run(name, func(t *testing.T) { + defer func() { + s := recover() + if test.shouldPanic != "" { + ps, _ := s.(string) + if test.shouldPanic != ps { + t.Fatalf(`expected panic "%s" but got "%s"`, test.shouldPanic, ps) + } + } else if s != nil { + t.Fatalf(`unexpected panic: "%v"`, s) + } + }() + + if err := os.Setenv("XID_MACHINE_ID", test.value); err != nil { + t.Fatal("failed to set env for test: " + err.Error()) + } + b := readMachineIDFromEnv() + if len(b) != 3 && test.expect != 0 { + t.Fatalf("got no response from readMachineIDFromEnv, expected %d", test.expect) + } + got := int(b[0])<<16 | int(b[1])<<8 | int(b[2]) + if got != test.expect { + t.Fatalf("expected machine id %d from env but got %d", test.expect, got) + } + }) + } +} + // func BenchmarkUUIDv1(b *testing.B) { // b.RunParallel(func(pb *testing.PB) { // for pb.Next() {