hoov_vin 1.0.0 → 2.0.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: 89cea6349464b8a846d7278e95ceb71f633dd8e86b33963eace43993b7c9a577
4
- data.tar.gz: eee44128336e248e192cfcdd702954625b50db1da7a8a17137de644a2e5c5f0d
3
+ metadata.gz: e18efeba30d241867cf8506a73dfda375bb900557770d66b31e6e3e3d8356db6
4
+ data.tar.gz: 16a5249cd3f0fc0212a8e5b1dd53cd4b94e939e2f06a56f87281a0a08cd1dd92
5
5
  SHA512:
6
- metadata.gz: 02a46a71bc71764d28c745af2966a057d25fb2638f30c8ca0cbcced195210276f63d67a1a32ec573d6cd3e4d282274b968abea057053ab3eab42b9cde587b680
7
- data.tar.gz: ee32ff79ef7106ca1ab4685f081a8e18f02b03a68873f146ce42a6e3681b75cfb46e6721f0c94f12fc24aa99a30620ffb1f17571b01b61ab9ac675a136aac348
6
+ metadata.gz: 42d79ddd2eec545f848978d2df2261b8dea695e2795973fd73b7a44eadb714ad7a3f463a3f99a03012065cabafb9f49fd3e9a67ea2f2dc5cb531939c40dd9106
7
+ data.tar.gz: 978da00531bd8f5465524505de3d2fe90e5ffda0eaca0dc755af72e66c9bec0be7babace3412926c18e62be7311a18550b825a8b55ce127d62bac26defc5750b
data/README.md CHANGED
@@ -10,11 +10,11 @@
10
10
  <a href="https://github.com/hoovbr/vin/releases">
11
11
  <img alt="Latest Release" src="https://img.shields.io/github/v/release/hoovbr/vin?sort=semver">
12
12
  </a>
13
- <a href="https://codeclimate.com/github/hoovbr/vin/maintainability">
14
- <img src="https://api.codeclimate.com/v1/badges/790449fb5d05f6a134a5/maintainability" />
13
+ <a href="https://qlty.sh/gh/hoovbr/projects/vin">
14
+ <img src="https://qlty.sh/gh/hoovbr/projects/vin/maintainability.svg" alt="Maintainability" />
15
15
  </a>
16
- <a href="https://codeclimate.com/github/hoovbr/vin/test_coverage">
17
- <img src="https://api.codeclimate.com/v1/badges/790449fb5d05f6a134a5/test_coverage" />
16
+ <a href="https://qlty.sh/gh/hoovbr/projects/vin">
17
+ <img src="https://qlty.sh/gh/hoovbr/projects/vin/coverage.svg" alt="Code Coverage" />
18
18
  </a>
19
19
  <a href="https://github.com/hoovbr/vin/actions/workflows/push.yml">
20
20
  <img alt="Tests & Linter" src="https://github.com/hoovbr/vin/actions/workflows/push.yml/badge.svg">
@@ -177,6 +177,13 @@ count = 100
177
177
  vin.generate_ids(data_type, count) # => [63801199693922306, 63801199693922307, … 98 other IDs … ]
178
178
  ```
179
179
 
180
+ ```ruby
181
+ # Generate IDs with custom timestamps (e.g. for back-porting legacy systems, passing your db's created_at timestamps)
182
+ timestamps = [1646160000000, 1646160001000, 1646160002000] # Unix milliseconds
183
+ vin.generate_ids_from_timestamps(data_type, timestamps: timestamps)
184
+ # => [id_with_timestamp1, id_with_timestamp2, id_with_timestamp3]
185
+ ```
186
+
180
187
  ```ruby
181
188
  id_number = vin.generate_id(data_type) # => 63801532235120742
182
189
  id = VIN::Id.new(id: id_number) # => #<VIN::Id:0x0000000108452ff0…>
@@ -188,6 +195,28 @@ id.timestamp.to_time # 2023-09-27 22:16:15.33 -0300 (Ruby Time object)
188
195
  id.timestamp.epoch #=> 1688258040000, time since UNIX epoch in milliseconds
