drone 1.0.4 → 1.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/Gemfile +13 -0
  2. data/Guardfile +12 -0
  3. data/Rakefile +22 -37
  4. data/drone.gemspec +2 -6
  5. data/examples/collectd.rb +51 -0
  6. data/examples/common.rb +24 -0
  7. data/examples/json.rb +49 -0
  8. data/examples/redis_storage.rb +60 -0
  9. data/examples/simple.rb +3 -3
  10. data/extensions/drone_collectd/Gemfile +7 -0
  11. data/extensions/drone_collectd/LICENSE +20 -0
  12. data/extensions/drone_collectd/README.md +24 -0
  13. data/extensions/drone_collectd/drone_collectd.gemspec +28 -0
  14. data/extensions/drone_collectd/lib/drone_collectd.rb +7 -0
  15. data/extensions/drone_collectd/lib/drone_collectd/collectd.rb +97 -0
  16. data/extensions/drone_collectd/lib/drone_collectd/parser.rb +86 -0
  17. data/extensions/drone_collectd/specs/common.rb +3 -0
  18. data/extensions/drone_collectd/specs/unit/parser_spec.rb +49 -0
  19. data/extensions/drone_json/Gemfile +6 -0
  20. data/extensions/drone_json/LICENSE +20 -0
  21. data/extensions/drone_json/README.md +9 -0
  22. data/extensions/drone_json/drone_json.gemspec +32 -0
  23. data/extensions/drone_json/lib/drone_json.rb +9 -0
  24. data/extensions/drone_json/lib/drone_json/json.rb +100 -0
  25. data/extensions/drone_json/specs/common.rb +63 -0
  26. data/extensions/drone_redis/Gemfile +7 -0
  27. data/extensions/drone_redis/drone_redis.gemspec +22 -0
  28. data/extensions/drone_redis/lib/drone_redis.rb +8 -0
  29. data/extensions/drone_redis/lib/drone_redis/redis.rb +218 -0
  30. data/lib/drone.rb +1 -0
  31. data/lib/drone/errors.rb +11 -0
  32. data/lib/drone/metrics/histogram.rb +7 -6
  33. data/lib/drone/metrics/meter.rb +1 -1
  34. data/lib/drone/monitoring.rb +2 -2
  35. data/lib/drone/storage/memory.rb +1 -0
  36. data/lib/drone/utils/exponentially_decaying_sample.rb +79 -24
  37. data/lib/drone/version.rb +1 -1
  38. data/specs/{unit → metrics}/histogram_spec.rb +5 -1
  39. data/specs/metrics/meter_spec.rb +10 -2
  40. data/specs/metrics/timer_spec.rb +7 -1
  41. data/specs/{unit/monitoring_spec.rb → monitoring_spec.rb} +25 -1
  42. data/specs/{unit → utils}/ewma_spec.rb +1 -0
  43. data/specs/utils/exponentially_decaying_sample_spec.rb +140 -0
  44. data/specs/{unit → utils}/uniform_sample_spec.rb +0 -0
  45. metadata +72 -93
  46. data/specs/unit/exponentially_decaying_sample_spec.rb +0 -86
@@ -6,6 +6,7 @@ module Drone
6
6
 
7
7
  require_lib("drone/version")
8
8
 
9
+ require_lib("drone/errors")
9
10
  require_lib("drone/monitoring")
10
11
 
11
12
  # Schedulers
@@ -0,0 +1,11 @@
1
+ module Drone
2
+ ##
3
+ # Generic base class for all the errors drone can raises
4
+ class DroneError < RuntimeError; end
5
+
6
+ ##
7
+ # Raised when trying to reuse a meter name with a different
8
+ # type.
9
+ class AlreadyDefined < DroneError; end
10
+
11
+ end
@@ -41,12 +41,13 @@ module Drone
41
41
 
42
42
  def clear
43
43
  @sample.clear()
