rhcf-timeseries 0.0.6 → 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: 9cef009b581b2e4025db5234bc0ba24cb970503d
4
- data.tar.gz: 5168fbcf5b232d4bc3e2687be9b57e4efd6a1cca
3
+ metadata.gz: 6ac0f81efa0f044a261c6919b0107b48e68efe24
4
+ data.tar.gz: 9e4248aec431ffb9c0f8dc07817c4af1189ba458
5
5
  SHA512:
6
- metadata.gz: 2954a59ea1374b1308483c17480463ce3c3e9fc8c34881d9da4aae2728101f50ae44a666b41c0fe424c9475169fb872cf318cad1a771b2c72698fede5943141d
7
- data.tar.gz: 6efdacb63cbc3f5f75a9b6568af314ba86f71b0555037daa36f673032de7480b023d3de7a50a1208911c43c9e510e63944d60ee2a281858fe8cd6f939bd7e91a
6
+ metadata.gz: 15e810d8f9f5b309f4fe92fcd0cc38a18d9ec3522ac85c00eb8f546113fd17cd23d5074fcaec349f49ae0699adb3e5cdede6c7ceff7b583844a25015a46dabaa
7
+ data.tar.gz: d3781b3ab20ef7f918d5f74353b37023323a7b4253b15b52d695efe2f2df50615aeaae8672c07cb3f9eab374ccb5408f56e2f57836695073d642733c4e3199d3
@@ -0,0 +1,34 @@
1
+ class Fixnum
2
+ def minutes
3
+ self * 60
4
+ end
5
+
6
+ def hours
7
+ self.minutes * 60
8
+ end
9
+
10
+ def days
11
+ self.hours * 24
12
+ end
13
+
14
+ def seconds
15
+ self
16
+ end
17
+
18
+ def weeks
19
+ self.days * 7
20
+ end
21
+
22
+ def years
23
+ self.days * 365
24
+ end
25
+
26
+ alias_method :day, :days
27
+ alias_method :week, :weeks
28
+ alias_method :hour, :hours
29
+ alias_method :second, :seconds
30
+ alias_method :minute, :minutes
31
+ alias_method :year, :years
32
+ end
33
+
34
+
@@ -0,0 +1,25 @@
1
+ module Rhcf
2
+ module Timeseries
3
+ EVENT_SET_TOKEN = 'ES'
4
+ EVENT_POINT_TOKEN = 'P'
5
+ DEFAULT_PREFIX = 'TS'
6
+
7
+ DEFAULT_RESOLUTIONS_MAP={
8
+ :ever => {span:Float::INFINITY, formatter: "ever", ttl: (2 * 366).days},
9
+ :year => {span: 365.days,formatter: "%Y", ttl: (2 * 366).days},
10
+ :week => {span: 1.week, formatter: "%Y-CW%w", ttl: 90.days},
11
+ :month => {span: 30.days, formatter: "%Y-%m", ttl: 366.days},
12
+ :day => {span: 1.day, formatter: "%Y-%m-%d", ttl: 30.days},
13
+ :hour => {span: 1.hour, formatter: "%Y-%m-%dT%H", ttl: 24.hours},
14
+ :minute => {span: 1.minute, formatter: "%Y-%m-%dT%H:%M", ttl: 120.minutes},
15
+ :second => {span: 1, formatter: "%Y-%m-%dT%H:%M:%S", ttl: 1.hour},
16
+ :"5seconds" => {span: 5.seconds, formatter: ->(time){ [time.strftime("%Y-%m-%dT%H:%M:") , time.to_i % 60/5, '*',5].join('') }, ttl: 1.hour},
17
+ :"5minutes" => {span: 5.minutes, formatter: ->(time){ [time.strftime("%Y-%m-%dT%H:") , (time.to_i/60) % 60/5, '*',5].join('') }, ttl: 3.hour},
18
+ :"15minutes" => {span: 15.minutes, formatter: ->(time){ [time.strftime("%Y-%m-%dT%H:") , (time.to_i/60) % 60/15, '*',15].join('') }, ttl: 24.hours}
19
+
20
+ }
21
+
22
+ DEFAULT_RESOLUTIONS = DEFAULT_RESOLUTIONS_MAP.keys
23
+ NAMESPACE_SEPARATOR = '|'
24
+ end
25
+ end
@@ -0,0 +1,119 @@
1
+ unless 1.respond_to?(:minute)
2
+ require_relative '../extensions/fixnum'
3
+ end
4
+ require 'rhcf/timeseries/constants'
5
+ require 'rhcf/timeseries/query'
6
+ require 'rhcf/timeseries/redis_strategies'
7
+
8
+ module Rhcf
9
+ module Timeseries
10
+
11
+ class Filter
12
+ attr_reader :regex
13
+ def initialize(keys, values)
14
+ @keys = keys
15
+ @values = values
16
+ end
17
+
18
+ def regex
19
+ @regex ||= Regexp.new('\A' + @keys.map{|key| @values[key]|| '.*'}.join('\/') + '\z')
20
+ end
21
+
22
+ def match?(value)
23
+ value =~ regex
24
+ end
25
+
26
+ def to_lua_pattern
27
+ @lua_pattern ||= @keys.map{|key| @values[key]|| '.*'}.join('/')
28
+ end
29
+ end
30
+
31
+ class Manager
32
+ DEFAULT_STRATEGY = RedisHgetallStrategy
33
+ attr_reader :prefix
34
+ def initialize(options = {})
35
+ @strategy = ( options[:strategy] || DEFAULT_STRATEGY).new
36
+ @resolution_ids = options[:resolutions] || DEFAULT_RESOLUTIONS
37
+ @prefix = [(options[:prefix] || DEFAULT_PREFIX) , @strategy.id].join(NAMESPACE_SEPARATOR)
38
+ @connection_to_use = options[:connection]
39
+ end
40
+
41
+ def on_connection(conn)
42
+ old_connection = @connection_to_use
43
+ @connection_to_use = conn
44
+ yield self
45
+ @connection_to_use = old_connection
46
+ end
47
+
48
+ def connection_to_use
49
+ @connection_to_use || fail("No connection given")
50
+ end
51
+
52
+ def store(subject, event_point_hash, moment = Time.now, descend_subject = true, descend_event = true)
53
+ resolutions = resolutions_of(moment)
54
+
55
+ descend(subject, descend_subject) do |subject_path|
56
+ event_point_hash.each do |event, point_value|
57
+ descend(event, descend_event) do |event_path|
58
+ resolutions.each do |res|
59
+ resolution_name, resolution_value = *res
60
+ store_point_value(subject_path, resolution_name, resolution_value, point_value, event_path)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ def resolutions_of(moment)
68
+ @resolution_ids.collect do |res_id|
69
+ [res_id, resolution_value_at(moment, res_id)]
70
+ end
71
+ end
72
+
73
+ def resolution_value_at(moment, res_id)
74
+ res_config = DEFAULT_RESOLUTIONS_MAP[res_id] # TODO configurable
75
+ if res_config.nil?
76
+ fail "No resolution config for id: #{res_id.class}:#{res_id}"
77
+ end
78
+
79
+ time_resolution_formater = res_config[:formatter]
80
+ case time_resolution_formater
81
+ when String
82
+ moment.strftime(time_resolution_formater)
83
+ when Proc
84
+ time_resolution_formater.call(moment)
85
+ else
86
+ fail ArgumentError, "Unexpected moment formater type #{time_resolution_formater.class}"
87
+ end
88
+ end
89
+
90
+ def descend(path, do_descend = true , &block)
91
+ return if path.empty? or path == "."
92
+ block.call(path)
93
+ descend(File.dirname(path), do_descend, &block) if do_descend
94
+ end
95
+
96
+ def store_point_value( subject_path, resolution_name, resolution_value, point_value, event_path)
97
+ @strategy.store_point_value(self, subject_path, resolution_name, resolution_value, point_value, event_path)
98
+ end
99
+
100
+ def find(subject, from, to = Time.now, filter = nil)
101
+ Rhcf::Timeseries::Query.new(subject, from, to, self, filter)
102
+ end
103
+
104
+ def resolution(id)
105
+ res = DEFAULT_RESOLUTIONS_MAP[id] # TODO configurable
106
+ fail ArgumentError, "Invalid resolution name #{id} for this time series" if res.nil?
107
+ res.merge(:id => id)
108
+ end
109
+
110
+ def resolutions
111
+ @_resolutions ||= @resolution_ids.map { |id| resolution(id) }
112
+ end
113
+
114
+ def crunch_values(subject, resolution_id, point, filter)
115
+ @strategy.crunch_values(self, subject, resolution_id, point, filter)
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,69 @@
1
+ module Rhcf
2
+ module Timeseries
3
+ class Query
4
+ def initialize(subject, from, to, series, filter = nil)
5
+ fail ArgumentError, "Argument 'from' can not be bigger then 'to'" if from > to
6
+ @series = series
7
+ @subject = subject
8
+ @from = from
9
+ @to = to
10
+
11
+ @filter = filter
12
+ end
13
+
14
+ def total(resolution_id=nil)
15
+ accumulator={}
16
+ points(resolution_id || better_resolution[:id]) do |data|
17
+
18
+ data[:values].each do |key, value|
19
+ accumulator[key]||=0
20
+ accumulator[key]+=value
21
+ end
22
+ end
23
+ accumulator
24
+ end
25
+
26
+ def points(resolution_id)
27
+ list =[]
28
+
29
+ point_range(resolution_id) do |point|
30
+
31
+ values = @series.crunch_values(@subject, resolution_id, point, @filter)
32
+
33
+ next if values.empty?
34
+ data = {moment: point, values: values }
35
+ if block_given?
36
+ yield data
37
+ else
38
+ list << data
39
+ end
40
+ end
41
+ list unless block_given?
42
+ end
43
+
44
+ def point_range(resolution_id)
45
+ resolution = @series.resolution(resolution_id)
46
+ span = resolution[:span]
47
+ ptr = @from.dup
48
+ while ptr < @to
49
+ point = @series.resolution_value_at(ptr, resolution_id)
50
+ yield point
51
+ ptr += span.to_i
52
+ end
53
+ rescue FloatDomainError
54
+ # OK
55
+ end
56
+
57
+ def better_resolution
58
+ span = @to.to_time - @from.to_time
59
+
60
+ resolutions = @series.resolutions.sort_by{|h| h[:span]}.reverse
61
+ 5.downto(1) do |div|
62
+ res = resolutions.find{|r| r[:span] < span / div }
63
+ return res if res
64
+ end
65
+ return nil
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,175 @@
1
+ module Rhcf
2
+ module Timeseries
3
+ class RedisHgetallStrategy
4
+ def id
5
+ 'H'
6
+ end
7
+
8
+ def crunch_values(manager, subject, resolution_id, point, filter)
9
+ values = hgetall(manager, EVENT_POINT_TOKEN, subject, resolution_id, point)
10
+ values.reject!{|event, value| !filter.match?(event) } if filter
11
+ values
12
+ end
13
+
14
+ def store_point_value(manager, subject_path, resolution_name, resolution_value, point_value, event_path)
15
+ key = [manager.prefix, EVENT_POINT_TOKEN ,subject_path, resolution_name, resolution_value].join(NAMESPACE_SEPARATOR)
16
+ manager.connection_to_use.hincrby(key, event_path, point_value)
17
+ manager.connection_to_use.expire(key, DEFAULT_RESOLUTIONS_MAP[resolution_name][:ttl])
18
+ end
19
+
20
+ def hgetall(manager, k,s,r,p)
21
+ key = [ manager.prefix, k,s,r,p].join(NAMESPACE_SEPARATOR)
22
+ manager.connection_to_use.hgetall(key).each_with_object({}) do |(_k, value), hash|
23
+ hash[_k] = value.to_i
24
+ end
25
+ end
26
+ end
27
+
28
+ class RedisStringBasedStrategy
29
+ def id
30
+ fail 'AbstractStrategy'
31
+ end
32
+
33
+ def store_point_value(manager, subject_path, resolution_name, resolution_value, point_value, event_path)
34
+ store_point_event(manager, resolution_name, resolution_value, subject_path, event_path)
35
+ key = [manager.prefix, EVENT_POINT_TOKEN ,subject_path, resolution_name, resolution_value, event_path].join(NAMESPACE_SEPARATOR)
36
+ manager.connection_to_use.incrby(key, point_value)
37
+ manager.connection_to_use.expire(key, DEFAULT_RESOLUTIONS_MAP[resolution_name][:ttl])
38
+ end
39
+
40
+ def store_point_event(manager, resolution_name, resolution_value, subject_path, event_path)
41
+ key = [manager.prefix, EVENT_SET_TOKEN, resolution_name, resolution_value, subject_path].join(NAMESPACE_SEPARATOR)
42
+ manager.connection_to_use.sadd(key, event_path)
43
+ manager.connection_to_use.expire(key, DEFAULT_RESOLUTIONS_MAP[resolution_name][:ttl])
44
+ end
45
+
46
+ def events_for_subject_on(manager, subject, point, resolution_id, filter)
47
+ key = [manager.prefix, EVENT_SET_TOKEN, resolution_id, point, subject].join(NAMESPACE_SEPARATOR)
48
+ events = manager.connection_to_use.smembers(key)
49
+ events = events.select{|event| filter.match?(event) } if filter
50
+ events
51
+ end
52
+ end
53
+
54
+ class RedisMgetStrategy < RedisStringBasedStrategy
55
+ def id
56
+ 'M'
57
+ end
58
+
59
+ def crunch_values(manager, subject, resolution_id, point, filter)
60
+ events = events_for_subject_on(manager, subject, point, resolution_id, filter)
61
+ mget(manager, EVENT_POINT_TOKEN, subject, resolution_id, point, events)
62
+ end
63
+
64
+ def mget(manager, k, s, r, p, es)
65
+ return {} if es.empty?
66
+ keys = es.map{|e| [manager.prefix, k, s, r, p, e].flatten.join(NAMESPACE_SEPARATOR)}
67
+ values = manager.connection_to_use.mget(*keys)
68
+ data = {}
69
+ keys.each_with_index do |key, index|
70
+ data[es[index]] = values[index].to_i
71
+ end
72
+ data
73
+ end
74
+ end
75
+
76
+ class RedisMgetLuaStrategy < RedisMgetStrategy
77
+ def id; 'ME'; end
78
+
79
+ def events_for_subject_on(manager, subject, point, resolution_id, filter)
80
+ key = [manager.prefix, EVENT_SET_TOKEN, resolution_id, point, subject].join(NAMESPACE_SEPARATOR)
81
+ events = if filter
82
+ manager.connection_to_use.evalsha(evalsha_for(:smembers_matching),
83
+ keys: [key], argv: [filter.to_lua_pattern])
84
+ else
85
+ manager.connection_to_use.smembers(key)
86
+ end
87
+ events
88
+ end
89
+
90
+ def crunch_values(manager, subject, resolution_id, point, filter)
91
+ register_lua_scripts!(manager.connection_to_use)
92
+ point_prefix = [manager.prefix, EVENT_POINT_TOKEN, subject, resolution_id, point].join(NAMESPACE_SEPARATOR)
93
+ set_key = [manager.prefix, EVENT_SET_TOKEN, resolution_id, point, subject].join(NAMESPACE_SEPARATOR)
94
+
95
+ data = manager.connection_to_use.evalsha(evalsha_for(:mget_matching_smembers),
96
+ keys: [set_key], argv: [point_prefix, filter && filter.to_lua_pattern])
97
+
98
+ return {} if data.nil?
99
+ result = {}
100
+ data.first.each_with_index do |evt, idx|
101
+ value = data.last[idx].to_i
102
+ result[evt] = value
103
+ end
104
+
105
+ result
106
+ end
107
+
108
+ def evalsha_for(sym_os_lua_script)
109
+ @lua_script_register[sym_os_lua_script] || fail("Script for '#{sym_os_lua_script}' not registered")
110
+ end
111
+
112
+ def register_lua_scripts!(connection)
113
+ @lua_script_register ||=
114
+ begin
115
+ smembers_matching = <<-EOF
116
+ local matches = {}
117
+ for _, val in ipairs(redis.call('smembers', KEYS[1])) do
118
+ if string.match(val, ARGV[1]) then
119
+ table.insert(matches, val)
120
+ end
121
+ end
122
+ return matches
123
+ EOF
124
+
125
+ mget_matching_smembers = <<-EOF
126
+ local matches = {}
127
+ local set_key = KEYS[1]
128
+ local key_prefix = ARGV[1]
129
+ local filter_pattern = ARGV[2]
130
+ local keys = {}
131
+ local keys_to_mget = {}
132
+
133
+ for _, val in ipairs(redis.call('smembers', set_key)) do
134
+ if (filter_pattern and string.match(val, filter_pattern)) or not filter_pattern then
135
+ table.insert(keys, val)
136
+ table.insert(keys_to_mget, key_prefix .. '#{NAMESPACE_SEPARATOR}' .. val)
137
+ end
138
+ end
139
+
140
+ if table.getn(keys) > 0 then
141
+ return {keys, redis.call('MGET', unpack(keys_to_mget)) }
142
+ end
143
+ return {{},{}}
144
+ EOF
145
+
146
+ {
147
+ mget_matching_smembers: connection.script(:load, mget_matching_smembers),
148
+ smembers_matching: connection.script(:load, smembers_matching)
149
+ }
150
+ end
151
+ end
152
+ end
153
+
154
+ class RedisGetStrategy < RedisStringBasedStrategy
155
+ def id
156
+ 'G'
157
+ end
158
+
159
+ def crunch_values(manager, subject, resolution_id, point, filter)
160
+ events = events_for_subject_on(manager, subject, point, resolution_id, filter)
161
+ values = {}
162
+ events.each do |event|
163
+ value = get(manager, EVENT_POINT_TOKEN, subject, resolution_id, point, event)
164
+ values[event] = value.to_i
165
+ end
166
+ values
167
+ end
168
+
169
+ def get(manager, *a_key)
170
+ a_key = [manager.prefix, a_key].flatten.join(NAMESPACE_SEPARATOR)
171
+ manager.connection_to_use.get(a_key)
172
+ end
173
+ end
174
+ end
175
+ end
@@ -1,5 +1,5 @@
1
1
  module Rhcf
