Current section
Files
Jump to
Current section
Files
src/snowglake.gleam
//// A module for generating unique IDs using the Twitter Snowflake algorithm.
import gleam/erlang/process
import gleam/int
import gleam/list
import gleam/otp/actor
import gleam/result
import gleam/string
import gleam/time/timestamp
/// The default epoch for the generator. Corresponds to the Twitter epoch.
pub const default_epoch: Int = 1_288_834_974_657
/// The maximum number of IDs that can be generated in a single millisecond.
const max_index: Int = 4096
/// Type of the generator. It is the actual
/// public interface for the generator and should be used to interact with it.
/// It holds the actor subject that is used to handle the generator state.
///
/// # Examples
/// ```gleam
/// import gleam/snowglake
///
/// pub type Context {
/// Context(generator: snowglake.Generator)
/// }
///
/// let assert Ok(generator) = snowglake.new_generator() |> snowglake.start()
/// let context = Context(generator: generator)
/// let id = context.generator |> snowglake.generate()
/// ```
pub opaque type Generator {
Generator(subject: process.Subject(Message))
}
/// The messages that the generator can receive.
pub opaque type Message {
Generate(reply_with: process.Subject(Int))
GenerateLazy(reply_with: process.Subject(Int))
GenerateMany(Int, process.Subject(List(Int)))
GenerateManyLazy(Int, process.Subject(List(Int)))
Shutdown
}
/// The Snowflake ID generator node.
/// A node holds the state of the generator and is used to generate IDs.
pub opaque type Node {
Node(epoch: Int, worker_id: Int, process_id: Int, last_ts: Int, index: Int)
}
/// Creates a new Snowflake ID generator with default settings.
pub fn new_generator() -> Node {
Node(epoch: default_epoch, worker_id: 0, process_id: 0, last_ts: 0, index: -1)
}
/// Sets the epoch for the generator.
pub fn with_epoch(node: Node, epoch: Int) -> Node {
Node(..node, epoch: epoch)
}
/// Sets the worker ID for the generator.
pub fn with_worker_id(node: Node, worker_id: Int) -> Node {
Node(..node, worker_id: worker_id)
}
/// Sets the process ID for the generator.
pub fn with_process_id(node: Node, process_id: Int) -> Node {
Node(..node, process_id: process_id)
}
/// Sets timestamp for the generator. Useful for lazy generation. It should not
/// be used along with normal generation.
pub fn with_timestamp(node: Node, last_ts: Int) -> Node {
Node(..node, last_ts: last_ts)
}
/// Starts the generator.
pub fn start(node: Node) -> Result(Generator, String) {
case node.epoch > get_now_milliseconds() {
True -> Error("epoch must be in the past")
False -> {
let node = case node.last_ts {
0 -> Node(..node, last_ts: node |> get_timestamp)
_ -> Node(..node, last_ts: node.last_ts |> int.subtract(node.epoch))
}
node
|> actor.new
|> actor.on_message(handle_message)
|> actor.start
|> result.map_error(format_start_error)
|> result.map(fn(started) { Generator(subject: started.data) })
}
}
}
fn format_start_error(error: actor.StartError) -> String {
let detail = case error {
actor.InitTimeout -> "initialisation timed out"
actor.InitFailed(reason) -> "initialisation failed: " <> reason
actor.InitExited(reason) ->
"initialisation exited: " <> string.inspect(reason)
}
"could not start actor: " <> detail
}
/// Generates a new Snowflake ID.
///
/// # Examples
/// ```gleam
/// import gleam/snowglake
///
/// let epoch = 1_420_070_400_000
/// let worker_id = 12
/// let process_id = 1
///
/// let assert Ok(generator) =
/// snowglake.new_generator()
/// |> snowglake.with_epoch(epoch)
/// |> snowglake.with_worker_id(worker_id)
/// |> snowglake.with_process_id(process_id)
/// |> snowglake.start()
///
/// let id = snowglake.generate(generator)
/// ```
pub fn generate(generator: Generator) -> Int {
actor.call(generator.subject, 10, Generate)
}
/// Generates a new Snowflake ID lazily.
/// It works like the `generate` function but it does not uses the current
/// timestamp, instead it consumes all the 4096 IDs of every millisecond.
/// It may be faster and useful in some cases than the `generate` function.
/// For example, to generate a batch of IDs or to generate IDs for a particular
/// time.
pub fn generate_lazy(generator: Generator) -> Int {
actor.call(generator.subject, 10, GenerateLazy)
}
/// Generates many Snowflake IDs.
///
/// # Examples
/// ```gleam
/// import gleam/snowglake
///
/// let assert Ok(generator) = snowglake.new_generator() |> snowglake.start()
/// let ids = snowglake.generate_many(generator, 5000)
/// ```
pub fn generate_many(generator: Generator, count: Int) -> List(Int) {
actor.call(generator.subject, 100, fn(reply) { GenerateMany(count, reply) })
}
/// Generates many Snowflake IDs lazily.
///
/// # Examples
/// ```gleam
/// import gleam/snowglake
///
/// let assert Ok(generator) = snowglake.new_generator() |> snowglake.start()
/// let ids = snowglake.generate_many_lazy(generator, 5000)
/// ```
pub fn generate_many_lazy(generator: Generator, count: Int) -> List(Int) {
actor.call(generator.subject, 100, fn(reply) {
GenerateManyLazy(count, reply)
})
}
/// Stops the generator.
pub fn stop(generator: Generator) {
actor.send(generator.subject, Shutdown)
}
/// Actor message handler.
fn handle_message(node: Node, message: Message) -> actor.Next(Node, Message) {
case message {
Generate(reply) -> {
let node = node |> setup
let id = node |> generate_id
actor.send(reply, id)
actor.continue(node)
}
GenerateLazy(reply) -> {
let node = node |> lazy_setup
let id = node |> generate_id
actor.send(reply, id)
actor.continue(node)
}
GenerateMany(count, reply) -> {
let node = node |> setup
let #(node, ids) = node |> generate_ids(count)
actor.send(reply, ids |> list.reverse())
actor.continue(node)
}
GenerateManyLazy(count, reply) -> {
let #(node, ids) = node |> generate_ids(count)
actor.send(reply, ids |> list.reverse())
actor.continue(node)
}
Shutdown -> actor.stop()
}
}
/// Generates a new Snowflake ID.
fn generate_id(node: Node) -> Int {
int.bitwise_shift_left(node.last_ts, 22)
|> int.bitwise_or(int.bitwise_shift_left(node.worker_id, 17))
|> int.bitwise_or(int.bitwise_shift_left(node.process_id, 12))
|> int.bitwise_or(node.index)
}
/// Generates many Snowflake IDs.
fn generate_ids(node: Node, count: Int) -> #(Node, List(Int)) {
case count {
0 -> #(node, [])
_ -> {
let node = node |> lazy_setup
let id = node |> generate_id
let #(node, ids) = node |> generate_ids(count - 1)
#(node, list.append(ids, [id]))
}
}
}
/// Sets up the node before generating a new ID.
/// Handles the case where multiple IDs are generated in the same millisecond.
/// It wait for the next millisecond if the 4096 were already generated.
fn setup(node: Node) -> Node {
let timestamp = node |> get_timestamp
case node {
Node(last_ts: lts, index: i, ..) if lts == timestamp && i < max_index -> {
Node(..node, index: i + 1)
}
Node(last_ts: lts, ..) if lts == timestamp -> node |> setup
_ -> Node(..node, index: 0, last_ts: timestamp)
}
}
/// Lazily sets up the node before generating a new ID.
/// It does not uses current timestamp to generate the next ID. It consumes
/// all the 4096 of every millisecond before moving to the next one.
/// It may be faster and useful in some cases.
fn lazy_setup(node: Node) -> Node {
let i = { node.index + 1 } % max_index
case i {
i if i == 0 && node.index != -1 ->
Node(..node, index: i, last_ts: node.last_ts + 1)
_ -> Node(..node, index: i)
}
}
/// Extracts the timestamp from a Snowflake ID using the provided epoch.
pub fn timestamp(id: Int, epoch: Int) -> Int {
id |> int.bitwise_shift_right(22) |> int.add(epoch)
}
/// Extracts the worker ID from a Snowflake ID.
pub fn worker_id(id: Int) -> Int {
id |> int.bitwise_and(0x3E0000) |> int.bitwise_shift_right(17)
}
/// Extracts the process ID from a Snowflake ID.
pub fn process_id(id: Int) -> Int {
id |> int.bitwise_and(0x1F000) |> int.bitwise_shift_right(12)
}
/// Gets the current timestamp using erlang os:system_time/1.
fn get_timestamp(node: Node) -> Int {
get_now_milliseconds() |> int.subtract(node.epoch)
}
pub fn get_now_milliseconds() -> Int {
let now = timestamp.system_time()
let #(seconds, nanoseconds) = timestamp.to_unix_seconds_and_nanoseconds(now)
let milliseconds_from_nanoseconds = case int.divide(nanoseconds, 1_000_000) {
Ok(value) -> value
Error(_) ->
panic as "unexpected divide-by-zero while computing milliseconds"
}
let ts_ms_int = seconds * 1000 + milliseconds_from_nanoseconds
ts_ms_int
}