44
- @count = 0
45
- @_min = MAX
46
- @_max = MIN
47
- @_sum = 0
48
- @varianceM = -1
49
- @varianceS = 0
44
+
45
+ @count = Drone::request_number("#{name}:count", 0)
46
+ @_min = Drone::request_number("#{name}:min", MAX)
47
+ @_max = Drone::request_number("#{name}:max", MIN)
48
+ @_sum = Drone::request_number("#{name}:max", 0)
49
+ @varianceM = Drone::request_number("#{name}:varianceM", -1)
50
+ @varianceS = Drone::request_number("#{name}:varianceS", 0)
50
51
  end
51
52
 
52
53
  def update(val)
@@ -7,7 +7,7 @@ require File.expand_path('../../utils/ewma', __FILE__)
7
7
  module Drone
8
8
  module Metrics
9
9
  ##
10
- # A meter measures mean throughput and one-, five-, and
10
+ # This meter measures mean throughput and one-, five-, and
11
11
  # fifteen-minute exponentially-weighted moving average throughputs.
12
12
  #
13
13
  class Meter < Metric
@@ -28,7 +28,7 @@ module Drone
28
28
  def monitor_rate(name)
29
29
  meter = Drone::find_metric(name) || Metrics::Meter.new(name)
30
30
  unless meter.is_a?(Metrics::Meter)
31
- raise(TypeError, "metric #{name} is already defined as #{rate.class}")
31
+ raise AlreadyDefined, "metric #{name} is already defined as #{meter.class}"
32
32
  end
33
33
 
34
34
  Drone::register_metric(meter)
@@ -47,7 +47,7 @@ module Drone
47
47
  def monitor_time(name)
48
48
  timer = Drone::find_metric(name) || Metrics::Timer.new(name)
49
49
  unless timer.is_a?(Metrics::Timer)
50
- raise(TypeError, "metric #{name} is already defined as #{rate.class}")
50
+ raise AlreadyDefined, "metric #{name} is already defined as #{timer.class}"
51
51
  end
52
52
  Drone::register_metric(timer)
53
53
  @_timer_waiting = timer
@@ -36,6 +36,7 @@ module Drone
36
36
  # dummy implementation, with memory storage nothing can
37
37
  # happen to our data
38
38
  set(new_value)
39
+ true
39
40
  end
40
41
 
41
42
  end
@@ -1,10 +1,29 @@
1
+ require 'flt'
1
2
  require File.expand_path('../../core', __FILE__)
2
3
 
3
4
  module Drone
4
- class ExponentiallyDecayingSample
5
- # 1 hour in ms
6
- RESCALE_THRESHOLD = (1 * 60 * 60 * 1000).freeze
7
5
 
6
+ ##
7
+ # An exponentially-decaying random sample
8
+ #
9
+ class ExponentiallyDecayingSample
10
+ # 1 hour
11
+ RESCALE_THRESHOLD = (1 * 60 * 60).freeze
12
+
13
+ ##
14
+ # Create a new dataset, if the decay factor is too big
15
+ # the flt ruby library will be used for internal computations
16
+ # to allow greater precision, the switch happens if alpha is
17
+ # higher than 0.1.
18
+ #
19
+ # @param [String] id A unique id representing this
20
+ # dataset.
21
+ # @param [Integer] reservoir_size the number of samples
22
+ # to keep.
23
+ # @param [Number] alpha the decay factor, the higher this
24
+ # number, the more biased the sample will be towards
25
+ # newer values.
26
+ #
8
27
  def initialize(id, reservoir_size, alpha)
9
28
  @id = id
10
29
  @values = Drone::request_hash("#{@id}:values")
@@ -28,25 +47,28 @@ module Drone
28
47
  (@values.size < count) ? @values.size : count
29
48
  end
30
49
 
31
-
32
50
  def update(val, time = current_time)
33
- priority = weight(time - @start_time.get) / rand()
34
- count = @count.inc
35
- if count <= @reservoir_size
51
+ priority = weight(time - @start_time.get) / generate_random()
52
+ new_count = @count.inc
53
+
54
+ if new_count <= @reservoir_size
36
55
  @values[priority] = val
