clusterid 1.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 +7 -0
- data/CHANGELOG.md +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +283 -0
- data/lib/clusterid/constants.rb +6 -0
- data/lib/clusterid/errors.rb +23 -0
- data/lib/clusterid/v1/clock.rb +14 -0
- data/lib/clusterid/v1/generator.rb +56 -0
- data/lib/clusterid/v1/null_serialization.rb +47 -0
- data/lib/clusterid/v1/serialization.rb +47 -0
- data/lib/clusterid/v1/value.rb +119 -0
- data/lib/clusterid/version.rb +8 -0
- data/lib/clusterid.rb +11 -0
- data/sig/clusterid.rbs +67 -0
- metadata +61 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 0b662076bc46a6188f0d587292d36cae6fb54b179d4b5e9077b114c8c36b6c1a
|
4
|
+
data.tar.gz: 9135abab52b8494ee37f0958d34dbceb9122e3b1babe6e5aa0113d76addb0d80
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 262e9dfefed1d936a8f5d406dc2d3d1cd0df8bf3508a4d72ba9cc085adc897035f5e93717594cffabf47289eaeff9d90d0923b195d4a1aa7961affad5ba54867
|
7
|
+
data.tar.gz: eed23a12338f989ca94729a64179a6965bb7b3fc8cfa4db6608a766b77ace168bfe6b2d6bced72e68eee86d4669e6e68071a0f96b4420768854b047d0d102043
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2022 Stephan Tarulli
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,283 @@
|
|
1
|
+
# ClusterID
|
2
|
+
Create unique identifiers for entities in distributed systems.
|
3
|
+
|
4
|
+
[](https://badge.fury.io/rb/clusterid)
|
5
|
+
|
6
|
+
## What's in the Box?
|
7
|
+
✅ Simple usage documentation written to get started fast. [Check it out!](#usage)
|
8
|
+
|
9
|
+
⚡ A fast implementation in pure ruby. [Check it out!](#benchmarks)
|
10
|
+
|
11
|
+
📚 YARD generated API documentation for the library. [Check it out!](https://tinychameleon.github.io/clusterid/)
|
12
|
+
|
13
|
+
🤖 RBS types for your type checking wants. [Check it out!](./sig/clusterid.rbs)
|
14
|
+
|
15
|
+
💎 Tests against many Ruby versions. [Check it out!](#ruby-versions)
|
16
|
+
|
17
|
+
🔒 MFA protection on all gem owners. [Check it out!](https://rubygems.org/gems/clusterid)
|
18
|
+
|
19
|
+
|
20
|
+
## Installation
|
21
|
+
|
22
|
+
Add this line to your application's Gemfile:
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
gem 'clusterid'
|
26
|
+
```
|
27
|
+
|
28
|
+
And then execute:
|
29
|
+
|
30
|
+
$ bundle install
|
31
|
+
|
32
|
+
Or install it yourself as:
|
33
|
+
|
34
|
+
$ gem install clusterid
|
35
|
+
|
36
|
+
### Ruby Versions
|
37
|
+
This gem is tested against the following Ruby versions:
|
38
|
+
|
39
|
+
- 2.6.0
|
40
|
+
- 2.6.9
|
41
|
+
- 2.7.0
|
42
|
+
- 2.7.5
|
43
|
+
- 3.0.0
|
44
|
+
- 3.0.3
|
45
|
+
- 3.1.0
|
46
|
+
- 3.1.1
|
47
|
+
|
48
|
+
|
49
|
+
## Usage
|
50
|
+
Create an intance of `ClusterId::V1::Generator` and then use the `generate` method to create `ClusterId::V1::Value` instances.
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
generator = ClusterId::V1::Generator.new
|
54
|
+
id = generator.generate
|
55
|
+
```
|
56
|
+
|
57
|
+
This kind of quick usage will use the default byte generator, clock, and serialization classes. The default serialization
|
58
|
+
classes are null objects which will:
|
59
|
+
|
60
|
+
- return `nil` for deserializing any data centre, environment, or type ID
|
61
|
+
- serialize any value for data centre, environment, or type ID as `0`.
|
62
|
+
|
63
|
+
If you would like to take advantage of these custom attributes see the [Custom Attributes & Serialization section below](#custom-attributes--serialization).
|
64
|
+
|
65
|
+
### Values
|
66
|
+
The `ClusterId::V1::Value` objects that are created have a small API which allows read access to the individual components
|
67
|
+
which make up the byte string. For example, to access the byte string for storage:
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
value = generator.generate
|
71
|
+
puts value.bytes
|
72
|
+
```
|
73
|
+
|
74
|
+
[See the API docs for the full method reference](https://tinychameleon.github.io/clusterid/ClusterId/V1/Value.html).
|
75
|
+
|
76
|
+
### Values are `Comparable`
|
77
|
+
The `ClusterId::V1::Value` class implements the `Comparable` interface, so you can sort them, check for equality, and use the typical comparison operators.
|
78
|
+
|
79
|
+
### Reconstituting `ClusterId::V1::Value` Objects
|
80
|
+
If you've saved the byte string from one of these objects somewhere, you can manually create it again if you have a `ClusterId::V1::Deserializer`
|
81
|
+
or a `ClusterId::V1::Generator`.
|
82
|
+
|
83
|
+
With a generator configured with the correct `Deserializer`, you can use the `from_byte_string` method:
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
byte_string = "..." # From storage, perhaps.
|
87
|
+
generator = ... # Some existing generator with the correct configuration.
|
88
|
+
value = generator.from_byte_string(byte_string)
|
89
|
+
```
|
90
|
+
|
91
|
+
Without a generator, you need to provide the `Deserializer` instance yourself:
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
byte_string = "..." # From storage, perhaps.
|
95
|
+
deserializer = ClusterId::V1::NullDeserializer.new
|
96
|
+
value = ClusterId::V1::Value.new(byte_string, deserializer)
|
97
|
+
```
|
98
|
+
|
99
|
+
### Custom Attributes & Serialization
|
100
|
+
You can create subclasses of `ClusterId::V1::Serializer` and `ClusterId::V1::Deserializer` to represent your custom values for data centre, environment, and type IDs.
|
101
|
+
The `Serializer` will need to accept your application's types for the custom attributes and return integer values, while the `Deserializer` should do the opposite.
|
102
|
+
|
103
|
+
You can use any type as long as you implement the serialization and deserialization steps. For example, here are two simple classes that use only `Symbols`.
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
class ExampleDeserializer < ClusterId::V1::Deserializer
|
107
|
+
def to_data_centre(i)
|
108
|
+
return :north_america if i == 1
|
109
|
+
:global
|
110
|
+
end
|
111
|
+
|
112
|
+
def to_environment(i)
|
113
|
+
return :production if i == 3
|
114
|
+
:non_production
|
115
|
+
end
|
116
|
+
|
117
|
+
def to_type_id(i)
|
118
|
+
return :user if i == 1
|
119
|
+
return :settings if i == 2
|
120
|
+
:unknown
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
class ExampleSerializer < ClusterId::V1::Serializer
|
125
|
+
def from_data_centre(s)
|
126
|
+
return 1 if s == :north_america
|
127
|
+
2
|
128
|
+
end
|
129
|
+
|
130
|
+
def from_environment(s)
|
131
|
+
return 3 if s == :production
|
132
|
+
1
|
133
|
+
end
|
134
|
+
|
135
|
+
def from_type_id(s)
|
136
|
+
return 1 if s == :user
|
137
|
+
return 2 if s == :settings
|
138
|
+
99
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Then pass them to the Generator constructor
|
143
|
+
g = ClusterId::V1::Generator.new(serializer: ExampleSerializer.new, deserializer: ExampleDeserializer.new)
|
144
|
+
```
|
145
|
+
|
146
|
+
Once you've implemented these custom classes you can provide your custom attributes to the `generate` method:
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
v = g.generate(data_centre: :north_america, environment: :production, type_id: :user)
|
150
|
+
puts v.type_id # Output: :user
|
151
|
+
```
|
152
|
+
|
153
|
+
#### NOTE
|
154
|
+
When you implement custom serialization classes you become responsible for ensuring the bit length requirements for each
|
155
|
+
attribute are correct. [See the API documentation for details](https://tinychameleon.github.io/clusterid/ClusterId/V1/Value.html).
|
156
|
+
|
157
|
+
### Custom Byte Generators
|
158
|
+
If you need to use a byte generator other than the default of `SecureRandom` you can implement your own.
|
159
|
+
The only requirement is to have a `bytes(n)` method which returns a byte string. For example:
|
160
|
+
|
161
|
+
```ruby
|
162
|
+
module GuaranteedRandom
|
163
|
+
DICE_ROLL = [4].cycle
|
164
|
+
|
165
|
+
def bytes(n)
|
166
|
+
DICE_ROLL.take(n).pack("C")
|
167
|
+
end
|
168
|
+
end
|
169
|
+
```
|
170
|
+
|
171
|
+
Once you have a custom byte generator you can provide it to the `ClusterId::V1::Generator` constructor:
|
172
|
+
|
173
|
+
```ruby
|
174
|
+
g = ClusterId::V1::Generator(byte_generator: GuaranteedRandom)
|
175
|
+
```
|
176
|
+
|
177
|
+
### Custom Clock
|
178
|
+
If you need to implement a custom clock for millisecond timestamps you can implement your own.
|
179
|
+
The only requirement is to have a `now_ms` method which returns the millisecond time as an `Integer`.
|
180
|
+
For example:
|
181
|
+
|
182
|
+
```ruby
|
183
|
+
module UltraAccurateClock
|
184
|
+
def now_ms
|
185
|
+
expensive_hardware.get_time_ms
|
186
|
+
end
|
187
|
+
end
|
188
|
+
```
|
189
|
+
|
190
|
+
Once you have created a custom clock you can provide it to the `ClusterId::V1::Generator` constructor:
|
191
|
+
|
192
|
+
```ruby
|
193
|
+
g = ClusterId::V1::Generator(clock: UltraAccurateClock)
|
194
|
+
```
|
195
|
+
|
196
|
+
|
197
|
+
## Contributing
|
198
|
+
|
199
|
+
### Development
|
200
|
+
To get started development on this gem run the `bin/setup` command. This will install dependencies and run the tests and linting tasks to ensure everything is working.
|
201
|
+
|
202
|
+
For an interactive console with the gem loaded run `bin/console`.
|
203
|
+
|
204
|
+
|
205
|
+
### Testing
|
206
|
+
Use the `bundle exec rake test` command to run unit tests. To install the gem onto your local machine for general integration testing use `bundle exec rake install`.
|
207
|
+
|
208
|
+
To test the gem against each supported version of Ruby use `bin/test_versions`. This will create a Docker image for each version and run the tests and linting steps.
|
209
|
+
|
210
|
+
|
211
|
+
### Releasing
|
212
|
+
Do the following to release a new version of this gem:
|
213
|
+
|
214
|
+
- Update the version number in [lib/clusterid/version.rb](./lib/clusterid/version.rb)
|
215
|
+
- Ensure necessary documentation changes are complete
|
216
|
+
- Ensure changes are in the [CHANGELOG.md](./CHANGELOG.md)
|
217
|
+
- Create the new release using `bundle exec rake release`
|
218
|
+
|
219
|
+
After this is done the following side-effects should be visible:
|
220
|
+
|
221
|
+
- A new git tag for the version number should exist
|
222
|
+
- Commits for the new version should be pushed to GitHub
|
223
|
+
- The new gem should be available on [rubygems.org](https://rubygems.org).
|
224
|
+
|
225
|
+
Finally, update the documentation hosted on GitHub Pages:
|
226
|
+
|
227
|
+
- Check-out the `gh-pages` branch
|
228
|
+
- Merge `main` into the `gh-pages` branch
|
229
|
+
- Generate the documentation with `bundle exec rake yard`
|
230
|
+
- Commit the documentation on the `gh-pages` branch
|
231
|
+
- Push the new documentation so GitHub Pages can deploy it
|
232
|
+
|
233
|
+
## Benchmarks
|
234
|
+
Benchmarking is tricky and the goal of a benchmark should be clear before attempting performance improvements. The goal of this library for performance is as follows:
|
235
|
+
|
236
|
+
> This library should be capable of generating new values and instantiating existing values at a rate which does not make it a bottleneck for the majority of web APIs.
|
237
|
+
|
238
|
+
Given the above goal statement, these benchmarks run on the following environment:
|
239
|
+
|
240
|
+
| Attribute | Value |
|
241
|
+
|:--|--:|
|
242
|
+
| Ruby Version | 3.1.0 |
|
243
|
+
| MacOS Version | Catalina 10.15.7 (19H1615) |
|
244
|
+
| MacOS Model Identifier | MacBookPro10,1 |
|
245
|
+
| MacOS Processor Name | Quad-Core Intel Core i7 |
|
246
|
+
| MacOS Processor Speed | 2.7 GHz |
|
247
|
+
| MacOS Number of Processors | 1 |
|
248
|
+
| MacOS Total Number of Cores | 4 |
|
249
|
+
| MacOS L2 Cache (per Core) | 256 KB |
|
250
|
+
| MacOS L3 Cache | 6 MB |
|
251
|
+
| MacOS Hyper-Threading Technology | Enabled |
|
252
|
+
| MacOS Memory | 16 GB |
|
253
|
+
|
254
|
+
### `ClusterId::V1`
|
255
|
+
The performance is approximately as follows when run using:
|
256
|
+
|
257
|
+
- The default byte generator and clock
|
258
|
+
- Simple serializer and deserializer classes
|
259
|
+
- A constant 16 bytes of data for `Value` instantiation
|
260
|
+
|
261
|
+
```
|
262
|
+
~/…/clusterid› bundle exec rake benchmark
|
263
|
+
date; bundle exec ruby test/benchmarks/current.rb
|
264
|
+
Sat Feb 26 17:47:17 PST 2022
|
265
|
+
Warming up --------------------------------------
|
266
|
+
generate 23.924k i/100ms
|
267
|
+
from_byte_string 129.369k i/100ms
|
268
|
+
value 135.606k i/100ms
|
269
|
+
Calculating -------------------------------------
|
270
|
+
generate 241.890k (± 2.8%) i/s - 1.220M in 5.048294s
|
271
|
+
from_byte_string 1.270M (± 5.2%) i/s - 6.339M in 5.004944s
|
272
|
+
value 1.358M (± 6.2%) i/s - 6.780M in 5.014634s
|
273
|
+
```
|
274
|
+
|
275
|
+
Being conservative in estimation and assuming:
|
276
|
+
|
277
|
+
- all nodes in a cluster have identical millisecond timestamp values
|
278
|
+
- 240 generated clusterid values per millisecond
|
279
|
+
- approximately 150,000 values generated for a 5 byte value yields a 1% collision rate
|
280
|
+
|
281
|
+
The `generate` method supports approximately 625 machines in a given cluster before the chance of a value collision reaches 1%.
|
282
|
+
The `value` instantiation has ample performance and should not be a bottleneck for generation of new values.
|
283
|
+
The `from_byte_string` API is nearly as fast as the value constructor and therefore should also not be a bottleneck.
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ClusterId
|
4
|
+
# The base error for all errors in {ClusterId}.
|
5
|
+
class Error < StandardError; end
|
6
|
+
|
7
|
+
# An error representing an invalid byte length for a value.
|
8
|
+
class InvalidByteLengthError < Error
|
9
|
+
# @param length [Integer] the byte length considered invalid
|
10
|
+
def initialize(length)
|
11
|
+
super("Expected #{BYTE_SIZE} bytes, got #{length}")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# An error representing an invalid data format version for a value.
|
16
|
+
class InvalidVersionError < Error
|
17
|
+
# @param expected [Integer] the expected data format version
|
18
|
+
# @param received [Integer] the received data format version
|
19
|
+
def initialize(expected, received)
|
20
|
+
super("Expected version #{expected}, got #{received}")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ClusterId
|
4
|
+
module V1
|
5
|
+
# A millisecond clock which extracts time information from the +Process+
|
6
|
+
# module.
|
7
|
+
class Clock
|
8
|
+
# @return [Integer] A millisecond timestamp since the Unix epoch
|
9
|
+
def self.now_ms
|
10
|
+
Process.clock_gettime Process::CLOCK_REALTIME, :millisecond
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "securerandom"
|
4
|
+
|
5
|
+
module ClusterId
|
6
|
+
module V1
|
7
|
+
# Create instances of {Value} objects based on time, random data, and serializable
|
8
|
+
# values for data centre, environment, and type IDs.
|
9
|
+
class Generator
|
10
|
+
# @note It is the caller's responsibility to ensure the serializer and deserializer
|
11
|
+
# follow the expected bit lengths for custom types. No overflow checks are done.
|
12
|
+
# Overflow will either be discarded or overwrite other bits. See the {Value}
|
13
|
+
# class for allowed bit lengths.
|
14
|
+
#
|
15
|
+
# @param serializer [Serializer] an object which serializes {Value} attributes
|
16
|
+
# @param deserializer [Deserializer] an object which deserializes {Value} attributes
|
17
|
+
# @param byte_generator [#bytes(n)] an object which provides random bytes
|
18
|
+
# @param clock [#now_ms] an object which provides millisecond timestamps
|
19
|
+
def initialize(serializer: NullSerializer.new, deserializer: NullDeserializer.new, byte_generator: SecureRandom, clock: Clock)
|
20
|
+
@serializer = serializer
|
21
|
+
@deserializer = deserializer
|
22
|
+
@byte_generator = byte_generator
|
23
|
+
@clock = clock
|
24
|
+
end
|
25
|
+
|
26
|
+
# Generate a {Value} for a given data centre, environment, and type ID.
|
27
|
+
#
|
28
|
+
# @param data_centre [D] a serializable value representing the data centre the {Value} is generated within
|
29
|
+
# @param environment [E] a serializable value representing the environment the {Value} is generated within
|
30
|
+
# @param type_id [T] a serializable value representing the type of entity for a generated {Value}
|
31
|
+
# @return [Value] a time-based, partially random value to identify an entity
|
32
|
+
def generate(data_centre: nil, environment: nil, type_id: nil)
|
33
|
+
raw_data = +""
|
34
|
+
# Nonce
|
35
|
+
raw_data += @byte_generator.bytes(5)
|
36
|
+
# Type ID
|
37
|
+
raw_data += [@serializer.from_type_id(type_id)].pack("S")
|
38
|
+
# Details (Version, Data Centre, Environment)
|
39
|
+
raw_data += [
|
40
|
+
(FORMAT_VERSION << 5) +
|
41
|
+
(@serializer.from_data_centre(data_centre) << 2) +
|
42
|
+
@serializer.from_environment(environment)
|
43
|
+
].pack("C")
|
44
|
+
# Timestamp
|
45
|
+
raw_data += [@clock.now_ms].pack("Q")
|
46
|
+
|
47
|
+
Value.new(raw_data.freeze, @deserializer)
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [Value] the {Value} represented by +s+
|
51
|
+
def from_byte_string(s)
|
52
|
+
Value.new(s, @deserializer)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ClusterId
|
4
|
+
module V1
|
5
|
+
# A default {Deserializer} that returns +nil+ for all attributes.
|
6
|
+
class NullDeserializer < Deserializer
|
7
|
+
# Null method which ignores all data centre values.
|
8
|
+
# @return [nil]
|
9
|
+
def to_data_centre(_)
|
10
|
+
nil
|
11
|
+
end
|
12
|
+
|
13
|
+
# Null method which ignores all environment values.
|
14
|
+
# @return [nil]
|
15
|
+
def to_environment(_)
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
|
19
|
+
# Null method which ignores all type ID values.
|
20
|
+
# @return [nil]
|
21
|
+
def to_type_id(_)
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# A default {Serializer} that returns +0+ for all attributes.
|
27
|
+
class NullSerializer < Serializer
|
28
|
+
# Zero method which ignores all data centre values.
|
29
|
+
# @return [0]
|
30
|
+
def from_data_centre(_)
|
31
|
+
0
|
32
|
+
end
|
33
|
+
|
34
|
+
# Zero method which ignores all environment values.
|
35
|
+
# @return [0]
|
36
|
+
def from_environment(_)
|
37
|
+
0
|
38
|
+
end
|
39
|
+
|
40
|
+
# Zero method which ignores all type ID values.
|
41
|
+
# @return [0]
|
42
|
+
def from_type_id(_)
|
43
|
+
0
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ClusterId
|
4
|
+
module V1
|
5
|
+
# The abstract base class of all custom serialization logic.
|
6
|
+
class Serializer
|
7
|
+
# @param t [T] the environment
|
8
|
+
# @return [Integer] the serialized environment
|
9
|
+
def from_environment(t)
|
10
|
+
raise NotImplementedError
|
11
|
+
end
|
12
|
+
|
13
|
+
# @param t [T] the data centre
|
14
|
+
# @return [Integer] the serialized data centre
|
15
|
+
def from_data_centre(t)
|
16
|
+
raise NotImplementedError
|
17
|
+
end
|
18
|
+
|
19
|
+
# @param t [T] the type ID
|
20
|
+
# @return [Integer] the serialized type ID
|
21
|
+
def from_type_id(t)
|
22
|
+
raise NotImplementedError
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# The abstract base class of all custom deserializtion logic.
|
27
|
+
class Deserializer
|
28
|
+
# @param i [Integer] the serialized environment
|
29
|
+
# @return [T] the deserialized environment
|
30
|
+
def to_environment(i)
|
31
|
+
raise NotImplementedError
|
32
|
+
end
|
33
|
+
|
34
|
+
# @param i [Integer] the serialized data centre
|
35
|
+
# @return [T] the deserialized data centre
|
36
|
+
def to_data_centre(i)
|
37
|
+
raise NotImplementedError
|
38
|
+
end
|
39
|
+
|
40
|
+
# @param i [Integer] the serialized type ID
|
41
|
+
# @return [T] the deserialized type ID
|
42
|
+
def to_type_id(i)
|
43
|
+
raise NotImplementedError
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "date"
|
4
|
+
|
5
|
+
module ClusterId
|
6
|
+
# Data format version 1 for {ClusterId} identifiers. This format supports
|
7
|
+
# custom value serialization for data centre, environment, and type IDs.
|
8
|
+
#
|
9
|
+
# @see Value
|
10
|
+
# @see Generator
|
11
|
+
module V1
|
12
|
+
# The data format version
|
13
|
+
FORMAT_VERSION = 1
|
14
|
+
|
15
|
+
# A {ClusterId} version 1 format value.
|
16
|
+
#
|
17
|
+
# A {Value} contains the following accessible properties:
|
18
|
+
#
|
19
|
+
# - a 64-bit millisecond timestamp as a +DateTime+
|
20
|
+
# - a 3-bit version number
|
21
|
+
# - a 3-bit data centre identifier
|
22
|
+
# - a 2-bit environment identifier
|
23
|
+
# - a 16-bit type identifier
|
24
|
+
# - a 40-bit random nonce
|
25
|
+
#
|
26
|
+
# It also provides access to the underlying bytes.
|
27
|
+
#
|
28
|
+
# Instances of {Value} are +Comparable+ based on the timestamp.
|
29
|
+
#
|
30
|
+
# === Random Nonce
|
31
|
+
# A 5 byte random nonce supports generating approximately 150,000 values before
|
32
|
+
# reaching a 1% chance of collision. This applies to each millisecond.
|
33
|
+
#
|
34
|
+
# === Data Layout
|
35
|
+
# The byte layout of a version 1 value in little-endian is:
|
36
|
+
# o-------------------------------------------------------------------------------o
|
37
|
+
# | byte 01 | byte 02 | byte 03 | byte 04 | byte 05 | byte 06 | byte 07 | byte 08 |
|
38
|
+
# |-------------------------------------------------------------------------------|
|
39
|
+
# | random nonce | type id | details |
|
40
|
+
# |-------------------------------------------------------------------------------|
|
41
|
+
# | byte 09 | byte 10 | byte 11 | byte 12 | byte 13 | byte 14 | byte 15 | byte 16 |
|
42
|
+
# |-------------------------------------------------------------------------------|
|
43
|
+
# | timestamp |
|
44
|
+
# o-------------------------------------------------------------------------------o
|
45
|
+
# With the +details+ byte encoding the following data:
|
46
|
+
# o---------------------------------------------------------------o
|
47
|
+
# | bit 8 | bit 7 | bit 6 | bit 5 | bit 4 | bit 3 | bit 2 | bit 1 |
|
48
|
+
# |---------------------------------------------------------------|
|
49
|
+
# | version | data centre | environment |
|
50
|
+
# o---------------------------------------------------------------o
|
51
|
+
class Value
|
52
|
+
include Comparable
|
53
|
+
|
54
|
+
# @return [String] the underlying value bytes
|
55
|
+
attr_reader :bytes
|
56
|
+
|
57
|
+
# [RUBY 2.6][RUBY 2.7]
|
58
|
+
# Referencing undefined instance variables causes warnings to be emitted,
|
59
|
+
# so +instance_variable_defined?+ is used to determine memoization status.
|
60
|
+
|
61
|
+
# @return [DateTime] the value's creation datetime
|
62
|
+
def datetime
|
63
|
+
return @dt if instance_variable_defined? :@dt
|
64
|
+
@dt = DateTime.strptime(bytes[8..].unpack1("Q").to_s, "%Q")
|
65
|
+
end
|
66
|
+
|
67
|
+
# @return [Integer] the value's random nonce
|
68
|
+
def nonce
|
69
|
+
return @nonce if instance_variable_defined? :@nonce
|
70
|
+
@nonce = (bytes[0..4] + "\x00\x00\x00").unpack1("Q")
|
71
|
+
end
|
72
|
+
|
73
|
+
# @return [Integer] the value's version
|
74
|
+
def version
|
75
|
+
FORMAT_VERSION
|
76
|
+
end
|
77
|
+
|
78
|
+
# @return [T] the value's deserialized environment
|
79
|
+
def environment
|
80
|
+
return @env if instance_variable_defined? :@env
|
81
|
+
@env = @deserializer.to_environment bytes[7].unpack1("C") & 0x03
|
82
|
+
end
|
83
|
+
|
84
|
+
# @return [T] the value's deserialized data centre
|
85
|
+
def data_centre
|
86
|
+
return @dc if instance_variable_defined? :@dc
|
87
|
+
@dc = @deserializer.to_data_centre (bytes[7].unpack1("C") & 0x1c) >> 2
|
88
|
+
end
|
89
|
+
|
90
|
+
# @return [T] the value's deserialized type ID
|
91
|
+
def type_id
|
92
|
+
return @tid if instance_variable_defined? :@tid
|
93
|
+
@tid = @deserializer.to_type_id bytes[5..6].unpack1("S")
|
94
|
+
end
|
95
|
+
|
96
|
+
# @param bytes [String] the value as a byte string
|
97
|
+
# @param deserializer [Deserializer] a {Deserializer} to decode custom value attributes
|
98
|
+
# @raise [InvalidByteLengthError] when the length of bytes is not {BYTE_SIZE}
|
99
|
+
# @raise [InvalidVersionError] when the data format version is not {FORMAT_VERSION}
|
100
|
+
def initialize(bytes, deserializer)
|
101
|
+
raise InvalidByteLengthError, bytes.length unless bytes.length == BYTE_SIZE
|
102
|
+
|
103
|
+
version = (bytes[7].unpack1("C") & 0xe0) >> 5
|
104
|
+
raise InvalidVersionError.new(FORMAT_VERSION, version) unless version == FORMAT_VERSION
|
105
|
+
|
106
|
+
@bytes = bytes
|
107
|
+
@deserializer = deserializer
|
108
|
+
end
|
109
|
+
|
110
|
+
# Compares {Value} objects using {#datetime}
|
111
|
+
#
|
112
|
+
# @param other [Value] the object to compare against
|
113
|
+
# @return [-1, 0, 1] -1 when less than +other+, 0 when equal to +other+, 1 when greater than +other+
|
114
|
+
def <=>(other)
|
115
|
+
datetime <=> other.datetime
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
data/lib/clusterid.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "clusterid/version"
|
4
|
+
require_relative "clusterid/constants"
|
5
|
+
require_relative "clusterid/errors"
|
6
|
+
|
7
|
+
require_relative "clusterid/v1/serialization"
|
8
|
+
require_relative "clusterid/v1/null_serialization"
|
9
|
+
require_relative "clusterid/v1/clock"
|
10
|
+
require_relative "clusterid/v1/value"
|
11
|
+
require_relative "clusterid/v1/generator"
|
data/sig/clusterid.rbs
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
module ClusterId
|
2
|
+
VERSION: String
|
3
|
+
BYTE_SIZE: 16
|
4
|
+
|
5
|
+
class Error < StandardError
|
6
|
+
end
|
7
|
+
|
8
|
+
class InvalidByteLengthError < Error
|
9
|
+
def initialize: (Integer) -> void
|
10
|
+
end
|
11
|
+
|
12
|
+
class InvalidVersionError < Error
|
13
|
+
def initialize: (Integer, Integer) -> void
|
14
|
+
end
|
15
|
+
|
16
|
+
module V1
|
17
|
+
FORMAT_VERSION: 1
|
18
|
+
|
19
|
+
class Deserializer[D, E, T]
|
20
|
+
def to_data_centre: (Integer) -> D
|
21
|
+
def to_environment: (Integer) -> E
|
22
|
+
def to_type_id: (Integer) -> T
|
23
|
+
end
|
24
|
+
|
25
|
+
class Serializer[D, E, T]
|
26
|
+
def from_data_centre: (D) -> Integer
|
27
|
+
def from_environment: (E) -> Integer
|
28
|
+
def from_type_id: (T) -> Integer
|
29
|
+
end
|
30
|
+
|
31
|
+
interface _ByteSource
|
32
|
+
def bytes: (Integer) -> String
|
33
|
+
end
|
34
|
+
|
35
|
+
interface _Clock
|
36
|
+
def now_ms: () -> Integer
|
37
|
+
end
|
38
|
+
|
39
|
+
class Value[D, E, T]
|
40
|
+
attr_reader bytes: String
|
41
|
+
def datetime: () -> DateTime
|
42
|
+
def nonce: () -> Integer
|
43
|
+
def version: () -> Integer
|
44
|
+
def environment: () -> E
|
45
|
+
def data_centre: () -> D
|
46
|
+
def type_id: () -> T
|
47
|
+
|
48
|
+
def initialize: (String, Deserializer[D, E, T]) -> void
|
49
|
+
end
|
50
|
+
|
51
|
+
class Generator[D, E, T]
|
52
|
+
def generate: (data_centre: D, environment: E, type_id: T) -> Value[D, E, T]
|
53
|
+
|
54
|
+
def initialize: (?serializer: Serializer[D, E, T], ?deserializer: Deserializer[D, E, T], ?byte_generator: _ByteSource, ?clock: _Clock) -> void
|
55
|
+
end
|
56
|
+
|
57
|
+
class Clock
|
58
|
+
include _Clock
|
59
|
+
end
|
60
|
+
|
61
|
+
class NullSerializer < Serializer[top, top, top]
|
62
|
+
end
|
63
|
+
|
64
|
+
class NullDeserializer < Deserializer[nil, nil, nil]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
metadata
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: clusterid
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Stephan Tarulli
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-02-27 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Create unique identifiers for entities in distributed systems
|
14
|
+
email:
|
15
|
+
- srt@tinychameleon.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- CHANGELOG.md
|
21
|
+
- LICENSE.txt
|
22
|
+
- README.md
|
23
|
+
- lib/clusterid.rb
|
24
|
+
- lib/clusterid/constants.rb
|
25
|
+
- lib/clusterid/errors.rb
|
26
|
+
- lib/clusterid/v1/clock.rb
|
27
|
+
- lib/clusterid/v1/generator.rb
|
28
|
+
- lib/clusterid/v1/null_serialization.rb
|
29
|
+
- lib/clusterid/v1/serialization.rb
|
30
|
+
- lib/clusterid/v1/value.rb
|
31
|
+
- lib/clusterid/version.rb
|
32
|
+
- sig/clusterid.rbs
|
33
|
+
homepage: https://github.com/tinychameleon/clusterid
|
34
|
+
licenses:
|
35
|
+
- MIT
|
36
|
+
metadata:
|
37
|
+
homepage_uri: https://github.com/tinychameleon/clusterid
|
38
|
+
source_code_uri: https://github.com/tinychameleon/clusterid
|
39
|
+
changelog_uri: https://github.com/tinychameleon/clusterid/blob/main/CHANGELOG.md
|
40
|
+
bug_tracker_uri: https://github.com/tinychameleon/clusterid/issues
|
41
|
+
documentation_uri: https://tinychameleon.github.com/clusterid/
|
42
|
+
post_install_message:
|
43
|
+
rdoc_options: []
|
44
|
+
require_paths:
|
45
|
+
- lib
|
46
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: 2.6.0
|
51
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
requirements: []
|
57
|
+
rubygems_version: 3.3.3
|
58
|
+
signing_key:
|
59
|
+
specification_version: 4
|
60
|
+
summary: Create unique identifiers for entities in distributed systems
|
61
|
+
test_files: []
|