dogtag 0.1.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/lib/dogtag.rb +48 -0
- data/lib/dogtag/generator.rb +34 -0
- data/lib/dogtag/id.rb +29 -0
- data/lib/dogtag/mixins/redis.rb +10 -0
- data/lib/dogtag/request.rb +63 -0
- data/lib/dogtag/response.rb +41 -0
- data/lib/dogtag/timestamp.rb +48 -0
- data/lib/dogtag/version.rb +3 -0
- data/lua/id-generation.lua +61 -0
- metadata +80 -0
checksums.yaml
ADDED
@@ -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
|
data/lib/dogtag.rb
ADDED
@@ -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
|
data/lib/dogtag/id.rb
ADDED
@@ -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,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,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: []
|