2
2
  module Timeseries
3
- VERSION = "0.0.6"
3
+ VERSION = "1.0.0"
4
4
  end
5
5
  end
@@ -25,8 +25,8 @@ Gem::Specification.new do |spec|
25
25
  #spec.add_development_dependency "guard"
26
26
  #spec.add_development_dependency "guard-rspec"
27
27
  #spec.add_development_dependency "guard-bundler"
28
- #spec.add_development_dependency "simplecov"
28
+ spec.add_development_dependency "simplecov"
29
29
  spec.add_development_dependency "timecop"
30
30
  spec.add_development_dependency "stackprof"
31
- #spec.add_dependency "activesupport"
31
+ spec.add_development_dependency "database_cleaner"
32
32
  end
@@ -0,0 +1,193 @@
1
+ require 'spec_helper'
2
+ require 'timecop'
3
+ require 'redis'
4
+ require 'rhcf/timeseries/manager'
5
+ require 'benchmark'
6
+ require 'stackprof'
7
+
8
+ describe Rhcf::Timeseries::Manager do
9
+ let(:redis){Redis.new}
10
+ subject{Rhcf::Timeseries::Manager.new(connection: redis)}
11
+
12
+ before(:each) do
13
+ Timecop.return
14
+ end
15
+
16
+ describe 'descending' do
17
+ it "is fast to store and read" do
18
+ total = 0
19
+ start_time = Time.now
20
+
21
+ bench = Benchmark.measure {
22
+ StackProf.run(mode: :cpu, out: p('/tmp/stackprof-cpu-store-descend.dump')) do
23
+ 1000.times do
24
+ total +=1
25
+ subject.store("a/b", {"e/f" => 1} ) #, time)
26
+ end
27
+ end
28
+ }
29
+
30
+
31
+ Benchmark.measure {
32
+ expect(subject.find("a", start_time - 11100, Time.now + 11100).total['e'].to_i).to eq(total)
33
+ }
34
+
35
+ Benchmark.measure {
36
+ expect(subject.find("a", start_time - 100000, Time.now + 100000).total(:year)['e/f'].to_i).to eq(total)
37
+ }
38
+
39
+ puts "Descend write speed %d points/seg | points:%d, duration:%0.3fs" % [speed = (1.0 * total / (bench.total + 0.00000001)), total, bench.total]
40
+ expect(speed).to be > 100
41
+ end
42
+ end
43
+
44
+ describe 'not descending' do
45
+ it "is be fast to store and read" do
46
+ total = 0
47
+ bench = Benchmark.measure {
48
+ StackProf.run(mode: :cpu, out: p('/tmp/stackprof-cpu-store-nodescend.dump')) do
49
+ 1000.times do
50
+ total +=1
51
+ subject.store("a/b/c/d", {"e/f/g/h" => 1} , Time.now, false, false)
52
+ end
53
+ end
54
+ }
55
+
56
+ puts "No descend write speed %d points/seg | points:%d, duration:%0.3fs" % [new_speed = (1.0 * total / (bench.total + 0.00000001)), total, bench.total]
57
+ expect(new_speed).to be > 300
58
+ end
59
+ end
60
+
61
+ describe "find and total" do
62
+ let(:start_time){ Time.parse("2000-01-01 00:00:00") }
63
+ before do
64
+ Timecop.travel(start_time)
65
+ subject.store("views/product/15", {"web/firefox/3" => 1})
66
+
67
+ Timecop.travel(15.minutes) #00:00:15
68
+ subject.store("views/product/13", {"web/firefox/3" => 1}, Time.now)
69
+ subject.store("views/product/13", {"web/firefox/3" => 1}, Time.now)
70
+ subject.store("views/product/13", {"web/firefox/3" => 0}, Time.now)
71
+
72
+ Timecop.travel(15.minutes) #00:00:30
73
+ subject.store("views/product/15", {"web/ie/6" => 3})
74
+
75
+ Timecop.travel(15.minutes) #00:00:45
76
+ subject.store("views/product/15", {"web/ie/6" => 2})
77
+
78
+ Timecop.travel(15.minutes) #00:00:00
79
+ subject.store("views/product/11", {"web/ie/5" => 2})
80
+
81
+ Timecop.travel(15.minutes) #00:00:15
82
+ subject.store("views/product/11", {"web/chrome/11"=> 4})
83
+
84
+ Timecop.travel(15.minutes) #00:00:30
85
+ subject.store("views/product/11", {"web/chrome/11"=> 2})
86
+ end
87
+
88
+ it "is similar to redistat" do
89
+
90
+ expect(subject.find("views/product", start_time, start_time + 55.minutes).total(:ever)).to eq({
91
+ "web" => 16.0,
92
+ "web/chrome" => 6.0,
93
+ "web/chrome/11" => 6.0,
94
+ "web/firefox" => 3.0,
95
+ "web/firefox/3" => 3.0,
96
+ "web/ie" => 7.0,
97
+ "web/ie/5" => 2.0,
98
+ "web/ie/6" => 5.0
99
+ })
100
+
101
+ expect(subject.find("views/product", start_time, start_time + 55.minutes).total(:year)).to eq({
102
+ "web" => 16.0,
103
+ "web/chrome" => 6.0,
104
+ "web/chrome/11" => 6.0,
105
+ "web/firefox" => 3.0,
106
+ "web/firefox/3" => 3.0,
107
+ "web/ie" => 7.0,
108
+ "web/ie/5" => 2.0,
109
+ "web/ie/6" => 5.0
110
+ })
111
+
112
+ expect( subject.find("views/product", start_time, start_time + 55.minutes).total ).to eq({
113
+ 'web' => 8,
114
+ 'web/firefox' => 3,
115
+ 'web/firefox/3' => 3,
116
+ 'web/ie' => 5,
117
+ 'web/ie/6' => 5,
118
+ })
119
+
120
+ expect(subject.find("views/product/15", start_time, start_time + 55.minutes).points(:minute)).to eq([
121
+ {:moment=>"2000-01-01T00:00", :values=>{"web/firefox"=>1, "web/firefox/3"=>1, "web"=>1}},
122
+ {:moment=>"2000-01-01T00:30", :values=>{"web"=>3, "web/ie/6"=>3, "web/ie"=>3}},
123
+ {:moment=>"2000-01-01T00:45", :values=>{"web"=>2, "web/ie/6"=>2, "web/ie"=>2}}
124
+ ])
125
+
126
+ expect(subject.find("views/product/13", start_time, start_time + 55.minutes).points(:minute)).to eq([
127
+ {:moment=>"2000-01-01T00:15", :values=>{"web/firefox"=>2, "web/firefox/3"=>2, "web"=>2}},
128
+ ])
129
+
130
+ expect(subject.find("views/product", start_time, start_time + 55.minutes).points(:minute)).to eq([
131
+ {:moment=>"2000-01-01T00:00", :values=>{"web/firefox"=>1, "web/firefox/3"=>1, "web"=>1}},
132
+ {:moment=>"2000-01-01T00:15", :values=>{"web/firefox"=>2, "web/firefox/3"=>2, "web"=>2}},
133
+ {:moment=>"2000-01-01T00:30", :values=>{"web"=>3, "web/ie/6"=>3, "web/ie"=>3}},
134
+ {:moment=>"2000-01-01T00:45", :values=>{"web"=>2, "web/ie/6"=>2, "web/ie"=>2}}
135
+ ])
136
+
137
+ expect(subject.find("views", start_time, start_time + 55.minutes).points(:minute)).to eq([
138
+ {:moment=>"2000-01-01T00:00", :values=>{"web/firefox"=>1, "web/firefox/3"=>1, "web"=>1}},
139
+ {:moment=>"2000-01-01T00:15", :values=>{"web/firefox"=>2, "web/firefox/3"=>2, "web"=>2}},
140
+ {:moment=>"2000-01-01T00:30", :values=>{"web"=>3, "web/ie/6"=>3, "web/ie"=>3}},
141
+ {:moment=>"2000-01-01T00:45", :values=>{"web"=>2, "web/ie/6"=>2, "web/ie"=>2}}
142
+ ])
143
+
144
+ expect(subject.find("views", start_time).points(:hour)).to eq([
145
+ {
146
+ :moment=>"2000-01-01T00",
147
+ :values=> {
148
+ "web/ie"=>5.0,
149
+ "web"=>8.0,
150
+ "web/firefox"=>3.0,
151
+ "web/ie/6"=>5.0,
152
+ "web/firefox/3"=>3.0}
153
+ },{
154
+ :moment=>"2000-01-01T01",
155
+ :values=>{
156
+ "web/ie"=>2.0,
157
+ "web/chrome"=>6.0,
158
+ "web/chrome/11"=>6.0,
159
+ "web"=>8.0,
160
+ "web/ie/5"=>2.0
161
+ }
162
+ }
163
+ ])
164
+ end
165
+ let(:filter) { Rhcf::Timeseries::Filter.new([:source, :browser], browser: 'firefox.*' )}
166
+ it "can find with filter" do
167
+ expect(subject.find("views", start_time, start_time + 55.minutes, filter).points(:minute)).to eq([
168
+ {:moment=>"2000-01-01T00:00", :values=>{"web/firefox"=>1, "web/firefox/3"=>1}},
169
+ {:moment=>"2000-01-01T00:15", :values=>{"web/firefox"=>2, "web/firefox/3"=>2}},
170
+ ])
171
+ end
172
+ end
173
+
174
+ it "causes no stack overflow" do
175
+ params_hash = {
176
+ sender_domain: 'example.com',
177
+ realm: 'realm',
178
+ destination_domain: 'lvh.me',
179
+ mail_server: 'aserver',
180
+ bind_interface: '11.1.1.11'
181
+ }
182
+
183
+ {
184
+ 'sender_domain' => '%{sender_domain}',
185
+ 'realm_and_sender_domain' => '%{realm}/%{sender_domain}',
186
+ 'mail_server_and_interface' => '%{mail_server}/%{bind_interface}',
187
+ 'realm_and_destination_domain' => '%{realm}/%{destination_domain}',
188
+ 'destination_domain' => '%{destination_domain}'
189
+ }.each do |known, unknown|
190
+ subject.store(known % params_hash, {[(unknown % params_hash),'sent'].join('/') => 1})
191
+ end
192
+ end
193
+ end
@@ -1,28 +1,28 @@
1
1
  require 'spec_helper'
