dogtag 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 040cf6345d43641af35bd1982b190cde7143c481
4
+ data.tar.gz: d2f72279c3599a6fc07bee47683b0a3d431bc425
5
+ SHA512:
6
+ metadata.gz: a0d87a8746ea3e7c9fb47a49ebb68e326810d871c59046119a319c2847cf149b7f14c2cd9b0f094ff6d5cb9dce39ba8d9ce6cbb3e571db942c50b27823536e39
7
+ data.tar.gz: ee27ededce69e8de0f1e6c1f859ef05e9c693f98d8616c60d574001843a334946893d9182101a26bb89d6ea6705dd6bc70774aca2d2f8867312c2430a70ba8fc
@@ -0,0 +1,48 @@
1
+ require 'redis'
2
+ require 'dogtag/mixins/redis'
3
+
4
+ module Dogtag
5
+ extend Dogtag::Mixins::Redis
6
+
7
+ CUSTOM_EPOCH = 1483228800000 # in milliseconds
8
+
9
+ TIMESTAMP_BITS = 41
10
+ LOGICAL_SHARD_ID_BITS = 10
11
+ SEQUENCE_BITS = 12
12
+
13
+ SEQUENCE_SHIFT = 0
14
+ LOGICAL_SHARD_ID_SHIFT = SEQUENCE_BITS
15
+ TIMESTAMP_SHIFT = SEQUENCE_BITS + LOGICAL_SHARD_ID_BITS
16
+
17
+ LOGICAL_SHARD_ID_KEY = 'dogtag-generator-logical-shard-id'.freeze
18
+
19
+ def self.logical_shard_id=(logical_shard_id)
20
+ redis.set LOGICAL_SHARD_ID_KEY, logical_shard_id
21
+ end
22
+
23
+ def self.generate_id
24
+ Generator.new(1).ids.first
25
+ end
26
+
27
+ def self.generate_ids(count)
28
+ ids = []
29
+
30
+ # The Lua script can't always return as many IDs as you may want. So we loop
31
+ # until we have the exact amount.
32
+ while ids.length < count
33
+ initial_id_count = ids.length
34
+ ids += Generator.new(count - ids.length).ids
35
+
36
+ # Ensure the ids array keeps growing as infinite loop insurance
37
+ return ids unless ids.length > initial_id_count
38
+ end
39
+
40
+ ids
41
+ end
42
+ end
43
+
44
+ require 'dogtag/generator'
45
+ require 'dogtag/id'
46
+ require 'dogtag/request'
47
+ require 'dogtag/response'
48
+ require 'dogtag/timestamp'
@@ -0,0 +1,34 @@
1
+ module Dogtag
2
+ class Generator
3
+ def initialize(count = 1)
4
+ @count = count
5
+ end
6
+
7
+ def ids
8
+ response.sequence.map do |sequence|
9
+ (
10
+ shifted_timestamp |
11
+ shifted_logical_shard_id |
12
+ (sequence << Dogtag::SEQUENCE_SHIFT)
13
+ )
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :count
20
+
21
+ def shifted_timestamp
22
+ timestamp = Timestamp.from_redis(response.seconds, response.microseconds_part)
23
+ timestamp.with_epoch(Dogtag::CUSTOM_EPOCH).milliseconds << Dogtag::TIMESTAMP_SHIFT
24
+ end
25
+
26
+ def shifted_logical_shard_id
27
+ response.logical_shard_id << Dogtag::LOGICAL_SHARD_ID_SHIFT
28
+ end
29
+
30
+ def response
31
+ @response ||= Request.new(count).response
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,29 @@
1
+ module Dogtag
2
+ class Id
3
+ SEQUENCE_MAP = ~(-1 << Dogtag::SEQUENCE_BITS) << Dogtag::SEQUENCE_SHIFT
4
+ LOGICAL_SHARD_ID_MAP = (~(-1 << Dogtag::LOGICAL_SHARD_ID_BITS)) << Dogtag::LOGICAL_SHARD_ID_SHIFT
5
+ TIMESTAMP_MAP = ~(-1 << Dogtag::TIMESTAMP_BITS) << Dogtag::TIMESTAMP_SHIFT
6
+
7
+ attr_reader :id
8
+
9
+ def initialize(id)
10
+ @id = id
11
+ end
12
+
13
+ def custom_timestamp
14
+ (id & TIMESTAMP_MAP) >> Dogtag::TIMESTAMP_SHIFT
15
+ end
16
+
17
+ def timestamp
18
+ @timestamp ||= Timestamp.new(custom_timestamp, epoch: Dogtag::CUSTOM_EPOCH)
19
+ end
20
+
21
+ def logical_shard_id
22
+ (id & LOGICAL_SHARD_ID_MAP) >> Dogtag::LOGICAL_SHARD_ID_SHIFT
23
+ end
24
+
25
+ def sequence
26
+ (id & SEQUENCE_MAP) >> Dogtag::SEQUENCE_SHIFT
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,10 @@
1
+ module Dogtag
2
+ module Mixins
3
+ module Redis
4
+ def redis
5
+ # TODO: Redis config
6
+ @redis ||= ::Redis.new
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,63 @@
1
+ module Dogtag
2
+ class Request
3
+ include Dogtag::Mixins::Redis
4
+
5
+ MAX_SEQUENCE = ~(-1 << Dogtag::SEQUENCE_BITS)
6
+ MIN_LOGICAL_SHARD_ID = 1
7
+ MAX_LOGICAL_SHARD_ID = ~(-1 << Dogtag::LOGICAL_SHARD_ID_BITS)
8
+ LUA_SCRIPT_PATH = 'lua/id-generation.lua'.freeze
9
+ MAX_TRIES = 5
10
+
11
+ def initialize(count = 1)
12
+ raise ArgumentError, 'count must be a number' unless count.is_a? Numeric
13
+ raise ArgumentError, 'count must be greater than zero' unless count > 0
14
+
15
+ @tries = 0
16
+ @count = count
17
+ end
18
+
19
+ def response
20
+ Response.new(try_redis_response)
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :count
26
+
27
+ def lua_script
28
+ File.read(
29
+ File.expand_path "../../#{LUA_SCRIPT_PATH}", File.dirname(__FILE__)
30
+ )
31
+ end
32
+
33
+ def lua_args
34
+ lua_args = [
35
+ MAX_SEQUENCE,
36
+ MIN_LOGICAL_SHARD_ID,
37
+ MAX_LOGICAL_SHARD_ID,
38
+ count
39
+ ]
40
+ end
41
+
42
+ # NOTE: If too many requests come in inside of a millisecond the Lua script
43
+ # will lock for 1ms and throw an error. This is meant to retry in those cases.
44
+ def try_redis_response
45
+ begin
46
+ @tries += 1
47
+ redis_response
48
+ rescue Redis::CommandError => err
49
+ if @tries < MAX_TRIES
50
+ # Exponentially sleep more and more on each try
51
+ sleep (@tries * @tries).to_f / 900
52
+ retry
53
+ else
54
+ raise err
55
+ end
56
+ end
57
+ end
58
+
59
+ def redis_response
60
+ @redis_response ||= redis.eval(lua_script, keys: lua_args)
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,41 @@
1
+ module Dogtag
2
+ class Response
3
+ START_SEQUENCE_INDEX = 0
4
+ END_SEQUENCE_INDEX = 1
5
+ LOGICAL_SHARD_ID_INDEX = 2
6
+ SECONDS_INDEX = 3
7
+ MICROSECONDS_INDEX = 4
8
+
9
+ def initialize(redis_response)
10
+ @redis_response = redis_response
11
+ end
12
+
13
+ def sequence
14
+ start_sequence..end_sequence
15
+ end
16
+
17
+ def start_sequence
18
+ redis_response[START_SEQUENCE_INDEX]
19
+ end
20
+
21
+ def end_sequence
22
+ redis_response[END_SEQUENCE_INDEX]
23
+ end
24
+
25
+ def logical_shard_id
26
+ redis_response[LOGICAL_SHARD_ID_INDEX]
27
+ end
28
+
29
+ def seconds
30
+ redis_response[SECONDS_INDEX]
31
+ end
32
+
33
+ def microseconds_part
34
+ redis_response[MICROSECONDS_INDEX]
35
+ end
36
+
37
+ private
38
+
39
+ attr_reader :redis_response
40
+ end
41
+ end
@@ -0,0 +1,48 @@
1
+ module Dogtag
2
+ class Timestamp
3
+ ONE_SECOND_IN_MILLIS = 1_000
4
+ ONE_MILLI_IN_MICRO_SECS = 1_000
5
+
6
+ attr_reader :milliseconds, :epoch
7
+
8
+ def initialize(milliseconds, epoch: 0)
9
+ @milliseconds = milliseconds
10
+ @epoch = epoch
11
+ end
12
+
13
+ def seconds
14
+ (milliseconds / ONE_SECOND_IN_MILLIS).floor
15
+ end
16
+
17
+ def microseconds_part
18
+ (milliseconds - (seconds * ONE_SECOND_IN_MILLIS)) * ONE_MILLI_IN_MICRO_SECS
19
+ end
20
+
21
+ alias to_i milliseconds
22
+
23
+ def to_time
24
+ Time.at(with_unix_epoch.seconds, with_unix_epoch.microseconds_part)
25
+ end
26
+
27
+ def with_unix_epoch
28
+ @with_unix_epoch ||= with_epoch(0)
29
+ end
30
+
31
+ def with_epoch(new_epoch)
32
+ new_milliseconds = milliseconds - (new_epoch - epoch)
33
+
34
+ self.class.new(new_milliseconds, epoch: new_epoch)
35
+ end
36
+
37
+ def self.from_redis(seconds_part, microseconds_part)
38
+ # NOTE: we're dropping the microseconds here because we don't need that
39
+ # level of precision
40
+ milliseconds = (
41
+ (seconds_part * ONE_SECOND_IN_MILLIS) +
42
+ (microseconds_part / ONE_MILLI_IN_MICRO_SECS)
43
+ )
44
+
45
+ new(milliseconds)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,3 @@
1
+ module Dogtag
2
+ VERSION = '0.1.0'.freeze
3
+ end
@@ -0,0 +1,61 @@
1
+ local lock_key = 'dogtag-generator-lock'
2
+ local sequence_key = 'dogtag-generator-sequence'
3
+ local logical_shard_id_key = 'dogtag-generator-logical-shard-id'
4
+
5
+ local max_sequence = tonumber(KEYS[1])
6
+ local min_logical_shard_id = tonumber(KEYS[2])
7
+ local max_logical_shard_id = tonumber(KEYS[3])
8
+ local num_ids = tonumber(KEYS[4])
9
+
10
+ if redis.call('EXISTS', lock_key) == 1 then
11
+ redis.log(redis.LOG_NOTICE, 'Dogtag: Cannot generate ID, waiting for lock to expire.')
12
+ return redis.error_reply('Dogtag: Cannot generate ID, waiting for lock to expire.')
13
+ end
14
+
15
+ --[[
16
+ Increment by a set number, this can
17
+ --]]
18
+ local end_sequence = redis.call('INCRBY', sequence_key, num_ids)
19
+ local start_sequence = end_sequence - num_ids + 1
20
+ local logical_shard_id = tonumber(redis.call('GET', logical_shard_id_key)) or -1
21
+
22
+ if end_sequence >= max_sequence then
23
+ --[[
24
+ As the sequence is about to roll around, we can't generate another ID until we're sure we're not in the same
25
+ millisecond since we last rolled. This is because we may have already generated an ID with the same time and
26
+ sequence, and we cannot allow even the smallest possibility of duplicates. It's also because if we roll the sequence
27
+ around, we will start generating IDs with smaller values than the ones previously in this millisecond - that would
28
+ break our k-ordering guarantees!
29
+
30
+ The only way we can handle this is to block for a millisecond, as we can't store the time due the purity constraints
31
+ of Redis Lua scripts.
32
+
33
+ In addition to a neat side-effect of handling leap seconds (where milliseconds will last a little bit longer to bring
34
+ time back to where it should be) because Redis uses system time internally to expire keys, this prevents any duplicate
35
+ IDs from being generated if the rate of generation is greater than the maximum sequence per millisecond.
36
+
37
+ Note that it only blocks even it rolled around *not* in the same millisecond; this is because unless we do this, the
38
+ IDs won't remain ordered.
39
+ --]]
40
+ redis.log(redis.LOG_NOTICE, 'Dogtag: Rolling sequence back to the start, locking for 1ms.')
41
+ redis.call('SET', sequence_key, '-1')
42
+ redis.call('PSETEX', lock_key, 1, 'lock')
43
+ end_sequence = max_sequence
44
+ end
45
+
46
+ --[[
47
+ The TIME command MUST be called after anything that mutates state, or the Redis server will error the script out.
48
+ This is to ensure the script is "pure" in the sense that randomness or time based input will not change the
49
+ outcome of the writes.
50
+
51
+ See the "Scripts as pure functions" section at http://redis.io/commands/eval for more information.
52
+ --]]
53
+ local time = redis.call('TIME')
54
+
55
+ return {
56
+ start_sequence,
57
+ end_sequence, -- Doesn't need conversion, the result of INCR or the variable set is always a number.
58
+ logical_shard_id,
59
+ tonumber(time[1]),
60
+ tonumber(time[2])
61
+ }
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dogtag
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Adam Crownoble
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-05-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: redis
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.5'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.5'
41
+ description: Generate unique IDs with Redis for distributed systems, based heavily
42
+ off of Icicle and Twitter Snowflake
43
+ email: adam@codenoble.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - lib/dogtag.rb
49
+ - lib/dogtag/generator.rb
50
+ - lib/dogtag/id.rb
51
+ - lib/dogtag/mixins/redis.rb
52
+ - lib/dogtag/request.rb
53
+ - lib/dogtag/response.rb
54
+ - lib/dogtag/timestamp.rb
55
+ - lib/dogtag/version.rb
56
+ - lua/id-generation.lua
57
+ homepage: https://github.com/zillyinc/dogtag
58
+ licenses: []
59
+ metadata: {}
60
+ post_install_message:
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubyforge_project:
76
+ rubygems_version: 2.6.11
77
+ signing_key:
78
+ specification_version: 4
79
+ summary: A Redis-powered Ruby ID generation client
80
+ test_files: []