Fix resource.UniqueId to be properly ordered over multiple runs

The timestamp prefix added in #8249 was removed in #10152 to ensure that
returned IDs really are properly ordered.  However, this meant that IDs were no
longer ordered over multiple invocations of terraform, which was the main
motivation for adding the timestamp in the first place.  This commit does a
hybrid: timestamp-plus-incrementing-counter instead of just incrementing counter
or timestamp-plus-random.
This commit is contained in:
David Glasser 2017-06-13 13:58:31 -07:00
parent 06d4247a75
commit 0a1f9156dc
2 changed files with 46 additions and 20 deletions

View File

@ -1,21 +1,17 @@
package resource package resource
import ( import (
"crypto/rand"
"fmt" "fmt"
"math/big" "strings"
"sync" "sync"
"time"
) )
const UniqueIdPrefix = `terraform-` const UniqueIdPrefix = `terraform-`
// idCounter is a randomly seeded monotonic counter for generating ordered // idCounter is a monotonic counter for generating ordered unique ids.
// 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 idMutex sync.Mutex
var idCounter = big.NewInt(0).SetBytes(randomBytes(12)) var idCounter uint32
// 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 {
@ -25,15 +21,20 @@ 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 an incrementing 26 digit value (to match // After the prefix, the ID consists of an incrementing 26 digit value (to match
// previous timestamp output). // previous timestamp output). After the prefix, the ID consists of a timestamp
// and an incrementing 8 hex digit value The timestamp means that multiple IDs
// created with the same prefix will sort in the order of their creation, even
// across multiple terraform executions, as long as the clock is not turned back
// between calls, and as long as any given terraform execution generates fewer
// than 4 billion IDs.
func PrefixedUniqueId(prefix string) string { func PrefixedUniqueId(prefix string) string {
// Be precise to 4 digits of fractional seconds, but remove the dot before the
// fractional seconds.
timestamp := strings.Replace(
time.Now().UTC().Format("20060102150405.0000"), ".", "", 1)
idMutex.Lock() idMutex.Lock()
defer idMutex.Unlock() defer idMutex.Unlock()
return fmt.Sprintf("%s%026x", prefix, idCounter.Add(idCounter, big.NewInt(1))) idCounter++
} return fmt.Sprintf("%s%s%08x", prefix, timestamp, idCounter)
func randomBytes(n int) []byte {
b := make([]byte, n)
rand.Read(b)
return b
} }

View File

@ -4,11 +4,19 @@ import (
"regexp" "regexp"
"strings" "strings"
"testing" "testing"
"time"
) )
var allDigits = regexp.MustCompile(`^\d+$`)
var allHex = regexp.MustCompile(`^[a-f0-9]+$`) var allHex = regexp.MustCompile(`^[a-f0-9]+$`)
func TestUniqueId(t *testing.T) { func TestUniqueId(t *testing.T) {
split := func(rest string) (timestamp, increment string) {
return rest[:18], rest[18:]
}
const prefix = "terraform-"
iterations := 10000 iterations := 10000
ids := make(map[string]struct{}) ids := make(map[string]struct{})
var id, lastId string var id, lastId string
@ -19,18 +27,24 @@ func TestUniqueId(t *testing.T) {
t.Fatalf("Got duplicated id! %s", id) t.Fatalf("Got duplicated id! %s", id)
} }
if !strings.HasPrefix(id, "terraform-") { if !strings.HasPrefix(id, prefix) {
t.Fatalf("Unique ID didn't have terraform- prefix! %s", id) t.Fatalf("Unique ID didn't have terraform- prefix! %s", id)
} }
rest := strings.TrimPrefix(id, "terraform-") rest := strings.TrimPrefix(id, prefix)
if len(rest) != 26 { if len(rest) != 26 {
t.Fatalf("Post-prefix part has wrong length! %s", rest) t.Fatalf("Post-prefix part has wrong length! %s", rest)
} }
if !allHex.MatchString(rest) { timestamp, increment := split(rest)
t.Fatalf("Random part not all hex! %s", rest)
if !allDigits.MatchString(timestamp) {
t.Fatalf("Timestamp not all digits! %s", timestamp)
}
if !allHex.MatchString(increment) {
t.Fatalf("Increment part not all hex! %s", increment)
} }
if lastId != "" && lastId >= id { if lastId != "" && lastId >= id {
@ -40,4 +54,15 @@ func TestUniqueId(t *testing.T) {
ids[id] = struct{}{} ids[id] = struct{}{}
lastId = id lastId = id
} }
id1 := UniqueId()
time.Sleep(time.Millisecond)
id2 := UniqueId()
timestamp1, _ := split(strings.TrimPrefix(id1, prefix))
timestamp2, _ := split(strings.TrimPrefix(id2, prefix))
if timestamp1 == timestamp2 {
t.Fatalf("Timestamp part should update at least once a millisecond %s %s",
id1, id2)
}
} }