2
- require 'rhcf/timeseries/redis'
2
+ require 'rhcf/timeseries/manager'
3
3
 
4
4
 
5
5
  describe "Query" do
6
6
  describe "#better_resolution" do
7
7
 
8
- let(:redis_connection){nil}
8
+ let(:redis){nil}
9
9
  describe "When having a smaller then 1/5 " do
10
- let(:series) { Rhcf::Timeseries::Redis.new(nil, redis_connection, resolutions: [:hour, :"15minutes", :minute]) }
10
+ let(:series) { Rhcf::Timeseries::Manager.new(connection: redis, resolutions: [:hour, :"15minutes", :minute]) }
11
11
  it { expect(series.find('bla', Time.now - 3600, Time.now).better_resolution[:id]).to eq :minute }
12
12
  end
13
13
 
14
14
  describe "When having a smaller but greather then 1/5" do
15
- let(:series) { Rhcf::Timeseries::Redis.new(nil, redis_connection, resolutions: [:hour, :"15minutes"]) }
15
+ let(:series) { Rhcf::Timeseries::Manager.new(connection: redis, resolutions: [:hour, :"15minutes"]) }
16
16
  it { expect(series.find('bla', Time.now - 3600, Time.now).better_resolution[:id]).to eq :"15minutes" }
17
17
  end
18
18
 
19
19
  describe "When having no smaller, only its size" do
20
- let(:series) { Rhcf::Timeseries::Redis.new(nil, redis_connection, resolutions: [:hour]) }
20
+ let(:series) { Rhcf::Timeseries::Manager.new(connection: redis, resolutions: [:hour]) }
21
21
  it { expect(series.find('bla', DateTime.parse('2015-01-01 01:50:00'), DateTime.parse('2015-01-01 03:10:00')).better_resolution[:id]).to eq :hour }
22
22
  end
23
23
 
24
24
  describe "When having only bigger" do
25
- let(:series) { Rhcf::Timeseries::Redis.new(nil, redis_connection, resolutions: [:month]) }
25
+ let(:series) { Rhcf::Timeseries::Manager.new(connection: redis, resolutions: [:month]) }
26
26
  it { expect(series.find('bla', DateTime.parse('2015-01-01 01:50:00'), DateTime.parse('2015-01-01 03:10:00')).better_resolution).to be_nil}
27
27
  end
28
28