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 +4 -4
- data/README.md +33 -4
- data/lib/vin/generator.rb +92 -9
- data/lib/vin/version.rb +1 -1
- data/lib/vin.rb +8 -4
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e18efeba30d241867cf8506a73dfda375bb900557770d66b31e6e3e3d8356db6
|
|
4
|
+
data.tar.gz: 16a5249cd3f0fc0212a8e5b1dd53cd4b94e939e2f06a56f87281a0a08cd1dd92
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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://
|
|
14
|
-
<img src="https://
|
|
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://
|
|
17
|
-
<img src="https://
|
|
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
|
|
12
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
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
|
|
12
|
-
generator.
|
|
11
|
+
def generate_id(data_type)
|
|
12
|
+
generator.generate_id(data_type)
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
def generate_ids(data_type, count
|
|
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
|
|
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:
|
|
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-
|
|
11
|
+
date: 2025-09-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: redis
|