ar-resque-counter-cache 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +81 -0
- data/Rakefile +18 -0
- data/VERSION +1 -0
- data/ar-resque-counter-cache.gemspec +63 -0
- data/lib/ar-resque-counter-cache.rb +5 -0
- data/lib/ar_resque_counter_cache/active_record.rb +72 -0
- data/lib/ar_resque_counter_cache/increment_counters_worker.rb +108 -0
- data/spec/ar_resque_counter_cache/active_record_spec.rb +21 -0
- data/spec/integration_spec.rb +113 -0
- data/spec/models.rb +42 -0
- data/spec/redis-test.conf +13 -0
- data/spec/spec_helper.rb +44 -0
- metadata +128 -0
data/README.md
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
ar-resque-counter-cache (formerly ar-async-counter-cache)
|
2
|
+
---------------------------------------------------------
|
3
|
+
|
4
|
+
This gem allows you to update ActiveRecord (2.3.x) counter cache columns
|
5
|
+
asynchronously using Resque (http://github.com/defunkt/resque). You may want to
|
6
|
+
do this in situations where you want really speedy inserts and have models that
|
7
|
+
"belong_to" many different parents; that is, you want the request making the
|
8
|
+
INSERT to return before waiting for the many UPDATE...SET counter cache SQL
|
9
|
+
queries to finish. You may also want to use this gem to avoid "Mysql::Error:
|
10
|
+
Lock wait timeout exceeded" issues: if you have a lot of children being created
|
11
|
+
at a time for a single parent row, MySQL can run into lock timeouts while
|
12
|
+
waiting for parent row to update its counter cache over and over. A while ago,
|
13
|
+
I remember
|
14
|
+
[seeing](http://robots.thoughtbot.com/post/159805685/tuning-the-toad) that
|
15
|
+
Thoughtbot was having a similar issue in its Hoptoad service...
|
16
|
+
|
17
|
+
How does ar-resque-counter-cache address these issues? It uses Redis as a
|
18
|
+
temporary counter cache and Resque to actually update the counter cache column
|
19
|
+
sometime in the future. For example, let's say a single Post gets 1000 comments
|
20
|
+
very quickly. This will set a key in Redis indicating that there is a delta of
|
21
|
+
+1000 for that Post's comments_count column. It will also queue 1000 Resque
|
22
|
+
jobs. This is where resque-lock-timeout comes in. Only one of those jobs will
|
23
|
+
be allowed to run at a time. Once a job acquires the lock it removes all other
|
24
|
+
instances of that job from the queue (see
|
25
|
+
IncrementCountersWorker.around\_perform\_lock1).
|
26
|
+
|
27
|
+
You use it like such:
|
28
|
+
|
29
|
+
class User < ActiveRecord::Base
|
30
|
+
has_many :comments
|
31
|
+
has_many :posts
|
32
|
+
end
|
33
|
+
|
34
|
+
class Post < ActiveRecord::Base
|
35
|
+
belongs_to :user, :async_counter_cache => true
|
36
|
+
has_many :comments
|
37
|
+
end
|
38
|
+
|
39
|
+
class Comment < ActiveRecord::Base
|
40
|
+
belongs_to :user, :async_counter_cache => true
|
41
|
+
belongs_to :post, :async_counter_cache => "count_of_comments"
|
42
|
+
end
|
43
|
+
|
44
|
+
Notice, you may specify the name of the counter cache column just as you can
|
45
|
+
with the normal belongs_to `:counter_cache` option. You also may not use both
|
46
|
+
the `:async_counter_cache` and `:counter_cache` options in the same belongs_to
|
47
|
+
call.
|
48
|
+
|
49
|
+
All you should need to do is require this gem in your project that uses
|
50
|
+
ActiveRecord and you should be good to go;
|
51
|
+
|
52
|
+
e.g. In your Gemfile:
|
53
|
+
|
54
|
+
gem 'ar-resque-counter-cache', 'x.x.x'
|
55
|
+
|
56
|
+
and then in RAILS_ROOT/config/environment.rb somewhere:
|
57
|
+
|
58
|
+
require 'ar-resque-counter-cache'
|
59
|
+
|
60
|
+
By default, the Resque job is placed on the `:counter_caches` queue:
|
61
|
+
|
62
|
+
@queue = :counter_caches
|
63
|
+
|
64
|
+
However, you can change this:
|
65
|
+
|
66
|
+
in RAILS_ROOT/config/environment.rb somewhere:
|
67
|
+
|
68
|
+
ArAsyncCounterCache.resque_job_queue = :low_priority
|
69
|
+
|
70
|
+
`ArAsyncCounterCache::IncrementCountersWorker.cache_and_enqueue` can also be
|
71
|
+
used to increment/decrement arbitrary counter cache columns (outside of
|
72
|
+
belongs_to associations):
|
73
|
+
|
74
|
+
ArAsyncCounterCache::IncrementCountersWorker.cache_and_enqueue(klass, id, column, direction)
|
75
|
+
|
76
|
+
Where:
|
77
|
+
|
78
|
+
* `klass` is the Class of the ActiveRecord object
|
79
|
+
* `id` is the id of the object
|
80
|
+
* `column` is the counter cache column
|
81
|
+
* `direction` is either `:increment` or `:decrement`
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
begin
|
2
|
+
require 'jeweler'
|
3
|
+
Jeweler::Tasks.new do |gemspec|
|
4
|
+
gemspec.name = "ar-resque-counter-cache"
|
5
|
+
gemspec.summary = "Increment ActiveRecord's counter cache column asynchronously using Resque (and resque-lock-timeout)."
|
6
|
+
gemspec.description = "Increment ActiveRecord's counter cache column asynchronously using Resque (and resque-lock-timeout)."
|
7
|
+
gemspec.email = "aaron.gibralter@gmail.com"
|
8
|
+
gemspec.homepage = "http://github.com/agibralter/ar-resque-counter-cache"
|
9
|
+
gemspec.authors = ["Aaron Gibralter"]
|
10
|
+
gemspec.add_dependency("activerecord", "~> 2.3.5")
|
11
|
+
gemspec.add_dependency("resque", "~> 1.10.0")
|
12
|
+
gemspec.add_dependency("resque-lock-timeout", "~> 0.2.1")
|
13
|
+
gemspec.files.exclude("pkg")
|
14
|
+
end
|
15
|
+
Jeweler::GemcutterTasks.new
|
16
|
+
rescue LoadError
|
17
|
+
puts "Jeweler not available. Install it with: gem install jeweler"
|
18
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{ar-resque-counter-cache}
|
8
|
+
s.version = "0.0.1"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Aaron Gibralter"]
|
12
|
+
s.date = %q{2010-10-13}
|
13
|
+
s.description = %q{Increment ActiveRecord's counter cache column asynchronously using Resque (and resque-lock-timeout).}
|
14
|
+
s.email = %q{aaron.gibralter@gmail.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"README.md"
|
17
|
+
]
|
18
|
+
s.files = [
|
19
|
+
"README.md",
|
20
|
+
"Rakefile",
|
21
|
+
"VERSION",
|
22
|
+
"ar-resque-counter-cache.gemspec",
|
23
|
+
"lib/ar-resque-counter-cache.rb",
|
24
|
+
"lib/ar_resque_counter_cache/active_record.rb",
|
25
|
+
"lib/ar_resque_counter_cache/increment_counters_worker.rb",
|
26
|
+
"spec/ar_resque_counter_cache/active_record_spec.rb",
|
27
|
+
"spec/integration_spec.rb",
|
28
|
+
"spec/models.rb",
|
29
|
+
"spec/redis-test.conf",
|
30
|
+
"spec/spec_helper.rb"
|
31
|
+
]
|
32
|
+
s.homepage = %q{http://github.com/agibralter/ar-resque-counter-cache}
|
33
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
34
|
+
s.require_paths = ["lib"]
|
35
|
+
s.rubygems_version = %q{1.3.7}
|
36
|
+
s.summary = %q{Increment ActiveRecord's counter cache column asynchronously using Resque (and resque-lock-timeout).}
|
37
|
+
s.test_files = [
|
38
|
+
"spec/ar_resque_counter_cache/active_record_spec.rb",
|
39
|
+
"spec/integration_spec.rb",
|
40
|
+
"spec/models.rb",
|
41
|
+
"spec/spec_helper.rb"
|
42
|
+
]
|
43
|
+
|
44
|
+
if s.respond_to? :specification_version then
|
45
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
46
|
+
s.specification_version = 3
|
47
|
+
|
48
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
49
|
+
s.add_runtime_dependency(%q<activerecord>, ["~> 2.3.5"])
|
50
|
+
s.add_runtime_dependency(%q<resque>, ["~> 1.10.0"])
|
51
|
+
s.add_runtime_dependency(%q<resque-lock-timeout>, ["~> 0.2.1"])
|
52
|
+
else
|
53
|
+
s.add_dependency(%q<activerecord>, ["~> 2.3.5"])
|
54
|
+
s.add_dependency(%q<resque>, ["~> 1.10.0"])
|
55
|
+
s.add_dependency(%q<resque-lock-timeout>, ["~> 0.2.1"])
|
56
|
+
end
|
57
|
+
else
|
58
|
+
s.add_dependency(%q<activerecord>, ["~> 2.3.5"])
|
59
|
+
s.add_dependency(%q<resque>, ["~> 1.10.0"])
|
60
|
+
s.add_dependency(%q<resque-lock-timeout>, ["~> 0.2.1"])
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module ArAsyncCounterCache
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
|
5
|
+
def update_async_counters(dir, *association_ids)
|
6
|
+
association_ids.each do |association_id|
|
7
|
+
reflection = self.class.reflect_on_association(association_id)
|
8
|
+
parent_class = reflection.klass
|
9
|
+
column = self.class.async_counter_types[association_id]
|
10
|
+
if parent_id = send(reflection.primary_key_name)
|
11
|
+
ArAsyncCounterCache::IncrementCountersWorker.cache_and_enqueue(parent_class, parent_id, column, dir)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
|
18
|
+
def belongs_to(association_id, options={})
|
19
|
+
column = async_counter_cache_column(options.delete(:async_counter_cache))
|
20
|
+
raise "Please don't use both async_counter_cache and counter_cache." if column && options[:counter_cache]
|
21
|
+
super(association_id, options)
|
22
|
+
if column
|
23
|
+
# Store the async_counter_cache column for the update_async_counters
|
24
|
+
# helper method.
|
25
|
+
self.async_counter_types[association_id] = column
|
26
|
+
# Fetch the reflection.
|
27
|
+
reflection = self.reflect_on_association(association_id)
|
28
|
+
parent_class = reflection.klass
|
29
|
+
# Let's make the column read-only like the normal belongs_to
|
30
|
+
# counter_cache.
|
31
|
+
parent_class.send(:attr_readonly, column.to_sym) if defined?(parent_class) && parent_class.respond_to?(:attr_readonly)
|
32
|
+
parent_id_column = reflection.primary_key_name
|
33
|
+
add_callbacks(parent_class.to_s, parent_id_column, column)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def async_counter_types
|
38
|
+
@async_counter_types ||= {}
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def add_callbacks(parent_class, parent_id_column, column)
|
44
|
+
base_method_name = "async_counter_cache_#{parent_class}_#{column}"
|
45
|
+
# Define after_create callback method.
|
46
|
+
method_name = "#{base_method_name}_after_create".to_sym
|
47
|
+
define_method(method_name) do
|
48
|
+
if parent_id = send(parent_id_column)
|
49
|
+
ArAsyncCounterCache::IncrementCountersWorker.cache_and_enqueue(parent_class, parent_id, column, :increment)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
after_create(method_name)
|
53
|
+
# Define after_destroy callback method.
|
54
|
+
method_name = "#{base_method_name}_after_destroy".to_sym
|
55
|
+
define_method(method_name) do
|
56
|
+
if parent_id = send(parent_id_column)
|
57
|
+
ArAsyncCounterCache::IncrementCountersWorker.cache_and_enqueue(parent_class, parent_id, column, :decrement)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
after_destroy(method_name)
|
61
|
+
end
|
62
|
+
|
63
|
+
def async_counter_cache_column(opt)
|
64
|
+
opt === true ? "#{self.table_name}_count" : opt
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.included(base)
|
69
|
+
base.extend(ClassMethods)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'resque'
|
2
|
+
require 'resque-lock-timeout'
|
3
|
+
|
4
|
+
module ArAsyncCounterCache
|
5
|
+
|
6
|
+
# The default Resque queue is :counter_caches.
|
7
|
+
def self.resque_job_queue=(queue)
|
8
|
+
IncrementCountersWorker.class_eval do
|
9
|
+
@queue = queue
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# The default lock_timeout is 60 seconds.
|
14
|
+
def self.resque_lock_timeout=(lock_timeout)
|
15
|
+
IncrementCountersWorker.class_eval do
|
16
|
+
@lock_timeout = lock_timeout
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# If you don't want to use Resque's Redis connection to store the temporary
|
21
|
+
# counter caches, you can set a different connection here.
|
22
|
+
def self.redis=(redis)
|
23
|
+
IncrementCountersWorker.class_eval do
|
24
|
+
@redis = redis
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# ArAsyncCounterCache will very quickly increment a counter cache in Redis,
|
29
|
+
# which will then later be updated by a Resque job. Using
|
30
|
+
# require-lock-timeout, we can ensure that only one job per ___ is running
|
31
|
+
# at a time.
|
32
|
+
class IncrementCountersWorker
|
33
|
+
|
34
|
+
extend ::Resque::Plugins::LockTimeout
|
35
|
+
@queue = :counter_caches
|
36
|
+
@lock_timeout = 60
|
37
|
+
|
38
|
+
def self.cache_and_enqueue(parent_class, id, column, direction)
|
39
|
+
parent_class = parent_class.to_s
|
40
|
+
key = cache_key(parent_class, id, column)
|
41
|
+
if direction == :increment
|
42
|
+
redis.incr(key)
|
43
|
+
elsif direction == :decrement
|
44
|
+
redis.decr(key)
|
45
|
+
else
|
46
|
+
raise ArgumentError, "Must call ArAsyncCounterCache::IncrementCountersWorker with :increment or :decrement"
|
47
|
+
end
|
48
|
+
::Resque.enqueue(self, parent_class, id, column)
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.redis
|
52
|
+
@redis || ::Resque.redis
|
53
|
+
end
|
54
|
+
|
55
|
+
# args: (parent_class, id, column)
|
56
|
+
def self.identifier(*args)
|
57
|
+
args.join('-')
|
58
|
+
end
|
59
|
+
|
60
|
+
# Try again later if lock is in use.
|
61
|
+
def self.lock_failed(*args)
|
62
|
+
::Resque.enqueue(self, *args)
|
63
|
+
end
|
64
|
+
|
65
|
+
# args: (parent_class, id, column)
|
66
|
+
def self.cache_key(*args)
|
67
|
+
"ar-resque-counter-cache:#{identifier(*args)}"
|
68
|
+
end
|
69
|
+
|
70
|
+
# The name of this method ensures that it runs within around_perform_lock.
|
71
|
+
#
|
72
|
+
# We've leveraged resque-lock-timeout to ensure that only one job is
|
73
|
+
# running at a time. Now, this around filter essentially ensures that only
|
74
|
+
# one job per parent-column can sit on the queue at once. Since the
|
75
|
+
# cache_key entry in redis stores the most up-to-date delta for the
|
76
|
+
# parent's counter cache, we don't have to actually perform the
|
77
|
+
# Klass.update_counters for every increment/decrement. We can batch
|
78
|
+
# process!
|
79
|
+
def self.around_perform_lock1(*args)
|
80
|
+
# Remove all other instances of this job (with the same args) from the
|
81
|
+
# queue. Uses LREM (http://code.google.com/p/redis/wiki/LremCommand) which
|
82
|
+
# takes the form: "LREM key count value" and if count == 0 removes all
|
83
|
+
# instances of value from the list.
|
84
|
+
redis_job_value = ::Resque.encode(:class => self.to_s, :args => args)
|
85
|
+
::Resque.redis.lrem("queue:#{@queue}", 0, redis_job_value)
|
86
|
+
yield
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.perform(parent_class, id, column)
|
90
|
+
key = cache_key(parent_class, id, column)
|
91
|
+
if (delta = redis.getset(key, 0).to_i) != 0
|
92
|
+
begin
|
93
|
+
parent_class = ::Resque.constantize(parent_class)
|
94
|
+
parent_class.find(id)
|
95
|
+
parent_class.update_counters(id, column => delta)
|
96
|
+
rescue Exception => e
|
97
|
+
# If anything happens, set back the counter cache.
|
98
|
+
if delta > 0
|
99
|
+
redis.incrby(key, delta)
|
100
|
+
elsif delta < 0
|
101
|
+
redis.decrby(key, -delta)
|
102
|
+
end
|
103
|
+
raise e
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ArAsyncCounterCache::ActiveRecord do
|
4
|
+
|
5
|
+
context "callbacks" do
|
6
|
+
|
7
|
+
subject { User.create(:name => "Susan") }
|
8
|
+
|
9
|
+
it "should increment" do
|
10
|
+
ArAsyncCounterCache::IncrementCountersWorker.should_receive(:cache_and_enqueue).with("User", subject.id, "posts_count", :increment)
|
11
|
+
subject.posts.create(:body => "I have a cat!")
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should increment" do
|
15
|
+
ArAsyncCounterCache::IncrementCountersWorker.stub(:cache_and_enqueue)
|
16
|
+
post = subject.posts.create(:body => "I have a cat!")
|
17
|
+
ArAsyncCounterCache::IncrementCountersWorker.should_receive(:cache_and_enqueue).with("User", subject.id, "posts_count", :decrement)
|
18
|
+
post.destroy
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "integration" do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@worker = Resque::Worker.new(:testing)
|
7
|
+
@worker.register_worker
|
8
|
+
@user1 = User.create(:name => "Susan")
|
9
|
+
@user2 = User.create(:name => "Bob")
|
10
|
+
@post1 = @user1.posts.create(:body => "I have a cat!")
|
11
|
+
@post2 = @user1.posts.create(:body => "I have a mouse!")
|
12
|
+
@comment1 = @post1.comments.create(:body => "Your cat sucks!", :user => @user2)
|
13
|
+
@comment2 = @post1.comments.create(:body => "No it doesn't!", :user => @user1)
|
14
|
+
@comment3 = @post2.comments.create(:body => "Your mouse also sucks!", :user => @user2)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should increment/decrement counter caches asynchronously in batches" do
|
18
|
+
# Should be asynchronous...
|
19
|
+
@user1.posts_count.should == 0
|
20
|
+
@user1.comments_count.should == 0
|
21
|
+
@user2.posts_count.should == 0
|
22
|
+
@user2.comments_count.should == 0
|
23
|
+
@post1.count_of_comments.should == 0
|
24
|
+
@post2.count_of_comments.should == 0
|
25
|
+
|
26
|
+
# 2 for posts incrementing users' posts counts
|
27
|
+
# 3 for comments incrementing users' comments counts
|
28
|
+
# 3 for comments incrementing posts' comments counts
|
29
|
+
Resque.size(:testing).should == 8
|
30
|
+
|
31
|
+
# [ArAsyncCounterCache::IncrementCountersWorker, "User", 1, "posts_count"]
|
32
|
+
perform_next_job
|
33
|
+
@user1.reload.posts_count.should == 2
|
34
|
+
@user1.reload.comments_count.should == 0
|
35
|
+
@user2.reload.posts_count.should == 0
|
36
|
+
@user2.reload.comments_count.should == 0
|
37
|
+
@post1.reload.count_of_comments.should == 0
|
38
|
+
@post2.reload.count_of_comments.should == 0
|
39
|
+
|
40
|
+
# [ArAsyncCounterCache::IncrementCountersWorker, "User", 2, "comments_count"]
|
41
|
+
perform_next_job
|
42
|
+
@user1.reload.posts_count.should == 2
|
43
|
+
@user1.reload.comments_count.should == 0
|
44
|
+
@user2.reload.posts_count.should == 0
|
45
|
+
@user2.reload.comments_count.should == 2
|
46
|
+
@post1.reload.count_of_comments.should == 0
|
47
|
+
@post2.reload.count_of_comments.should == 0
|
48
|
+
|
49
|
+
# [ArAsyncCounterCache::IncrementCountersWorker, "Post", 1, "count_of_comments"]
|
50
|
+
perform_next_job
|
51
|
+
@user1.reload.posts_count.should == 2
|
52
|
+
@user1.reload.comments_count.should == 0
|
53
|
+
@user2.reload.posts_count.should == 0
|
54
|
+
@user2.reload.comments_count.should == 2
|
55
|
+
@post1.reload.count_of_comments.should == 2
|
56
|
+
@post2.reload.count_of_comments.should == 0
|
57
|
+
|
58
|
+
# [ArAsyncCounterCache::IncrementCountersWorker, "User", 1, "comments_count"]
|
59
|
+
perform_next_job
|
60
|
+
@user1.reload.posts_count.should == 2
|
61
|
+
@user1.reload.comments_count.should == 1
|
62
|
+
@user2.reload.posts_count.should == 0
|
63
|
+
@user2.reload.comments_count.should == 2
|
64
|
+
@post1.reload.count_of_comments.should == 2
|
65
|
+
@post2.reload.count_of_comments.should == 0
|
66
|
+
|
67
|
+
# [ArAsyncCounterCache::IncrementCountersWorker, "Post", 2, "count_of_comments"]
|
68
|
+
perform_next_job
|
69
|
+
@user1.reload.posts_count.should == 2
|
70
|
+
@user1.reload.comments_count.should == 1
|
71
|
+
@user2.reload.posts_count.should == 0
|
72
|
+
@user2.reload.comments_count.should == 2
|
73
|
+
@post1.reload.count_of_comments.should == 2
|
74
|
+
@post2.reload.count_of_comments.should == 1
|
75
|
+
|
76
|
+
# Should be done...
|
77
|
+
Resque.size(:testing).should == 0
|
78
|
+
|
79
|
+
@comment1.destroy
|
80
|
+
@comment2.destroy
|
81
|
+
@comment3.destroy
|
82
|
+
|
83
|
+
# Should be asynchronous...
|
84
|
+
@user1.reload.posts_count.should == 2
|
85
|
+
@user1.reload.comments_count.should == 1
|
86
|
+
@user2.reload.posts_count.should == 0
|
87
|
+
@user2.reload.comments_count.should == 2
|
88
|
+
@post1.reload.count_of_comments.should == 2
|
89
|
+
@post2.reload.count_of_comments.should == 1
|
90
|
+
|
91
|
+
perform_all_jobs
|
92
|
+
|
93
|
+
@user1.reload.posts_count.should == 2
|
94
|
+
@user1.reload.comments_count.should == 0
|
95
|
+
@user2.reload.posts_count.should == 0
|
96
|
+
@user2.reload.comments_count.should == 0
|
97
|
+
@post1.reload.count_of_comments.should == 0
|
98
|
+
@post2.reload.count_of_comments.should == 0
|
99
|
+
end
|
100
|
+
|
101
|
+
def perform_next_job
|
102
|
+
return unless job = @worker.reserve
|
103
|
+
@worker.perform(job)
|
104
|
+
@worker.done_working
|
105
|
+
end
|
106
|
+
|
107
|
+
def perform_all_jobs
|
108
|
+
while job = @worker.reserve
|
109
|
+
@worker.perform(job)
|
110
|
+
@worker.done_working
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
data/spec/models.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
ActiveRecord::Migration.verbose = false
|
2
|
+
ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
|
3
|
+
|
4
|
+
class CreateModelsForTest < ActiveRecord::Migration
|
5
|
+
def self.up
|
6
|
+
create_table :users do |t|
|
7
|
+
t.string :name
|
8
|
+
t.integer :posts_count, :default => 0
|
9
|
+
t.integer :comments_count, :default => 0
|
10
|
+
end
|
11
|
+
create_table :posts do |t|
|
12
|
+
t.string :body
|
13
|
+
t.belongs_to :user
|
14
|
+
t.integer :count_of_comments, :default => 0
|
15
|
+
end
|
16
|
+
create_table :comments do |t|
|
17
|
+
t.string :body
|
18
|
+
t.belongs_to :user
|
19
|
+
t.belongs_to :post
|
20
|
+
end
|
21
|
+
end
|
22
|
+
def self.down
|
23
|
+
drop_table(:users)
|
24
|
+
drop_table(:posts)
|
25
|
+
drop_table(:comments)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class User < ActiveRecord::Base
|
30
|
+
has_many :comments
|
31
|
+
has_many :posts
|
32
|
+
end
|
33
|
+
|
34
|
+
class Post < ActiveRecord::Base
|
35
|
+
belongs_to :user, :async_counter_cache => true
|
36
|
+
has_many :comments
|
37
|
+
end
|
38
|
+
|
39
|
+
class Comment < ActiveRecord::Base
|
40
|
+
belongs_to :user, :async_counter_cache => true
|
41
|
+
belongs_to :post, :async_counter_cache => "count_of_comments"
|
42
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
spec_dir = File.expand_path(File.dirname(__FILE__))
|
2
|
+
$:.unshift(File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')))
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
# Ensure resque for tests.
|
6
|
+
require 'resque'
|
7
|
+
require 'ar-resque-counter-cache'
|
8
|
+
require 'spec'
|
9
|
+
require 'models'
|
10
|
+
|
11
|
+
cwd = Dir.getwd
|
12
|
+
Dir.chdir(spec_dir)
|
13
|
+
|
14
|
+
if !system("which redis-server")
|
15
|
+
puts '', "** can't find `redis-server` in your path"
|
16
|
+
abort ''
|
17
|
+
end
|
18
|
+
|
19
|
+
at_exit do
|
20
|
+
if (pid = `cat redis-test.pid`.strip) =~ /^\d+$/
|
21
|
+
puts "Killing test redis server with pid #{pid}..."
|
22
|
+
`rm -f dump.rdb`
|
23
|
+
`rm -f redis-test.pid`
|
24
|
+
Process.kill("KILL", pid.to_i)
|
25
|
+
Dir.chdir(cwd)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
puts "Starting redis for testing at localhost:9736..."
|
30
|
+
`redis-server #{spec_dir}/redis-test.conf`
|
31
|
+
Resque.redis = '127.0.0.1:9736'
|
32
|
+
|
33
|
+
Spec::Runner.configure do |config|
|
34
|
+
config.before(:all) do
|
35
|
+
ArAsyncCounterCache.resque_job_queue = :testing
|
36
|
+
end
|
37
|
+
config.before(:each) do
|
38
|
+
ActiveRecord::Base.silence { CreateModelsForTest.migrate(:up) }
|
39
|
+
Resque.redis.flushall
|
40
|
+
end
|
41
|
+
config.after(:each) do
|
42
|
+
ActiveRecord::Base.silence { CreateModelsForTest.migrate(:down) }
|
43
|
+
end
|
44
|
+
end
|
metadata
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ar-resque-counter-cache
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Aaron Gibralter
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-10-13 00:00:00 -04:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: activerecord
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 9
|
30
|
+
segments:
|
31
|
+
- 2
|
32
|
+
- 3
|
33
|
+
- 5
|
34
|
+
version: 2.3.5
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: resque
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
hash: 63
|
46
|
+
segments:
|
47
|
+
- 1
|
48
|
+
- 10
|
49
|
+
- 0
|
50
|
+
version: 1.10.0
|
51
|
+
type: :runtime
|
52
|
+
version_requirements: *id002
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: resque-lock-timeout
|
55
|
+
prerelease: false
|
56
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
hash: 21
|
62
|
+
segments:
|
63
|
+
- 0
|
64
|
+
- 2
|
65
|
+
- 1
|
66
|
+
version: 0.2.1
|
67
|
+
type: :runtime
|
68
|
+
version_requirements: *id003
|
69
|
+
description: Increment ActiveRecord's counter cache column asynchronously using Resque (and resque-lock-timeout).
|
70
|
+
email: aaron.gibralter@gmail.com
|
71
|
+
executables: []
|
72
|
+
|
73
|
+
extensions: []
|
74
|
+
|
75
|
+
extra_rdoc_files:
|
76
|
+
- README.md
|
77
|
+
files:
|
78
|
+
- README.md
|
79
|
+
- Rakefile
|
80
|
+
- VERSION
|
81
|
+
- ar-resque-counter-cache.gemspec
|
82
|
+
- lib/ar-resque-counter-cache.rb
|
83
|
+
- lib/ar_resque_counter_cache/active_record.rb
|
84
|
+
- lib/ar_resque_counter_cache/increment_counters_worker.rb
|
85
|
+
- spec/ar_resque_counter_cache/active_record_spec.rb
|
86
|
+
- spec/integration_spec.rb
|
87
|
+
- spec/models.rb
|
88
|
+
- spec/redis-test.conf
|
89
|
+
- spec/spec_helper.rb
|
90
|
+
has_rdoc: true
|
91
|
+
homepage: http://github.com/agibralter/ar-resque-counter-cache
|
92
|
+
licenses: []
|
93
|
+
|
94
|
+
post_install_message:
|
95
|
+
rdoc_options:
|
96
|
+
- --charset=UTF-8
|
97
|
+
require_paths:
|
98
|
+
- lib
|
99
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
100
|
+
none: false
|
101
|
+
requirements:
|
102
|
+
- - ">="
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
hash: 3
|
105
|
+
segments:
|
106
|
+
- 0
|
107
|
+
version: "0"
|
108
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
109
|
+
none: false
|
110
|
+
requirements:
|
111
|
+
- - ">="
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
hash: 3
|
114
|
+
segments:
|
115
|
+
- 0
|
116
|
+
version: "0"
|
117
|
+
requirements: []
|
118
|
+
|
119
|
+
rubyforge_project:
|
120
|
+
rubygems_version: 1.3.7
|
121
|
+
signing_key:
|
122
|
+
specification_version: 3
|
123
|
+
summary: Increment ActiveRecord's counter cache column asynchronously using Resque (and resque-lock-timeout).
|
124
|
+
test_files:
|
125
|
+
- spec/ar_resque_counter_cache/active_record_spec.rb
|
126
|
+
- spec/integration_spec.rb
|
127
|
+
- spec/models.rb
|
128
|
+
- spec/spec_helper.rb
|