redisrank 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.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/.document +5 -0
  3. data/.gitignore +27 -0
  4. data/.rspec +2 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE +20 -0
  7. data/README.md +297 -0
  8. data/Rakefile +69 -0
  9. data/lib/redisrank.rb +106 -0
  10. data/lib/redisrank/buffer.rb +110 -0
  11. data/lib/redisrank/collection.rb +20 -0
  12. data/lib/redisrank/connection.rb +89 -0
  13. data/lib/redisrank/core_ext.rb +5 -0
  14. data/lib/redisrank/core_ext/bignum.rb +8 -0
  15. data/lib/redisrank/core_ext/date.rb +8 -0
  16. data/lib/redisrank/core_ext/fixnum.rb +8 -0
  17. data/lib/redisrank/core_ext/hash.rb +20 -0
  18. data/lib/redisrank/core_ext/time.rb +3 -0
  19. data/lib/redisrank/date.rb +88 -0
  20. data/lib/redisrank/event.rb +98 -0
  21. data/lib/redisrank/finder.rb +245 -0
  22. data/lib/redisrank/finder/date_set.rb +99 -0
  23. data/lib/redisrank/key.rb +84 -0
  24. data/lib/redisrank/label.rb +69 -0
  25. data/lib/redisrank/mixins/database.rb +11 -0
  26. data/lib/redisrank/mixins/date_helper.rb +8 -0
  27. data/lib/redisrank/mixins/options.rb +41 -0
  28. data/lib/redisrank/mixins/synchronize.rb +52 -0
  29. data/lib/redisrank/model.rb +77 -0
  30. data/lib/redisrank/result.rb +18 -0
  31. data/lib/redisrank/scope.rb +18 -0
  32. data/lib/redisrank/summary.rb +90 -0
  33. data/lib/redisrank/version.rb +3 -0
  34. data/redisrank.gemspec +31 -0
  35. data/spec/Find Results +3349 -0
  36. data/spec/buffer_spec.rb +104 -0
  37. data/spec/collection_spec.rb +20 -0
  38. data/spec/connection_spec.rb +67 -0
  39. data/spec/core_ext/hash_spec.rb +26 -0
  40. data/spec/database_spec.rb +10 -0
  41. data/spec/date_spec.rb +95 -0
  42. data/spec/event_spec.rb +86 -0
  43. data/spec/finder/date_set_spec.rb +527 -0
  44. data/spec/finder_spec.rb +205 -0
  45. data/spec/key_spec.rb +129 -0
  46. data/spec/label_spec.rb +86 -0
  47. data/spec/model_helper.rb +31 -0
  48. data/spec/model_spec.rb +191 -0
  49. data/spec/options_spec.rb +36 -0
  50. data/spec/redis-test.conf +9 -0
  51. data/spec/result_spec.rb +23 -0
  52. data/spec/scope_spec.rb +27 -0
  53. data/spec/spec_helper.rb +18 -0
  54. data/spec/summary_spec.rb +177 -0
  55. data/spec/synchronize_spec.rb +125 -0
  56. data/spec/thread_safety_spec.rb +39 -0
  57. metadata +235 -0
