tabs 0.9.1 → 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.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/.ruby-version +1 -1
  4. data/.travis.yml +3 -0
  5. data/README.md +85 -5
  6. data/lib/tabs/config.rb +37 -2
  7. data/lib/tabs/metrics/counter.rb +14 -5
  8. data/lib/tabs/metrics/task.rb +8 -6
  9. data/lib/tabs/metrics/task/token.rb +25 -10
  10. data/lib/tabs/metrics/value.rb +35 -27
  11. data/lib/tabs/resolution.rb +26 -10
  12. data/lib/tabs/resolutionable.rb +36 -13
  13. data/lib/tabs/resolutions/day.rb +9 -1
  14. data/lib/tabs/resolutions/hour.rb +9 -1
  15. data/lib/tabs/resolutions/minute.rb +9 -1
  16. data/lib/tabs/resolutions/month.rb +9 -1
  17. data/lib/tabs/resolutions/week.rb +13 -7
  18. data/lib/tabs/resolutions/year.rb +9 -1
  19. data/lib/tabs/storage.rb +39 -17
  20. data/lib/tabs/tabs.rb +12 -4
  21. data/lib/tabs/version.rb +1 -1
  22. data/spec/lib/tabs/config_spec.rb +60 -0
  23. data/spec/lib/tabs/metrics/counter_spec.rb +44 -1
  24. data/spec/lib/tabs/{task_spec.rb → metrics/task_spec.rb} +31 -3
  25. data/spec/lib/tabs/metrics/value_spec.rb +36 -0
  26. data/spec/lib/tabs/resolution_spec.rb +26 -3
  27. data/spec/lib/tabs/resolutionable_spec.rb +53 -0
  28. data/spec/lib/tabs/resolutions/day_spec.rb +23 -0
  29. data/spec/lib/tabs/resolutions/hour_spec.rb +23 -0
  30. data/spec/lib/tabs/resolutions/minute_spec.rb +23 -0
  31. data/spec/lib/tabs/resolutions/month_spec.rb +23 -0
  32. data/spec/lib/tabs/resolutions/week_spec.rb +24 -0
  33. data/spec/lib/tabs/resolutions/year_spec.rb +23 -0
  34. data/spec/lib/tabs/storage_spec.rb +138 -0
  35. data/spec/lib/tabs_spec.rb +28 -1
  36. data/spec/spec_helper.rb +9 -1
  37. data/spec/support/custom_resolutions.rb +10 -2
  38. data/tabs.gemspec +6 -21
  39. metadata +48 -81
@@ -0,0 +1,60 @@
1
+ require "spec_helper"
2
+ require File.expand_path("../../../support/custom_resolutions", __FILE__)
3
+
4
+ describe Tabs::Config do
5
+ context "#decimal_precision" do
6
+
7
+ before do
8
+ @precision = Tabs::Config.decimal_precision
9
+ end
10
+
11
+ after do
12
+ Tabs::Config.decimal_precision = @precision
13
+ end
14
+
15
+ it "should set/get the decimal precision" do
16
+ Tabs::Config.decimal_precision = 4
17
+ expect(Tabs::Config.decimal_precision).to eq(4)
18
+ end
19
+ end
20
+
21
+ context "#register_resolution" do
22
+ it "should register a resolution" do
23
+ Tabs::Resolution.register(WellFormedResolution)
24
+ expect(Tabs::Resolution.all).to include(:seconds)
25
+ end
26
+ end
27
+
28
+ context "#unregister_resolution" do
29
+ it "should unregister a resolution" do
30
+ Tabs::Resolution.unregister(:minute)
31
+ expect(Tabs::Resolution.all).to_not include(:minute)
32
+ end
33
+ end
34
+
35
+ context "#set_expirations" do
36
+
37
+ after do
38
+ Tabs::Config.reset_expirations
39
+ end
40
+
41
+ it "should allow multiple resolutions to be expired" do
42
+ Tabs::Config.set_expirations({ minute: 1.day, hour: 1.week })
43
+ expect(Tabs::Config.expiration_settings[:minute]).to eq(1.day)
44
+ expect(Tabs::Config.expiration_settings[:hour]).to eq(1.week)
45
+ end
46
+
47
+ it "should raise ResolutionMissingError if expiration passed in for invalid resolution" do
48
+ expect{ Tabs::Config.set_expirations({ missing_resolution: 1.day }) }
49
+ .to raise_error(Tabs::ResolutionMissingError)
50
+ end
51
+
52
+ end
53
+
54
+ context "#prefix" do
55
+ it "should allow custom prefix for tabs keys" do
56
+ Tabs::Config.prefix = "rspec"
57
+ expect(Tabs::Config.prefix).to eq("rspec")
58
+ end
59
+ end
60
+ end
@@ -25,6 +25,13 @@ describe Tabs::Metrics::Counter do
25
25
  expect(stats).to include({ "timestamp" => time, "count" => 1 })
