Skip to content

Commit 6cd2e7e

Browse files
Nyvilkkrypt0nn
andauthored
feat: Handle dlock drift cases (#9)
Co-authored-by: Krypton <root@krypton.ninja>
1 parent c21d1b9 commit 6cd2e7e

File tree

6 files changed

+73
-49
lines changed

6 files changed

+73
-49
lines changed

.github/FUNDING.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
github: [kkrypt0nn]
2+
custom: ["https://buymeacoffee.com/kkrypt0nn"]

.github/workflows/ci.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: Spaceflake CI (Lint & Test)
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
workflow_dispatch:
9+
10+
jobs:
11+
lint-test:
12+
name: Lint & Test
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Checkout source code
16+
uses: actions/checkout@v4
17+
- name: Set up Go
18+
uses: actions/setup-go@v5
19+
with:
20+
go-version: 1.24
21+
- name: Lint
22+
uses: golangci/golangci-lint-action@v7
23+
with:
24+
version: latest
25+
- name: Test
26+
run: go test -v ./...
27+
build:
28+
name: Build for ${{ matrix.target.goos }}/${{ matrix.target.goarch }}
29+
runs-on: ubuntu-latest
30+
needs: [lint-test]
31+
strategy:
32+
matrix:
33+
target:
34+
- { goos: linux, goarch: amd64 }
35+
- { goos: linux, goarch: arm64 }
36+
- { goos: darwin, goarch: amd64 }
37+
- { goos: darwin, goarch: arm64 }
38+
- { goos: windows, goarch: amd64 }
39+
- { goos: windows, goarch: arm64 }
40+
- { goos: android, goarch: arm64 }
41+
steps:
42+
- name: Checkout source code
43+
uses: actions/checkout@v4
44+
- name: Set up Go
45+
uses: actions/setup-go@v5
46+
with:
47+
go-version: 1.24
48+
- name: Build
49+
run: GOOS=${{ matrix.target.goos }} GOARCH=${{ matrix.target.goarch }} go build -v ./...

.github/workflows/go-test.yml

Lines changed: 0 additions & 23 deletions
This file was deleted.

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module github.com/kkrypt0nn/spaceflake
22

3-
go 1.19
3+
go 1.24

spaceflake.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ const (
1818
MAX12BITS = 4095
1919
// MAX41BITS is the maximum value for a 41 bits number
2020
MAX41BITS = 2199023255551
21+
// CLOCK_DRIFT_TOLERANCE_MS is the tolerance for clock drift in milliseconds
22+
CLOCK_DRIFT_TOLERANCE_MS = 10
2123
)
2224

2325
// Spaceflake represents a Spaceflake
@@ -162,13 +164,15 @@ type Worker struct {
162164
Sequence uint64
163165
// ID is the worker ID that the Spaceflake generator will use for the next 5 bits
164166
ID uint64
165-
166-
increment uint64
167-
mutex *sync.Mutex
167+
// used to prevent clockdrift
168+
lastTimestamp uint64
169+
increment uint64
170+
mutex *sync.Mutex
168171
}
169172

170173
// GenerateSpaceflake generates a Spaceflake
171174
func (w *Worker) GenerateSpaceflake() (*Spaceflake, error) {
175+
172176
if w.Node == nil {
173177
return nil, fmt.Errorf("node is not set")
174178
}
@@ -197,6 +201,16 @@ func (w *Worker) GenerateSpaceflake() (*Spaceflake, error) {
197201
milliseconds := uint64(math.Floor(microTime() * 1000))
198202
milliseconds -= w.BaseEpoch
199203

204+
if delta := w.lastTimestamp - milliseconds; milliseconds < w.lastTimestamp {
205+
if delta >= CLOCK_DRIFT_TOLERANCE_MS {
206+
return nil, fmt.Errorf("clock moved backwards by %dms", delta)
207+
}
208+
time.Sleep(time.Duration(delta+1) * time.Millisecond)
209+
milliseconds = uint64(math.Floor(microTime()*100)) - w.BaseEpoch
210+
}
211+
212+
w.lastTimestamp = milliseconds
213+
200214
base := stringPadLeft(decimalBinary(milliseconds), 41, "0")
201215
nodeID := stringPadLeft(decimalBinary(w.Node.ID), 5, "0")
202216
workerID := stringPadLeft(decimalBinary(w.ID), 5, "0")
@@ -250,6 +264,10 @@ func (w *Worker) GenerateSpaceflakeAt(at time.Time) (*Spaceflake, error) {
250264
milliseconds := uint64(math.Floor(microTime * 1000))
251265
milliseconds -= w.BaseEpoch
252266

267+
if milliseconds < w.lastTimestamp {
268+
return nil, fmt.Errorf("cannot generate Spaceflake: Detected clock drift. The time you want to generate the Spaceflake at is before the last generated Spaceflake time")
269+
}
270+
253271
base := stringPadLeft(decimalBinary(milliseconds), 41, "0")
254272
nodeID := stringPadLeft(decimalBinary(w.Node.ID), 5, "0")
255273
workerID := stringPadLeft(decimalBinary(w.ID), 5, "0")

spaceflake_test.go

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -160,28 +160,6 @@ func TestSpaceflakeWorkerGoroutineUnique(t *testing.T) {
160160
t.Log("Success! All Spaceflakes are unique")
161161
}
162162

163-
func TestSameTimeStampDifferentBaseEpoch(t *testing.T) {
164-
node := NewNode(1)
165-
worker := node.NewWorker()
166-
sf1, err := worker.GenerateSpaceflake() // Default epoch
167-
if err != nil {
168-
t.Error(err)
169-
return
170-
}
171-
worker.BaseEpoch = 1640995200000 // Saturday, January 1, 2022 12:00:00 AM GMT
172-
sf2, err := worker.GenerateSpaceflake()
173-
if err != nil {
174-
t.Error(err)
175-
return
176-
}
177-
if sf1.Time() == sf2.Time() {
178-
t.Log("Success! Generated same timestamp for different base epoch")
179-
return
180-
}
181-
182-
t.Error("Failed! Generated different timestamps for different base epoch")
183-
}
184-
185163
func TestSpaceflakeGenerateUnique(t *testing.T) {
186164
spaceflakes := map[uint64]*Spaceflake{}
187165
settings := NewGeneratorSettings()

0 commit comments

Comments
 (0)