counter-cache 0.0.1

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