26
26
  end
27
27
 
28
+ it "raises ResolutionMissingError if unregistered resolution requested" do
29
+ time = Time.utc(now.year, now.month, now.day, now.hour) - 2.hours
30
+ metric.increment(time)
31
+ Tabs::Resolution.unregister(:hour)
32
+ expect { metric.stats(((now - 3.hours)..now), :hour) }.to raise_error(Tabs::ResolutionMissingError)
33
+ end
34
+
28
35
  end
29
36
 
30
37
  describe "total count" do
@@ -47,6 +54,8 @@ describe Tabs::Metrics::Counter do
47
54
  end
48
55
 
49
56
  def create_span(time_unit)
57
+ metric.increment
58
+ Timecop.freeze(now + 1.send(time_unit))
50
59
  metric.increment
51
60
  Timecop.freeze(now + 3.send(time_unit))
52
61
  metric.increment
@@ -94,7 +103,7 @@ describe Tabs::Metrics::Counter do
94
103
  it "returns zeros for time periods which do not have any events" do
95
104
  create_span(:days)
96
105
  stats = metric.stats(now..(now + 7.days), :day)
97
- expect(stats).to include({ "timestamp" => (now + 1.day), "count" => 0 })
106
+ expect(stats.detect{|s| s["timestamp"] == (now + 2.day)}["count"]).to eq(0)
98
107
  end
99
108
 
100
109
  context "for weekly metrics" do
@@ -106,6 +115,7 @@ describe Tabs::Metrics::Counter do
106
115
  it "returns the expected results for a weekly metric" do
107
116
  create_span(:weeks)
108
117
  stats = metric.stats(period, :week)
118
+ expect(stats.detect{|s| s["timestamp"] == (now + 1.week).beginning_of_week}["count"]).to eq(1)
109
119
  expect(stats).to include({ "timestamp" => (now + 3.weeks).beginning_of_week, "count" => 1 })
110
120
  expect(stats).to include({ "timestamp" => (now + 6.weeks).beginning_of_week, "count" => 2 })
111
121
  end
@@ -150,4 +160,37 @@ describe Tabs::Metrics::Counter do
150
160
 
151
161
  end
152
162
 
163
+ describe ".drop_by_resolution!" do
164
+ before do
165
+ Timecop.freeze(now)
166
+ 2.times { metric.increment }
167
+ metric.drop_by_resolution!(:minute)
168
+ end
169
+
170
+ it "deletes all metrics for a resolution" do
171
+ stats = metric.stats((now - 1.minute)..(now + 1.minute), :minute)
172
+ expect(stats.total).to eq(0)
173
+ end
174
+ end
175
+
176
+ describe "expiration of counter metrics" do
177
+ let(:expires_setting){ 6.hours }
178
+ let(:now){ Time.utc(2050, 1, 1, 0, 0) }
179
+
180
+ before do
181
+ Tabs::Config.set_expirations({ minute: expires_setting })
182
+ end
183
+
184
+ after do
185
+ Tabs::Config.reset_expirations
186
+ end
187
+
188
+ it "sets an expiration when recording a value" do
189
+ metric.increment(now)
190
+ redis_expire_date = Time.now + Tabs::Storage.ttl(metric.storage_key(:minute, now))
191
+ expire_date = now + expires_setting + Tabs::Resolutions::Minute.to_seconds
192
+ expect(redis_expire_date).to be_within(2.seconds).of(expire_date)
193
+ end
194
+ end
195
+
153
196
  end
@@ -10,7 +10,7 @@ describe Tabs::Metrics::Task do
10
10
 
11
11
  describe ".start" do
12
12
 
