redlics 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/.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,234 @@
|
|
1
|
+
module Redlics
|
2
|
+
|
3
|
+
# Query class
|
4
|
+
class Query
|
5
|
+
|
6
|
+
|
7
|
+
# Include Redlics operators.
|
8
|
+
include Redlics::Operators
|
9
|
+
|
10
|
+
|
11
|
+
# Gives read access to the listed instance variables.
|
12
|
+
attr_reader :namespaces
|
13
|
+
|
14
|
+
|
15
|
+
# Initialization of a query object
|
16
|
+
#
|
17
|
+
# @param event [String] event name with eventual Redis namespace separator
|
18
|
+
# @param time_object [Symbol] time object predefined in Redlics::TimeFrame.init_with_symbol
|
19
|
+
# @param time_object [Hash] time object with keys `from` and `to`
|
20
|
+
# @param time_object [Range] time object as range
|
21
|
+
# @param time_object [Time] time object
|
22
|
+
# @param options [Hash] configuration options
|
23
|
+
# @return [Redlics::Query] query object
|
24
|
+
def initialize(event, time_object, options = {})
|
25
|
+
@event = event.freeze
|
26
|
+
@time_object = time_object.freeze
|
27
|
+
@options = options
|
28
|
+
@namespaces = []
|
29
|
+
ObjectSpace.define_finalizer(self, self.class.finalize(namespaces)) if Redlics.config.auto_clean
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
# Get or process counts on Redis.
|
34
|
+
# @return [Integer] count result of given query
|
35
|
+
def counts
|
36
|
+
@counts ||= (
|
37
|
+
result = Redlics.script(Redlics::LUA_SCRIPT, [], ['counts'.to_msgpack, realize_counts!.to_msgpack,
|
38
|
+
{ bucketized: Redlics.config.bucket }.to_msgpack])
|
39
|
+
result.is_a?(Array) ? result.map(&:to_i).reduce(0, :+) : result.to_i
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
# Get or process tracks on Redis.
|
45
|
+
# @return [Integer] tracks result of given query
|
46
|
+
def tracks
|
47
|
+
@tracks ||= Redlics.redis.bitcount(track_bits)
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
# Get or process track bits on Redis.
|
52
|
+
# @return [String] key of track bits result
|
53
|
+
def track_bits
|
54
|
+
@track_bits ||= (
|
55
|
+
@track_bits_namespace = Key.unique_namespace
|
56
|
+
@namespaces << @track_bits_namespace
|
57
|
+
Redlics.script(Redlics::LUA_SCRIPT, [], ['operation'.to_msgpack, realize_tracks!.to_msgpack,
|
58
|
+
{ operator: 'OR', dest: Key.with_namespace(@track_bits_namespace) }.to_msgpack])
|
59
|
+
@track_bits_namespace
|
60
|
+
)
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
# Check if object id exists in track bits.
|
65
|
+
# @return [Boolean] true if exists, false if not
|
66
|
+
# @return [NilClass] nil if no object id is given
|
67
|
+
def exists?
|
68
|
+
@exists ||= @options[:id] ? Redlics.redis.getbit(track_bits, @options[:id]) == 1 : nil
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
# Get or process counts and plot.
|
73
|
+
#
|
74
|
+
# @return [Hash] with date times and counts
|
75
|
+
# @return [NilClass] nil if result has errors
|
76
|
+
def plot_counts
|
77
|
+
@plot_counts ||= (
|
78
|
+
result = JSON.parse(
|
79
|
+
Redlics.script(Redlics::LUA_SCRIPT, [], ['plot_counts'.to_msgpack, realize_counts!.to_msgpack,
|
80
|
+
{ bucketized: Redlics.config.bucket }.to_msgpack])
|
81
|
+
)
|
82
|
+
format_plot(Redlics::CONTEXTS[:counter], result)
|
83
|
+
)
|
84
|
+
rescue JSON::ParserError
|
85
|
+
nil
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
# Get or process tracks and plot.
|
90
|
+
#
|
91
|
+
# @return [Hash] with date times and counts
|
92
|
+
# @return [NilClass] nil if result has errors
|
93
|
+
def plot_tracks
|
94
|
+
@plot_tracks ||= (
|
95
|
+
result = JSON.parse(
|
96
|
+
Redlics.script(Redlics::LUA_SCRIPT, [], ['plot_tracks'.to_msgpack, realize_tracks!.to_msgpack,
|
97
|
+
{}.to_msgpack])
|
98
|
+
)
|
99
|
+
format_plot(Redlics::CONTEXTS[:tracker], result)
|
100
|
+
)
|
101
|
+
rescue JSON::ParserError
|
102
|
+
nil
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
# Get or process counts and show keys to analyze.
|
107
|
+
# @return [Array] list of keys to analyze
|
108
|
+
def realize_counts!
|
109
|
+
@realize_counts ||= (
|
110
|
+
keys = Key.timeframed(Redlics::CONTEXTS[:counter], @event, @time_object, @options)
|
111
|
+
raise Exception::LuaRangeError if keys.length > 8000
|
112
|
+
keys
|
113
|
+
)
|
114
|
+
end
|
115
|
+
|
116
|
+
|
117
|
+
# Get or process tracks and show keys to analyze.
|
118
|
+
# @return [Array] list of keys to analyze
|
119
|
+
def realize_tracks!
|
120
|
+
@realize_tracks ||= (
|
121
|
+
keys = Key.timeframed(Redlics::CONTEXTS[:tracker], @event, @time_object, @options)
|
122
|
+
raise Exception::LuaRangeError if keys.length > 8000
|
123
|
+
keys
|
124
|
+
)
|
125
|
+
end
|
126
|
+
|
127
|
+
|
128
|
+
# Reset processed data (also operation keys on Redis).
|
129
|
+
#
|
130
|
+
# @param space [Symbol] define space to reset
|
131
|
+
# @param space [String] define space to reset
|
132
|
+
# @return [Boolean] true
|
133
|
+
def reset!(space = nil)
|
134
|
+
space = space.to_sym if space
|
135
|
+
case space
|
136
|
+
when :counts, :plot_counts, :plot_tracks, :realize_counts, :realize_tracks
|
137
|
+
instance_variable_set("@#{space}", nil)
|
138
|
+
when :tracks, :exists
|
139
|
+
instance_variable_set("@#{space}", nil)
|
140
|
+
reset_track_bits
|
141
|
+
when :counter
|
142
|
+
@counts, @plot_counts, @realize_counts = [nil] * 3
|
143
|
+
when :tracker
|
144
|
+
@tracks, @exists, @plot_tracks, @realize_tracks = [nil] * 4
|
145
|
+
reset_track_bits
|
146
|
+
else
|
147
|
+
@counts, @tracks, @exists, @plot_counts, @plot_tracks, @realize_counts, @realize_tracks = [nil] * 7
|
148
|
+
reset_track_bits
|
149
|
+
self.class.reset_redis_namespaces(@namespaces)
|
150
|
+
@namespaces = []
|
151
|
+
end
|
152
|
+
return true
|
153
|
+
end
|
154
|
+
|
155
|
+
|
156
|
+
# Check if query is a leaf. A query is always a leaf.
|
157
|
+
# This method is required for query operations.
|
158
|
+
# @return [Boolean] true
|
159
|
+
def is_leaf?
|
160
|
+
true
|
161
|
+
end
|
162
|
+
|
163
|
+
|
164
|
+
# Singleton class
|
165
|
+
class << self
|
166
|
+
|
167
|
+
# Short query access to analyze data.
|
168
|
+
#
|
169
|
+
# @param *args [Array] list of arguments of the query
|
170
|
+
# @return [Redlics::Query] instantiated query object
|
171
|
+
def analyze(*args)
|
172
|
+
options = args.last.instance_of?(Hash) ? args.pop : {}
|
173
|
+
query = case args.size
|
174
|
+
when 2
|
175
|
+
Query.new(args[0], args[1], options)
|
176
|
+
when 3
|
177
|
+
Query.new(args[0], args[1], options.merge!({ id: args[2].to_i }))
|
178
|
+
end
|
179
|
+
return yield query if block_given?
|
180
|
+
query
|
181
|
+
end
|
182
|
+
|
183
|
+
|
184
|
+
# Finalize query called from garbage collector.
|
185
|
+
#
|
186
|
+
# @param namespaces [Array] list of created operation keys in Redis
|
187
|
+
# @return [Integer] result of Redis delete keys
|
188
|
+
# @return [NilClass] nil if namespaces are empty
|
189
|
+
def finalize(namespaces)
|
190
|
+
proc { reset_redis_namespaces(namespaces); puts 'Redlics clean!' }
|
191
|
+
end
|
192
|
+
|
193
|
+
|
194
|
+
# Reset Redis created namespace keys.
|
195
|
+
#
|
196
|
+
# @param namespaces [Array] list of created operation keys in Redis
|
197
|
+
# @return [Integer] result of Redis delete keys
|
198
|
+
# @return [NilClass] nil if namespaces are empty
|
199
|
+
def reset_redis_namespaces(namespaces)
|
200
|
+
Redlics.redis.del(namespaces) if namespaces.any?
|
201
|
+
end
|
202
|
+
|
203
|
+
end
|
204
|
+
|
205
|
+
|
206
|
+
private
|
207
|
+
|
208
|
+
# Format plot result with time objects as keys.
|
209
|
+
#
|
210
|
+
# @param context [Hash] the hash of a context defined in Redlics::CONTEXTS
|
211
|
+
# @param result [Hash] the result hash with Redis keys as hash keys
|
212
|
+
# @return [Hash] the formatted result hash
|
213
|
+
def format_plot(context, result)
|
214
|
+
granularity = Granularity.validate(context, @options[:granularity]).first
|
215
|
+
pattern = Redlics.config.granularities[granularity][:pattern]
|
216
|
+
el = Key.bucketize?(context, @options) ? -2 : -1
|
217
|
+
result.keys.each { |k|
|
218
|
+
result[Time.strptime(k.split(Redlics.config.separator)[el], pattern)] = result.delete(k)
|
219
|
+
}
|
220
|
+
result
|
221
|
+
end
|
222
|
+
|
223
|
+
|
224
|
+
# Reset track bits (also operation key on Redis).
|
225
|
+
# @return [NilClass] nil
|
226
|
+
def reset_track_bits
|
227
|
+
self.class.reset_redis_namespaces([@track_bits_namespace])
|
228
|
+
@namespaces.delete(@track_bits_namespace)
|
229
|
+
@track_bits, @track_bits_namespace = nil, nil
|
230
|
+
end
|
231
|
+
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
@@ -0,0 +1,135 @@
|
|
1
|
+
module Redlics
|
2
|
+
class Query
|
3
|
+
|
4
|
+
# Operation class
|
5
|
+
class Operation
|
6
|
+
|
7
|
+
# Include Redlics operators.
|
8
|
+
include Redlics::Operators
|
9
|
+
|
10
|
+
|
11
|
+
# Gives read access to the listed instance variables.
|
12
|
+
attr_reader :namespaces
|
13
|
+
|
14
|
+
|
15
|
+
# Initialization of a query operation object.
|
16
|
+
#
|
17
|
+
# @param operator [String] operator to calculate
|
18
|
+
# @param queries [Array] queries to calculate with the given operator
|
19
|
+
# @return [Redlics::Query::Operation] query operation object
|
20
|
+
def initialize(operator, queries)
|
21
|
+
@operator = operator.upcase.freeze
|
22
|
+
@queries = queries.freeze
|
23
|
+
@track_bits = nil
|
24
|
+
@namespaces = []
|
25
|
+
ObjectSpace.define_finalizer(self, self.class.finalize(namespaces)) if Redlics.config.auto_clean
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
# Get or process tracks on Redis.
|
30
|
+
# @return [Integer] tracks result of given query operation
|
31
|
+
def tracks
|
32
|
+
@tracks ||= (
|
33
|
+
Redlics.redis.bitcount(@track_bits || traverse)
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
# Get or process track bits on Redis.
|
39
|
+
# @return [String] key of track bits result
|
40
|
+
def track_bits
|
41
|
+
@track_bits ||= (
|
42
|
+
keys = []
|
43
|
+
track_bits_namespace = Key.unique_namespace
|
44
|
+
@namespaces << track_bits_namespace
|
45
|
+
if @operator == 'NOT'
|
46
|
+
keys << Key.with_namespace(@queries[0].track_bits)
|
47
|
+
else
|
48
|
+
@queries.each { |q| keys << Key.with_namespace(q.track_bits) }
|
49
|
+
end
|
50
|
+
Redlics.script(Redlics::LUA_SCRIPT, [], ['operation'.to_msgpack, keys.to_msgpack,
|
51
|
+
{ operator: @operator, dest: Key.with_namespace(track_bits_namespace) }.to_msgpack])
|
52
|
+
track_bits_namespace
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
# Check if object id exists in track bits.
|
58
|
+
#
|
59
|
+
# @param [Integer] the object id to check
|
60
|
+
# @return [Boolean] true if exists, false if not
|
61
|
+
def exists?(id)
|
62
|
+
Redlics.redis.getbit(@track_bits || traverse, id.to_i) == 1
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
# Reset processed data (also operation keys on Redis).
|
67
|
+
#
|
68
|
+
# @param space [Symbol] define space to reset
|
69
|
+
# @param space [String] define space to reset
|
70
|
+
# @return [Boolean] true
|
71
|
+
def reset!(space = nil)
|
72
|
+
space = space.to_sym if space
|
73
|
+
case space
|
74
|
+
when :tree
|
75
|
+
@queries.each { |q| q.reset!(:tree) }
|
76
|
+
reset!
|
77
|
+
else
|
78
|
+
@tracks, @track_bits = nil, nil
|
79
|
+
self.class.reset_redis_namespaces(@namespaces)
|
80
|
+
@namespaces = []
|
81
|
+
end
|
82
|
+
return true
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
# Check if query operation is a leaf in the binary tree.
|
87
|
+
# @return [Boolean] true if a leaf, false if not
|
88
|
+
def is_leaf?
|
89
|
+
is_a?(Redlics::Query::Operation) && @track_bits.nil?
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
# Singleton class
|
94
|
+
class << self
|
95
|
+
|
96
|
+
# Finalize query operation called from garbage collector.
|
97
|
+
#
|
98
|
+
# @param namespaces [Array] list of created operation keys in Redis
|
99
|
+
# @return [Integer] result of Redis delete keys
|
100
|
+
# @return [NilClass] nil if namespaces are empty
|
101
|
+
def finalize(namespaces)
|
102
|
+
proc { reset_redis_namespaces(namespaces) }
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
# Reset Redis created namespace keys.
|
107
|
+
#
|
108
|
+
# @param namespaces [Array] list of created operation keys in Redis
|
109
|
+
# @return [Integer] result of Redis delete keys
|
110
|
+
# @return [NilClass] nil if namespaces are empty
|
111
|
+
def reset_redis_namespaces(namespaces)
|
112
|
+
Redlics.redis.del(namespaces) if namespaces.any?
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
|
121
|
+
# Traverse query operation binary tree and calculate operation leafs.
|
122
|
+
# @return [String] result operation key in Redis
|
123
|
+
def traverse
|
124
|
+
if @operator == 'NOT'
|
125
|
+
@queries[0].traverse unless @queries[0].is_leaf?
|
126
|
+
track_bits
|
127
|
+
else
|
128
|
+
@queries.each { |q| q.traverse unless q.is_leaf? }
|
129
|
+
track_bits
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module Redlics
|
2
|
+
|
3
|
+
# Time Frame class
|
4
|
+
class TimeFrame
|
5
|
+
|
6
|
+
# Gives read access to the listed instance variables.
|
7
|
+
attr_reader :from, :to, :granularity
|
8
|
+
|
9
|
+
|
10
|
+
# Initialization of a time frame object.
|
11
|
+
#
|
12
|
+
# @param context [Hash] the hash of a context defined in Redlics::CONTEXTS
|
13
|
+
# @param time_object [Symbol] time object predefined in Redlics::TimeFrame.init_with_symbol
|
14
|
+
# @param time_object [Hash] time object with keys `from` and `to`
|
15
|
+
# @param time_object [Range] time object as range
|
16
|
+
# @param time_object [Time] time object
|
17
|
+
# @param options [Hash] configuration options
|
18
|
+
# @return [Redlics::TimeFrame] time frame object
|
19
|
+
def initialize(context, time_object, options = {})
|
20
|
+
raise ArgumentError, 'TimeFrame should be initialized with Symbol, Hash, Range or Time' unless [Symbol, Hash, Range, Time].include?(time_object.class)
|
21
|
+
@from, @to = self.send("init_with_#{time_object.class.name.demodulize.underscore}", time_object, context)
|
22
|
+
@granularity = Granularity.validate(context, options[:granularity]).first
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
# Construct keys by time frame steps.
|
27
|
+
# @return [Array] keys
|
28
|
+
def splat
|
29
|
+
[].tap do |keys|
|
30
|
+
(from.to_i .. to.to_i).step(Redlics.config.granularities[@granularity][:step]) do |t|
|
31
|
+
keys << (block_given? ? (yield Time.at(t)) : Time.at(t))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
# Initialize time frames `from` and `to` by time.
|
40
|
+
#
|
41
|
+
# @param time [Time] a time
|
42
|
+
# @param context [Hash] the hash of a context defined in Redlics::CONTEXTS
|
43
|
+
# @return [Array] with `from` and `to` time
|
44
|
+
def init_with_time(time, context)
|
45
|
+
[time.beginning_of_day, time.end_of_day]
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
# Initialize time frames `from` and `to` by symbol.
|
50
|
+
#
|
51
|
+
# @param symbol [Symbol] a time span
|
52
|
+
# @param context [Hash] the hash of a context defined in Redlics::CONTEXTS
|
53
|
+
# @return [Array] with `from` and `to` time
|
54
|
+
def init_with_symbol(symbol, context)
|
55
|
+
case symbol
|
56
|
+
when :hour, :day, :week, :month, :year
|
57
|
+
return 1.send(symbol).ago, Time.now
|
58
|
+
when :today
|
59
|
+
return Time.now.beginning_of_day, Time.now
|
60
|
+
when :yesterday
|
61
|
+
return 1.day.ago.beginning_of_day, 1.day.ago.end_of_day
|
62
|
+
when :this_week
|
63
|
+
return Time.now.beginning_of_week, Time.now
|
64
|
+
when :last_week
|
65
|
+
return 1.week.ago.beginning_of_week, 1.week.ago.end_of_week
|
66
|
+
when :this_month
|
67
|
+
return Time.now.beginning_of_month, Time.now
|
68
|
+
when :last_month
|
69
|
+
return 1.month.ago.beginning_of_month, 1.month.ago.end_of_month
|
70
|
+
when :this_year
|
71
|
+
return Time.now.beginning_of_year, Time.now
|
72
|
+
when :last_year
|
73
|
+
return 1.year.ago.beginning_of_year, 1.year.ago.end_of_year
|
74
|
+
else
|
75
|
+
return default(context), Time.now
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
# Initialize time frames `from` and `to` by hash.
|
81
|
+
#
|
82
|
+
# @param hash [Hash] a time hash with keys `from` and `to`
|
83
|
+
# @param context [Hash] the hash of a context defined in Redlics::CONTEXTS
|
84
|
+
# @return [Array] with `from` and `to` time
|
85
|
+
def init_with_hash(hash, context)
|
86
|
+
[ hash[:from] && hash[:from].is_a?(String) && Time.parse(hash[:from]) || hash[:from] || default(context),
|
87
|
+
hash[:to] && hash[:to].is_a?(String) && Time.parse(hash[:to]) || hash[:to] || Time.now ]
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
# Initialize time frames `from` and `to` by hash.
|
92
|
+
#
|
93
|
+
# @param range [Range] a time range
|
94
|
+
# @param context [Hash] the hash of a context defined in Redlics::CONTEXTS
|
95
|
+
# @return [Array] with `from` and `to` time
|
96
|
+
def init_with_range(range, context)
|
97
|
+
init_with_hash({ from: range.first, to: range.last }, context)
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
# Get default granularity by given context.
|
102
|
+
#
|
103
|
+
# @param context [Hash] the hash of a context defined in Redlics::CONTEXTS
|
104
|
+
# @return [ActiveSupport::TimeWithZone] a time
|
105
|
+
def default(context)
|
106
|
+
Redlics.config.granularities[Granularity.default(context).first][:step].ago
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|