37
56
  else
38
- first = @values.keys.min
57
+ first = @values.keys[0]
39
58
  if first < priority
40
- @values[priority] = val
41
- while @values.delete(first) == nil
42
- first = @values.keys.min
59
+ old_val, @values[priority] = @values[priority], val
60
+ unless old_val
61
+ while @values.delete(first) == nil
62
+ first = @values.keys[0]
63
+ end
43
64
  end
44
65
  end
45
66
  end
46
67
 
47
68
  now = current_time()
48
- if now >= @next_scale_time.get
49
- rescale(now)
69
+ next_scale = @next_scale_time.get
70
+ if now >= next_scale
71
+ rescale(now, next_scale)
50
72
  end
51
73
  end
52
74
 
@@ -55,26 +77,59 @@ module Drone
55
77
  buff << @values[key]
56
78
  end
57
79
  end
58
-
59
- def rescale(now)
60
- @next_scale_time.set( current_time() + RESCALE_THRESHOLD )
61
- new_start = current_time()
62
- old_start = @start_time.get_and_set(new_start)
63
-
64
- @values = Hash[ @values.map{ |k,v|
65
- [k * Math.exp(-@alpha * (new_start - old_start)), v]
66
- }]
67
80
 
81
+ def rescale(now, next_scale)
82
+ if @next_scale_time.compare_and_set(next_scale, now + RESCALE_THRESHOLD)
83
+ new_start = current_time()
84
+ old_start = @start_time.get_and_set( new_start )
85
+ time_diff = new_start - old_start
86
+
87
+ @values = Hash[ @values.map{ |k,v|
88
+ [k * math_exp(-@alpha * time_diff), v]
89
+ }]
90
+
91
+ end
68
92
  end
69
93
 
70
94
  private
95
+
96
+ def use_flt?
97
+ @alpha > 0.1
98
+ end
99
+
100
+ def math_exp(n)
101
+ if use_flt?
102
+ Flt::DecNum(Rational(n)).exp()
103
+ else
104
+ Math.exp(n)
105
+ end
106
+ end
107
+
108
+ ##
109
+ # Generates a non-zero random number
110
+ # According to the ruby documentation rand() could return 0
111
+ # so we ensure this will never happen
112
+ #
113
+ # @return [Float] The random number
114
+ #
115
+ def generate_random()
116
+ begin
117
+ r = Kernel.rand()
118
+ end while r == 0.0
119
+
120
+ if use_flt?
121
+ Flt::DecNum(Rational(r))
122
+ else
123
+ r
124
+ end
125
+ end
71
126
 
72
127
  def current_time
73
- Time.now.to_f * 1000
128
+ Time.now.to_f
74
129
  end
75
130
 
76
131
  def weight(n)
77
- Math.exp(@alpha * n)
132
+ math_exp(@alpha * n)
78
133
  end
79
134
 
80
135
  end
@@ -1,3 +1,3 @@
1
1
  module Drone
2
- VERSION = "1.0.4"
2
+ VERSION = "1.0.5"
3
3
  end
@@ -9,7 +9,7 @@ describe 'Histogram' do
9
9
  Drone::init_drone()
10
10
  end
11
11
 
12
- describe "A histogram with zero recorded valeus" do
12
+ describe "A histogram with zero recorded values" do
13
13
  before do
14
14
  @histogram = Histogram.new("id1", UniformSample.new("id1:sample", 100))
15
15
  end
@@ -17,6 +17,10 @@ describe 'Histogram' do
17
17
  should "have a count of 0" do
18
18
  @histogram.count.should == 0
19
19
  end
20
+
21
+ should "have a variance of 0" do
22
+ @histogram.send(:variance).should == 0
23
+ end
20
24
 
21
25
  should "have a max of 0" do
22
26
  @histogram.max.should == 0