13
- let(:token) { stub(:token) }
13
+ let(:token) { double(:token) }
14
14
  let(:time) { Time.now }
15
15
 
16
16
  it "calls start on the given token" do
@@ -29,11 +29,10 @@ describe Tabs::Metrics::Task do
29
29
 
30
30
  describe ".complete" do
31
31
 
32
- let(:token) { stub(:token) }
32
+ let(:token) { double(:token) }
33
33
  let(:time) { Time.now }
34
34
 
35
35
  it "calls complete on the given token" do
36
- token = stub(:token)
37
36
  Tabs::Metrics::Task::Token.should_receive(:new).with(token_1, "foo").and_return(token)
38
37
  token.should_receive(:complete)
39
38
  metric.complete(token_1)
@@ -53,6 +52,16 @@ describe Tabs::Metrics::Task do
53
52
 
54
53
  describe ".stats" do
55
54
 
55
+ it "returns zeroes across the board for no stats" do
56
+ stats = metric.stats((now - 5.minutes)..(now + 5.minutes), :minute)
57
+
58
+ expect(stats.started_within_period).to eq 0
59
+ expect(stats.completed_within_period).to eq 0
60
+ expect(stats.started_and_completed_within_period).to eq 0
61
+ expect(stats.completion_rate).to eq 0.0
62
+ expect(stats.average_completion_time).to eq 0.0
63
+ end
64
+
56
65
  it "returns the expected value" do
57
66
  Timecop.freeze(now)
58
67
  metric.start(token_1)
@@ -63,6 +72,25 @@ describe Tabs::Metrics::Task do
63
72
  Timecop.freeze(now + 3.minutes)
64
73
  metric.complete(token_3)
65
74
  stats = metric.stats((now - 5.minutes)..(now + 5.minutes), :minute)
75
+
76
+ expect(stats.started_within_period).to eq 3
77
+ expect(stats.completed_within_period).to eq 2
78
+ expect(stats.started_and_completed_within_period).to eq 2
79
+ expect(stats.completion_rate).to eq 0.18182
80
+ expect(stats.average_completion_time).to eq 1.5
81
+ end
82
+
83
+ it "returns the expected value for a week" do
84
+ Timecop.freeze(now)
85
+ metric.start(token_1)
86
+ metric.start(token_2)
87
+ Timecop.freeze(now + 1.week)
88
+ metric.complete(token_1)
89
+ metric.start(token_3)
90
+ Timecop.freeze(now + 3.weeks)
91
+ metric.complete(token_3)
92
+ stats = metric.stats((now - 5.weeks)..(now + 5.weeks), :week)
93
+
66
94
  expect(stats.started_within_period).to eq 3
67
95
  expect(stats.completed_within_period).to eq 2
68
96
  expect(stats.started_and_completed_within_period).to eq 2
@@ -40,6 +40,8 @@ describe Tabs::Metrics::Value do
40
40
 
41
41
  def create_span(time_unit)
42
42
  metric.record(5)
43
+ Timecop.freeze(now + 1.send(time_unit))
44
+ metric.record(25)
43
45
  Timecop.freeze(now + 3.send(time_unit))
44
46
  metric.record(10)
45
47
  Timecop.freeze(now + 6.send(time_unit))
@@ -78,6 +80,8 @@ describe Tabs::Metrics::Value do
78
80
  it "returns the expected results for a weekly metric" do
79
81
  create_span(:weeks)
80
82
  stats = metric.stats(now..(now + 7.weeks), :week)
83
+ second_week_stats = stats.detect{|s| s["timestamp"] == (now + 1.week).beginning_of_week }
84
+ expect(second_week_stats["count"]).to eq(1)
81
85
  expect(stats).to include({ "timestamp" => (now + 3.weeks).beginning_of_week, "count"=>1, "min"=>10, "max"=>10, "sum"=>10, "avg"=>10})
82
86
  expect(stats).to include({ "timestamp" => (now + 6.weeks).beginning_of_week, "count"=>2, "min"=>15, "max"=>20, "sum"=>35, "avg"=>17.5})
83
87
  end
@@ -121,4 +125,36 @@ describe Tabs::Metrics::Value do
121
125
 
122
126
  end
123
127
 
