snowflaked 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2d0d8f2af68a50e38cc1aa32c1adc0641fbcdb2d0f01d0bc6e942520da33089f
4
- data.tar.gz: 47f1b23d9a38e2f60ab43f7059c8efd27b783012fbdc8f510ed220b221e3200d
3
+ metadata.gz: 8614c6c1857513375925fcc710cae86379a5f3937e1128a76e7c0c9316e717d7
4
+ data.tar.gz: 2a229cc5321964620d03261fcaa7b757e2a46ae31519ba9d03a0bb18a4b37f90
5
5
  SHA512:
6
- metadata.gz: '0278267978cd6a55be0f569659f36ff841c874cbeb2e813cc7f88c1789cf6d8b7ef21886d0bffe1f3268b9c34e1dede2b2eea75ba77468760acbf956d069d57f'
7
- data.tar.gz: 8f2dc54ef2896404ee4698644844ca2611ccb7b6339735ffeb0a7e2dc38854b1df73035c9e7f66a92202c6b5001760b814907303007db2c327dd32195b1d917f
6
+ metadata.gz: d54e7b37a266cfa15f98d07e9723db9d218aff9301b7c1c0939d82f750ea91cbbfde65a7470b2b29a34d08b23b8a8a969e88e07af13b7f1af16f6eb6322b5648
7
+ data.tar.gz: a032245eec1c604d91bd3c51fefb3d5345cf693e8ff50c34dab29bd022d5c2c7fc77cabe32084e7a5955c25caaa7ffb903abc90d1d7d61b233fd6b0040f90a65
data/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![Downloads](https://img.shields.io/gem/dt/snowflaked.svg)](https://rubygems.org/gems/snowflaked)
6
6
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.txt)
7
7
 
8
- A high-performance, thread-safe Snowflake ID generator for Ruby, powered by Rust.
8
+ A database-agnostic, high-performance, thread-safe Snowflake ID generator for Ruby, powered by Rust.
9
9
 
10
10
  Snowflake IDs are 64-bit unique identifiers that encode a timestamp, machine ID, and sequence number. They're time-sortable (IDs created later are always larger), making them ideal for distributed systems where you need unique IDs without coordination between machines. Unlike UUIDs, Snowflake IDs are smaller, sortable, and index-friendly for databases.
11
11
 
@@ -143,6 +143,12 @@ Snowflaked.machine_id(id)
143
143
  # => 42
