clusterid 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,4 @@
1
+ # Changelog
2
+
3
+ ## v1.0.0 - 2022-02-26
4
+ - Initial release
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,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClusterId
4
+ # The expected byte size of {ClusterId} values.
5
+ BYTE_SIZE = 16
6
+ end
@@ -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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Create unique identifiers for entities in distributed systems.
4
+ # @since 1.0.0
5
+ module ClusterId
6
+ # The gem version.
7
+ VERSION = "1.0.0"
8
+ 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: []