Fix resource.UniqueId to be properly ordered

UniqueId attempted to provide an ordered unique id by using a nanosecond
timestamp, but doesn't take into account that time is not monotonic
increasing. This provides an implementation that will always be
increasing.
This commit is contained in:
James Bardin 2016-11-15 21:09:32 -05:00
parent 4e2865a719
commit e28e11d44c
2 changed files with 18 additions and 40 deletions

View File

@ -2,14 +2,21 @@ package resource
import ( import (
"crypto/rand" "crypto/rand"
"encoding/base32"
"fmt" "fmt"
"strings" "math/big"
"time" "sync"
) )
const UniqueIdPrefix = `terraform-` const UniqueIdPrefix = `terraform-`
// idCounter is a randomly seeded monotonic counter for generating ordered
// unique ids. It uses a big.Int so we can easily increment a long numeric
// string. The max possible hex value here with 12 random bytes is
// "01000000000000000000000000", so there's no chance of rollover during
// operation.
var idMutex sync.Mutex
var idCounter = big.NewInt(0).SetBytes(randomBytes(12))
// Helper for a resource to generate a unique identifier w/ default prefix // Helper for a resource to generate a unique identifier w/ default prefix
func UniqueId() string { func UniqueId() string {
return PrefixedUniqueId(UniqueIdPrefix) return PrefixedUniqueId(UniqueIdPrefix)
@ -17,33 +24,12 @@ func UniqueId() string {
// Helper for a resource to generate a unique identifier w/ given prefix // Helper for a resource to generate a unique identifier w/ given prefix
// //
// After the prefix, the ID consists of a timestamp and 12 random base32 // After the prefix, the ID consists of an incrementing 26 digit value (to match
// characters. The timestamp means that multiple IDs created with the same // previous timestamp output).
// prefix will sort in the order of their creation.
func PrefixedUniqueId(prefix string) string { func PrefixedUniqueId(prefix string) string {
// Be precise to the level nanoseconds, but remove the dot before the idMutex.Lock()
// nanosecond. We assume that the randomCharacters call takes at least a defer idMutex.Unlock()
// nanosecond, so that multiple calls to this function from the same goroutine return fmt.Sprintf("%s%026x", prefix, idCounter.Add(idCounter, big.NewInt(1)))
// will have distinct ordered timestamps.
timestamp := strings.Replace(
time.Now().UTC().Format("20060102150405.000000000"),
".",
"", 1)
// This uses 3 characters, so that the length of the unique ID is the same as
// it was before we added the timestamp prefix, which happened to be 23
// characters.
return fmt.Sprintf("%s%s%s", prefix, timestamp, randomCharacters(3))
}
func randomCharacters(n int) string {
// Base32 has 5 bits of information per character.
b := randomBytes(n * 8 / 5)
chars := strings.ToLower(
strings.Replace(
base32.StdEncoding.EncodeToString(b),
"=", "", -1))
// Trim extra characters.
return chars[:n]
} }
func randomBytes(n int) []byte { func randomBytes(n int) []byte {

View File

@ -6,8 +6,7 @@ import (
"testing" "testing"
) )
var allDigits = regexp.MustCompile(`^\d+$`) var allHex = regexp.MustCompile(`^[a-f0-9]+$`)
var allBase32 = regexp.MustCompile(`^[a-z234567]+$`)
func TestUniqueId(t *testing.T) { func TestUniqueId(t *testing.T) {
iterations := 10000 iterations := 10000
@ -30,15 +29,8 @@ func TestUniqueId(t *testing.T) {
t.Fatalf("Post-prefix part has wrong length! %s", rest) t.Fatalf("Post-prefix part has wrong length! %s", rest)
} }
timestamp := rest[:23] if !allHex.MatchString(rest) {
random := rest[23:] t.Fatalf("Random part not all hex! %s", rest)
if !allDigits.MatchString(timestamp) {
t.Fatalf("Timestamp not all digits! %s", timestamp)
}
if !allBase32.MatchString(random) {
t.Fatalf("Random part not all base32! %s", random)
} }
if lastId != "" && lastId >= id { if lastId != "" && lastId >= id {