@@ -48,14 +48,22 @@ EM.describe 'Meter Metrics' do
48
48
 
49
49
  describe "A meter metric with three events" do
50
50
  before do
51
- @meter = Metrics::Meter.new("thangs")
52
- @meter.mark(3)
51
+ Delorean.time_travel_to("2 second ago") do
52
+ @meter = Metrics::Meter.new("thangs")
53
+ @meter.mark(3)
54
+ end
53
55
  end
54
56
 
55
57
  should "have a count of three" do
56
58
  @meter.count.should == 3
57
59
  done
58
60
  end
61
+
62
+ should "have a mean rate of 0 events/sec" do
63
+ @meter.mean_rate.should.be.close?(1.5, 0.01)
64
+ done
65
+ end
66
+
59
67
  end
60
68
 
61
69
  end
@@ -9,7 +9,7 @@ EM.describe 'Timer Metrics' do
9
9
  Drone::start_monitoring()
10
10
  end
11
11
 
12
- describe "A blank timer" do
12
+ describe "A newly created timer" do
13
13
  before do
14
14
  @timer = Metrics::Timer.new('id')
15
15
  end
@@ -91,6 +91,12 @@ EM.describe 'Timer Metrics' do
91
91
  @timer.stdDev.should.be.close?(11.401, 0.001)
92
92
  done
93
93
  end
94
+
95
+ it 'can be cleared' do
96
+ @timer.clear()
97
+ @timer.count.should == 0
98
+ done
99
+ end
94
100
 
95
101
  should "calculate the median/p95/p98/p99/p999" do
96
102
  median, p95, p98, p99, p999 = @timer.percentiles(0.5, 0.95, 0.98, 0.99, 0.999)
@@ -1,4 +1,4 @@
1
- require File.expand_path('../../common', __FILE__)
1
+ require File.expand_path('../common', __FILE__)
2
2
 
3
3
  require 'drone'
4
4
  require 'drone/monitoring'
@@ -21,11 +21,35 @@ EM.describe 'Monitoring' do
21
21
  monitor_rate("users/with_block")
22
22
  def method_with_block(&block); block.call; end
23
23
 
24
+ monitor_time("users/timed")
25
+ def timed_method; end;
26
+
24
27
  end
25
28
  @obj = @klass.new
26
29
 
27
30
  end
28
31
 
32
+ should "raise an error if the metric name os already used (rate => time)" do
33
+ proc{
34
+ @klass.instance_eval do
35
+ monitor_time("users/no_args")
36
+ end
37
+ }.should.raise(Drone::AlreadyDefined)
38
+
39
+ done
40
+ end
41
+
42
+ should "raise an error if the metric name os already used (time => rate)" do
43
+
44
+ proc{
45
+ @klass.instance_eval do
46
+ monitor_rate("users/timed")
47
+ end
48
+ }.should.raise(Drone::AlreadyDefined)
49
+
50
+ done
51
+ end
52
+
29
53
  should 'reuse same meter for every instances of this class' do
30
54
  meter = Drone::find_metric("users/no_args")
31
55
  meter.count.should == 0
@@ -23,6 +23,7 @@ describe 'EWMA' do
23
23
 
24
24
  should "have a rate of 0.6 events/sec after the first tick" do
25
25
  @ewma.rate.should.be.close(0.6, 0.000001)
26
+ @ewma.rate(:ms).should.be.close(0.0006, 0.000001)
26
27
  end
27
28
 