128
+ describe ".drop_by_resolution!" do
129
+ before do
130
+ Timecop.freeze(now)
131
+ 2.times { metric.record(rand(30)) }
132
+ metric.drop_by_resolution!(:minute)
133
+ end
134
+
135
+ it "deletes all metrics for a resolution" do
136
+ stats = metric.stats((now - 1.minute)..(now + 1.minute), :minute)
137
+ expect(stats.sum).to eq(0)
138
+ end
139
+ end
140
+
141
+ describe "expiration of value metrics" do
142
+ let(:expires_setting){ 6.hours }
143
+ let(:now){ Time.utc(2050, 1, 1, 0, 0) }
144
+
145
+ before do
146
+ Tabs::Config.set_expirations({ minute: expires_setting })
147
+ end
148
+
149
+ after do
150
+ Tabs::Config.reset_expirations
151
+ end
152
+
153
+ it "sets an expiration when recording a value" do
154
+ metric.record(17, now)
155
+ redis_expire_date = Time.now + Tabs::Storage.ttl(metric.storage_key(:minute, now))
156
+ expire_date = now + expires_setting + Tabs::Resolutions::Minute.to_seconds
157
+ expect(redis_expire_date).to be_within(2.seconds).of(expire_date)
158
+ end
159
+ end
124
160
  end
@@ -2,10 +2,11 @@ require "spec_helper"
2
2
  require File.expand_path("../../../support/custom_resolutions", __FILE__)
3
3
 
4
4
  describe Tabs::Resolution do
5
+
5
6
  describe "#register" do
6
7
  it "registers a new resolution" do
7
- Tabs::Resolution.register(:test, Tabs::Resolutions::Minute)
8
- expect(Tabs::Resolution.all).to include :test
8
+ Tabs::Resolution.register(WellFormedResolution)
9
+ expect(Tabs::Resolution.all).to include WellFormedResolution.name
9
10
  end
10
11
 
11
12
  context "with a custom resolution" do
@@ -14,7 +15,7 @@ describe Tabs::Resolution do
14
15
  end
15
16
 
16
17
  it "gets stats for custom resolution" do
17
- Tabs::Resolution.register(:seconds, WellFormedResolution)
18
+ Tabs::Resolution.register(WellFormedResolution)
18
19
  Timecop.freeze(Time.now)
19
20
 
20
21
  Tabs.increment_counter("foo")
@@ -24,6 +25,28 @@ describe Tabs::Resolution do
24
25
  it "raises an error when method not implemented" do
25
26
  expect{BadlyFormedResolution.normalize}.to raise_error
26
27
  end
28
+
29
+ it "disregards already registered resolutions" do
30
+ expect { Tabs::Resolution.register(Tabs::Resolutions::Minute) }.to_not raise_error
31
+ end
27
32
  end
28
33
  end
34
+
35
+ describe "#unregister" do
36
+ it "unregisters a single resolution" do
37
+ Tabs::Resolution.unregister(:minute)
38
+ expect(Tabs::Resolution.all).to_not include(:minute)
39
+ end
40
+
41
+ it "unregisters an array of resolutions" do
42
+ Tabs::Resolution.unregister([:minute, :hour])
43
+ expect(Tabs::Resolution.all).to_not include(:hour)
44
+ expect(Tabs::Resolution.all).to_not include(:minute)
45
+ end
46
+
47
+ it "disregards passing in an unrecognized resolution" do
48
+ expect { Tabs::Resolution.unregister(:invalid_resolution) }.to_not raise_error
49
+ end
50
+ end
51
+
29
52
  end
