counter-cache 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +10 -0
  5. data/Gemfile +4 -0
  6. data/Guardfile +9 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +29 -0
  9. data/Rakefile +6 -0
  10. data/counter-cache.gemspec +30 -0
  11. data/lib/counter/cache.rb +28 -0
  12. data/lib/counter/cache/active_record_updater.rb +23 -0
  13. data/lib/counter/cache/config.rb +12 -0
  14. data/lib/counter/cache/counters/buffer_counter.rb +25 -0
  15. data/lib/counter/cache/counters/buffer_counter/enqueuer.rb +27 -0
  16. data/lib/counter/cache/counters/buffer_counter/key.rb +23 -0
  17. data/lib/counter/cache/counters/buffer_counter/relation_finder.rb +29 -0
  18. data/lib/counter/cache/counters/buffer_counter/saver.rb +70 -0
  19. data/lib/counter/cache/counters/buffer_counter/updater.rb +54 -0
  20. data/lib/counter/cache/options_parser.rb +69 -0
  21. data/lib/counter/cache/redis.rb +40 -0
  22. data/lib/counter/cache/version.rb +5 -0
  23. data/spec/features/counter_spec.rb +95 -0
  24. data/spec/lib/counter/cache/active_record_updater_spec.rb +26 -0
  25. data/spec/lib/counter/cache/buffer_counter/enqueuer_spec.rb +53 -0
  26. data/spec/lib/counter/cache/buffer_counter/key_spec.rb +13 -0
  27. data/spec/lib/counter/cache/buffer_counter/relation_finder_spec.rb +49 -0
  28. data/spec/lib/counter/cache/buffer_counter/saver_spec.rb +84 -0
  29. data/spec/lib/counter/cache/buffer_counter/updater_spec.rb +96 -0
  30. data/spec/lib/counter/cache/buffer_counter_spec.rb +27 -0
  31. data/spec/lib/counter/cache/config_spec.rb +36 -0
  32. data/spec/lib/counter/cache/options_parser_spec.rb +176 -0
  33. data/spec/lib/counter/cache/redis_spec.rb +44 -0
  34. data/spec/lib/counter/cache_spec.rb +14 -0
  35. data/spec/spec_helper.rb +93 -0
  36. data/spec/support/models.rb +87 -0
  37. data/spec/support/worker_adapter.rb +8 -0
  38. metadata +208 -0
