Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix resource.UniqueId to be properly ordered over multiple runs #15280

Merged
merged 1 commit into from
Jun 15, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 17 additions & 16 deletions helper/resource/id.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
package resource

import (
"crypto/rand"
"fmt"
"math/big"
"strings"
"sync"
"time"
)

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.
// idCounter is a monotonic counter for generating ordered unique ids.
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
func UniqueId() string {
Expand All @@ -25,15 +21,20 @@ func UniqueId() string {
// 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
// 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 {
// 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()
defer idMutex.Unlock()
return fmt.Sprintf("%s%026x", prefix, idCounter.Add(idCounter, big.NewInt(1)))
}

func randomBytes(n int) []byte {
b := make([]byte, n)
rand.Read(b)
return b
idCounter++
return fmt.Sprintf("%s%s%08x", prefix, timestamp, idCounter)
}
33 changes: 29 additions & 4 deletions helper/resource/id_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,19 @@ import (
"regexp"
"strings"
"testing"
"time"
)

var allDigits = regexp.MustCompile(`^\d+$`)
var allHex = regexp.MustCompile(`^[a-f0-9]+$`)

func TestUniqueId(t *testing.T) {
split := func(rest string) (timestamp, increment string) {
return rest[:18], rest[18:]
}

const prefix = "terraform-"

iterations := 10000
ids := make(map[string]struct{})
var id, lastId string
Expand All @@ -19,18 +27,24 @@ func TestUniqueId(t *testing.T) {
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)
}

rest := strings.TrimPrefix(id, "terraform-")
rest := strings.TrimPrefix(id, prefix)

if len(rest) != 26 {
t.Fatalf("Post-prefix part has wrong length! %s", rest)
}

if !allHex.MatchString(rest) {
t.Fatalf("Random part not all hex! %s", rest)
timestamp, increment := split(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 {
Expand All @@ -40,4 +54,15 @@ func TestUniqueId(t *testing.T) {
ids[id] = struct{}{}
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)
}
}