metricstore 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -3,11 +3,13 @@
3
3
  .bundle
4
4
  .config
5
5
  .yardoc
6
+ .DS_Store
6
7
  Gemfile.lock
7
8
  InstalledFiles
8
9
  _yardoc
9
10
  coverage
10
11
  doc/
12
+ profile/
11
13
  lib/bundler/man
12
14
  pkg
13
15
  rdoc
@@ -0,0 +1,15 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'bundler' do
5
+ watch('Gemfile')
6
+ watch(%r{.+\.gemspec})
7
+ end
8
+
9
+ guard 'rspec', :cli => '-c --format documentation -r ./spec/spec_helper.rb',
10
+ :version => 2 do
11
+ watch(%r{^spec/.+_spec\.rb})
12
+ watch(%r{^lib/(.+)\.rb}) { |m| "spec/lib/#{m[1]}_spec.rb" }
13
+ watch('spec/spec_helper.rb') { "spec/" }
14
+ watch('lib/metricstore.rb') { "spec/" }
15
+ end
data/README.md CHANGED
@@ -31,91 +31,110 @@ Or install it yourself as:
31
31
 
32
32
  # Configure...
33
33
  m.ttl_of_hours = 31_556_926 # 1 year, default
34
- m.ttl_of_minutes = 86_400 # 24 hours, default
35
- m.ttl_of_group_members = 7200 # 2 hours, default
34
+ m.max_ttl_of_dimension[:session_id] = 7200 # 2 hours
36
35
  m.list_threshold = 1000 # default
37
36
 
37
+ # Open the connection
38
+ m.open
39
+
38
40
  # (Suppose that the current time is 17:05 UTC on April 13, 2012.)
39
41
 
40
- m.counter(:when => Time.now, :metric => "logins", :where =>
42
+ m.counter(:when => Time.now, :what => "logins", :where =>
41
43
  {:user => 'joe', :ip => '10.20.30.40'})
42
- m.counter(:when => Time.now, :metric => "logins", :where =>
44
+ m.counter(:when => Time.now, :what => "logins", :where =>
43
45
  {:user => 'bob', :ip => '10.20.30.40'})
44
- m.counter(:when => Time.now, :metric => "logins", :where =>
46
+ m.counter(:when => Time.now, :what => "logins", :where =>
45
47
  {:user => 'joe', :ip => '10.20.30.50'})
46
48
 
47
- m.measure(:when => Time.now, :metric => "load_time", :value => 340, :where =>
49
+ m.measure(:when => Time.now, :what => "load_time", :value => 340, :where =>
48
50
  {:page => '/welcome/', :session_id => "h0zhmb1c-u1xfgw305e"})
49
- m.measure(:when => Time.now, :metric => "load_time", :value => 501, :where =>
51
+ m.measure(:when => Time.now, :what => "load_time", :value => 501, :where =>
50
52
  {:page => '/welcome/', :session_id => "h0zhmb2q-643dotlcgd"})
51
- m.measure(:when => Time.now, :metric => "load_time", :value => 212, :where =>
53
+ m.measure(:when => Time.now, :what => "load_time", :value => 212, :where =>
52
54
  {:page => '/welcome/', :session_id => "h0zhmb1c-u1xfgw305e"})
53
- m.measure(:when => Time.now, :metric => "load_time", :value => 343, :where =>
55
+ m.measure(:when => Time.now, :what => "load_time", :value => 343, :where =>
54
56
  {:page => '/welcome/', :session_id => "h0zhmb2q-643dotlcgd"})
55
57
 
56
58
  # Now we can query...
57
59
 
58
- m.count(:when => "2012-04-13-17", :metric => "logins")
60
+ m.count(:when => "2012-04-13-17", :what => "logins")
59
61
  => 3
60
- m.list(:when => "2012-04-13-17", :metric => "logins", :list => :user)
62
+ m.list(:when => "2012-04-13-17", :what => "logins", :list => :user)
61
63
  => ['joe', 'bob']
62
- m.count(:when => "2012-04-13-17", :metric => "logins", :where => {:user => 'joe'})
64
+ m.count(:when => "2012-04-13-17", :what => "logins", :where => {:user => 'joe'})
63
65
  => 2
64
- m.count(:when => "2012-04-13-17", :metric => "logins", :where => {:user => 'bob'})
66
+ m.count(:when => "2012-04-13-17", :what => "logins", :where => {:user => 'bob'})
65
67
  => 1
66
- m.list(:when => "2012-04-13-17", :metric => "logins", :where => {:user => 'joe'}, :list => :ip)
68
+ m.list(:when => "2012-04-13-17", :what => "logins", :where => {:user => 'joe'}, :list => :ip)
67
69
  => ['10.20.30.40', '10.20.30.50']
68
- m.list(:when => "2012-04-13-17", :metric => "logins", :where => {:user => 'bob'}, :list => :ip)
70
+ m.list(:when => "2012-04-13-17", :what => "logins", :where => {:user => 'bob'}, :list => :ip)
69
71
  => ['10.20.30.40']
70
- m.count(:when => "2012-04-13-17", :metric => "logins", :where => {:user => 'joe', :ip => '10.20.30.40'})
72
+ m.count(:when => "2012-04-13-17", :what => "logins", :where => {:user => 'joe', :ip => '10.20.30.40'})
71
73
  => 1
72
74
 
73
- m.count(:when => "2012-04-13-17", :metric => "load_time")
75
+ m.count(:when => "2012-04-13-17", :what => "load_time")
74
76
  => 4
75
- m.sum(:when => "2012-04-13-17", :metric => "load_time")
77
+ m.sum(:when => "2012-04-13-17", :what => "load_time")
76
78
  => 1396
77
- m.average(:when => "2012-04-13-17", :metric => "load_time")
79
+ m.average(:when => "2012-04-13-17", :what => "load_time")
78
80
  => 349.0
79
- m.maximum(:when => "2012-04-13-17", :metric => "load_time")
81
+ m.maximum(:when => "2012-04-13-17", :what => "load_time")
80
82
  => 501
81
- m.minimum(:when => "2012-04-13-17", :metric => "load_time")
83
+ m.minimum(:when => "2012-04-13-17", :what => "load_time")
82
84
  => 212
83
- m.stddev(:when => "2012-04-13-17", :metric => "load_time")
85
+ m.stddev(:when => "2012-04-13-17", :what => "load_time")
84
86
  => 102.45730818248154
85
- m.list(:when => "2012-04-13-17", :metric => "load_time", :list => :page)
87
+ m.list(:when => "2012-04-13-17", :what => "load_time", :list => :page)
86
88
  => ['/welcome/']
87
89
 
88
90
  # We can do queries related to groups as well, with some limitations.
89
- # We only guarantee the accuracy of the result if all related data was
90
- # loaded from start-to-finish within :ttl_of_group_members seconds.
91
+ # We only guarantee the accuracy of a particular group summary if for every
92
+ # member in the group, all the metrics related to that member were loaded
93
+ # from start-to-finish before the preceeding such metric expired its TTL.
94
+ #
91
95
  # Note: a range is the difference between the minimum and maximum metric,
92
96
  # for an individual group.
93
- m.count_of_groups(:when => "2012-04-13-17", :metric => "load_time", :group => :session_id)
97
+
98
+ m.count_of_groups(:when => "2012-04-13-17", :what => "load_time", :group => :session_id)
94
99
  => 2
95
- m.sum_of_ranges(:when => "2012-04-13-17", :metric => "load_time", :group => :session_id)
100
+ m.sum_of_ranges(:when => "2012-04-13-17", :what => "load_time", :group => :session_id)
96
101
  => 286
97
- m.average_range(:when => "2012-04-13-17", :metric => "load_time", :group => :session_id)
102
+ m.average_range(:when => "2012-04-13-17", :what => "load_time", :group => :session_id)
98
103
  => 143
99
- m.maximum_range(:when => "2012-04-13-17", :metric => "load_time", :group => :session_id)
104
+ m.maximum_range(:when => "2012-04-13-17", :what => "load_time", :group => :session_id)
100
105
  => 158
101
- m.minimum_range(:when => "2012-04-13-17", :metric => "load_time", :group => :session_id)
106
+ m.minimum_range(:when => "2012-04-13-17", :what => "load_time", :group => :session_id)
102
107
  => 128
103
- m.stddev_of_ranges(:when => "2012-04-13-17", :metric => "load_time", :group => :session_id)
108
+ m.stddev_of_ranges(:when => "2012-04-13-17", :what => "load_time", :group => :session_id)
104
109
  => 15.0
105
110
 
106
111
 
107
112
  # Supposing there were instead millions of counter and measure operations,
108
113
  # metricstore may reach its list_threshold. Some queries will fail.
109
114
 
110
- m.list(:page, "2012-04-13-17", "load_time")
115
+ m.list(:when => "2012-04-13-17", :what => "load_time", :list => :page)
111
116
  => ['/welcome/', '/projects/']
112
117
 
113
- m.list(:session_id, "2012-04-13-17", "load_time")
114
- metricstore::DataLossError: Too many session_id for "2012-04-13-17", "load_time".
118
+ m.list(:when => "2012-04-13-17", :what => "load_time", :list => :session_id)
119
+ Metricstore::DataLossError: Too many session_id for "2012-04-13-17", "load_time".
115
120
 
116
- m.estimated_list_size(:session_id, "2012-04-13-17", "load_time")
121
+ m.estimated_list_size(:when => "2012-04-13-17", :what => "load_time", :list => :session_id)
117
122
  => 3560831
118
123
 
124
+ m.close
125
+
126
+ ## EventMachine
127
+
128
+ The Metricstore client's write methods (counter, measure) are designed to run
129
+ within an [EventMachine](http://rubyeventmachine.com/) reactor. This allows
130
+ writes to be batched up together (only when there's a backlog), and to re-try
131
+ in the case of intermittent connection problems or other non-fatal errors. You
132
+ will want to design your app to leave the reactor running.
133
+
134
+ If it does not make sense to leave a reactor running in your app, you can
135
+ make your updates within a temporary reactor using the client's "run" method.
136
+ Be aware though, that the "run" method itself will block until the write backlog
137
+ is clear again.
119
138
 
120
139
  ## Contributing
121
140
 
data/Rakefile CHANGED
@@ -1,2 +1,12 @@
1
- #!/usr/bin/env rake
2
1
  require "bundler/gem_tasks"
2
+
3
+ require 'bundler/setup'
4
+ Bundler.require(:default)
5
+
6
+ Dir['tasks/*.rake'].sort.each { |task| load task }
7
+
8
+ # Add rake tasks from selected gems
9
+ gem_names = []
10
+ gem_names.each do |gem_name|
11
+ Dir[File.join(Gem.searcher.find(gem_name).full_gem_path, '**', '*.rake')].each{|rake_file| load rake_file }
12
+ end
@@ -1,5 +1,27 @@
1
- require "memtrics/version"
1
+ require "metricstore/version"
2
+ require "metricstore/exceptions"
3
+ require "metricstore/monkey_patches"
4
+ require "metricstore/client"
5
+ require "metricstore/couchbase_client"
6
+ require "metricstore/mock_key_value_client"
7
+ require "metricstore/updater"
8
+ require "metricstore/count_incrementer"
9
+ require "metricstore/incrementer"
10
+ require "metricstore/inserter"
11
+ require "metricstore/range_updater"
12
+ require "metricstore/hyper_log_log"
2
13
 
3
14
  module Metricstore
4
- # Your code goes here...
15
+
16
+ def self.couchbase(*args, &callback)
17
+ couchbase_client = CouchbaseClient.new(*args, &callback)
18
+ Client.new(
19
+ :kvstore => couchbase_client,
20
+ :sleep_interval => 0.1,
21
+ :max_healthy_errors => 2,
22
+ :max_unhandled_errors => 8,
23
+ :max_retry_delay_in_seconds => 60.0
24
+ )
25
+ end
26
+
5
27
  end
@@ -0,0 +1,423 @@
1
+ require 'cgi'
2
+
3
+ module Metricstore
4
+ class Client
5
+
6
+ CARDINALITY_ESTIMATOR_ERROR_RATE = 0.05
7
+
8
+ # :kvstore - the underlying key-value store.
9
+ # :sleep_interval - sleep cycle length in seconds (default: 0.1).
10
+ # :max_retry_delay_in_seconds - maximum time to wait after an error.
11
+ # :max_unhandled_errors - maximum retries before handling errors.
12
+ # Set this >= max_healthy_errors.
13
+ # :max_healthy_errors - maximum retries before healthy? returns false.
14
+ # Set this <= max_unhandled_errors.
15
+ def initialize(opts={})
16
+ @ttl_of_hours = 31_556_926 # 1 year
17
+
18
+ @kvstore = required(opts, :kvstore)
19
+ @sleep_interval = required(opts, :sleep_interval)
20
+ @max_healthy_errors = required(opts, :max_healthy_errors)
21
+ @max_unhandled_errors = required(opts, :max_unhandled_errors)
22
+ @max_retry_delay_in_seconds = required(opts, :max_retry_delay_in_seconds)
23
+ @max_ttl_of_dimension = {}
24
+
25
+ updater_options = {
26
+ :kvstore => @kvstore,
27
+ :sleep_interval => @sleep_interval,
28
+ :max_healthy_errors => @max_healthy_errors,
29
+ :max_unhandled_errors => @max_unhandled_errors,
30
+ :max_retry_delay_in_seconds => @max_retry_delay_in_seconds
31
+ }
32
+ @open = false
33
+ @inserter = Inserter.new(updater_options)
34
+ bucket_count = 1 << HyperLogLog.bits_needed(CARDINALITY_ESTIMATOR_ERROR_RATE)
35
+ @inserter.list_threshold = (2.5 * bucket_count).ceil
36
+ @incrementer = Incrementer.new(updater_options)
37
+ @range_updater = RangeUpdater.new(updater_options)
38
+ @count_incrementer = CountIncrementer.new(updater_options)
39
+
40
+ range_updater.handle_update_result = Proc.new do |key, result, ttl|
41
+ if key.start_with?("range:") && !result.nil?
42
+ new_or_grew, amount = result
43
+ if new_or_grew == :new || new_or_grew == :grew
44
+ _, time_block, metric_name, dimensions = key.split(/[\/\?]/)
45
+ unless dimensions.nil?
46
+ dimensions = dimensions.split('&')
47
+ dimensions.size.times do |i|
48
+ dimensions2 = dimensions.clone
49
+ group, dimension_value = dimensions2.delete_at(i).split('=')
50
+ key_suffix = "#{time_block}/#{metric_name}/#{group}?#{dimensions2.join('&')}"
51
+ incrementer.increment("rangesum:/#{key_suffix}", amount, ttl)
52
+ incrementer.increment("rangesumsqr:/#{key_suffix}", amount * amount, ttl)
53
+ range_updater.update_range("rangerange:/#{key_suffix}", amount, ttl)
54
+ if new_or_grew == :new
55
+ count_incrementer.increment("rangecount:/#{key_suffix}", 1, ttl)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ # Use of this method is discouraged.
65
+ # Set up your own EventMachine reactor instead.
66
+ # Nevertheless, for a one-off, infrequent connection, this works too.
67
+ def run(&callback)
68
+ require 'em-synchrony'
69
+ raise("Already running") if @open
70
+ EM.synchrony do
71
+ open
72
+ callback.call
73
+ timer = EM.add_periodic_timer(0.01) do
74
+ if backlog == 0
75
+ EM.cancel_timer(timer)
76
+ EM.next_tick do
77
+ close
78
+ EM.stop
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ def open
86
+ inserter.start!
87
+ incrementer.start!
88
+ range_updater.start!
89
+ count_incrementer.start!
90
+ @open = true
91
+ end
92
+
93
+ def close
94
+ @open = false
95
+ inserter.stop!
96
+ incrementer.stop!
97
+ range_updater.stop!
98
+ count_incrementer.stop!
99
+ end
100
+
101
+ def backlog
102
+ inserter.backlog + incrementer.backlog + range_updater.backlog + count_incrementer.backlog
103
+ end
104
+
105
+ attr_accessor :ttl_of_hours
106
+ attr_accessor :max_ttl_of_dimension
107
+
108
+ def list_threshold
109
+ inserter.list_threshold
110
+ end
111
+
112
+ def list_threshold=(threshold)
113
+ inserter.list_threshold = threshold
114
+ end
115
+
116
+ # A write method.
117
+ # :what => a String. Required.
118
+ # :when => a Time. Defaults to "now".
119
+ # :where => a Hash<String, String> (dimension_name => value).
120
+ # Time complexity of this method grows factorially with the size of the :where hash.
121
+ def counter(args={})
122
+ assert_open!
123
+ hour = date_as_hour((args[:when] || Time.now).utc)
124
+ metric = escape(required(args, :what).to_s)
125
+ where = (args[:where] || {}).map{|k,v| [k, v, escape(k) << '=' << escape(v), max_ttl_of_dimension[k]] }
126
+ where.all_combinations do |dimensions|
127
+ key = counter_key(hour, metric, dimensions.sort.map{|k,v,s,ttl| s}.join('&'))
128
+ ttl = (dimensions.map{|k,v,s,ttl| ttl} << ttl_of_hours).compact.min
129
+ count_incrementer.increment(key, 1, ttl)
130
+ end
131
+ where.size.times do |i|
132
+ where2 = where.clone
133
+ list, dimension_value, _ = where2.delete_at(i)
134
+ list = escape(list)
135
+ key_middle = "#{hour}/#{metric}/#{list}?"
136
+ where2.all_combinations do |dimensions|
137
+ key_suffix = "#{key_middle}#{dimensions.sort.map{|k,v,s,ttl| s}.join('&')}"
138
+ ttl = (dimensions.map{|k,v,s,ttl| ttl} << ttl_of_hours).compact.min
139
+ inserter.insert("list:/#{key_suffix}", dimension_value, ttl)
140
+ estimator = HyperLogLog::Builder.new(CARDINALITY_ESTIMATOR_ERROR_RATE, Proc.new do |idx, val|
141
+ range_updater.update_range("hyperloglog:#{idx.to_i}:/#{key_suffix}", val, ttl)
142
+ end)
143
+ estimator.add(dimension_value)
144
+ end
145
+ end
146
+ end
147
+
148
+ # A write method.
149
+ # :value => an integer. Required.
150
+ # :what => a String. Required.
151
+ # :when => a Time. Defaults to "now".
152
+ # :where => a Hash<String, String> (dimension_name => value).
153
+ # Time complexity of this method grows factorially with the size of the :where hash.
154
+ def measure(args={})
155
+ assert_open!
156
+ value = required(args, :value).to_i
157
+ hour = date_as_hour((args[:when] || Time.now).utc)
158
+ metric = escape(required(args, :what).to_s)
159
+ where = (args[:where] || {}).map{|k,v| [k, v, escape(k) << '=' << escape(v), max_ttl_of_dimension[k]] }
160
+ where.all_combinations do |dimensions|
161
+ dimensions_string = dimensions.sort.map{|k,v,s,ttl| s}.join('&')
162
+ ttl = (dimensions.map{|k,v,s,ttl| ttl} << ttl_of_hours).compact.min
163
+ suffix = build_key('', hour, metric, dimensions_string)
164
+ count_incrementer.increment("count#{suffix}", 1, ttl)
165
+ incrementer.increment("sum#{suffix}", value, ttl)
166
+ range_updater.update_range("range#{suffix}", value, ttl)
167
+ incrementer.increment("sumsqr#{suffix}", value*value, ttl)
168
+ end
169
+ where.size.times do |i|
170
+ where2 = where.clone
171
+ list, dimension_value, _ = where2.delete_at(i)
172
+ list = escape(list)
173
+ key_middle = "#{hour}/#{metric}/#{list}?"
174
+ where2.all_combinations do |dimensions|
175
+ key_suffix = "#{key_middle}#{dimensions.sort.map{|k,v,s,ttl| s}.join('&')}"
176
+ ttl = (dimensions.map{|k,v,s,ttl| ttl} << ttl_of_hours).compact.min
177
+ inserter.insert("list:/#{key_suffix}", dimension_value, ttl)
178
+ estimator = HyperLogLog::Builder.new(CARDINALITY_ESTIMATOR_ERROR_RATE, Proc.new do |idx, val|
179
+ range_updater.update_range("hyperloglog:#{idx.to_i}:/#{key_suffix}", val, ttl)
180
+ end)
181
+ estimator.add(dimension_value)
182
+ end
183
+ end
184
+ end
185
+
186
+ def count(args={})
187
+ time_block = required(args, :hour)
188
+ metric_name = escape(required(args, :what).to_s)
189
+ dimensions = (args[:where] || {}).sort.map{|k,v| escape(k) << '=' << escape(v)}.join('&')
190
+ result, cas = kvstore.fetch(counter_key(time_block, metric_name, dimensions))
191
+ result || 0
192
+ end
193
+
194
+ def list(args={})
195
+ time_block = required(args, :hour)
196
+ metric_name = escape(required(args, :what).to_s)
197
+ list_name = escape(required(args, :list).to_s)
198
+ dimensions = (args[:where] || {}).sort.map{|k,v| escape(k) << '=' << escape(v)}.join('&')
199
+ result, cas = kvstore.fetch(list_key(time_block, metric_name, list_name, dimensions))
200
+ if result == 'overflow'
201
+ error_message = "Too many #{args[:list]} for #{time_block}, #{args[:what]}"
202
+ error_message << ", where #{args[:where].inspect}" unless dimensions.empty?
203
+ raise(Metricstore::DataLossError, error_message)
204
+ else
205
+ result || []
206
+ end
207
+ end
208
+
209
+ def sum(args={})
210
+ time_block = required(args, :hour)
211
+ metric_name = escape(required(args, :what).to_s)
212
+ dimensions = (args[:where] || {}).sort.map{|k,v| escape(k) << '=' << escape(v)}.join('&')
213
+ result, cas = kvstore.fetch(sum_key(time_block, metric_name, dimensions))
214
+ result || 0
215
+ end
216
+
217
+ def average(args={})
218
+ time_block = required(args, :hour)
219
+ metric_name = escape(required(args, :what).to_s)
220
+ dimensions = (args[:where] || {}).sort.map{|k,v| escape(k) << '=' << escape(v)}.join('&')
221
+ count, cas = kvstore.fetch(counter_key(time_block, metric_name, dimensions))
222
+ sum, cas = kvstore.fetch(sum_key(time_block, metric_name, dimensions))
223
+ return nil if count.nil? || sum.nil? || count == 0
224
+ sum.to_f / count
225
+ end
226
+
227
+ def maximum(args={})
228
+ time_block = required(args, :hour)
229
+ metric_name = escape(required(args, :what).to_s)
230
+ dimensions = (args[:where] || {}).sort.map{|k,v| escape(k) << '=' << escape(v)}.join('&')
231
+ range, cas = kvstore.fetch(range_key(time_block, metric_name, dimensions))
232
+ range.nil? ? nil : range[1]
233
+ end
234
+
235
+ def minimum(args={})
236
+ time_block = required(args, :hour)
237
+ metric_name = escape(required(args, :what).to_s)
238
+ dimensions = (args[:where] || {}).sort.map{|k,v| escape(k) << '=' << escape(v)}.join('&')
239
+ range, cas = kvstore.fetch(range_key(time_block, metric_name, dimensions))
240
+ range.nil? ? nil : range[0]
241
+ end
242
+
243
+ def stddev(args={})
244
+ time_block = required(args, :hour)
245
+ metric_name = escape(required(args, :what).to_s)
246
+ dimensions = (args[:where] || {}).sort.map{|k,v| escape(k) << '=' << escape(v)}.join('&')
247
+ count, cas = kvstore.fetch(counter_key(time_block, metric_name, dimensions))
248
+ sum, cas = kvstore.fetch(sum_key(time_block, metric_name, dimensions))
249
+ sumsqr, cas = kvstore.fetch(sumsqr_key(time_block, metric_name, dimensions))
250
+ return nil if count.nil? || sum.nil? || sumsqr.nil? || count == 0
251
+ Math.sqrt(count * sumsqr - sum*sum) / count
252
+ end
253
+
254
+ def count_of_groups(args={})
255
+ group = escape(required(args, :group))
256
+ time_block = required(args, :hour)
257
+ metric_name = escape(required(args, :what).to_s)
258
+ dimensions = (args[:where] || {}).sort.map{|k,v| escape(k) << '=' << escape(v)}.join('&')
259
+ result, cas = kvstore.fetch(group_counter_key(time_block, metric_name, group, dimensions))
260
+ result || 0
261
+ end
262
+
263
+ def sum_of_ranges(args={})
264
+ group = escape(required(args, :group))
265
+ time_block = required(args, :hour)
266
+ metric_name = escape(required(args, :what).to_s)
267
+ dimensions = (args[:where] || {}).sort.map{|k,v| escape(k) << '=' << escape(v)}.join('&')
268
+ result, cas = kvstore.fetch(range_sum_key(time_block, metric_name, group, dimensions))
269
+ result || 0
270
+ end
271
+
272
+ def average_range(args={})
273
+ group = escape(required(args, :group))
274
+ time_block = required(args, :hour)
275
+ metric_name = escape(required(args, :what).to_s)
276
+ dimensions = (args[:where] || {}).sort.map{|k,v| escape(k) << '=' << escape(v)}.join('&')
277
+ count, cas = kvstore.fetch(group_counter_key(time_block, metric_name, group, dimensions))
278
+ sum, cas = kvstore.fetch(range_sum_key(time_block, metric_name, group, dimensions))
279
+ return nil if count.nil? || sum.nil? || count == 0
280
+ sum.to_f / count
281
+ end
282
+
283
+ def maximum_range(args={})
284
+ group = escape(required(args, :group))
285
+ time_block = required(args, :hour)
286
+ metric_name = escape(required(args, :what).to_s)
287
+ dimensions = (args[:where] || {}).sort.map{|k,v| escape(k) << '=' << escape(v)}.join('&')
288
+ range, cas = kvstore.fetch(group_range_key(time_block, metric_name, group, dimensions))
289
+ range.nil? ? nil : range[1]
290
+ end
291
+
292
+ def minimum_range(args={})
293
+ group = escape(required(args, :group))
294
+ time_block = required(args, :hour)
295
+ metric_name = escape(required(args, :what).to_s)
296
+ dimensions = (args[:where] || {}).sort.map{|k,v| escape(k) << '=' << escape(v)}.join('&')
297
+ range, cas = kvstore.fetch(group_range_key(time_block, metric_name, group, dimensions))
298
+ range.nil? ? nil : range[0]
299
+ end
300
+
301
+ def stddev_of_ranges(args={})
302
+ group = escape(required(args, :group))
303
+ time_block = required(args, :hour)
304
+ metric_name = escape(required(args, :what).to_s)
305
+ dimensions = (args[:where] || {}).sort.map{|k,v| escape(k) << '=' << escape(v)}.join('&')
306
+ count, cas = kvstore.fetch(group_counter_key(time_block, metric_name, group, dimensions))
307
+ sum, cas = kvstore.fetch(range_sum_key(time_block, metric_name, group, dimensions))
308
+ sumsqr, cas = kvstore.fetch(range_sumsqr_key(time_block, metric_name, group, dimensions))
309
+ return nil if count.nil? || sum.nil? || sumsqr.nil? || count == 0
310
+ Math.sqrt(count * sumsqr - sum*sum) / count
311
+ end
312
+
313
+ def estimated_list_size(args={})
314
+ time_block = required(args, :hour)
315
+ metric_name = escape(required(args, :what).to_s)
316
+ list_name = escape(required(args, :list).to_s)
317
+ dimensions = (args[:where] || {}).sort.map{|k,v| escape(k) << '=' << escape(v)}.join('&')
318
+ list, cas = kvstore.fetch(list_key(time_block, metric_name, list_name, dimensions))
319
+ if list == 'overflow'
320
+ bucket_count = 1 << HyperLogLog.bits_needed(CARDINALITY_ESTIMATOR_ERROR_RATE)
321
+ buckets = Enumerator.new do |yielder|
322
+ bucket_count.times do |i|
323
+ key = hyperloglog_key(i, time_block, metric_name, list_name, dimensions)
324
+ range, cas = kvstore.fetch(key)
325
+ yielder << (range.nil? ? nil : range[1])
326
+ end
327
+ end
328
+ HyperLogLog.estimate_cardinality(buckets)
329
+ else
330
+ list.size
331
+ end
332
+ end
333
+
334
+ private
335
+
336
+ attr_reader :kvstore
337
+ attr_reader :inserter
338
+ attr_reader :incrementer
339
+ attr_reader :range_updater
340
+ attr_reader :count_incrementer
341
+
342
+ attr_reader :sleep_interval
343
+ attr_reader :max_healthy_errors
344
+ attr_reader :max_unhandled_errors
345
+ attr_reader :max_retry_delay_in_seconds
346
+
347
+ def date_as_hour(date)
348
+ date.strftime('%Y-%m-%d-%H')
349
+ end
350
+
351
+ def build_key(prefix, time_block, metric, dimensions, list_group=nil)
352
+ key = ''
353
+ key << prefix
354
+ key << ':/'
355
+ key << time_block
356
+ key << '/'
357
+ key << metric
358
+ unless list_group.nil?
359
+ key << '/'
360
+ key << list_group
361
+ end
362
+ key << '?'
363
+ key << dimensions
364
+ key
365
+ end
366
+
367
+ def counter_key(time_block, metric, dimensions)
368
+ build_key('count', time_block, metric, dimensions)
369
+ end
370
+
371
+ def list_key(time_block, metric, list_name, dimensions)
372
+ build_key('list', time_block, metric, dimensions, list_name)
373
+ rescue Exception => e
374
+ puts e.message
375
+ puts e.backtrace
376
+ raise
377
+ end
378
+
379
+ def sum_key(time_block, metric, dimensions)
380
+ build_key('sum', time_block, metric, dimensions)
381
+ end
382
+
383
+ def sumsqr_key(time_block, metric, dimensions)
384
+ build_key('sumsqr', time_block, metric, dimensions)
385
+ end
386
+
387
+ def range_key(time_block, metric, dimensions)
388
+ build_key('range', time_block, metric, dimensions)
389
+ end
390
+
391
+ def group_counter_key(time_block, metric, group_name, dimensions)
392
+ build_key('rangecount', time_block, metric, dimensions, group_name)
393
+ end
394
+
395
+ def group_range_key(time_block, metric, group_name, dimensions)
396
+ build_key('rangerange', time_block, metric, dimensions, group_name)
397
+ end
398
+
399
+ def range_sum_key(time_block, metric, group_name, dimensions)
400
+ build_key('rangesum', time_block, metric, dimensions, group_name)
401
+ end
402
+
403
+ def range_sumsqr_key(time_block, metric, group_name, dimensions)
404
+ build_key('rangesumsqr', time_block, metric, dimensions, group_name)
405
+ end
406
+
407
+ def hyperloglog_key(index, time_block, metric, list_name, dimensions)
408
+ build_key("hyperloglog:#{index.to_i}", time_block, metric, dimensions, list_name)
409
+ end
410
+
411
+ def required(args, argument_name)
412
+ args[argument_name] || raise(ArgumentError, "missing argument: #{argument_name}")
413
+ end
414
+
415
+ def assert_open!
416
+ raise "Client has not been opened" unless @open
417
+ end
418
+
419
+ def escape(s)
420
+ CGI.escape(s.to_s)
421
+ end
422
+ end
423
+ end