@@ -0,0 +1,69 @@
1
+ module Counter
2
+ module Cache
3
+ class OptionsParser < Struct.new(:options)
4
+ def worker_adapter
5
+ options[:worker_adapter] || Counter::Cache.configuration.default_worker_adapter
6
+ end
7
+
8
+ def source_object_class_name
9
+ options[:source_object_class_name]
10
+ end
11
+
12
+ def column
13
+ options[:column]
14
+ end
15
+
16
+ def relation
17
+ options[:relation]
18
+ end
19
+
20
+ def relation_class_name
21
+ options[:relation_class_name]
22
+ end
23
+
24
+ def relation_id
25
+ options[:relation_id]
26
+ end
27
+
28
+ def method
29
+ options[:method]
30
+ end
31
+
32
+ def cached?
33
+ option_or_true options[:cache]
34
+ end
35
+
36
+ def recalculation?
37
+ option_or_true options[:recalculation]
38
+ end
39
+
40
+ def polymorphic?
41
+ options[:polymorphic]
42
+ end
43
+
44
+ def if_value
45
+ options[:if]
46
+ end
47
+
48
+ def wait(source_object)
49
+ wait = options[:wait]
50
+ if wait.respond_to?(:call)
51
+ wait.call(source_object)
52
+ else
53
+ wait
54
+ end
55
+ end
56
+
57
+ def recalculation_delay
58
+ options[:recalculation_delay] || Counter::Cache.configuration.recalculation_delay
59
+ end
60
+
61
+ protected
62
+
63
+ def option_or_true(val)
64
+ val || val.nil?
65
+ end
66
+
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,40 @@
1
+ module Counter
2
+ module Cache
3
+ class Redis
4
+ def incr(key)
5
+ with_redis do |redis|
6
+ redis.incr key
7
+ end
8
+ end
9
+
10
+ def decr(key)
11
+ with_redis do |redis|
12
+ redis.decr(key)
13
+ end
14
+ end
15
+
16
+ def get(key)
17
+ with_redis do |redis|
18
+ redis.get(key)
19
+ end
20
+ end
21
+
22
+ def del(key)
23
+ with_redis do |redis|
24
+ redis.del(key)
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def with_redis
31
+ redis_pool = Counter::Cache.configuration.redis_pool
32
+ return yield redis_pool unless redis_pool.respond_to?(:with)
33
+
34
+ redis_pool.with do |redis|
35
+ yield redis
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,5 @@
1
+ module Counter
2
+ module Cache
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,95 @@
1
+ require 'spec_helper'
2
+ require 'support/models'
3
+ require 'support/worker_adapter'
4
+ require 'fakeredis'
5
+
6
+ RSpec.describe "Counting" do
7
+
8
+ before do
9
+ ActiveRecord::Base.silence { CreateModelsForTest.migrate(:up) }
10
+ Counter::Cache.configure do |c|
11
+ c.redis_pool = Redis.new
12
+ c.default_worker_adapter = TestWorkerAdapter.new
13
+ end
14
+ end
15
+
16
+ after do
17
+ ActiveRecord::Base.silence { CreateModelsForTest.migrate(:down) }
18
+ end
19
+
20
+ let(:user) { User.create }
21
+
22
+ describe '#posts_count' do
23
+ it 'increments' do
24
+ expect {
25
+ user.posts.create
26
+ }.to change { user.reload.posts_count }.by(1)
27
+ end
28
+
29
+ it 'decrements' do
30
+ post = user.posts.create
31
+ expect {
32
+ post.destroy
33
+ }.to change { user.reload.posts_count }.by(-1)
34
+ end
35
+ end
36
+
37
+ describe '#posts_ar_count' do
38
+ it 'increments' do
39
+ expect {
40
+ user.posts.create
41
+ }.to change { user.reload.posts_ar_count }.by(1)
42
+ end
43
+
44
+ it 'decrements' do
45
+ post = user.posts.create
46
+ expect {
47
+ post.destroy
48
+ }.to change { user.reload.posts_ar_count }.by(-1)
49
+ end
50
+ end
51
+
52
+ describe '#polymorphic followers_count' do
53
+ let(:follower_user) { User.create }
54
+ it 'increments' do
55
+ expect {
56
+ Follow.create(user: follower_user, followee: user)
57
+ }.to change { user.reload.followers_count }.by(1)
58
+ end
59
+
60
+ it 'decrements' do
61
+ follow = Follow.create(user: follower_user, followee: user)
62
+ expect {
63
+ follow.destroy
64
+ }.to change { user.reload.followers_count }.by(-1)
65
+ end
66
+ end
67
+
68
+ describe '#users_i_follow_count' do
69
+ let(:follower_user) { User.create }
70
+
71
+ it 'increments' do
72
+ expect {
73
+ Follow.create(user: follower_user, followee: user)
74
+ }.to change { follower_user.reload.users_i_follow_count }.by(1)
75
+ end
76
+
77
+ it 'decrements' do
78
+ follow = Follow.create(user: follower_user, followee: user)
79
+ expect {
80
+ follow.destroy
81
+ }.to change { follower_user.reload.users_i_follow_count }.by(-1)
82
+ end
83
+ end
84
+
85
+ describe '#bogus_followed_count' do
86
+ let(:follower_user) { User.create }
87
+
88
+ it 'eventually recalculates' do
89
+ expect(user.reload.bogus_followed_count).to_not eq(101)
90
+ Follow.create(user: follower_user, followee: user)
91
+ expect(user.reload.bogus_followed_count).to eq(101)
92
+ end
93
+ end
94
+ end
95
+
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Counter::Cache::ActiveRecordUpdater do
4
+ let(:counter_class) { double }
5
+ let(:counter) { double }
6
+ let(:options) { { counter_class: counter_class } }
7
+ subject { Counter::Cache::ActiveRecordUpdater.new(options) }
8
+
9
+ let(:record) { double }
10
+
11
+ describe "#after_create" do
12
+ it "Calls update on counter instance" do
13
+ expect(counter).to receive(:update).with(:incr)
14
+ expect(counter_class).to receive(:new).with(record, options).and_return(counter)
15
+ subject.after_create(record)
16
+ end
17
+ end
18
+
19
+ describe "#after_destroy" do
20
+ it "Calls update on counter instance" do
21
+ expect(counter).to receive(:update).with(:decr)
22
+ expect(counter_class).to receive(:new).with(record, options).and_return(counter)
23
+ subject.after_destroy(record)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,53 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Counter::Cache::Counters::BufferCounter::Enqueuer do
4
+ let(:worker_adapter) { double }
5
+ let(:options) { double(worker_adapter: worker_adapter,
6
+ wait: 10,
7
+ column: "boo",
8
+ method: "calculate_boo",
9
+ cached?: true,
10
+ recalculation?: false,
11
+ recalculation_delay: 20) }
12
+
13
+ let(:source_object_class_name) { "BooUser" }
14
+ let(:relation_id) { 1 }
15
+ let(:relation_type) { "Boo" }
16
+
17
+ let(:enqueuer) { Counter::Cache::Counters::BufferCounter::Enqueuer.new(options, source_object_class_name, relation_id, relation_type, "SuperCounter") }
18
+
19
+ describe '#enqueue' do
20
+ before do
21
+ expect(worker_adapter).to receive(:enqueue).with(10,
22
+ "BooUser",
23
+ { relation_class_name: "Boo",
24
+ relation_id: 1,
25
+ column: "boo",
26
+ method: "calculate_boo",
27
+ cache: true,
28
+ counter: "SuperCounter" })
29
+ end
30
+
31
+ describe 'when recalculation is true' do
32
+ before do
33
+ expect(options).to receive(:recalculation?).and_return(true)
34
+ end
35
+
36
+ it "enqueues two jobs" do
37
+ expect(worker_adapter).to receive(:enqueue).with(20,
38
+ "BooUser",
39
+ { relation_class_name: "Boo",
40
+ relation_id: 1,
41
+ column: "boo",
42
+ method: "calculate_boo",
43
+ cache: false,
44
+ counter: "SuperCounter" })
45
+ enqueuer.enqueue!(double)
46
+ end
47
+ end
48
+
49
+ it 'enqueues one job' do
50
+ enqueuer.enqueue!(double)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,13 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Counter::Cache::Counters::BufferCounter::Key do
4
+ let(:options) { double(relation_class_name: "Boo", relation: "boo", column: "boos_count", relation_id: nil) }
5
+ let(:source_object) { double(boo_id: 1) }
6
+ let(:key) { Counter::Cache::Counters::BufferCounter::Key.new(source_object, options) }
7
+
8
+ describe '#to_s' do
9
+ it 'returns the key with the class, id, and column' do
10
+ expect(key.to_s).to eq("cc:Bo:1:boos")
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,49 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Counter::Cache::Counters::BufferCounter::RelationFinder do
4
+ let(:options) { double }
5
+ let(:source_object) { double }
6
+ let(:finder) { Counter::Cache::Counters::BufferCounter::RelationFinder.new(source_object, options) }
7
+
8
+ describe '#relation_class' do
9
+ context 'when relation_class_name is present' do
10
+ let(:options) { double(relation_class_name: "Boo") }
11
+
12
+ it 'returns the relation_class_name' do
13
+ expect(finder.relation_class).to eq("Boo")
14
+ end
15
+ end
16
+
17
+ context 'when polymorphic?' do
18
+ let(:options) { double(polymorphic?: true, relation: "boo", relation_class_name: nil) }
19
+ let(:source_object) { double(boo_type: "Boo") }
20
+
21
+ it 'asks for the type' do
22
+ expect(finder.relation_class).to eq("Boo")
23
+ end
24
+ end
25
+
26
+ context 'no relation_class_name or polymorphic' do
27
+ let(:options) { double(relation_class_name: nil, polymorphic?: false, relation: "boo") }
28
+
29
+ before do
30
+ reflection = double
31
+ expect(reflection).to receive_message_chain("class_name.to_s.camelize") { "Boo" }
32
+ expect(source_object).to receive(:reflections).and_return({:boo => reflection})
33
+ end
34
+
35
+ it 'asks active record for the class name' do
36
+ expect(finder.relation_class).to eq("Boo")
37
+ end
38
+ end
39
+ end
40
+
41
+ describe '#relation_id' do
42
+ let(:options) { double(relation: "boo", relation_id: nil) }
43
+ let(:source_object) { double(boo_id: 123) }
44
+
45
+ it 'calls relation_id on the source object' do
46
+ expect(finder.relation_id).to eq(123)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,84 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Counter::Cache::Counters::BufferCounter::Saver do
4
+ class Boo
5
+ end
6
+
7
+ let(:relation_object) { double(boo_count: 2) }
8
+ let(:options) { double(relation_class_name: "Boo", relation_id: 1, column: "boo_count", method: nil, source_object_class_name: Boo) }
9
+ let(:saver) { Counter::Cache::Counters::BufferCounter::Saver.new(options) }
10
+
11
+ describe '#save!' do
12
+ let(:counting_store) { double(get: nil) }
13
+
14
+ before do
15
+ allow(Boo).to receive(:find_by_id).and_return(relation_object)
16
+ allow(Counter::Cache.configuration).to receive(:counting_data_store).and_return(counting_store)
17
+ end
18
+
19
+ describe "yielding" do
20
+ let(:counting_store) { double(get: 2) }
21
+
22
+ before do
23
+ allow(options).to receive(:cached?).and_return(true)
24
+ expect(relation_object).to receive(:boo_count=).with(4)
25
+ expect(relation_object).to receive(:save!)
26
+ expect(counting_store).to receive(:del)
27
+ end
28
+
29
+ describe "with block" do
30
+ it "yields" do
31
+ expect { |b| saver.save!(&b) }.to yield_with_args(2, 4, relation_object, "boo_count")
32
+ end
33
+ end
34
+ end
35
+
36
+ describe 'when cached? is true' do
37
+ let(:counting_store) { double(get: 2) }
38
+
39
+ before do
40
+ allow(options).to receive(:cached?).and_return(true)
41
+ expect(counting_store).to receive(:del)
42
+ end
43
+
44
+ it 'saves the value' do
45
+ expect(relation_object).to receive(:boo_count=).with(4)
46
+ expect(relation_object).to receive(:save!)
47
+ saver.save!
48
+ end
49
+ end
50
+
51
+ describe 'when cached? is false' do
52
+ before do
53
+ allow(options).to receive(:cached?).and_return(false)
54
+ end
55
+
56
+ describe 'when method is passed' do
57
+ before do
58
+ allow(options).to receive(:method).and_return("call_this_thing")
59
+ allow(relation_object).to receive(:call_this_thing).and_return(4)
60
+ end
61
+
62
+ it 'saves the value' do
63
+ expect(relation_object).to receive(:boo_count=).with(4)
64
+ expect(relation_object).to receive(:save!)
65
+ saver.save!
66
+ end
67
+ end
68
+
69
+ describe 'when method is not passed' do
70
+
71
+ before do
72
+ allow(Boo).to receive(:table_name).and_return("boos")
73
+ allow(relation_object).to receive(:boos).and_return(double(count: 4))
74
+ end
75
+
76
+ it 'saves the value' do
77
+ expect(relation_object).to receive(:boo_count=).with(4)
78
+ expect(relation_object).to receive(:save!)
79
+ saver.save!
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,96 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Counter::Cache::Counters::BufferCounter::Updater do
4
+ let(:options) { double(relation: "boo", relation_class_name: "Boo", column: "boo", relation_id: nil) }
5
+ let(:source_object) { double(boo_id: 1) }
6
+ let(:updater) { Counter::Cache::Counters::BufferCounter::Updater.new(source_object, options, "Hello") }
7
+
8
+ describe "#update" do
9
+ describe "when valid" do
10
+ it "sends direction and enqueues" do
11
+ expect(updater).to receive(:valid?) { true }
12
+ expect(updater).to receive(:decr) { true }
13
+ expect(updater).to receive(:enqueue) { true }
14
+ updater.update!(:decr)
15
+ end
16
+ end
17
+
18
+ describe "when invalid" do
19
+ it "sends direction and enqueues" do
20
+ expect(updater).to receive(:valid?) { false }
21
+ expect(updater).to receive(:decr).never
22
+ expect(updater).to receive(:enqueue).never
23
+ updater.update!(:decr)
24
+ end
25
+ end
26
+ end
27
+
28
+ describe "#enqueue" do
29
+ it 'constructs and calls enqueue! on the enqueue' do
30
+ expect(Counter::Cache::Counters::BufferCounter::Enqueuer).to receive(:new).with(options,
31
+ source_object.class.name,
32
+ 1,
33
+ "Boo",
34
+ "Hello")
35
+ .and_return(double(enqueue!: true))
36
+ updater.send(:enqueue)
37
+ end
38
+ end
39
+
40
+ describe "#decr" do
41
+ it 'calls decr on the redis instance' do
42
+ expect_any_instance_of(Counter::Cache::Redis).to receive(:decr)
43
+ updater.send(:decr)
44
+ end
45
+ end
46
+
47
+ describe "#incr" do
48
+ it 'calls decr on the redis instance' do
49
+ expect_any_instance_of(Counter::Cache::Redis).to receive(:incr)
50
+ updater.send(:incr)
51
+ end
52
+ end
53
+
54
+ describe "valid?" do
55
+ describe "With no if value" do
56
+ let(:options) { double(relation: "boo", if_value: nil, relation_id: nil) }
57
+
58
+ describe 'with relation_id' do
59
+ let(:source_object) { double(boo_id: 123) }
60
+
61
+ it 'returns true' do
62
+ expect(updater.send(:valid?)).to eq(true)
63
+ end
64
+ end
65
+
66
+ describe 'without relation_id' do
67
+ let(:source_object) { double(boo_id: nil) }
68
+
69
+ it 'returns false' do
70
+ expect(updater.send(:valid?)).to eq(false)
71
+ end
72
+ end
73
+ end
74
+
75
+ describe "With if object" do
76
+ let(:source_object) { double(boo_id: 123) }
77
+ let(:options) { double(relation: "boo", if_value: if_value, relation_id: nil) }
78
+
79
+ describe 'if value returns false' do
80
+ let(:if_value) { double(call: true) }
81
+
82
+ it 'returns true' do
83
+ expect(updater.send(:valid?)).to eq(true)
84
+ end
85
+ end
86
+
87
+ describe 'if value returns true' do
88
+ let(:if_value) { double(call: false) }
89
+
90
+ it 'returns false' do
91
+ expect(updater.send(:valid?)).to eq(false)
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end