@@ -0,0 +1,191 @@
1
+ require "spec_helper"
2
+ require "model_helper"
3
+
4
+ describe Redisrank::Model do
5
+ include Redisrank::Database
6
+
7
+ before(:each) do
8
+ @time = Time.utc(2010, 8, 28, 12, 0, 0)
9
+ ModelHelper1.redis.flushdb
10
+ ModelHelper2.redis.flushdb
11
+ ModelHelper3.redis.flushdb
12
+ ModelHelper4.redis.flushdb
13
+ end
14
+
15
+ it "should should name itself correctly" do
16
+ ModelHelper1.send(:name).should == "ModelHelper1"
17
+ ModelHelper2.send(:name).should == "ModelHelper2"
18
+ end
19
+
20
+ it "should return a Finder" do
21
+ two_hours_ago = 2.hours.ago
22
+ one_hour_ago = 1.hour.ago
23
+ finder = ModelHelper1.find('label', two_hours_ago, one_hour_ago)
24
+ finder.should be_a(Redisrank::Finder)
25
+ finder.options[:scope].to_s.should == 'ModelHelper1'
26
+ finder.options[:label].to_s.should == 'label'
27
+ finder.options[:from].should == two_hours_ago
28
+ finder.options[:till].should == one_hour_ago
29
+ end
30
+
31
+ it "should #find_event" do
32
+ Redisrank::Event.should_receive(:find).with('ModelHelper1', 1)
33
+ ModelHelper1.find_event(1)
34
+ end
35
+
36
+ it "should listen to model-defined options" do
37
+ ModelHelper2.depth.should == :day
38
+ ModelHelper2.store_event.should == true
39
+ ModelHelper2.hashed_label.should == true
40
+ ModelHelper2.scope.should be_nil
41
+ ModelHelper2.expire.should be_nil
42
+
43
+ ModelHelper1.depth.should == nil
44
+ ModelHelper1.store_event.should == nil
45
+ ModelHelper1.hashed_label.should == nil
46
+ ModelHelper1.depth(:hour)
47
+ ModelHelper1.depth.should == :hour
48
+ ModelHelper1.store_event(true)
49
+ ModelHelper1.store_event.should == true
50
+ ModelHelper1.hashed_label(true)
51
+ ModelHelper1.hashed_label.should == true
52
+ ModelHelper1.options[:depth] = nil
53
+ ModelHelper1.options[:store_event] = nil
54
+ ModelHelper1.options[:hashed_label] = nil
55
+ ModelHelper1.depth.should == nil
56
+ ModelHelper1.store_event.should == nil
57
+ ModelHelper1.hashed_label.should == nil
58
+
59
+ ModelHelper4.scope.should == "FancyHelper"
60
+ ModelHelper4.send(:name).should == "FancyHelper"
61
+ ModelHelper4.expire.should == {:hour => 24*3600}
62
+ end
63
+
64
+ it "should store and fetch stats" do
65
+ ModelHelper1.store("sheep.black", {:count => 6, :weight => 461}, @time.hours_ago(4))
66
+ ModelHelper1.store("sheep.black", {:count => 2, :weight => 156}, @time)
67
+
68
+ stats = ModelHelper1.fetch("sheep.black", @time.hours_ago(2), @time.hours_since(1))
69
+ stats.rank["count"].should == 2
70
+ stats.rank["weight"].should == 156
71
+
72
+ stats = ModelHelper1.fetch("sheep.black", @time.hours_ago(5), @time.hours_since(1))
73
+ stats.rank[:count].should == 6
74
+ stats.rank[:weight].should == 461
75
+
76
+ ModelHelper1.store("sheep.white", {:count => 5, :weight => 50}, @time.hours_ago(4))
77
+ ModelHelper1.store("sheep.white", {:count => 4, :weight => 500}, @time)
78
+
79
+ stats = ModelHelper1.fetch("sheep.white", @time.hours_ago(2), @time.hours_since(1))
80
+ stats.rank[:count].should == 4
81
+ stats.rank[:weight].should == 500
82
+
83
+ stats = ModelHelper1.fetch("sheep.white", @time.hours_ago(5), @time.hours_since(1))
84
+ stats.rank[:count].should == 5
85
+ stats.rank[:weight].should == 500
86
+ end
87
+
88
+ it "should store and fetch grouping enabled stats" do
89
+ ModelHelper1.store("sheep/black", {:count => 6, :weight => 461}, @time.hours_ago(4))
90
+ ModelHelper1.store("sheep/black", {:count => 2, :weight => 156}, @time)
91
+ ModelHelper1.store("sheep/white", {:count => 5, :weight => 393}, @time.hours_ago(4))
92
+ ModelHelper1.store("sheep/white", {:count => 4, :weight => 100}, @time)
93
+
94
+ stats = ModelHelper1.fetch("sheep/black", @time.hours_ago(2), @time.hours_since(1))
95
+ stats.rank["count"].should == 2
96
+ stats.rank["weight"].should == 156
97
+
98
+ stats = ModelHelper1.fetch("sheep/black", @time.hours_ago(5), @time.hours_since(1))
99
+ stats.rank[:count].should == 6
100
+ stats.rank[:weight].should == 461
101
+
102
+ stats = ModelHelper1.fetch("sheep/white", @time.hours_ago(2), @time.hours_since(1))
103
+ stats.rank[:count].should == 4
104
+ stats.rank[:weight].should == 100
105
+
106
+ stats = ModelHelper1.fetch("sheep/white", @time.hours_ago(5), @time.hours_since(1))
107
+ stats.rank[:count].should == 5
108
+ stats.rank[:weight].should == 393
109
+
110
+ stats = ModelHelper1.fetch("sheep", @time.hours_ago(2), @time.hours_since(1))
111
+ stats.rank[:count].should == 4
112
+ stats.rank[:weight].should == 156
113
+
114
+ stats = ModelHelper1.fetch("sheep", @time.hours_ago(5), @time.hours_since(1))
115
+ stats.rank[:count].should == 6
116
+ stats.rank[:weight].should == 461
117
+ end
118
+
119
+ it "should connect to different Redis servers on a per-model basis" do
120
+ ModelHelper3.redis.client.db.should == 14
121
+
122
+ ModelHelper3.store("sheep.black", {:count => 6, :weight => 461}, @time.hours_ago(4), :label_indexing => false)
123
+ ModelHelper3.store("sheep.black", {:count => 2, :weight => 156}, @time, :label_indexing => false)
124
+
125
+ db.keys("*").should be_empty
126
+ ModelHelper1.redis.keys("*").should be_empty
127
+ db("ModelHelper3").keys("*").count.should be(5)
128
+ ModelHelper3.redis.keys("*").count.should be(5)
129
+
130
+ stats = ModelHelper3.fetch("sheep.black", @time.hours_ago(2), @time.hours_since(1), :label_indexing => false)
131
+ stats.rank["count"].should == 2
132
+ stats.rank["weight"].should == 156
133
+ stats = ModelHelper3.fetch("sheep.black", @time.hours_ago(5), @time.hours_since(1), :label_indexing => false)
134
+ stats.rank[:count].should == 6
135
+ stats.rank[:weight].should == 461
136
+
137
+ ModelHelper3.connect_to(:port => 8379, :db => 13)
138
+ ModelHelper3.redis.client.db.should == 13
139
+
140
+ stats = ModelHelper3.fetch("sheep.black", @time.hours_ago(5), @time.hours_since(1), :label_indexing => false)
141
+ stats.rank.should == {}
142
+
143
+ ModelHelper3.connect_to(:port => 8379, :db => 14)
144
+ ModelHelper3.redis.client.db.should == 14
145
+
146
+ stats = ModelHelper3.fetch("sheep.black", @time.hours_ago(5), @time.hours_since(1), :label_indexing => false)
147
+ stats.rank[:count].should == 6
148
+ stats.rank[:weight].should == 461
149
+ end
150
+
151
+ describe "Write Buffer" do
152
+ before(:each) do
153
+ Redisrank.buffer_size = 20
154
+ end
155
+
156
+ after(:each) do
157
+ Redisrank.buffer_size = 0
158
+ end
159
+
160
+ it "should buffer calls in memory before committing to Redis" do
161
+ 14.times do
162
+ ModelHelper1.store("sheep.black", {:count => 1, :weight => 461}, @time.hours_ago(4))
163
+ end
164
+ ModelHelper1.fetch("sheep.black", @time.hours_ago(5), @time.hours_since(1)).rank.should == {}
165
+
166
+ 5.times do
167
+ ModelHelper1.store("sheep.black", {:count => 1, :weight => 156}, @time)
168
+ end
169
+ ModelHelper1.fetch("sheep.black", @time.hours_ago(5), @time.hours_since(1)).rank.should == {}
170
+
171
+ ModelHelper1.store("sheep.black", {:count => 1, :weight => 156}, @time)
172
+ stats = ModelHelper1.fetch("sheep.black", @time.hours_ago(5), @time.hours_since(1))
173
+ stats.rank["count"].should == 1
174
+ stats.rank["weight"].should == 461
175
+ end
176
+
177
+ it "should force flush buffer when #flush(true) is called" do
178
+ ModelHelper1.fetch("sheep.black", @time.hours_ago(5), @time.hours_since(1)).rank.should == {}
179
+ 14.times do
180
+ ModelHelper1.store("sheep.black", {:count => 1, :weight => 461}, @time.hours_ago(4))
181
+ end
182
+ ModelHelper1.fetch("sheep.black", @time.hours_ago(5), @time.hours_since(1)).rank.should == {}
183
+ Redisrank.buffer.flush(true)
184
+
185
+ stats = ModelHelper1.fetch("sheep.black", @time.hours_ago(5), @time.hours_since(1))
186
+ stats.rank["count"].should == 1
187
+ stats.rank["weight"].should == 461
188
+ end
189
+ end
190
+
191
+ end
@@ -0,0 +1,36 @@
1
+ require "spec_helper"
2
+
3
+ describe Redisrank::Options do
4
+
5
+ before(:each) do
6
+ @helper = OptionsHelper.new
7
+ @helper.parse_options(:wtf => 'dude', :foo => 'booze')
8
+ end
9
+
10
+ it "should #parse_options" do
11
+ @helper.options[:hello].should == 'world'
12
+ @helper.options[:foo].should == 'booze'
13
+ @helper.options[:wtf].should == 'dude'
14
+ @helper.raw_options.should_not have_key(:hello)
15
+ end
16
+
17
+ it "should create option_accessors" do
18
+ @helper.hello.should == 'world'
19
+ @helper.hello('woooo')
20
+ @helper.hello.should == 'woooo'
21
+ end
22
+
23
+ end
24
+
25
+ class OptionsHelper
26
+ include Redisrank::Options
27
+
28
+ option_accessor :hello
29
+
30
+ def default_options
31
+ { :hello => 'world',
32
+ :foo => 'bar' }
33
+ end
34
+
35
+
36
+ end
@@ -0,0 +1,9 @@
1
+ daemonize yes
2
+ dir ./spec/db
3
+ pidfile ./redis.pid
4
+ port 8379
5
+ bind 127.0.0.1
6
+ timeout 300
7
+ loglevel debug
8
+ logfile stdout
9
+ databases 16
@@ -0,0 +1,23 @@
1
+ require "spec_helper"
2
+
3
+ describe Redisrank::Result do
4
+
5
+ it "should should initialize properly" do
6
+ options = {:from => "from", :till => "till"}
7
+ result = Redisrank::Result.new(options)
8
+ result.from.should == "from"
9
+ result.till.should == "till"
10
+ end
11
+
12
+ it "should have merge_to_max method" do
13
+ result = Redisrank::Result.new
14
+ result[:world].should be_nil
15
+ result.merge_to_max(:world, 3)
16
+ result[:world].should == 3
17
+ result.merge_to_max(:world, 8)
18
+ result[:world].should == 8
19
+ result.merge_to_max(:world, 3)
20
+ result[:world].should == 8
21
+ end
22
+
23
+ end
@@ -0,0 +1,27 @@
1
+ require "spec_helper"
2
+
3
+ describe Redisrank::Scope do
4
+ include Redisrank::Database
5
+
6
+ before(:all) do
7
+ db.flushdb
8
+ end
9
+
10
+ before(:each) do
11
+ @name = "PageViews"
12
+ @scope = Redisrank::Scope.new(@name)
13
+ end
14
+
15
+ it "should initialize properly" do
16
+ @scope.to_s.should == @name
17
+ end
18
+
19
+ it "should increment next_id" do
20
+ scope = Redisrank::Scope.new("Visitors")
21
+ @scope.next_id.should == 1
22
+ scope.next_id.should == 1
23
+ @scope.next_id.should == 2
24
+ scope.next_id.should == 2
25
+ end
26
+
27
+ end
@@ -0,0 +1,18 @@
1
+ # add project-relative load paths
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
4
+
5
+ require 'simplecov'
6
+ SimpleCov.start do
7
+ add_filter '/spec'
8
+ add_filter '/vendor'
9
+ end
10
+
11
+ # require stuff
12
+ require 'redisrank'
13
+ require 'rspec'
14
+ require 'rspec/autorun'
15
+
16
+ # use the test Redisrank instance
17
+ Redisrank.connect(:port => 8379, :db => 15, :thread_safe => true)
18
+ Redisrank.redis.flushdb
@@ -0,0 +1,177 @@
1
+ require "spec_helper"
2
+
3
+ describe Redisrank::Summary do
4
+ include Redisrank::Database
5
+
6
+ before(:each) do
7
+ db.flushdb
8
+ @scope = "PageViews"
9
+ @label = "about_us"
10
+ @date = Time.now
11
+ @key = Redisrank::Key.new(@scope, @label, @date, {:depth => :day})
12
+ @stats = {"views" => 3, "visitors" => 2}
13
+ @expire = {:hour => 24*3600}
14
+ end
15
+
16
+ it "should update a single summary properly" do
17
+ Redisrank::Summary.send(:update_fields, @key, @stats, :hour)
18
+ summary = db.zrevrange(@key.to_s(:hour), 0, -1, :with_scores => true)
19
+ expect(summary.count).to be 2
20
+ views = summary.first
21
+ expect(views.first).to eq('views')
22
+ expect(views.last).to be 3.0
23
+ visitors = summary.last
24
+ expect(visitors.first).to eq('visitors')
25
+ expect(visitors.last).to be 2.0
26
+
27
+ Redisrank::Summary.send(:update_fields, @key, @stats, :hour)
28
+ summary = db.zrevrange(@key.to_s(:hour), 0, -1, :with_scores => true)
29
+ expect(summary.count).to be 2
30
+ views = summary.first
31
+ expect(views.first).to eq('views')
32
+ expect(views.last).to be 3.0
33
+ visitors = summary.last
34
+ expect(visitors.first).to eq('visitors')
35
+ expect(visitors.last).to be 2.0
36
+ end
37
+
38
+ it "should set key expiry properly" do
39
+ Redisrank::Summary.update_all(@key, @stats, :hour,{:expire => @expire})
40
+ ((24*3600)-1..(24*3600)+1).should include(db.ttl(@key.to_s(:hour)))
41
+ [:day, :month, :year].each do |depth|
42
+ db.ttl(@key.to_s(depth)).should == -1
43
+ end
44
+
45
+ db.flushdb
46
+ Redisrank::Summary.update_all(@key, @stats, :hour, {:expire => {}})
47
+ [:hour, :day, :month, :year].each do |depth|
48
+ db.ttl(@key.to_s(depth)).should == -1
49
+ end
50
+ end
51
+
52
+ it "should update all summaries properly" do
53
+ Redisrank::Summary.update_all(@key, @stats, :sec)
54
+ [:year, :month, :day, :hour, :min, :sec, :usec].each do |depth|
55
+ summary = db.zrevrange(@key.to_s(depth), 0, -1, :with_scores => true)
56
+ if depth != :usec
57
+ summary.count.should eq(2)
58
+ views = summary.first
59
+ expect(views.first).to eq('views')
60
+ expect(views.last).to be 3.0
61
+ visitors = summary.last
62
+ expect(visitors.first).to eq('visitors')
63
+ expect(visitors.last).to be 2.0
64
+ else
65
+ summary.count.should eq(0)
66
+ end
67
+ end
68
+ end
69
+
70
+ it "should update summaries even if no label is set" do
71
+ key = Redisrank::Key.new(@scope, nil, @date, {:depth => :day})
72
+ Redisrank::Summary.send(:update_fields, key, @stats, :hour)
73
+ summary = db.zrevrange(key.to_s(:hour), 0, -1, :with_scores => true)
74
+ views = summary.first
75
+ expect(views.first).to eq('views')
76
+ expect(views.last).to be 3.0
77
+ visitors = summary.last
78
+ expect(visitors.first).to eq('visitors')
79
+ expect(visitors.last).to be 2.0
80
+ end
81
+
82
+ it "should inject stats key grouping summaries" do
83
+ hash = { "count/hello" => 3, "count/world" => 7,
84
+ "death/bomb" => 4, "death/unicorn" => 3,
85
+ :"od/sugar" => 7, :"od/meth" => 8 }
86
+ res = Redisrank::Summary.send(:inject_group_summaries, hash)
87
+ res.should == { "count" => 10, "count/hello" => 3, "count/world" => 7,
88
+ "death" => 7, "death/bomb" => 4, "death/unicorn" => 3,
89
+ "od" => 15, :"od/sugar" => 7, :"od/meth" => 8 }
90
+ end
91
+
92
+ it "should properly store key group summaries" do
93
+ stats = {"views" => 3, "visitors/eu" => 2, "visitors/us" => 4}
94
+ Redisrank::Summary.update_all(@key, stats, :hour)
95
+ summary = db.zrevrange(@key.to_s(:hour), 0, -1, :with_scores => true)
96
+ summary.count.should eq(4)
97
+
98
+ views = summary.select{|s| s.first == 'views'}.first
99
+ visitors = summary.select{|s| s.first == 'visitors'}.first
100
+ eu = summary.select{|s| s.first == 'visitors/eu'}.first
101
+ us = summary.select{|s| s.first == 'visitors/us'}.first
102
+
103
+ views.last.should == 3.0
104
+ visitors.last.should == 6.0
105
+ eu.last.should == 2.0
106
+ us.last.should == 4.0
107
+ end
108
+
109
+ it "should not store key group summaries when option is disabled" do
110
+ stats = {"views" => 3, "visitors/eu" => 2, "visitors/us" => 4}
111
+ Redisrank::Summary.update_all(@key, stats, :hour, {:enable_grouping => false})
112
+ summary = db.zrevrange(@key.to_s(:hour), 0, -1, :with_scores => true)
113
+ summary.count.should eq(3)
114
+
115
+ views = summary.select{|s| s.first == 'views'}.first
116
+ eu = summary.select{|s| s.first == 'visitors/eu'}.first
117
+ us = summary.select{|s| s.first == 'visitors/us'}.first
118
+
119
+ views.last.should == 3.0
120
+ eu.last.should == 2.0
121
+ us.last.should == 4.0
122
+ end
123
+
124
+ it "should store label-based grouping enabled stats" do
125
+ stats = {"views" => 3, "visitors/eu" => 2, "visitors/us" => 4}
126
+ label = "views/about_us"
127
+ key = Redisrank::Key.new(@scope, label, @date)
128
+ Redisrank::Summary.update_all(key, stats, :hour)
129
+
130
+ key.groups[0].label.to_s.should == "views/about_us"
131
+ key.groups[1].label.to_s.should == "views"
132
+ child1 = key.groups[0]
133
+ parent = key.groups[1]
134
+
135
+ label = "views/contact"
136
+ key = Redisrank::Key.new(@scope, label, @date)
137
+ Redisrank::Summary.update_all(key, stats, :hour)
138
+
139
+ key.groups[0].label.to_s.should == "views/contact"
140
+ key.groups[1].label.to_s.should == "views"
141
+ child2 = key.groups[0]
142
+
143
+ summary = db.zrevrange(child1.to_s(:hour), 0, -1, :with_scores => true)
144
+ summary.count.should eq(4)
145
+
146
+ views = summary.select{|s| s.first == 'views'}.first
147
+ eu = summary.select{|s| s.first == 'visitors/eu'}.first
148
+ us = summary.select{|s| s.first == 'visitors/us'}.first
149
+
150
+ views.last.should == 3.0
151
+ eu.last.should == 2.0
152
+ us.last.should == 4.0
153
+
154
+ summary = db.zrevrange(child2.to_s(:hour), 0, -1, :with_scores => true)
155
+ summary.count.should eq(4)
156
+
157
+ views = summary.select{|s| s.first == 'views'}.first
158
+ eu = summary.select{|s| s.first == 'visitors/eu'}.first
159
+ us = summary.select{|s| s.first == 'visitors/us'}.first
160
+
161
+ views.last.should == 3.0
162
+ eu.last.should == 2.0
163
+ us.last.should == 4.0
164
+
165
+ summary = db.zrevrange(parent.to_s(:hour), 0, -1, :with_scores => true)
166
+ summary.count.should eq(4)
167
+
168
+ views = summary.select{|s| s.first == 'views'}.first
169
+ eu = summary.select{|s| s.first == 'visitors/eu'}.first
170
+ us = summary.select{|s| s.first == 'visitors/us'}.first
171
+
172
+ views.last.should == 3.0
173
+ eu.last.should == 2.0
174
+ us.last.should == 4.0
175
+ end
176
+
177
+ end