diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..6d9ee9e --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: [kkrypt0nn] +custom: ["https://buymeacoffee.com/kkrypt0nn"] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ffb1b13 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,49 @@ +name: Spaceflake CI (Lint & Test) + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +jobs: + lint-test: + name: Lint & Test + runs-on: ubuntu-latest + steps: + - name: Checkout source code + uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: 1.24 + - name: Lint + uses: golangci/golangci-lint-action@v7 + with: + version: latest + - name: Test + run: go test -v ./... + build: + name: Build for ${{ matrix.target.goos }}/${{ matrix.target.goarch }} + runs-on: ubuntu-latest + needs: [lint-test] + strategy: + matrix: + target: + - { goos: linux, goarch: amd64 } + - { goos: linux, goarch: arm64 } + - { goos: darwin, goarch: amd64 } + - { goos: darwin, goarch: arm64 } + - { goos: windows, goarch: amd64 } + - { goos: windows, goarch: arm64 } + - { goos: android, goarch: arm64 } + steps: + - name: Checkout source code + uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: 1.24 + - name: Build + run: GOOS=${{ matrix.target.goos }} GOARCH=${{ matrix.target.goarch }} go build -v ./... diff --git a/.github/workflows/go-test.yml b/.github/workflows/go-test.yml deleted file mode 100644 index 7363393..0000000 --- a/.github/workflows/go-test.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Go Test - -on: - push: - branches: - - main - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - name: Set up Go - uses: actions/setup-go@v2 - with: - go-version: 1.19 - - - name: Build - run: go build -v ./... - - - name: Test - run: go test -v ./... diff --git a/go.mod b/go.mod index 0c942e5..ff25c64 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/kkrypt0nn/spaceflake -go 1.19 +go 1.24 diff --git a/spaceflake.go b/spaceflake.go index 33b74fb..5ff45e5 100644 --- a/spaceflake.go +++ b/spaceflake.go @@ -18,6 +18,8 @@ const ( MAX12BITS = 4095 // MAX41BITS is the maximum value for a 41 bits number MAX41BITS = 2199023255551 + // CLOCK_DRIFT_TOLERANCE_MS is the tolerance for clock drift in milliseconds + CLOCK_DRIFT_TOLERANCE_MS = 10 ) // Spaceflake represents a Spaceflake @@ -162,13 +164,15 @@ type Worker struct { Sequence uint64 // ID is the worker ID that the Spaceflake generator will use for the next 5 bits ID uint64 - - increment uint64 - mutex *sync.Mutex + // used to prevent clockdrift + lastTimestamp uint64 + increment uint64 + mutex *sync.Mutex } // GenerateSpaceflake generates a Spaceflake func (w *Worker) GenerateSpaceflake() (*Spaceflake, error) { + if w.Node == nil { return nil, fmt.Errorf("node is not set") } @@ -197,6 +201,16 @@ func (w *Worker) GenerateSpaceflake() (*Spaceflake, error) { milliseconds := uint64(math.Floor(microTime() * 1000)) milliseconds -= w.BaseEpoch + if delta := w.lastTimestamp - milliseconds; milliseconds < w.lastTimestamp { + if delta >= CLOCK_DRIFT_TOLERANCE_MS { + return nil, fmt.Errorf("clock moved backwards by %dms", delta) + } + time.Sleep(time.Duration(delta+1) * time.Millisecond) + milliseconds = uint64(math.Floor(microTime()*100)) - w.BaseEpoch + } + + w.lastTimestamp = milliseconds + base := stringPadLeft(decimalBinary(milliseconds), 41, "0") nodeID := stringPadLeft(decimalBinary(w.Node.ID), 5, "0") workerID := stringPadLeft(decimalBinary(w.ID), 5, "0") @@ -250,6 +264,10 @@ func (w *Worker) GenerateSpaceflakeAt(at time.Time) (*Spaceflake, error) { milliseconds := uint64(math.Floor(microTime * 1000)) milliseconds -= w.BaseEpoch + if milliseconds < w.lastTimestamp { + 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") + } + base := stringPadLeft(decimalBinary(milliseconds), 41, "0") nodeID := stringPadLeft(decimalBinary(w.Node.ID), 5, "0") workerID := stringPadLeft(decimalBinary(w.ID), 5, "0") diff --git a/spaceflake_test.go b/spaceflake_test.go index cb10251..527bc5e 100644 --- a/spaceflake_test.go +++ b/spaceflake_test.go @@ -160,28 +160,6 @@ func TestSpaceflakeWorkerGoroutineUnique(t *testing.T) { t.Log("Success! All Spaceflakes are unique") } -func TestSameTimeStampDifferentBaseEpoch(t *testing.T) { - node := NewNode(1) - worker := node.NewWorker() - sf1, err := worker.GenerateSpaceflake() // Default epoch - if err != nil { - t.Error(err) - return - } - worker.BaseEpoch = 1640995200000 // Saturday, January 1, 2022 12:00:00 AM GMT - sf2, err := worker.GenerateSpaceflake() - if err != nil { - t.Error(err) - return - } - if sf1.Time() == sf2.Time() { - t.Log("Success! Generated same timestamp for different base epoch") - return - } - - t.Error("Failed! Generated different timestamps for different base epoch") -} - func TestSpaceflakeGenerateUnique(t *testing.T) { spaceflakes := map[uint64]*Spaceflake{} settings := NewGeneratorSettings()