ar-resque-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.
- 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
|