snowflake

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 18, 2026 License: Apache-2.0 Imports: 5 Imported by: 0

README

Snowflake

Unique, roughly time-ordered 64-bit IDs, in the style of Twitter's Snowflake.

 0 | 41 bits: ms since epoch | 10 bits: node ID | 12 bits: sequence
  • About 69 years of IDs from the epoch (default 2026-01-01 UTC)
  • Up to 1024 nodes, each generating up to 4096 IDs per millisecond
  • IDs are positive int64s and increase over time on each node
  • Uses the monotonic clock, so wall clock adjustments can't make IDs go backwards
  • Node IDs can be assigned automatically from MySQL, PostgreSQL or MongoDB

Installation

go get github.com/0x48core/gokit/snowflake

Requires Go 1.25+.

Usage

Fixed node ID

When each process already has a unique number (for example a StatefulSet ordinal):

node, err := snowflake.New(3)
if err != nil {
	return err
}

id, err := node.Generate()
Node ID from a database

Every running process needs a different node ID. A registry leases one from a shared database table and keeps the lease alive in the background:

import (
	"database/sql"

	_ "github.com/jackc/pgx/v5/stdlib"

	"github.com/0x48core/gokit/snowflake"
	"github.com/0x48core/gokit/snowflake/postgres"
)

db, err := sql.Open("pgx", "postgres://user:pass@localhost:5432/app")
if err != nil {
	return err
}

registry, err := postgres.New(db)
if err != nil {
	return err
}

node, err := snowflake.NewWithRegistry(ctx, registry)
if err != nil {
	return err
}
defer registry.Release(context.Background())

id, err := node.Generate()

The MySQL and MongoDB registries work the same way:

registry, err := mysql.New(db) // db opened with github.com/go-sql-driver/mysql
registry, err := mongodb.New(client.Database("app").Collection(mongodb.DefaultCollection))
Parsing an ID
parts := node.Parse(id) // or snowflake.Parse(id, snowflake.DefaultEpoch)
fmt.Println(parts.Time, parts.NodeID, parts.Sequence)

How node ID leasing works

  1. On first use, the registry creates a table (or collection) with one row per node ID, 0 to 1023.
  2. Acquire takes the lowest node ID whose lease has expired and sets it to expire after the TTL (default one minute).
  3. A background goroutine renews the lease every TTL/3.
  4. Release frees the node ID right away. If a process crashes instead, its node ID becomes free once the lease expires.

All expiry times come from the database clock, so clock differences between hosts don't matter.

If renewal fails twice in a row, or another process has taken the node ID, the lease counts as lost. Generate then returns ErrLeaseLost instead of risking duplicate IDs. Watch registry.Lost() to restart or acquire a new node ID.

Backend Package Default table / collection Tested with
MySQL snowflake/mysql snowflake_nodes MySQL 8.4
PostgreSQL snowflake/postgres snowflake_nodes PostgreSQL 17 (needs 9.5+)
MongoDB snowflake/mongodb your choice, e.g. snowflake_nodes MongoDB 7 (needs 4.2+)

Registry options: WithTTL, WithOwner, and WithTable (MySQL and PostgreSQL).

To use another store, implement snowflake.NodeRegistry:

type NodeRegistry interface {
	Acquire(ctx context.Context) (int64, error)
	Release(ctx context.Context) error
	Lost() <-chan struct{} // nil if the node ID can never be lost
}

API

Name Description
New(nodeID, opts...) Creates a Node with a fixed node ID in [0, MaxNodeID].
NewWithRegistry(ctx, registry, opts...) Acquires a node ID from the registry and creates a Node with it.
WithEpoch(t) Sets the epoch. Every node sharing an ID space must use the same one.
(*Node).Generate() Returns a new ID. Waits for the next millisecond if 4096 IDs were already generated in this one.
(*Node).Parse(id), Parse(id, epoch) Splits an ID into Time, NodeID and Sequence.
ErrLeaseLost Returned by Generate after the registry lost the node ID.
ErrNoNodeIDAvailable Returned by a registry when all 1024 node IDs are leased.
ErrTimeOverflow Returned once the 41-bit time range is used up (about 69 years after the epoch).

Development

make fmt               # goimports, gofumpt, gci
make lint              # golangci-lint + go vet
make test              # unit tests; registry tests are skipped
make test-integration  # all tests against MySQL, PostgreSQL and MongoDB in Docker

The registry tests read SNOWFLAKE_MYSQL_DSN, SNOWFLAKE_POSTGRES_DSN and SNOWFLAKE_MONGODB_URI, and skip any backend whose variable is unset.

