clusterid 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Gem Version](https://badge.fury.io/rb/clusterid.svg)](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: []
|