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:
parent
06d4247a75
commit
0a1f9156dc
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue