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:
parent
4e2865a719
commit
e28e11d44c
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue