metricstore 0.0.1 → 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.
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