28
29
  {
@@ -0,0 +1,140 @@
1
+ require File.expand_path('../../common', __FILE__)
2
+
3
+ require 'drone/utils/exponentially_decaying_sample'
4
+ include Drone
5
+
6
+ describe 'Exponentially Decaying Sample' do
7
+ before do
8
+ Drone::init_drone(nil, Storage::Memory.new)
9
+ end
10
+
11
+ describe "A sample of 100 out of 1000 elements" do
12
+ before do
13
+ @population = (0...100)
14
+ @sample = ExponentiallyDecayingSample.new('id1', 1000, 0.99)
15
+ @population.step(1){|n| @sample.update(n) }
16
+ end
17
+
18
+
19
+ should "have 100 elements" do
20
+ @sample.size.should == 100
21
+ @sample.values.size.should == 100
22
+ end
23
+
24
+ should "only have elements from the population" do
25
+ arr = @sample.values - @population.to_a
26
+ arr.should == []
27
+ end
28
+
29
+ end
30
+
31
+
32
+ describe "A sample of 100 out of 10 elements" do
33
+ before do
34
+ @population = (0...100)
35
+ @sample = ExponentiallyDecayingSample.new('id1', 100, 0.99)
36
+ @population.step(1){|n| @sample.update(n) }
37
+ end
38
+
39
+ should "have 100 elements" do
40
+ @sample.size.should == 100
41
+ @sample.values.size.should == 100
42
+ end
43
+
44
+ should "only have elements from the population" do
45
+ arr = @sample.values - @population.to_a
46
+ arr.should == []
47
+ end
48
+
49
+ should "rescale after 1 hour2" do
50
+ Delorean.time_travel_to("1 hours from now") do
51
+ @sample.update(42)
52
+ end
53
+
54
+ @sample.size.should == 100
55
+ @sample.values.size.should == 100
56
+ end
57
+
58
+ end
59
+
60
+
61
+ describe "A heavily-biased sample of 100 out of 1000 elements" do
62
+ before do
63
+ @population = (0...100)
64
+ @sample = ExponentiallyDecayingSample.new('id1', 100, 0.01)
65
+ @population.step(1){|n| @sample.update(n) }
66
+ end
67
+
68
+ should "have 100 elements" do
69
+ @sample.size.should == 100
70
+ @sample.values.size.should == 100
71
+ end
72
+
73
+ should "only have elements from the population" do
74
+ arr = @sample.values - @population.to_a
75
+ arr.should == []
76
+ end
77
+
78
+ should "rescale after 1 hour" do
79
+ Delorean.time_travel_to("1 hours from now") do
80
+ @sample.update(42)
81
+ end
82
+
83
+ @sample.size.should == 100
84
+ @sample.values.size.should == 100
85
+ end
86
+
87
+ end
88
+
89
+ describe "A heavily-biased sample of 1000 out of 1000 elements" do
90
+ before do
91
+ @population = (0...1000)
92
+ @sample = ExponentiallyDecayingSample.new('id1', 1000, 0.01)
93
+ @population.step(1){|n| @sample.update(n) }
94
+ end
95
+
96
+ it "should have 1000 elements" do
97
+ @sample.size.should == 1000
98
+ @sample.values.length.should == 1000
99
+ end
100
+
101
+ it "should only have elements from the population" do
102
+ values = @sample.values
103
+ @population.each do |datum|
104
+ values.should.include?(datum)
105
+ end
106
+ end
107
+
108
+ it "should replace an element when updating" do
109
+ Delorean.time_travel_to("10 minutes from now") do
110
+ @sample.update(4242)
111
+ @sample.size.should == 1000
112
+ @sample.values.should.include?(4242)
113
+ end
114
+ end
115
+
116
+ it "should rescale so that newer events are higher in priority in the hash" do
117
+ Delorean.time_travel_to("1 hour from now") do
118
+ @sample.update(2121)
119
+ @sample.size.should == 1000
120
+ end
121
+
122
+ Delorean.time_travel_to("2 hours from now") do
123
+ @sample.update(4242)
124
+ @sample.size.should == 1000
125
+
126
+ values = @sample.values
127
+
128
+ values.length.should == 1000
129
+ values.should.include?(4242)
130
+ values.should.include?(2121)
131
+
132
+ # Most recently added values in time should be at the end with the highest priority
133
+ values[999].should == 4242
134
+ values[998].should == 2121
135
+ end
136
+
137
+ end
138
+ end
139
+
140
+ end