dogtag 0.1.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 040cf6345d43641af35bd1982b190cde7143c481
4
- data.tar.gz: d2f72279c3599a6fc07bee47683b0a3d431bc425
3
+ metadata.gz: 3873a444c41218413a933994fd93c9683588e6f5
4
+ data.tar.gz: 561a47c6e821efc9d602d04b985ae1e05e1f1809
5
5
  SHA512:
6
- metadata.gz: a0d87a8746ea3e7c9fb47a49ebb68e326810d871c59046119a319c2847cf149b7f14c2cd9b0f094ff6d5cb9dce39ba8d9ce6cbb3e571db942c50b27823536e39
7
- data.tar.gz: ee27ededce69e8de0f1e6c1f859ef05e9c693f98d8616c60d574001843a334946893d9182101a26bb89d6ea6705dd6bc70774aca2d2f8867312c2430a70ba8fc
6
+ metadata.gz: 0ddf7a7f873c0c3b6a31c80b189cde967aea42569afbec71aeb95ae3361049176d43a525d4270da2ad4dc701fdab2f64b2f9c9aa69da67cbfbeda5f10a79e9db
7
+ data.tar.gz: dd72cbd167d3c91f5fd530108a17f1ab4ecf6fe50fd4efca81aaee763bbd5c69a8904d98ab947f2dd7e070a12b828b9bedbec78e543330013a7e544bf319cdb8
@@ -6,32 +6,58 @@ module Dogtag
6
6
 
7
7
  CUSTOM_EPOCH = 1483228800000 # in milliseconds
8
8
 
9
- TIMESTAMP_BITS = 41
10
- LOGICAL_SHARD_ID_BITS = 10
11
- SEQUENCE_BITS = 12
9
+ TIMESTAMP_BITS = 40
10
+ LOGICAL_SHARD_ID_BITS = 5
11
+ DATA_TYPE_BITS = 8
12
+ SEQUENCE_BITS = 10
13
+
14
+ MIN_LOGICAL_SHARD_ID = 0
15
+ MAX_LOGICAL_SHARD_ID = ~(-1 << LOGICAL_SHARD_ID_BITS)
16
+ LOGICAL_SHARD_ID_ALLOWED_RANGE = (MIN_LOGICAL_SHARD_ID..MAX_LOGICAL_SHARD_ID).freeze
17
+
18
+ MIN_DATA_TYPE = 0
19
+ MAX_DATA_TYPE = ~(-1 << DATA_TYPE_BITS)
20
+ DATA_TYPE_ALLOWED_RANGE = (MIN_DATA_TYPE..MAX_DATA_TYPE).freeze
21
+
22
+ MAX_SEQUENCE = ~(-1 << SEQUENCE_BITS)
12
23
 
13
24
  SEQUENCE_SHIFT = 0
14
- LOGICAL_SHARD_ID_SHIFT = SEQUENCE_BITS
15
- TIMESTAMP_SHIFT = SEQUENCE_BITS + LOGICAL_SHARD_ID_BITS
25
+ DATA_TYPE_SHIFT = SEQUENCE_BITS
26
+ LOGICAL_SHARD_ID_SHIFT = SEQUENCE_BITS + DATA_TYPE_BITS
27
+ TIMESTAMP_SHIFT = SEQUENCE_BITS + DATA_TYPE_BITS + LOGICAL_SHARD_ID_BITS
28
+
29
+ LOGICAL_SHARD_ID_RANGE_KEY = 'dogtag-generator-logical-shard-id-range'.freeze
16
30
 
17
- LOGICAL_SHARD_ID_KEY = 'dogtag-generator-logical-shard-id'.freeze
31
+ def self.logical_shard_id_range=(logical_shard_id_range)
32
+ unless logical_shard_id_range.is_a? Range
33
+ raise ArgumentError, 'logical_shard_id_range must be a range'
34
+ end
35
+
36
+ unless (Dogtag::LOGICAL_SHARD_ID_ALLOWED_RANGE.to_a & logical_shard_id_range.to_a) == logical_shard_id_range.to_a
37
+ raise ArgumentError, "logical_shard_id_range is outside the allowed range of #{Dogtag::LOGICAL_SHARD_ID_ALLOWED_RANGE}"
38
+ end
18
39
 
19
- def self.logical_shard_id=(logical_shard_id)
20
- redis.set LOGICAL_SHARD_ID_KEY, logical_shard_id
40
+ if redis.exists LOGICAL_SHARD_ID_RANGE_KEY
41
+ redis.lset LOGICAL_SHARD_ID_RANGE_KEY, 0, logical_shard_id_range.min
42
+ redis.lset LOGICAL_SHARD_ID_RANGE_KEY, 1, logical_shard_id_range.max
43
+ else
44
+ redis.rpush LOGICAL_SHARD_ID_RANGE_KEY, logical_shard_id_range.min
45
+ redis.rpush LOGICAL_SHARD_ID_RANGE_KEY, logical_shard_id_range.max
46
+ end
21
47
  end
