drone 1.0.4 → 1.0.5

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.
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