@@ -0,0 +1,53 @@
1
+ require "spec_helper"
2
+
3
+ describe Tabs::Resolutionable do
4
+
5
+ module TestResolution
6
+ include Tabs::Resolutionable
7
+ extend self
8
+
9
+ def name
10
+ :test
11
+ end
12
+
13
+ def to_seconds
14
+ 1000
15
+ end
16
+
17
+ end
18
+
19
+ describe "interface exceptions" do
20
+
21
+ ["serialize", "deserialize", "from_seconds", "add", "normalize"].each do |method|
22
+ it "are raised when the #{method} method is not implemented" do
23
+ expect { TestResolution.send(method) }.to raise_error
24
+ end
25
+ end
26
+
27
+ end
28
+
29
+ describe "#expire" do
30
+ let(:expires_setting){ 1.day }
31
+
32
+ before do
33
+ Tabs::Config.register_resolution(TestResolution)
34
+ Tabs::Config.set_expirations(test: expires_setting)
35
+ end
36
+
37
+ after do
38
+ Tabs::Config.reset_expirations
39
+ Tabs::Config.unregister_resolutions(:test)
40
+ end
41
+
42
+ it "sets the expiration for the given key" do
43
+ now = Time.utc(2050, 1, 1, 0, 0, 0)
44
+ Tabs::Storage.set("foo", "bar")
45
+ TestResolution.expire("foo", now)
46
+ redis_expire_date = Time.now + Tabs::Storage.ttl("foo")
47
+ expire_date = now + expires_setting + TestResolution.to_seconds
48
+ expect(redis_expire_date).to be_within(2.seconds).of(expire_date)
49
+ end
50
+
51
+ end
52
+
53
+ end
@@ -0,0 +1,23 @@
1
+ require "spec_helper"
2
+
3
+ describe Tabs::Resolutions::Day do
4
+ let(:timestamp){ Time.new(2000, 1, 1, 12, 15) }
5
+
6
+ context "#normalize" do
7
+ it "should normalize the date to year, month, day" do
8
+ expect(subject.normalize(timestamp)).to eq(timestamp.utc.change(hour: 0))
9
+ end
10
+ end
11
+
12
+ context "#serialize" do
13
+ it "should return YYYY-MM-DD" do
14
+ expect(subject.serialize(timestamp)).to eq("2000-01-01")
15
+ end
16
+ end
17
+
18
+ context "#deserialize" do
19
+ it "should convert string into date" do
20
+ expect(subject.deserialize("2000-01-01")).to eq(timestamp.utc.change(hour: 0))
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ require "spec_helper"
2
+
3
+ describe Tabs::Resolutions::Hour do
4
+ let(:timestamp){ Time.utc(2000, 1, 1, 14, 12) }
5
+
6
+ context "#normalize" do
7
+ it "should normalize the date to year, month, day, hour" do
8
+ expect(subject.normalize(timestamp)).to eq(timestamp.change(min: 0))
9
+ end
10
+ end
11
+
12
+ context "#serialize" do
13
+ it "should return YYYY-MM-DD-HH" do
14
+ expect(subject.serialize(timestamp)).to eq("2000-01-01-14")
15
+ end
16
+ end
17
+
18
+ context "#deserialize" do
19
+ it "should convert string into date" do
20
+ expect(subject.deserialize("2000-01-01-14")).to eq(timestamp.change(min: 0))
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ require "spec_helper"
2
+
3
+ describe Tabs::Resolutions::Minute do
4
+ let(:timestamp){ Time.utc(2000, 1, 1, 14, 12, 44) }
5
+
6
+ context "#normalize" do
7
+ it "should normalize the date to year, month, day, hour, minute" do
8
+ expect(subject.normalize(timestamp)).to eq(timestamp.change(sec: 0))
9
+ end
10
+ end
11
+
12
+ context "#serialize" do
13
+ it "should return YYYY-MM-DD-HH-MM" do
14
+ expect(subject.serialize(timestamp)).to eq("2000-01-01-14-12")
15
+ end
16
+ end
17
+
18
+ context "#deserialize" do
19
+ it "should convert string into date" do
20
+ expect(subject.deserialize("2000-01-01-14-12")).to eq(timestamp.change(sec: 0))
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ require "spec_helper"
2
+
3
+ describe Tabs::Resolutions::Month do
4
+ let(:timestamp){ Time.utc(2000, 1, 15) }
5
+
6
+ context "#normalize" do
7
+ it "should normalize the date to year, month" do
8
+ expect(subject.normalize(timestamp)).to eq(timestamp.change(day: 1))
9
+ end
10
+ end
11
+
12
+ context "#serialize" do
13
+ it "should return YYYY-MM" do
14
+ expect(subject.serialize(timestamp)).to eq("2000-01")
15
+ end
16
+ end
17
+
18
+ context "#deserialize" do
19
+ it "should convert string into date" do
20
+ expect(subject.deserialize("2000-01")).to eq(timestamp.change(day: 1))
21
+ end
22
+ end
23
+ end