22
48
 
23
- def self.generate_id
24
- Generator.new(1).ids.first
49
+ def self.generate_id(data_type)
50
+ Generator.new(data_type, 1).ids.first
25
51
  end
26
52
 
27
- def self.generate_ids(count)
53
+ def self.generate_ids(data_type, count)
28
54
  ids = []
29
55
 
30
56
  # The Lua script can't always return as many IDs as you may want. So we loop
31
57
  # until we have the exact amount.
32
58
  while ids.length < count
33
59
  initial_id_count = ids.length
34
- ids += Generator.new(count - ids.length).ids
60
+ ids += Generator.new(data_type, count - ids.length).ids
35
61
 
36
62
  # Ensure the ids array keeps growing as infinite loop insurance
37
63
  return ids unless ids.length > initial_id_count
@@ -1,6 +1,18 @@
1
1
  module Dogtag
2
2
  class Generator
3
- def initialize(count = 1)
3
+ def initialize(data_type, count = 1)
4
+ unless data_type.is_a? Integer
5
+ raise ArgumentError, 'data_type must be an integer'
6
+ end
7
+
8
+ unless Dogtag::DATA_TYPE_ALLOWED_RANGE.include? data_type
9
+ raise ArgumentError, "data_type is outside the allowed range of #{Dogtag::DATA_TYPE_ALLOWED_RANGE}"
10
+ end
11
+
12
+ raise ArgumentError, 'count must be an integer' unless count.is_a? Integer
13
+ raise ArgumentError, 'count must be a positive number' if count < 1
14
+
15
+ @data_type = data_type
4
16
  @count = count
5
17
  end
6
18
 
@@ -9,6 +21,7 @@ module Dogtag
9
21
  (
10
22
  shifted_timestamp |
11
23
  shifted_logical_shard_id |
24
+ shifted_data_type |
12
25
  (sequence << Dogtag::SEQUENCE_SHIFT)
13
26
  )
14
27
  end
@@ -16,19 +29,23 @@ module Dogtag
16
29
 
17
30
  private
18
31
 
19
- attr_reader :count
32
+ attr_reader :data_type, :count
20
33
 
21
34
  def shifted_timestamp
22
35
  timestamp = Timestamp.from_redis(response.seconds, response.microseconds_part)
23
36
  timestamp.with_epoch(Dogtag::CUSTOM_EPOCH).milliseconds << Dogtag::TIMESTAMP_SHIFT
24
37
  end
25
38
 
39
+ def shifted_data_type
40
+ data_type << Dogtag::DATA_TYPE_SHIFT
41
+ end
42
+
26
43
  def shifted_logical_shard_id
27
44
  response.logical_shard_id << Dogtag::LOGICAL_SHARD_ID_SHIFT
28
45
  end
29
46
 
30
47
  def response
31
- @response ||= Request.new(count).response
48
+ @response ||= Request.new(data_type, count).response
32
49
  end
33
50
  end
34
51
  end
@@ -1,6 +1,7 @@
1
1
  module Dogtag
2
2
  class Id
3
3
  SEQUENCE_MAP = ~(-1 << Dogtag::SEQUENCE_BITS) << Dogtag::SEQUENCE_SHIFT
4
+ DATA_TYPE_MAP = ~(-1 << Dogtag::DATA_TYPE_BITS) << Dogtag::DATA_TYPE_SHIFT
4
5
  LOGICAL_SHARD_ID_MAP = (~(-1 << Dogtag::LOGICAL_SHARD_ID_BITS)) << Dogtag::LOGICAL_SHARD_ID_SHIFT
5
6
  TIMESTAMP_MAP = ~(-1 << Dogtag::TIMESTAMP_BITS) << Dogtag::TIMESTAMP_SHIFT
6
7
 
@@ -22,6 +23,10 @@ module Dogtag
22
23
  (id & LOGICAL_SHARD_ID_MAP) >> Dogtag::LOGICAL_SHARD_ID_SHIFT
23
24
  end
24
25
 
26
+ def data_type
27
+ (id & DATA_TYPE_MAP) >> Dogtag::DATA_TYPE_SHIFT
28
+ end
29
+
25
30
  def sequence
26
31
  (id & SEQUENCE_MAP) >> Dogtag::SEQUENCE_SHIFT
27
32
  end
@@ -2,7 +2,7 @@ module Dogtag
2
2
  module Mixins
3
3
  module Redis
4
4
  def redis
5
- # TODO: Redis config
5
+ # TODO: Redis config for multiple servers
6
6
  @redis ||= ::Redis.new
7
7
  end
8
8
  end
@@ -2,17 +2,19 @@ module Dogtag
2
2
  class Request
3
3
  include Dogtag::Mixins::Redis
4
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
5
  LUA_SCRIPT_PATH = 'lua/id-generation.lua'.freeze
9
6
  MAX_TRIES = 5
10
7
 
11
- def initialize(count = 1)
8
+ def initialize(data_type, count = 1)
9
+ raise ArgumentError, 'data_type must be a number' unless data_type.is_a? Numeric
10
+ unless Dogtag::DATA_TYPE_ALLOWED_RANGE.include? data_type
11
+ raise ArgumentError, "data_type is outside the allowed range of #{Dogtag::DATA_TYPE_ALLOWED_RANGE}"
12
+ end
12
13
  raise ArgumentError, 'count must be a number' unless count.is_a? Numeric
13
14
  raise ArgumentError, 'count must be greater than zero' unless count > 0
14
15
 
15
16
  @tries = 0
17
+ @data_type = data_type
16
18
  @count = count
17
19
  end
18
20
 
@@ -22,7 +24,7 @@ module Dogtag
22
24
 
23
25
  private
24
26
 
25
- attr_reader :count
27
+ attr_reader :data_type, :count
26
28
 
27
29
  def lua_script
28
30
  File.read(
@@ -31,12 +33,7 @@ module Dogtag
31
33
  end
32
34
 
33
35
  def lua_args
34
- lua_args = [
35
- MAX_SEQUENCE,
36
- MIN_LOGICAL_SHARD_ID,
37
- MAX_LOGICAL_SHARD_ID,
38
- count
39
- ]
36
+ lua_args = [ MAX_SEQUENCE, data_type, count ]
40
37
  end
41
38
 
42
39
  # NOTE: If too many requests come in inside of a millisecond the Lua script
@@ -1,3 +1,3 @@
1
1
  module Dogtag
2
- VERSION = '0.1.0'.freeze
2
+ VERSION = '1.0.0'.freeze
3
3
  end
@@ -1,23 +1,51 @@
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'
1
+ local logical_shard_id_range_key = 'dogtag-generator-logical-shard-id-range'
2
+ local last_logical_shard_id_key = 'dogtag-generator-last-logical-shard-id'
4
3
 
5
4
  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])
5
+ local data_type = tonumber(KEYS[2])
6
+ local num_ids = tonumber(KEYS[3])
7
+
8
+ -- Allow one server to acts as multiple shards
9
+ local logical_shard_id_range = redis.call('SORT', logical_shard_id_range_key)
10
+ if next(logical_shard_id_range) == nil then
11
+ redis.log(redis.LOG_WARNING, 'Dogtag: ' .. logical_shard_id_range_key .. ' has not been set.')
12
+ return redis.error_reply('Dogtag: ' .. logical_shard_id_range_key .. ' has not been set.')
13
+ end
14
+ local logical_shard_id_min = tonumber(logical_shard_id_range[1])
15
+ local logical_shard_id_max = tonumber(logical_shard_id_range[2])
16
+
17
+ local logical_shard_id = nil
18
+ if redis.call('EXISTS', last_logical_shard_id_key) == 0 then
19
+ logical_shard_id = logical_shard_id_min
20
+ else
21
+ local last_shard_id = tonumber(redis.call('GET', last_logical_shard_id_key))
22
+
23
+ if last_shard_id >= logical_shard_id_max or last_shard_id < logical_shard_id_min then
24
+ logical_shard_id = logical_shard_id_min
25
+ else
26
+ logical_shard_id = last_shard_id + 1
27
+ end
28
+ end
29
+
30
+ redis.call('SET', last_logical_shard_id_key, logical_shard_id)
31
+
32
+ --[[
33
+ Scope lock and sequence keys to the specific data_type being requested.
34
+ Ideally, we'd also use the logical_shard_id in the keys so that any per-millisecond limitations would only be per-shard,
35
+ but unfortunately the whole "pure function" limitation keeps us from using a random shard_id here. The best solution may
36
+ be to round robin the shard ID by incrementing a Redis key on each call.
37
+ ]]--
38
+ local lock_key = 'dogtag-generator-lock-' .. logical_shard_id .. '-' .. data_type
39
+ local sequence_key = 'dogtag-generator-sequence-' .. logical_shard_id .. '-' .. data_type
9
40
 
10
41
  if redis.call('EXISTS', lock_key) == 1 then
11
42
  redis.log(redis.LOG_NOTICE, 'Dogtag: Cannot generate ID, waiting for lock to expire.')
12
43
  return redis.error_reply('Dogtag: Cannot generate ID, waiting for lock to expire.')
13
44
  end
14
45
 
15
- --[[
16
- Increment by a set number, this can
17
- --]]
46
+ -- Increment by a set number
18
47
  local end_sequence = redis.call('INCRBY', sequence_key, num_ids)
19
48
  local start_sequence = end_sequence - num_ids + 1
20
- local logical_shard_id = tonumber(redis.call('GET', logical_shard_id_key)) or -1
21
49
 
22
50
  if end_sequence >= max_sequence then
23
51
  --[[
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dogtag
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Crownoble
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-05-09 00:00:00.000000000 Z
11
+ date: 2017-05-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis