redlics 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/.travis.yml +24 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +64 -0
- data/Rakefile +9 -0
- data/lib/redlics.rb +94 -0
- data/lib/redlics/config.rb +62 -0
- data/lib/redlics/connection.rb +79 -0
- data/lib/redlics/counter.rb +94 -0
- data/lib/redlics/exception.rb +30 -0
- data/lib/redlics/granularity.rb +49 -0
- data/lib/redlics/key.rb +244 -0
- data/lib/redlics/lua/script.lua +82 -0
- data/lib/redlics/operators.rb +51 -0
- data/lib/redlics/query.rb +234 -0
- data/lib/redlics/query/operation.rb +135 -0
- data/lib/redlics/time_frame.rb +110 -0
- data/lib/redlics/tracker.rb +64 -0
- data/lib/redlics/version.rb +4 -0
- data/redlics.gemspec +33 -0
- data/test/redlics_test.rb +13 -0
- metadata +167 -0
@@ -0,0 +1,30 @@
|
|
1
|
+
module Redlics
|
2
|
+
|
3
|
+
# Exception namespace
|
4
|
+
module Exception
|
5
|
+
|
6
|
+
# Error Pattern namespace
|
7
|
+
module ErrorPatterns
|
8
|
+
NOSCRIPT = /^NOSCRIPT/.freeze
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
# Lua Range Error class
|
13
|
+
#
|
14
|
+
# Maximal Lua stack size for the method `unpack` is by default 8000.
|
15
|
+
# To change this parameter in Redis an own make and build of Redis is needed.
|
16
|
+
# @see https://github.com/antirez/redis/blob/3.2/deps/lua/src/luaconf.h
|
17
|
+
class LuaRangeError < StandardError;
|
18
|
+
|
19
|
+
# Initialization with default error message.
|
20
|
+
#
|
21
|
+
# @param msg [String] the error message
|
22
|
+
# @return [Redlics::Exception::LuaRangeError] error message
|
23
|
+
def initialize(msg = 'Too many keys (max. 8000 keys defined by LUAI_MAXCSTACK)')
|
24
|
+
super(msg)
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Redlics
|
2
|
+
|
3
|
+
# Granularity namespace
|
4
|
+
module Granularity
|
5
|
+
|
6
|
+
extend self
|
7
|
+
|
8
|
+
|
9
|
+
# Validate granularities by given context.
|
10
|
+
#
|
11
|
+
# @param context [Hash] the hash of a context defined in Redlics::CONTEXTS
|
12
|
+
# @param granularities [Range] granularity range
|
13
|
+
# @param granularities [String] single granularity
|
14
|
+
# @param granularities [Array] granularity array
|
15
|
+
# @return [Array] includes all valid granularities
|
16
|
+
def validate(context, granularities)
|
17
|
+
check(granularities) || default(context)
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
# Get default granularities by given context.
|
22
|
+
#
|
23
|
+
# @param context [Hash] the hash of a context defined in Redlics::CONTEXTS
|
24
|
+
# @return [Array] includes all valid default granularities
|
25
|
+
def default(context)
|
26
|
+
check(Redlics.config["#{context[:long]}_granularity"]) || [Redlics.config.granularities.keys.first]
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# Check if granularities are defined in the configuration.
|
33
|
+
#
|
34
|
+
# @param granularities [Range] granularity range
|
35
|
+
# @param granularities [String] single granularity
|
36
|
+
# @param granularities [Array] granularity array
|
37
|
+
# @return [Array] includes all valid granularities
|
38
|
+
def check(granularities)
|
39
|
+
keys = Redlics.config.granularities.keys
|
40
|
+
checked = if granularities.is_a?(Range)
|
41
|
+
keys[keys.index(granularities.first)..keys.index(granularities.last)]
|
42
|
+
else
|
43
|
+
[granularities].flatten & Redlics.config.granularities.keys
|
44
|
+
end
|
45
|
+
checked.any? ? checked : nil
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
data/lib/redlics/key.rb
ADDED
@@ -0,0 +1,244 @@
|
|
1
|
+
module Redlics
|
2
|
+
|
3
|
+
# Key namespace
|
4
|
+
module Key
|
5
|
+
|
6
|
+
extend self
|
7
|
+
|
8
|
+
|
9
|
+
# Construct the key name with given parameters.
|
10
|
+
#
|
11
|
+
# @param context [Hash] the hash of a context defined in Redlics::CONTEXTS
|
12
|
+
# @param event [String] event name with eventual Redis namespace separator
|
13
|
+
# @param granularity [Symbol] existing granularity
|
14
|
+
# @param past [Time] a time object
|
15
|
+
# @param options [Hash] configuration options
|
16
|
+
# @return [String] unbucketized key name
|
17
|
+
# @return [Array] bucketized key name
|
18
|
+
def name(context, event, granularity, past, options = {})
|
19
|
+
past ||= Time.now
|
20
|
+
granularity = Granularity.validate(context, granularity).first
|
21
|
+
event = encode_event(event) if Redlics.config.encode[:events]
|
22
|
+
key = "#{context[:short]}#{Redlics.config.separator}#{event}#{Redlics.config.separator}#{time_format(granularity, past)}"
|
23
|
+
key = with_namespace(key) if options[:namespaced]
|
24
|
+
return bucketize(key, options[:id]) if bucketize?(context, options)
|
25
|
+
return unbucketize(key, options[:id]) if context[:long] == :counter && !options[:id].nil?
|
26
|
+
key
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
# Construct an array with all keys of a time frame in a given granularity.
|
31
|
+
#
|
32
|
+
# @param context [Hash] the hash of a context defined in Redlics::CONTEXTS
|
33
|
+
# @param event [String] event name with eventual Redis namespace separator
|
34
|
+
# @param time_object [Symbol] time object predefined in Redlics::TimeFrame.init_with_symbol
|
35
|
+
# @param time_object [Hash] time object with keys `from` and `to`
|
36
|
+
# @param time_object [Range] time object as range
|
37
|
+
# @param time_object [Time] time object
|
38
|
+
# @param options [Hash] configuration options
|
39
|
+
# @return [Array] array with all keys of a time frame in a given granularity
|
40
|
+
def timeframed(context, event, time_object, options = {})
|
41
|
+
options = { namespaced: true }.merge(options)
|
42
|
+
timeframe = TimeFrame.new(context, time_object, options)
|
43
|
+
timeframe.splat do |time|
|
44
|
+
name(context, event, timeframe.granularity, time, options)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
# Prepend namespace to a key.
|
50
|
+
#
|
51
|
+
# @param key [String] the key name
|
52
|
+
# @return [String] the key name with prepended namespace
|
53
|
+
def with_namespace(key)
|
54
|
+
return key unless Redlics.config.namespace.length > 0
|
55
|
+
return key if key.split(Redlics.config.separator).first == Redlics.config.namespace.to_s
|
56
|
+
"#{Redlics.config.namespace}#{Redlics.config.separator}#{key}"
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
# Encode a number with a mapping table.
|
61
|
+
#
|
62
|
+
# @param number [Integer] the number to encode
|
63
|
+
# @return [String] the encoded number as string
|
64
|
+
def encode(number)
|
65
|
+
encoded = ''
|
66
|
+
number = number.to_s
|
67
|
+
number = (number.size % 2) != 0 ? "0#{number}" : number
|
68
|
+
token = 0
|
69
|
+
while token <= number.size - 1
|
70
|
+
encoded += encode_map[number[token..token+1].to_i.to_s.to_sym].to_s
|
71
|
+
token += 2
|
72
|
+
end
|
73
|
+
encoded
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
# Decode a number with a mapping table.
|
78
|
+
#
|
79
|
+
# @param string [String] the string to encode
|
80
|
+
# @return [Integer] the decoded string as integer
|
81
|
+
def decode(string)
|
82
|
+
decoded = ''
|
83
|
+
string = string.to_s
|
84
|
+
token = 0
|
85
|
+
while token <= string.size - 1
|
86
|
+
number = decode_map[string[token].to_s.to_sym].to_s
|
87
|
+
decoded += number.size == 1 ? "0#{number}" : number
|
88
|
+
token += 1
|
89
|
+
end
|
90
|
+
decoded.to_i
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
# Check if a key exists in Redis.
|
95
|
+
#
|
96
|
+
# @param string [String] the key name to check
|
97
|
+
# @return [Boolean] true id key exists, false if not
|
98
|
+
def exists?(key)
|
99
|
+
Redlics.redis.exists(key) == 1
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
# Check if Redlics can bucketize.
|
104
|
+
#
|
105
|
+
# @param context [Hash] the hash of a context defined in Redlics::CONTEXTS
|
106
|
+
# @param options [Hash] configuration options
|
107
|
+
# @return [Boolean] true if can bucketize, false if not
|
108
|
+
def bucketize?(context, options = {})
|
109
|
+
context[:long] == :counter && Redlics.config.bucket && !options[:id].nil?
|
110
|
+
end
|
111
|
+
|
112
|
+
|
113
|
+
# Create a unique operation key in Redis.
|
114
|
+
# @return [String] the created unique operation key
|
115
|
+
def unique_namespace
|
116
|
+
loop do
|
117
|
+
ns = operation
|
118
|
+
unless exists?(ns)
|
119
|
+
Redlics.redis.pipelined do |redis|
|
120
|
+
redis.set(ns, 0)
|
121
|
+
redis.expire(ns, Redlics.config.operation_expiration)
|
122
|
+
end
|
123
|
+
break ns
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
# Create a operation key.
|
132
|
+
# @return [String] the created operation key
|
133
|
+
def operation
|
134
|
+
"#{Redlics::CONTEXTS[:operation][:short]}#{Redlics.config.separator}#{SecureRandom.uuid}"
|
135
|
+
end
|
136
|
+
|
137
|
+
|
138
|
+
# Get the time format pattern of a granularity.
|
139
|
+
#
|
140
|
+
# @param granularity [Symbol] existing granularity
|
141
|
+
# @param past [Time] a time object
|
142
|
+
# @return [String] pattern of defined granularity
|
143
|
+
def time_format(granularity, past)
|
144
|
+
past.strftime(Redlics.config.granularities[granularity][:pattern])
|
145
|
+
end
|
146
|
+
|
147
|
+
|
148
|
+
# Encode ids in event names.
|
149
|
+
#
|
150
|
+
# @param event [String] event name with eventual Redis namespace separator
|
151
|
+
# @return [String] event name with encoded ids
|
152
|
+
def encode_event(event)
|
153
|
+
event.to_s.split(Redlics.config.separator).map { |v| v.match(/\A\d+\z/) ? encode(v) : v }.join(Redlics.config.separator)
|
154
|
+
end
|
155
|
+
|
156
|
+
|
157
|
+
# Bucketize key name with id.
|
158
|
+
#
|
159
|
+
# @param key [String] key name
|
160
|
+
# @param id [Integer] object id
|
161
|
+
# @return [Array] bucketized key name and value
|
162
|
+
def bucketize(key, id)
|
163
|
+
bucket = id.to_i / Redlics.config.bucket_size.to_i
|
164
|
+
value = id.to_i % Redlics.config.bucket_size.to_i
|
165
|
+
if Redlics.config.encode[:ids]
|
166
|
+
bucket = encode(bucket)
|
167
|
+
value = encode(value)
|
168
|
+
end
|
169
|
+
["#{key}#{Redlics.config.separator}#{bucket}", value]
|
170
|
+
end
|
171
|
+
|
172
|
+
|
173
|
+
# Unbucketize key name with id. Encode the id if configured to encode.
|
174
|
+
#
|
175
|
+
# @param key [String] key name
|
176
|
+
# @param id [Integer] object id
|
177
|
+
# @return [String] unbucketized key name with eventual encoded object id
|
178
|
+
def unbucketize(key, id)
|
179
|
+
id = encode(id) if Redlics.config.encode[:ids]
|
180
|
+
"#{key}#{Redlics.config.separator}#{id}"
|
181
|
+
end
|
182
|
+
|
183
|
+
|
184
|
+
# Defined encode map.
|
185
|
+
# @return [Hash] the encode map with numbers as keys
|
186
|
+
def encode_map
|
187
|
+
@encode_map ||= replace_separator_encode({
|
188
|
+
'0' => '1', '1' => '2', '2' => '3', '3' => '4', '4' => '5', '5' => '6', '6' => '7', '7' => '8', '8' => '9', '9' => '0',
|
189
|
+
'10' => '-', '11' => '=', '12' => '!', '13' => '@', '14' => '#', '15' => '$', '16' => '%', '17' => '^', '18' => '&', '19' => '*',
|
190
|
+
'20' => '(', '21' => ')', '22' => '_', '23' => '+', '24' => 'a', '25' => 'b', '26' => 'c', '27' => 'd', '28' => 'e', '29' => 'f',
|
191
|
+
'30' => 'g', '31' => 'h', '32' => 'i', '33' => 'j', '34' => 'k', '35' => 'l', '36' => 'm', '37' => 'n', '38' => 'o', '39' => 'p',
|
192
|
+
'40' => 'q', '41' => 'r', '42' => 's', '43' => 't', '44' => 'u', '45' => 'v', '46' => 'w', '47' => 'x', '48' => 'y', '49' => 'z',
|
193
|
+
'50' => 'A', '51' => 'B', '52' => 'C', '53' => 'D', '54' => 'E', '55' => 'F', '56' => 'G', '57' => 'H', '58' => 'I', '59' => 'J',
|
194
|
+
'60' => 'K', '61' => 'L', '62' => 'M', '63' => 'N', '64' => 'O', '65' => 'P', '66' => 'Q', '67' => 'R', '68' => 'S', '69' => 'T',
|
195
|
+
'70' => 'U', '71' => 'V', '72' => 'W', '73' => 'X', '74' => 'Y', '75' => 'Z', '76' => '[', '77' => ']', '78' => '\\', '79' => ';',
|
196
|
+
'80' => ',', '81' => '.', '82' => '/', '83' => '{', '84' => '}', '85' => '|', '86' => '§', '87' => '<', '88' => '>', '89' => '?',
|
197
|
+
'90' => '`', '91' => '~', '92' => 'ä', '93' => 'Ä', '94' => 'ü', '95' => 'Ü', '96' => 'ö', '97' => 'Ö', '98' => 'é', '99' => 'É' }).freeze
|
198
|
+
end
|
199
|
+
|
200
|
+
|
201
|
+
# Defined decode map.
|
202
|
+
# @return [Hash] the decode map with numbers as values
|
203
|
+
def decode_map
|
204
|
+
@decode_map ||= replace_separator_decode({
|
205
|
+
'1' => '0', '2' => '1', '3' => '2', '4' => '3', '5' => '4', '6' => '5', '7' => '6', '8' => '7', '9' => '8', '0' => '9',
|
206
|
+
'-' => '10', '=' => '11', '!' => '12', '@' => '13', '#' => '14', '$' => '15', '%' => '16', '^' => '17', '&' => '18', '*' => '19',
|
207
|
+
'(' => '20', ')' => '21', '_' => '22', '+' => '23', 'a' => '24', 'b' => '25', 'c' => '26', 'd' => '27', 'e' => '28', 'f' => '29',
|
208
|
+
'g' => '30', 'h' => '31', 'i' => '32', 'j' => '33', 'k' => '34', 'l' => '35', 'm' => '36', 'n' => '37', 'o' => '38', 'p' => '39',
|
209
|
+
'q' => '40', 'r' => '41', 's' => '42', 't' => '43', 'u' => '44', 'v' => '45', 'w' => '46', 'x' => '47', 'y' => '48', 'z' => '49',
|
210
|
+
'A' => '50', 'B' => '51', 'C' => '52', 'D' => '53', 'E' => '54', 'F' => '55', 'G' => '56', 'H' => '57', 'I' => '58', 'J' => '59',
|
211
|
+
'K' => '60', 'L' => '61', 'M' => '62', 'N' => '63', 'O' => '64', 'P' => '65', 'Q' => '66', 'R' => '67', 'S' => '68', 'T' => '69',
|
212
|
+
'U' => '70', 'V' => '71', 'W' => '72', 'X' => '73', 'Y' => '74', 'Z' => '75', '[' => '76', ']' => '77', '\\' => '78', ';' => '79',
|
213
|
+
',' => '80', '.' => '81', '/' => '82', '{' => '83', '}' => '84', '|' => '85', '§' => '86', '<' => '87', '>' => '88', '?' => '89',
|
214
|
+
'`' => '90', '~' => '91', 'ä' => '92', 'Ä' => '93', 'ü' => '94', 'Ü' => '95', 'ö' => '96', 'Ö' => '97', 'é' => '98', 'É' => '99' }).freeze
|
215
|
+
end
|
216
|
+
|
217
|
+
|
218
|
+
# Replace defined separator in configuration from the encode map.
|
219
|
+
#
|
220
|
+
# @param map [Hash] encode map hash
|
221
|
+
# @return [Hash] encode map hash without defined separator in configuration.
|
222
|
+
def replace_separator_encode(map)
|
223
|
+
unless Redlics.config.separator == ':'
|
224
|
+
key = map.key(Redlics.config.separator)
|
225
|
+
map[key] = ':' if key
|
226
|
+
end
|
227
|
+
map
|
228
|
+
end
|
229
|
+
|
230
|
+
|
231
|
+
# Replace defined separator in configuration from the decode map.
|
232
|
+
#
|
233
|
+
# @param map [Hash] decode map hash
|
234
|
+
# @return [Hash] decode map hash without defined separator in configuration.
|
235
|
+
def replace_separator_decode(map)
|
236
|
+
unless Redlics.config.separator == ':'
|
237
|
+
key = Redlics.config.separator.to_s.to_sym
|
238
|
+
map[':'.to_sym] = map.delete(key) if map.key?(key)
|
239
|
+
end
|
240
|
+
map
|
241
|
+
end
|
242
|
+
|
243
|
+
end
|
244
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
---
|
2
|
+
redis.log(redis.LOG_NOTICE, 'Redlics')
|
3
|
+
|
4
|
+
local func = cmsgpack.unpack(ARGV[1])
|
5
|
+
local keys = cmsgpack.unpack(ARGV[2])
|
6
|
+
local options = cmsgpack.unpack(ARGV[3])
|
7
|
+
|
8
|
+
|
9
|
+
local function operate(operator, keys)
|
10
|
+
redis.call('BITOP', operator, options['dest'], unpack(keys))
|
11
|
+
return options['dest']
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
local function AND(keys) return operate('AND', keys) end
|
16
|
+
local function OR(keys) return operate('OR', keys) end
|
17
|
+
local function XOR(keys) return operate('XOR', keys) end
|
18
|
+
local function NOT(keys) return operate('NOT', keys) end
|
19
|
+
local function MINUS(keys)
|
20
|
+
local items = keys
|
21
|
+
local src = table.remove(items, 1)
|
22
|
+
local and_op = AND(keys)
|
23
|
+
return XOR({ src, and_op })
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
local function operation(keys, options)
|
28
|
+
if options['operator'] == 'MINUS' then
|
29
|
+
return MINUS(keys)
|
30
|
+
else
|
31
|
+
return operate(options['operator'], keys)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
local function counts(keys, options)
|
37
|
+
local result
|
38
|
+
if options['bucketized'] then
|
39
|
+
result = 0
|
40
|
+
for i,v in ipairs(keys) do
|
41
|
+
result = result + (redis.call('HGET', v[1], v[2]) or 0)
|
42
|
+
end
|
43
|
+
else
|
44
|
+
result = redis.call('MGET', unpack(keys))
|
45
|
+
end
|
46
|
+
return result
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
local function plot_counts(keys, options)
|
51
|
+
local plot = {}
|
52
|
+
if options['bucketized'] then
|
53
|
+
for i,v in ipairs(keys) do
|
54
|
+
plot[v[1]..v[2]] = (redis.call('HGET', v[1], v[2]) or 0)
|
55
|
+
end
|
56
|
+
else
|
57
|
+
local values = redis.call('MGET', unpack(keys))
|
58
|
+
for i,v in ipairs(keys) do
|
59
|
+
plot[v] = values[i]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
return cjson.encode(plot)
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
local function plot_tracks(keys, options)
|
67
|
+
local plot = {}
|
68
|
+
for i,v in ipairs(keys) do
|
69
|
+
plot[v] = redis.call('bitcount', keys[i])
|
70
|
+
end
|
71
|
+
return cjson.encode(plot)
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
local exportFuncs = {
|
76
|
+
operation = operation,
|
77
|
+
counts = counts,
|
78
|
+
plot_counts = plot_counts,
|
79
|
+
plot_tracks = plot_tracks
|
80
|
+
}
|
81
|
+
|
82
|
+
return exportFuncs[func](keys, options)
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Redlics
|
2
|
+
|
3
|
+
# Operators namespace
|
4
|
+
module Operators
|
5
|
+
|
6
|
+
# AND (&) operator.
|
7
|
+
#
|
8
|
+
# @param query [Redlics::Query] Redlics query object
|
9
|
+
# @return [Redlics::Query::Operation] a Redlics query operation object
|
10
|
+
def &(query)
|
11
|
+
Query::Operation.new('AND', [self, query])
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
# OR (|) operator.
|
16
|
+
#
|
17
|
+
# @param query [Redlics::Query] Redlics query object
|
18
|
+
# @return [Redlics::Query::Operation] a Redlics query operation object
|
19
|
+
def |(query)
|
20
|
+
Query::Operation.new('OR', [self, query])
|
21
|
+
end
|
22
|
+
alias_method :+, :|
|
23
|
+
|
24
|
+
|
25
|
+
# XOR (^) operator.
|
26
|
+
#
|
27
|
+
# @param query [Redlics::Query] Redlics query object
|
28
|
+
# @return [Redlics::Query::Operation] a Redlics query operation object
|
29
|
+
def ^(query)
|
30
|
+
Query::Operation.new('XOR', [self, query])
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
# NOT (-, ~) operator.
|
35
|
+
# @return [Redlics::Query::Operation] a Redlics query operation object
|
36
|
+
def -@()
|
37
|
+
Query::Operation.new('NOT', [self])
|
38
|
+
end
|
39
|
+
alias_method :~@, :-@
|
40
|
+
|
41
|
+
|
42
|
+
# MINUS (-) operator.
|
43
|
+
#
|
44
|
+
# @param query [Redlics::Query] Redlics query object
|
45
|
+
# @return [Redlics::Query::Operation] a Redlics query operation object
|
46
|
+
def -(query)
|
47
|
+
Query::Operation.new('MINUS', [self, query])
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|