144
144
  ```
145
145
 
146
+ ## Benchmarks
147
+
148
+ See [BENCHMARKS.md](benchmarks/README.md) for more details.
149
+
150
+ tl;dr: Snowflake IDs have a negligible performance impact compared to database-backed IDs.
151
+
146
152
  ## Requirements
147
153
 
148
154
  - Ruby >= 3.2
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "snowflaked"
3
- version = "0.1.3"
3
+ version = "0.2.0"
4
4
  edition = "2021"
5
5
  publish = false
6
6
 
@@ -10,3 +10,4 @@ crate-type = ["cdylib"]
10
10
  [dependencies]
11
11
  magnus = { version = "0.8" }
12
12
  snowflaked = { version = "1.0.3", features = ["sync"] }
13
+ arc-swap = "1.8.2"
@@ -1,7 +1,8 @@
1
+ use arc_swap::ArcSwapOption;
1
2
  use magnus::{function, prelude::*, Error, RHash, Ruby};
2
3
  use snowflaked::sync::Generator;
3
4
  use snowflaked::{Builder, Snowflake};
4
- use std::sync::RwLock;
5
+ use std::sync::Arc;
5
6
  use std::time::UNIX_EPOCH;
6
7
 
7
8
  struct GeneratorState {
@@ -11,68 +12,88 @@ struct GeneratorState {
11
12
  init_pid: u32,
12
13
  }
13
14
 
14
- static STATE: RwLock<Option<GeneratorState>> = RwLock::new(None);
15
+ static STATE: ArcSwapOption<GeneratorState> = ArcSwapOption::const_empty();
15
16
 
16
- fn init_generator(machine_id: u16, epoch_ms: Option<u64>) -> bool {
17
- let current_pid = std::process::id();
18
- let epoch_offset = epoch_ms.unwrap_or(0);
19
-
20
- let mut state = STATE.write().unwrap();
17
+ fn build_generator(machine_id: u16, epoch_offset: u64) -> Generator {
18
+ let epoch = UNIX_EPOCH + std::time::Duration::from_millis(epoch_offset);
19
+ Builder::new().instance(machine_id).epoch(epoch).build()
20
+ }
21
21
 
22
- if state.as_ref().is_some_and(|s| s.init_pid == current_pid) {
23
- return false;
22
+ fn ensure_state(machine_id: u16, epoch_offset: u64, current_pid: u32) -> (Arc<GeneratorState>, bool) {
23
+ if let Some(s) = &*STATE.load() {
24
+ if s.init_pid == current_pid {
25
+ return (Arc::clone(s), false);
26
+ }
24
27
  }
25
28
 
26
- let epoch = UNIX_EPOCH + std::time::Duration::from_millis(epoch_offset);
27
- let generator = Builder::new().instance(machine_id).epoch(epoch).build();
28
-
29
- *state = Some(GeneratorState {
30
- generator,
29
+ let new_state = Arc::new(GeneratorState {
30
+ generator: build_generator(machine_id, epoch_offset),
31
31
  epoch_offset,
32
32
  machine_id,
33
33
  init_pid: current_pid,
34
34
  });
35
35
 
36
- true
37
- }
36
+ let prev_state = STATE.rcu(|current| {
37
+ if let Some(c) = current {
38
+ if c.init_pid == current_pid {
39
+ return Arc::clone(c);
40
+ }
41
+ }
42
+ Arc::clone(&new_state)
43
+ });
38
44
 
39
- fn generate(ruby: &Ruby) -> Result<u64, Error> {
40
- let state = STATE.read().unwrap();
45
+ match prev_state.as_ref() {
46
+ Some(s) if s.init_pid == current_pid => (Arc::clone(s), false),
47
+ _ => (new_state, true),
48
+ }
49
+ }
41
50
 
42
- let s = state.as_ref().ok_or_else(|| {
43
- Error::new(
44
- ruby.exception_runtime_error(),
45
- "Generator not initialized. Call Snowflaked.configure or Snowflaked.id first.",
46
- )
47
- })?;
51
+ fn init_generator(machine_id: u16, epoch_ms: Option<u64>) -> bool {
52
+ let (_, swapped) = ensure_state(machine_id, epoch_ms.unwrap_or(0), std::process::id());
53
+ swapped
54
+ }
48
55
 
49
- if s.init_pid != std::process::id() {
56
+ fn validate_config(ruby: &Ruby, s: &GeneratorState, machine_id: u16, epoch_offset: u64) -> Result<(), Error> {
57
+ if s.machine_id != machine_id || s.epoch_offset != epoch_offset {
50
58
  return Err(Error::new(
51
59
  ruby.exception_runtime_error(),
52
- "Fork detected: generator was initialized in a different process. This should not happen if using Snowflaked.id - please report this bug.",
60
+ "Generator already initialized with a different machine_id or epoch for this process",
53
61
  ));
54
62
  }
63
+ Ok(())
64
+ }
65
+
66
+ fn generate(ruby: &Ruby, machine_id: u16, epoch_ms: Option<u64>) -> Result<u64, Error> {
67
+ let epoch_offset = epoch_ms.unwrap_or(0);
68
+ let (state, _) = ensure_state(machine_id, epoch_offset, std::process::id());
55
69
 
56
- Ok(s.generator.generate())
70
+ validate_config(ruby, &state, machine_id, epoch_offset)?;
71
+ Ok(state.generator.generate())
57
72
  }
58
73
 
59
- fn timestamp_ms(id: u64) -> u64 {
60
- let timestamp_raw = id.timestamp();
61
- let state = STATE.read().unwrap();
62
- let epoch_offset = state.as_ref().map(|s| s.epoch_offset).unwrap_or(0);
63
- timestamp_raw.saturating_add(epoch_offset)
74
+ fn epoch_offset(ruby: &Ruby) -> Result<u64, Error> {
75
+ STATE
76
+ .load()
77
+ .as_ref()
78
+ .map(|s| s.epoch_offset)
79
+ .ok_or_else(|| Error::new(ruby.exception_runtime_error(), "Generator not initialized"))
64
80
  }
65
81
 
66
82
  fn parse(ruby: &Ruby, id: u64) -> Result<RHash, Error> {
83
+ let offset = epoch_offset(ruby)?;
67
84
  let hash = ruby.hash_new();
68
85
 
69
- hash.aset(ruby.to_symbol("timestamp_ms"), timestamp_ms(id))?;
70
- hash.aset(ruby.to_symbol("machine_id"), machine_id_from_id(id))?;
71
- hash.aset(ruby.to_symbol("sequence"), sequence(id))?;
86
+ hash.aset(ruby.to_symbol("timestamp_ms"), id.timestamp().saturating_add(offset))?;
87
+ hash.aset(ruby.to_symbol("machine_id"), id.instance())?;
88
+ hash.aset(ruby.to_symbol("sequence"), id.sequence())?;
72
89
 
73
90
  Ok(hash)
74
91
  }
75
92
 
93
+ fn timestamp_ms(ruby: &Ruby, id: u64) -> Result<u64, Error> {
94
+ epoch_offset(ruby).map(|offset| id.timestamp().saturating_add(offset))
95
+ }
96
+
76
97
  fn machine_id_from_id(id: u64) -> u64 {
77
98
  id.instance()
78
99
  }
@@ -82,13 +103,11 @@ fn sequence(id: u64) -> u64 {
82
103
  }
83
104
 
84
105
  fn is_initialized() -> bool {
85
- let state = STATE.read().unwrap();
86
- state.as_ref().is_some_and(|s| s.init_pid == std::process::id())
106
+ STATE.load().as_ref().is_some_and(|s| s.init_pid == std::process::id())
87
107
  }
88
108
 
89
109
  fn configured_machine_id() -> Option<u16> {
90
- let state = STATE.read().unwrap();
91
- state.as_ref().and_then(|s| if s.init_pid == std::process::id() { Some(s.machine_id) } else { None })
110
+ STATE.load().as_ref().filter(|s| s.init_pid == std::process::id()).map(|s| s.machine_id)
92
111
  }
93
112
 
94
113
  #[magnus::init]
@@ -97,7 +116,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
97
116
  let internal = module.define_module("Native")?;
98
117
 
99
118
  internal.define_singleton_method("init_generator", function!(init_generator, 2))?;
100
- internal.define_singleton_method("generate", function!(generate, 0))?;
119
+ internal.define_singleton_method("generate", function!(generate, 2))?;
101
120
  internal.define_singleton_method("parse", function!(parse, 1))?;
102
121
  internal.define_singleton_method("timestamp_ms", function!(timestamp_ms, 1))?;
103
122
  internal.define_singleton_method("machine_id", function!(machine_id_from_id, 1))?;
@@ -14,25 +14,27 @@ module Snowflaked
14
14
  attrs = attributes.map(&:to_sym)
15
15
  attrs |= [:id] if id
16
16
  self._snowflake_attributes = attrs
17
+ @_snowflake_attributes_with_columns = nil
17
18
  end
18
19
 
19
20
  def _snowflake_columns_from_comments
20
21
  return @_snowflake_columns_from_comments if defined?(@_snowflake_columns_from_comments)
21
22
 
22
- @_snowflake_columns_from_comments = if table_exists?
23
- columns.filter_map { |col| col.name.to_sym if col.comment == Snowflaked::SchemaDefinitions::COMMENT }
24
- else
25
- []
26
- end
23
+ @_snowflake_columns_from_comments = table_exists? ? columns.filter_map { |col| col.name.to_sym if col.comment == Snowflaked::SchemaDefinitions::COMMENT } : []
24
+ end
25
+
26
+ def _snowflake_attributes_with_columns
27
+ @_snowflake_attributes_with_columns ||= (_snowflake_attributes | _snowflake_columns_from_comments)
27
28
  end
28
29
  end
29
30
 
30
31
  private
31
32
 
32
33
  def _generate_snowflake_ids
33
- attributes_to_generate = self.class._snowflake_attributes | self.class._snowflake_columns_from_comments
34
+ attributes = self.class._snowflake_attributes_with_columns
35
+ return if attributes.empty?
34
36
 
35
- attributes_to_generate.each do |attribute|
37
+ attributes.each do |attribute|
36
38
  next if self[attribute].present?
37
39
 
38
40
  self[attribute] = Snowflaked.id
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Snowflaked
4
- VERSION = "0.1.3"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/snowflaked.rb CHANGED
@@ -29,8 +29,7 @@ module Snowflaked
29
29
  end
30
30
 
31
31
  def machine_id_value
32
- id = @machine_id || default_machine_id
33
- id % (MAX_MACHINE_ID + 1)
32
+ (@machine_id || default_machine_id) % (MAX_MACHINE_ID + 1)
34
33
  end
35
34
 
36
35
  def epoch_ms
@@ -46,8 +45,7 @@ module Snowflaked
46
45
  end
47
46
 
48
47
  def env_machine_id
49
- id = ENV["SNOWFLAKED_MACHINE_ID"] || ENV.fetch("MACHINE_ID", nil)
50
- id&.to_i
48
+ (ENV["SNOWFLAKED_MACHINE_ID"] || ENV.fetch("MACHINE_ID", nil))&.to_i
51
49
  end
52
50
 
53
51
  def hostname_pid_hash
@@ -68,8 +66,8 @@ module Snowflaked
68
66
  end
69
67
 
70
68
  def id
71
- ensure_initialized!
72
- Native.generate
69
+ config = configuration
70
+ Native.generate(config.machine_id_value, config.epoch_ms)
73
71
  end
74
72
 
75
73
  def parse(id)
@@ -78,16 +76,17 @@ module Snowflaked
78
76
  end
79
77
 
80
78
  def timestamp(id)
81
- time_ms = Native.timestamp_ms(id)
82
- Time.at(time_ms / 1000, (time_ms % 1000) * 1000, :usec)
79
+ ensure_initialized!
80
+ seconds, milliseconds = Native.timestamp_ms(id).divmod(1000)
81
+ Time.at(seconds, milliseconds * 1000, :usec)
83
82
  end
84
83
 
85
84
  def machine_id(id)
86
- ensure_initialized!
87
85
  Native.machine_id(id)
88
86
  end
89
87
 
90
88
  def timestamp_ms(id)
89
+ ensure_initialized!
91
90
  Native.timestamp_ms(id)
92
91
  end
93
92
 
@@ -100,8 +99,7 @@ module Snowflaked
100
99
  def ensure_initialized!
101
100
  return if Native.initialized?
102
101
 
103
- config = configuration
104
- Native.init_generator(config.machine_id_value, config.epoch_ms)
102
+ Native.init_generator(configuration.machine_id_value, configuration.epoch_ms)
105
103
  end
106
104
  end
107
105
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: snowflaked
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Luiz Eduardo Kowalski