Documentation

Overview

Package snowflake generates unique, roughly time-ordered 64-bit IDs.

An ID is a positive int64 laid out as:

0 | 41 bits: milliseconds since epoch | 10 bits: node ID | 12 bits: sequence

which gives about 69 years of IDs per epoch, up to 1024 nodes, and up to 4096 IDs per millisecond per node.

Index

Constants

View Source
const (

	// MaxNodeID is the largest node ID a Node accepts.
	MaxNodeID = 1<<nodeBits - 1
)

Variables

View Source
var (
	// ErrInvalidNodeID is returned when a node ID is outside [0, MaxNodeID].
	ErrInvalidNodeID = errors.New("snowflake: invalid node ID")
	// ErrInvalidEpoch is returned when the epoch is in the future.
	ErrInvalidEpoch = errors.New("snowflake: epoch is in the future")
	// ErrTimeOverflow is returned once the 41-bit time range since the epoch is used up.
	ErrTimeOverflow = errors.New("snowflake: time since epoch exceeds 41 bits")
	// ErrLeaseLost is returned when the node ID lease from a NodeRegistry is lost,
	// because another node may now be using the same node ID.
	ErrLeaseLost = errors.New("snowflake: node ID lease lost")
)
View Source
var DefaultEpoch = time.Date(2026, time.January, 1, 0, 0, 0, 0, time.UTC)

DefaultEpoch is the epoch used when no WithEpoch option is given.

View Source
var ErrNoNodeIDAvailable = errors.New("snowflake: no node ID available")

ErrNoNodeIDAvailable is returned by a NodeRegistry when all node IDs are in use.

Functions

This section is empty.

Types

type Node

type Node struct {
	// contains filtered or unexported fields
}

Node generates IDs for a single node ID. It is safe for concurrent use.

func New

func New(nodeID int64, opts ...Option) (*Node, error)

New creates a Node with the given node ID. Two nodes running at the same time must never share a node ID; use NewWithRegistry to have one assigned automatically.

func NewWithRegistry

func NewWithRegistry(ctx context.Context, registry NodeRegistry, opts ...Option) (*Node, error)

NewWithRegistry acquires a node ID from registry and creates a Node with it. Once the registry reports the node ID as lost, Generate returns ErrLeaseLost.

Call registry.Release when the Node is no longer needed.

func (*Node) Generate

func (n *Node) Generate() (int64, error)

Generate returns a new unique ID. If 4096 IDs were already generated in the current millisecond, it waits for the next one.

func (*Node) NodeID

func (n *Node) NodeID() int64

NodeID returns the node ID embedded in every ID this Node generates.

func (*Node) Parse

func (n *Node) Parse(id int64) Parts

Parse splits an ID generated by this Node into its parts.

type NodeRegistry

type NodeRegistry interface {
	// Acquire reserves a free node ID in [0, MaxNodeID] for this process.
	Acquire(ctx context.Context) (int64, error)
	// Release gives the node ID back so another process can use it.
	Release(ctx context.Context) error
	// Lost returns a channel that is closed if the node ID can no longer be
	// guaranteed to be ours. It may return nil if that can never happen.
	Lost() <-chan struct{}
}

NodeRegistry assigns node IDs so that no two running nodes share one.

The mysql and mongodb subpackages provide registries that lease node IDs from a database table or collection.

type Option

type Option func(*config)

Option configures a Node.

func WithEpoch

func WithEpoch(epoch time.Time) Option

WithEpoch sets the time IDs are counted from. Every node generating IDs for the same keyspace must use the same epoch.

type Parts

type Parts struct {
	Time     time.Time
	NodeID   int64
	Sequence int64
}

Parts are the components of an ID.

func Parse

func Parse(id int64, epoch time.Time) Parts

Parse splits an ID into its parts. epoch must be the epoch the ID was generated with.

Directories

Path Synopsis
internal
lease
Package lease keeps a node ID lease alive by renewing it in the background.
Package lease keeps a node ID lease alive by renewing it in the background.
Package mongodb provides a snowflake.NodeRegistry that leases node IDs from a MongoDB collection.
Package mongodb provides a snowflake.NodeRegistry that leases node IDs from a MongoDB collection.
Package mysql provides a snowflake.NodeRegistry that leases node IDs from a MySQL table.
Package mysql provides a snowflake.NodeRegistry that leases node IDs from a MySQL table.
Package postgres provides a snowflake.NodeRegistry that leases node IDs from a PostgreSQL table.
Package postgres provides a snowflake.NodeRegistry that leases node IDs from a PostgreSQL table.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL