snowflaked 0.1.4 → 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: 4a6f86e1432fc8b11f282674da8568ffecb835bb5aba3d5c52c124acb4a55ace
4
- data.tar.gz: 2d13a338cceed05710c0afa3e56896d8199d90b1934ac259e256bed49ba8c3cf
3
+ metadata.gz: 8614c6c1857513375925fcc710cae86379a5f3937e1128a76e7c0c9316e717d7
4
+ data.tar.gz: 2a229cc5321964620d03261fcaa7b757e2a46ae31519ba9d03a0bb18a4b37f90
5
5
  SHA512:
6
- metadata.gz: 54d45cfb0b0f503e8cc839ac7530439582e08ee11a98e3ec8a79bd9cc3bb0539cd8a0096e2d6618a9015c712bc02e8423ee18dc3d325eb695bdb4e413ad998dd
7
- data.tar.gz: 4f291445d0e558e1e856f146d5972d52ac068ee907cb20aa4a2ea8d9eb0be818c3d4b53738e0b3ade960662aa8a5f05e6e616f210e0c7a3de7c37d089cbe9454
6
+ metadata.gz: d54e7b37a266cfa15f98d07e9723db9d218aff9301b7c1c0939d82f750ea91cbbfde65a7470b2b29a34d08b23b8a8a969e88e07af13b7f1af16f6eb6322b5648
7
+ data.tar.gz: a032245eec1c604d91bd3c51fefb3d5345cf693e8ff50c34dab29bd022d5c2c7fc77cabe32084e7a5955c25caaa7ffb903abc90d1d7d61b233fd6b0040f90a65
data/README.md CHANGED
@@ -145,7 +145,7 @@ Snowflaked.machine_id(id)
145
145
 
146
146
  ## Benchmarks
147
147
 
148
- See [BENCHMARKS.md](benchmarks/BENCHMARKS.md) for more details.
148
+ See [BENCHMARKS.md](benchmarks/README.md) for more details.
149
149
 
150
150
  tl;dr: Snowflake IDs have a negligible performance impact compared to database-backed IDs.
151
151
 
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "snowflaked"
3
- version = "0.1.4"
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))?;
@@ -6,7 +6,7 @@ module Snowflaked
6
6
 
7
7
  included do
8
8
  class_attribute :_snowflake_attributes, instance_writer: false, default: [:id]
9
- after_initialize :_generate_snowflake_ids, if: :new_record?
9
+ before_validation :_generate_snowflake_ids, on: :create
10
10
  end
11
11
 
12
12
  class_methods do
@@ -20,11 +20,7 @@ module Snowflaked
20
20
  def _snowflake_columns_from_comments
21
21
  return @_snowflake_columns_from_comments if defined?(@_snowflake_columns_from_comments)
22
22
 
23
- @_snowflake_columns_from_comments = if table_exists?
24
- columns.filter_map { |col| col.name.to_sym if col.comment == Snowflaked::SchemaDefinitions::COMMENT }
25
- else
26
- []
27
- end
23
+ @_snowflake_columns_from_comments = table_exists? ? columns.filter_map { |col| col.name.to_sym if col.comment == Snowflaked::SchemaDefinitions::COMMENT } : []
28
24
  end
29
25
 
30
26
  def _snowflake_attributes_with_columns
@@ -35,10 +31,10 @@ module Snowflaked
35
31
  private
36
32
 
37
33
  def _generate_snowflake_ids
38
- attributes_to_generate = self.class._snowflake_attributes_with_columns
39
- return if attributes_to_generate.empty?
34
+ attributes = self.class._snowflake_attributes_with_columns
35
+ return if attributes.empty?
40
36
 
41
- attributes_to_generate.each do |attribute|
37
+ attributes.each do |attribute|
42
38
  next if self[attribute].present?
43
39
 
44
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.4"
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.4
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Luiz Eduardo Kowalski