Skip to content

Commit

Permalink
Merge pull request #2 from SigNoz/feat/epoch_nano_27_byte
Browse files Browse the repository at this point in the history
feat: support for nanosecond epoch with 12 byte payload
  • Loading branch information
nityanandagohain authored Dec 16, 2024
2 parents d337249 + ac4292b commit 8d205ef
Show file tree
Hide file tree
Showing 8 changed files with 534 additions and 50 deletions.
4 changes: 2 additions & 2 deletions cmd/ksuid/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"text/template"
"time"

"github.com/segmentio/ksuid"
"github.com/signoz/ksuid"
)

var (
Expand Down Expand Up @@ -128,7 +128,7 @@ func printTemplate(id ksuid.KSUID) {
String string
Raw string
Time time.Time
Timestamp uint32
Timestamp uint64
Payload string
}{
String: id.String(),
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/segmentio/ksuid
module github.com/signoz/ksuid

go 1.12
41 changes: 21 additions & 20 deletions ksuid.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ import (
const (
// KSUID's epoch starts more recently so that the 32-bit number space gives a
// significantly higher useful lifetime of around 136 years from March 2017.
// This number (14e8) was picked to be easy to remember.
epochStamp int64 = 1400000000
// This number (14e8 in seconds) was picked to be easy to remember.
epochStamp int64 = 1400000000000000000

// Timestamp is a uint32
timestampLengthInBytes = 4
timestampLengthInBytes = 8

// Payload is 16-bytes
payloadLengthInBytes = 16
payloadLengthInBytes = 12

// KSUIDs are 20 bytes when binary encoded
byteLength = timestampLengthInBytes + payloadLengthInBytes
Expand All @@ -38,8 +38,9 @@ const (
)

// KSUIDs are 20 bytes:
// 00-03 byte: uint32 BE UTC timestamp with custom epoch
// 04-19 byte: random "payload"
//
// 00-07 byte: uint64 BE UTC timestamp with nanosecond epoch
// 08-19 byte: random "payload"
type KSUID [byteLength]byte

var (
Expand Down Expand Up @@ -71,8 +72,8 @@ func (i KSUID) Time() time.Time {

// The timestamp portion of the ID as a bare integer which is uncorrected
// for KSUID's special epoch.
func (i KSUID) Timestamp() uint32 {
return binary.BigEndian.Uint32(i[:timestampLengthInBytes])
func (i KSUID) Timestamp() uint64 {
return binary.BigEndian.Uint64(i[:timestampLengthInBytes])
}

// The 16-byte random payload without the timestamp
Expand Down Expand Up @@ -201,12 +202,12 @@ func ParseOrNil(s string) KSUID {
return ksuid
}

func timeToCorrectedUTCTimestamp(t time.Time) uint32 {
return uint32(t.Unix() - epochStamp)
func timeToCorrectedUTCTimestamp(t time.Time) uint64 {
return uint64(t.UnixNano() - epochStamp)
}

func correctedUTCTimestampToTime(ts uint32) time.Time {
return time.Unix(int64(ts)+epochStamp, 0)
func correctedUTCTimestampToTime(ts uint64) time.Time {
return time.Unix(0, int64(ts)+epochStamp)
}

// Generates a new KSUID. In the strange case that random bytes
Expand Down Expand Up @@ -241,7 +242,7 @@ func NewRandomWithTime(t time.Time) (ksuid KSUID, err error) {
}

ts := timeToCorrectedUTCTimestamp(t)
binary.BigEndian.PutUint32(ksuid[:timestampLengthInBytes], ts)
binary.BigEndian.PutUint64(ksuid[:timestampLengthInBytes], ts)
return
}

Expand All @@ -254,7 +255,7 @@ func FromParts(t time.Time, payload []byte) (KSUID, error) {
var ksuid KSUID

ts := timeToCorrectedUTCTimestamp(t)
binary.BigEndian.PutUint32(ksuid[:timestampLengthInBytes], ts)
binary.BigEndian.PutUint64(ksuid[:timestampLengthInBytes], ts)

copy(ksuid[timestampLengthInBytes:], payload)

Expand Down Expand Up @@ -353,11 +354,11 @@ func quickSort(a []KSUID, lo int, hi int) {

// Next returns the next KSUID after id.
func (id KSUID) Next() KSUID {
zero := makeUint128(0, 0)
zero := makeUint96(0, 0)

t := id.Timestamp()
u := uint128Payload(id)
v := add128(u, makeUint128(0, 1))
u := uint96Payload(id)
v := add96(u, makeUint96(0, 1))

if v == zero { // overflow
t++
Expand All @@ -368,11 +369,11 @@ func (id KSUID) Next() KSUID {

// Prev returns the previoud KSUID before id.
func (id KSUID) Prev() KSUID {
max := makeUint128(math.MaxUint64, math.MaxUint64)
max := makeUint96(math.MaxUint32, math.MaxUint64)

t := id.Timestamp()
u := uint128Payload(id)
v := sub128(u, makeUint128(0, 1))
u := uint96Payload(id)
v := sub96(u, makeUint96(0, 1))

if v == max { // overflow
t--
Expand Down
76 changes: 74 additions & 2 deletions ksuid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"sort"
"strings"
"sync"
"testing"
"time"
)
Expand Down Expand Up @@ -307,9 +308,11 @@ func TestPrevNext(t *testing.T) {
func TestGetTimestamp(t *testing.T) {
nowTime := time.Now()
x, _ := NewRandomWithTime(nowTime)
y, _ := NewRandomWithTime(time.Now())
fmt.Println(x, y)
xTime := int64(x.Timestamp())
unix := nowTime.Unix()
if xTime != unix - epochStamp {
unix := nowTime.UnixNano()
if xTime != unix-epochStamp {
t.Fatal(xTime, "!=", unix)
}
}
Expand Down Expand Up @@ -387,3 +390,72 @@ func BenchmarkNew(b *testing.B) {
}
})
}

// TestTimeMonotonicity verifies timestamps are monotonically increasing
func TestTimeMonotonicity(t *testing.T) {
count := 10000
ids := make([]KSUID, count)

for i := 0; i < count; i++ {
ids[i] = New()
}

// Verify timestamps are monotonic
for i := 1; i < count; i++ {
if ids[i].Time().Before(ids[i-1].Time()) {
t.Errorf("Time monotonicity violated at index %d", i)
}
}
}

// TestConcurrentUniqueness verifies no collisions in concurrent generation
func TestConcurrentUniqueness(t *testing.T) {
count := 100000
ids := make([]KSUID, count)
var wg sync.WaitGroup

for i := 0; i < count; i++ {
wg.Add(1)
go func(index int) {
defer wg.Done()
ids[index] = New()
}(i)
}
wg.Wait()

// Check for duplicates
seen := make(map[KSUID]bool)
for _, id := range ids {
if seen[id] {
t.Error("Duplicate KSUID found")
}
seen[id] = true
}
}

// BenchmarkCollisionProbability generates many IDs in same nanosecond
func BenchmarkCollisionProbability(b *testing.B) {
b.StopTimer()
ids := make([]KSUID, b.N)

// Force same timestamp for all IDs
timestamp := time.Now()
b.StartTimer()

for i := 0; i < b.N; i++ {
ids[i], _ = NewRandomWithTime(timestamp)
}

// Check for collisions
seen := make(map[KSUID]bool)
collisions := 0

for _, id := range ids {
if seen[id] {
collisions++
}
seen[id] = true
}

b.ReportMetric(float64(collisions), "collisions")
}
5 changes: 3 additions & 2 deletions rand.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ type randSourceReader struct {

func (r *randSourceReader) Read(b []byte) (int, error) {
// optimized for generating 16 bytes payloads
binary.LittleEndian.PutUint64(b[:8], r.source.Uint64())
binary.LittleEndian.PutUint64(b[8:], r.source.Uint64())
val := r.source.Uint64()
binary.LittleEndian.PutUint32(b[:4], uint32(val)) // Use lower 32 bits
binary.LittleEndian.PutUint64(b[4:], r.source.Uint64()) // Generate new 64 bits
return 16, nil
}
Loading

0 comments on commit 8d205ef

Please sign in to comment.