189
196
  ```
190
197
 
198
+ ## Important Limitations
199
+
200
+ ### Maximum IDs per Timestamp
201
+
202
+ **⚠️ CRITICAL:** When using `generate_ids_from_timestamps`, you cannot generate more than `max_sequence` IDs for the same timestamp. The `max_sequence` value is determined by your `sequence_bits` configuration, e.g.:
203
+
204
+ - With `sequence_bits: 11` → max 2,048 IDs per timestamp
205
+ - With `sequence_bits: 12` → max 4,096 IDs per timestamp
206
+ - With `sequence_bits: 10` → max 1,024 IDs per timestamp
207
+
208
+ **Why this matters:** If you request more IDs than `max_sequence` for the same timestamp, the sequence counter would wrap around and start from 0 again, creating IDs with identical timestamp and sequence values. This would result in duplicate IDs.
209
+
210
+ **Protection:** The `generate_ids_from_timestamps` method automatically validates this constraint and **raises an ArgumentError** if you try to exceed the limit, preventing duplicate IDs from being generated.
211
+
212
+ If you are back-porting a legacy system that has more than `max_sequence` records with the same timestamp, you will need to either:
213
+
214
+ 1. **Recommended: Group the timestamps in batches**, ensuring that each batch has the size of `max_sequence` and when it exceeds, you increment the timestamp by 1 millisecond. Repeat this process with all timestamps again once finished, until you don't have to increment any timestamp in the entire sequence anymore.
215
+ - This means your VINs will not have exactly the same timestamp as the legacy IDs, but they will be very close, and you will avoid collisions. This is a good trade-off, especially when back-porting legacy systems.
216
+ 2. **Re-consider the number of sequence bits** to ensure that it fits your business needs.
217
+
218
+ Note that this problem doesn't happen when generating IDs during standard usage via `generate_id` or `generate_ids` without passing timestamps as arguments, as those methods internally manage the timestamp and sequence to ensure uniqueness.
219
+
191
220
  # Configuration
192
221
 
193
222
  The VIN generator can be configured with the following parameters:
data/lib/vin/generator.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require "vin/config"
2
2
 
3
3
  class VIN
4
+ # rubocop:disable Metrics/ClassLength
4
5
  class Generator
5
6
  attr_reader :data_type, :count, :config, :custom_timestamp
6
7
 
@@ -8,20 +9,54 @@ class VIN
8
9
  @config = config
9
10
  end
10
11
 
11
- def generate_ids(data_type, count = 1, timestamp: nil)
12
- raise(ArgumentError, "data_type must be an integer") unless data_type.is_a?(Integer)
12
+ def generate_id(data_type)
13
+ validate_data_type!(data_type)
14
+
15
+ generate_single_batch(data_type, 1, nil).first
16
+ end
17
+
18
+ def generate_ids(data_type, count)
19
+ validate_data_type!(data_type)
20
+ validate_count!(count)
21
+
22
+ generate_single_batch(data_type, count, nil)
23
+ end
24
+
25
+ def generate_ids_from_timestamps(data_type, timestamps:)
26
+ validate_data_type!(data_type)
27
+ raise(ArgumentError, "timestamps must be an array") unless timestamps.is_a?(Array)
28
+ raise(ArgumentError, "timestamps array cannot be empty") if timestamps.empty?
29
+
30
+ timestamps.each { |ts| validate_timestamp!(ts) }
13
31
 
14
- unless config.data_type_allowed_range.include?(data_type)
15
- raise(ArgumentError, "data_type is outside the allowed range of #{config.data_type_allowed_range}")
32
+ # Check for potential duplicate ID issue
33
+ timestamp_counts = timestamps.tally
34
+ max_count = timestamp_counts.values.max
35
+ if max_count > config.max_sequence
36
+ raise ArgumentError,
37
+ "Cannot generate #{max_count} IDs for the same timestamp. " \
38
+ "Maximum allowed is #{config.max_sequence} (2^#{config.sequence_bits} - 1). " \
39
+ "Requesting more would produce duplicate IDs."
16
40
  end
17
41
 
42
+ generate_ids_for_multiple_timestamps(data_type, timestamps)
43
+ end
44
+
45
+ private
46
+
47
+ def validate_data_type!(data_type)
48
+ raise(ArgumentError, "data_type must be an integer") unless data_type.is_a?(Integer)
49
+
50
+ return if config.data_type_allowed_range.include?(data_type)
51
+ raise(ArgumentError, "data_type is outside the allowed range of #{config.data_type_allowed_range}")
52
+ end
53
+
54
+ def validate_count!(count)
18
55
  raise(ArgumentError, "count must be an integer") unless count.is_a?(Integer)
19
56
  raise(ArgumentError, "count must be a positive number") if count < 1
57
+ end
20
58
 
21
- if timestamp
22
- validate_timestamp!(timestamp)
23
- end
24
-
59
+ def generate_single_batch(data_type, count, timestamp)
25
60
  @data_type = data_type
26
61
  @count = count
27
62
  @custom_timestamp = timestamp
@@ -39,7 +74,54 @@ class VIN
39
74
  result
40
75
  end
41
76
 
42
- private
77
+ def generate_ids_for_multiple_timestamps(data_type, timestamps)
78
+ # Group timestamps to optimize Redis calls
79
+ # Each unique timestamp needs only one Redis call, then we can reuse sequences
80
+ timestamp_groups = {}
81
+ timestamps.each_with_index do |ts, index|
82
+ timestamp_groups[ts] ||= []
83
+ timestamp_groups[ts] << index
84
+ end
85
+
86
+ # Result array to maintain order
87
+ results = Array.new(timestamps.length)
88
+
89
+ # Process each unique timestamp
90
+ timestamp_groups.each do |ts, indices|
91
+ needed_count = indices.length
92
+ ids = []
93
+
94
+ # The Lua script can't always return as many IDs as you want. So we loop
95
+ # until we have the exact amount.
96
+ while ids.length < needed_count
97
+ @data_type = data_type
98
+ @count = needed_count - ids.length
99
+ @custom_timestamp = ts
100
+
101
+ batch_ids = response.sequence.map do |sequence|
102
+ (
103
+ shifted_timestamp |
104
+ shifted_logical_shard_id |
105
+ shifted_data_type |
106
+ (sequence << config.sequence_shift)
107
+ )
108
+ end
109
+
110
+ ids += batch_ids
111
+ @response = nil
112
+
113
+ # Safety check to prevent infinite loops
114
+ break unless batch_ids.any?
115
+ end
116
+
117
+ # Place IDs in correct positions to maintain order
118
+ indices.each_with_index do |original_index, i|
119
+ results[original_index] = ids[i] if i < ids.length
120
+ end
121
+ end
122
+
123
+ results
124
+ end
43
125
 
44
126
  def shifted_timestamp
45
127
  timestamp = if custom_timestamp
@@ -70,4 +152,5 @@ class VIN
70
152
  @response ||= Request.new(config, data_type, count, custom_timestamp: custom_timestamp).response
71
153
  end
72
154
  end
155
+ # rubocop:enable Metrics/ClassLength
73
156
  end
data/lib/vin/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class VIN
2
- VERSION = "1.0.0".freeze
2
+ VERSION = "2.0.0".freeze
3
3
  end
data/lib/vin.rb CHANGED
@@ -8,23 +8,27 @@ class VIN
8
8
  @config = config || VIN::Config.new
9
9
  end
10
10
 
11
- def generate_id(data_type, timestamp: nil)
12
- generator.generate_ids(data_type, 1, timestamp: timestamp).first
11
+ def generate_id(data_type)
12
+ generator.generate_id(data_type)
13
13
  end
14
14
 
15
- def generate_ids(data_type, count, timestamp: nil)
15
+ def generate_ids(data_type, count)
16
16
  ids = []
17
17
  # The Lua script can't always return as many IDs as you may want. So we loop
18
18
  # until we have the exact amount.
19
19
  while ids.length < count
20
20
  initial_id_count = ids.length
21
- ids += generator.generate_ids(data_type, count - ids.length, timestamp: timestamp)
21
+ ids += generator.generate_ids(data_type, count - ids.length)
22
22
  # Ensure the ids array keeps growing as infinite loop insurance
23
23
  return ids unless ids.length > initial_id_count
24
24
  end
25
25
  ids
26
26
  end
27
27
 
28
+ def generate_ids_from_timestamps(data_type, timestamps:)
29
+ generator.generate_ids_from_timestamps(data_type, timestamps: timestamps)
30
+ end
31
+
28
32
  private
29
33
 
30
34
  def generator
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hoov_vin
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roger Oba
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-08-19 00:00:00.000000000 Z
11
+ date: 2025-09-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis