ar-async-counter-cache 0.0.3 → 0.1.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 +71 -0
- data/Rakefile +6 -3
- data/VERSION +1 -1
- data/ar-async-counter-cache.gemspec +18 -13
- data/lib/ar-async-counter-cache.rb +1 -7
- data/lib/ar_async_counter_cache/active_record.rb +30 -34
- data/lib/ar_async_counter_cache/increment_counters_worker.rb +108 -0
- data/spec/ar_async_counter_cache/active_record_spec.rb +11 -13
- data/spec/integration_spec.rb +52 -11
- metadata +50 -12
- data/README +0 -68
- data/lib/ar_async_counter_cache/increment_counters_job.rb +0 -26
- data/pkg/ar-async-counter-cache-0.0.1.gem +0 -0
data/README.md
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
ar-async-counter-cache
|
2
|
+
----------------------
|
3
|
+
|
4
|
+
This gem allows you to update ActiveRecord counter cache columns
|
5
|
+
asynchronously using Resque (http://github.com/defunkt/resque). You may want
|
6
|
+
to do this in situations where you want really speedy inserts and have models
|
7
|
+
that "belong_to" many different parents and also allows for fast inserts of
|
8
|
+
many children for the same parent.
|
9
|
+
|
10
|
+
For example, let's say a single Post gets 1000 comments very quickly. This
|
11
|
+
will set a key in Redis indicating that there is a delta of +1000 for that
|
12
|
+
Post's comments_count column. It will also queue 1000 Resque jobs. This is
|
13
|
+
where resque-lock-timeout comes in. Only one of those jobs will be allowed to
|
14
|
+
run at a time. Once a job acquires the lock it removes all other instances of
|
15
|
+
that job from the queue (see IncrementCountersWorker.around_perform_lock1).
|
16
|
+
|
17
|
+
You use it like such:
|
18
|
+
|
19
|
+
class User < ActiveRecord::Base
|
20
|
+
has_many :comments
|
21
|
+
has_many :posts
|
22
|
+
end
|
23
|
+
|
24
|
+
class Post < ActiveRecord::Base
|
25
|
+
belongs_to :user, :async_counter_cache => true
|
26
|
+
has_many :comments
|
27
|
+
end
|
28
|
+
|
29
|
+
class Comment < ActiveRecord::Base
|
30
|
+
belongs_to :user, :async_counter_cache => true
|
31
|
+
belongs_to :post, :async_counter_cache => "count_of_comments"
|
32
|
+
end
|
33
|
+
|
34
|
+
Notice, you may specify the name of the counter cache column just as you can
|
35
|
+
with the normal belongs_to `:counter_cache` option. You also may not use both
|
36
|
+
the `:async_counter_cache` and `:counter_cache` options in the same belongs_to
|
37
|
+
call.
|
38
|
+
|
39
|
+
All you should need to do is require this gem in your project that uses
|
40
|
+
ActiveRecord and you should be good to go;
|
41
|
+
|
42
|
+
e.g. In your Gemfile:
|
43
|
+
|
44
|
+
gem 'ar-async-counter-cache', '0.1.0'
|
45
|
+
|
46
|
+
and then in RAILS_ROOT/config/environment.rb somewhere:
|
47
|
+
|
48
|
+
require 'ar-async-counter-cache'
|
49
|
+
|
50
|
+
By default, the Resque job is placed on the `:counter_caches` queue:
|
51
|
+
|
52
|
+
@queue = :counter_caches
|
53
|
+
|
54
|
+
However, you can change this:
|
55
|
+
|
56
|
+
in RAILS_ROOT/config/environment.rb somewhere:
|
57
|
+
|
58
|
+
ArAsyncCounterCache.resque_job_queue = :low_priority
|
59
|
+
|
60
|
+
`ArAsyncCounterCache::IncrementCountersWorker.cache_and_enqueue` can also be
|
61
|
+
used to increment/decrement arbitrary counter cache columns (outside of
|
62
|
+
belongs_to associations):
|
63
|
+
|
64
|
+
ArAsyncCounterCache::IncrementCountersWorker.cache_and_enqueue(klass, id, column, direction)
|
65
|
+
|
66
|
+
Where:
|
67
|
+
|
68
|
+
* `klass` is the Class of the ActiveRecord object
|
69
|
+
* `id` is the id of the object
|
70
|
+
* `column` is the counter cache column
|
71
|
+
* `direction` is either `:increment` or `:decrement`
|
data/Rakefile
CHANGED
@@ -2,12 +2,15 @@ begin
|
|
2
2
|
require 'jeweler'
|
3
3
|
Jeweler::Tasks.new do |gemspec|
|
4
4
|
gemspec.name = "ar-async-counter-cache"
|
5
|
-
gemspec.summary = "Increment ActiveRecord's counter cache column asynchronously
|
6
|
-
gemspec.description = "Increment ActiveRecord's counter cache column asynchronously
|
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
7
|
gemspec.email = "aaron.gibralter@gmail.com"
|
8
8
|
gemspec.homepage = "http://github.com/agibralter/ar-async-counter-cache"
|
9
9
|
gemspec.authors = ["Aaron Gibralter"]
|
10
|
-
gemspec.add_dependency("activerecord", "
|
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")
|
11
14
|
end
|
12
15
|
Jeweler::GemcutterTasks.new
|
13
16
|
rescue LoadError
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.1.1
|
@@ -5,26 +5,25 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{ar-async-counter-cache}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.1.1"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Aaron Gibralter"]
|
12
|
-
s.date = %q{2010-06
|
13
|
-
s.description = %q{Increment ActiveRecord's counter cache column asynchronously
|
12
|
+
s.date = %q{2010-10-06}
|
13
|
+
s.description = %q{Increment ActiveRecord's counter cache column asynchronously using Resque (and resque-lock-timeout).}
|
14
14
|
s.email = %q{aaron.gibralter@gmail.com}
|
15
15
|
s.extra_rdoc_files = [
|
16
|
-
"README"
|
16
|
+
"README.md"
|
17
17
|
]
|
18
18
|
s.files = [
|
19
19
|
".gitignore",
|
20
|
-
"README",
|
20
|
+
"README.md",
|
21
21
|
"Rakefile",
|
22
22
|
"VERSION",
|
23
23
|
"ar-async-counter-cache.gemspec",
|
24
24
|
"lib/ar-async-counter-cache.rb",
|
25
25
|
"lib/ar_async_counter_cache/active_record.rb",
|
26
|
-
"lib/ar_async_counter_cache/
|
27
|
-
"pkg/ar-async-counter-cache-0.0.1.gem",
|
26
|
+
"lib/ar_async_counter_cache/increment_counters_worker.rb",
|
28
27
|
"spec/ar_async_counter_cache/active_record_spec.rb",
|
29
28
|
"spec/integration_spec.rb",
|
30
29
|
"spec/models.rb",
|
@@ -34,8 +33,8 @@ Gem::Specification.new do |s|
|
|
34
33
|
s.homepage = %q{http://github.com/agibralter/ar-async-counter-cache}
|
35
34
|
s.rdoc_options = ["--charset=UTF-8"]
|
36
35
|
s.require_paths = ["lib"]
|
37
|
-
s.rubygems_version = %q{1.3.
|
38
|
-
s.summary = %q{Increment ActiveRecord's counter cache column asynchronously
|
36
|
+
s.rubygems_version = %q{1.3.7}
|
37
|
+
s.summary = %q{Increment ActiveRecord's counter cache column asynchronously using Resque (and resque-lock-timeout).}
|
39
38
|
s.test_files = [
|
40
39
|
"spec/ar_async_counter_cache/active_record_spec.rb",
|
41
40
|
"spec/integration_spec.rb",
|
@@ -47,13 +46,19 @@ Gem::Specification.new do |s|
|
|
47
46
|
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
48
47
|
s.specification_version = 3
|
49
48
|
|
50
|
-
if Gem::Version.new(Gem::
|
51
|
-
s.add_runtime_dependency(%q<activerecord>, ["
|
49
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
50
|
+
s.add_runtime_dependency(%q<activerecord>, ["~> 2.3.5"])
|
51
|
+
s.add_runtime_dependency(%q<resque>, ["~> 1.10.0"])
|
52
|
+
s.add_runtime_dependency(%q<resque-lock-timeout>, ["~> 0.2.1"])
|
52
53
|
else
|
53
|
-
s.add_dependency(%q<activerecord>, ["
|
54
|
+
s.add_dependency(%q<activerecord>, ["~> 2.3.5"])
|
55
|
+
s.add_dependency(%q<resque>, ["~> 1.10.0"])
|
56
|
+
s.add_dependency(%q<resque-lock-timeout>, ["~> 0.2.1"])
|
54
57
|
end
|
55
58
|
else
|
56
|
-
s.add_dependency(%q<activerecord>, ["
|
59
|
+
s.add_dependency(%q<activerecord>, ["~> 2.3.5"])
|
60
|
+
s.add_dependency(%q<resque>, ["~> 1.10.0"])
|
61
|
+
s.add_dependency(%q<resque-lock-timeout>, ["~> 0.2.1"])
|
57
62
|
end
|
58
63
|
end
|
59
64
|
|
@@ -1,11 +1,5 @@
|
|
1
1
|
require 'active_record'
|
2
|
-
|
3
|
-
begin
|
4
|
-
require 'resque'
|
5
|
-
require 'ar_async_counter_cache/increment_counters_job'
|
6
|
-
rescue LoadError
|
7
|
-
end
|
8
|
-
|
2
|
+
require 'ar_async_counter_cache/increment_counters_worker'
|
9
3
|
require 'ar_async_counter_cache/active_record'
|
10
4
|
|
11
5
|
ActiveRecord::Base.send(:include, ArAsyncCounterCache::ActiveRecord)
|
@@ -2,17 +2,13 @@ module ArAsyncCounterCache
|
|
2
2
|
|
3
3
|
module ActiveRecord
|
4
4
|
|
5
|
-
def
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
def update_async_counters(dir, *parent_types)
|
13
|
-
(parent_types.empty? ? self.class.async_counter_types.keys : parent_types).each do |parent_type|
|
14
|
-
if (col = self.class.async_counter_types[parent_type]) && (parent = send(parent_type))
|
15
|
-
parent.class.send("#{dir}_counter", col, parent.id)
|
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)
|
16
12
|
end
|
17
13
|
end
|
18
14
|
end
|
@@ -20,21 +16,22 @@ module ArAsyncCounterCache
|
|
20
16
|
module ClassMethods
|
21
17
|
|
22
18
|
def belongs_to(association_id, options={})
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
#
|
29
|
-
|
30
|
-
|
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
|
31
29
|
# Let's make the column read-only like the normal belongs_to
|
32
30
|
# counter_cache.
|
33
|
-
|
34
|
-
|
35
|
-
|
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)
|
36
34
|
end
|
37
|
-
super(association_id, options)
|
38
35
|
end
|
39
36
|
|
40
37
|
def async_counter_types
|
@@ -43,23 +40,22 @@ module ArAsyncCounterCache
|
|
43
40
|
|
44
41
|
private
|
45
42
|
|
46
|
-
def add_callbacks
|
43
|
+
def add_callbacks(parent_class, parent_id_column, column)
|
44
|
+
base_method_name = "async_counter_cache_#{parent_class}_#{column}"
|
47
45
|
# Define after_create callback method.
|
48
|
-
method_name = "
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
end
|
53
|
-
else
|
54
|
-
define_method(method_name) do
|
55
|
-
self.async_increment_counters
|
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)
|
56
50
|
end
|
57
51
|
end
|
58
52
|
after_create(method_name)
|
59
53
|
# Define before_destroy callback method.
|
60
|
-
method_name = "
|
54
|
+
method_name = "#{base_method_name}_before_destroy".to_sym
|
61
55
|
define_method(method_name) do
|
62
|
-
|
56
|
+
if parent_id = send(parent_id_column)
|
57
|
+
ArAsyncCounterCache::IncrementCountersWorker.cache_and_enqueue(parent_class, parent_id, column, :decrement)
|
58
|
+
end
|
63
59
|
end
|
64
60
|
before_destroy(method_name)
|
65
61
|
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-async-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
|
@@ -2,22 +2,20 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe ArAsyncCounterCache::ActiveRecord do
|
4
4
|
|
5
|
-
describe ArAsyncCounterCache::ActiveRecord::ClassMethods do
|
6
|
-
it "should set async_counter_types" do
|
7
|
-
Post.async_counter_types.should == {:user => "posts_count"}
|
8
|
-
Comment.async_counter_types.should == {:user => "comments_count", :post => "count_of_comments"}
|
9
|
-
end
|
10
|
-
end
|
11
|
-
|
12
5
|
context "callbacks" do
|
13
6
|
|
14
|
-
|
15
|
-
|
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!")
|
16
12
|
end
|
17
|
-
|
18
|
-
it "should
|
19
|
-
|
20
|
-
|
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
|
21
19
|
end
|
22
20
|
end
|
23
21
|
end
|
data/spec/integration_spec.rb
CHANGED
@@ -5,7 +5,6 @@ describe "integration" do
|
|
5
5
|
before(:each) do
|
6
6
|
@worker = Resque::Worker.new(:testing)
|
7
7
|
@worker.register_worker
|
8
|
-
# Models...
|
9
8
|
@user1 = User.create(:name => "Susan")
|
10
9
|
@user2 = User.create(:name => "Bob")
|
11
10
|
@post1 = @user1.posts.create(:body => "I have a cat!")
|
@@ -15,40 +14,82 @@ describe "integration" do
|
|
15
14
|
@comment3 = @post2.comments.create(:body => "Your mouse also sucks!", :user => @user2)
|
16
15
|
end
|
17
16
|
|
18
|
-
it "should
|
17
|
+
it "should increment/decrement counter caches asynchronously in batches" do
|
18
|
+
# Should be asynchronous...
|
19
19
|
@user1.posts_count.should == 0
|
20
20
|
@user1.comments_count.should == 0
|
21
21
|
@user2.posts_count.should == 0
|
22
22
|
@user2.comments_count.should == 0
|
23
23
|
@post1.count_of_comments.should == 0
|
24
24
|
@post2.count_of_comments.should == 0
|
25
|
-
end
|
26
25
|
|
27
|
-
|
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"]
|
28
32
|
perform_next_job
|
29
|
-
@user1.reload.posts_count.should ==
|
33
|
+
@user1.reload.posts_count.should == 2
|
30
34
|
@user1.reload.comments_count.should == 0
|
31
35
|
@user2.reload.posts_count.should == 0
|
32
36
|
@user2.reload.comments_count.should == 0
|
33
37
|
@post1.reload.count_of_comments.should == 0
|
34
38
|
@post2.reload.count_of_comments.should == 0
|
35
|
-
end
|
36
39
|
|
37
|
-
|
38
|
-
|
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
|
39
69
|
@user1.reload.posts_count.should == 2
|
40
70
|
@user1.reload.comments_count.should == 1
|
41
71
|
@user2.reload.posts_count.should == 0
|
42
72
|
@user2.reload.comments_count.should == 2
|
43
73
|
@post1.reload.count_of_comments.should == 2
|
44
74
|
@post2.reload.count_of_comments.should == 1
|
45
|
-
end
|
46
75
|
|
47
|
-
|
48
|
-
|
76
|
+
# Should be done...
|
77
|
+
Resque.size(:testing).should == 0
|
78
|
+
|
49
79
|
@comment1.destroy
|
50
80
|
@comment2.destroy
|
51
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
|
+
|
52
93
|
@user1.reload.posts_count.should == 2
|
53
94
|
@user1.reload.comments_count.should == 0
|
54
95
|
@user2.reload.posts_count.should == 0
|
metadata
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ar-async-counter-cache
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
+
hash: 25
|
4
5
|
prerelease: false
|
5
6
|
segments:
|
6
7
|
- 0
|
7
|
-
-
|
8
|
-
-
|
9
|
-
version: 0.
|
8
|
+
- 1
|
9
|
+
- 1
|
10
|
+
version: 0.1.1
|
10
11
|
platform: ruby
|
11
12
|
authors:
|
12
13
|
- Aaron Gibralter
|
@@ -14,16 +15,18 @@ autorequire:
|
|
14
15
|
bindir: bin
|
15
16
|
cert_chain: []
|
16
17
|
|
17
|
-
date: 2010-06
|
18
|
+
date: 2010-10-06 00:00:00 -04:00
|
18
19
|
default_executable:
|
19
20
|
dependencies:
|
20
21
|
- !ruby/object:Gem::Dependency
|
21
22
|
name: activerecord
|
22
23
|
prerelease: false
|
23
24
|
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
24
26
|
requirements:
|
25
|
-
- -
|
27
|
+
- - ~>
|
26
28
|
- !ruby/object:Gem::Version
|
29
|
+
hash: 9
|
27
30
|
segments:
|
28
31
|
- 2
|
29
32
|
- 3
|
@@ -31,24 +34,55 @@ dependencies:
|
|
31
34
|
version: 2.3.5
|
32
35
|
type: :runtime
|
33
36
|
version_requirements: *id001
|
34
|
-
|
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).
|
35
70
|
email: aaron.gibralter@gmail.com
|
36
71
|
executables: []
|
37
72
|
|
38
73
|
extensions: []
|
39
74
|
|
40
75
|
extra_rdoc_files:
|
41
|
-
- README
|
76
|
+
- README.md
|
42
77
|
files:
|
43
78
|
- .gitignore
|
44
|
-
- README
|
79
|
+
- README.md
|
45
80
|
- Rakefile
|
46
81
|
- VERSION
|
47
82
|
- ar-async-counter-cache.gemspec
|
48
83
|
- lib/ar-async-counter-cache.rb
|
49
84
|
- lib/ar_async_counter_cache/active_record.rb
|
50
|
-
- lib/ar_async_counter_cache/
|
51
|
-
- pkg/ar-async-counter-cache-0.0.1.gem
|
85
|
+
- lib/ar_async_counter_cache/increment_counters_worker.rb
|
52
86
|
- spec/ar_async_counter_cache/active_record_spec.rb
|
53
87
|
- spec/integration_spec.rb
|
54
88
|
- spec/models.rb
|
@@ -64,26 +98,30 @@ rdoc_options:
|
|
64
98
|
require_paths:
|
65
99
|
- lib
|
66
100
|
required_ruby_version: !ruby/object:Gem::Requirement
|
101
|
+
none: false
|
67
102
|
requirements:
|
68
103
|
- - ">="
|
69
104
|
- !ruby/object:Gem::Version
|
105
|
+
hash: 3
|
70
106
|
segments:
|
71
107
|
- 0
|
72
108
|
version: "0"
|
73
109
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
110
|
+
none: false
|
74
111
|
requirements:
|
75
112
|
- - ">="
|
76
113
|
- !ruby/object:Gem::Version
|
114
|
+
hash: 3
|
77
115
|
segments:
|
78
116
|
- 0
|
79
117
|
version: "0"
|
80
118
|
requirements: []
|
81
119
|
|
82
120
|
rubyforge_project:
|
83
|
-
rubygems_version: 1.3.
|
121
|
+
rubygems_version: 1.3.7
|
84
122
|
signing_key:
|
85
123
|
specification_version: 3
|
86
|
-
summary: Increment ActiveRecord's counter cache column asynchronously
|
124
|
+
summary: Increment ActiveRecord's counter cache column asynchronously using Resque (and resque-lock-timeout).
|
87
125
|
test_files:
|
88
126
|
- spec/ar_async_counter_cache/active_record_spec.rb
|
89
127
|
- spec/integration_spec.rb
|
data/README
DELETED
@@ -1,68 +0,0 @@
|
|
1
|
-
ar-async-counter-cache
|
2
|
-
----------------------
|
3
|
-
|
4
|
-
This gem allows you to update ActiveRecord counter cache columns
|
5
|
-
asynchronously. You may want to do this in situations where you want really
|
6
|
-
speedy inserts and have models that "belong_to" many different parents.
|
7
|
-
|
8
|
-
You use it like such:
|
9
|
-
|
10
|
-
class User < ActiveRecord::Base
|
11
|
-
has_many :comments
|
12
|
-
has_many :posts
|
13
|
-
end
|
14
|
-
|
15
|
-
class Post < ActiveRecord::Base
|
16
|
-
belongs_to :user, :async_counter_cache => true
|
17
|
-
has_many :comments
|
18
|
-
end
|
19
|
-
|
20
|
-
class Comment < ActiveRecord::Base
|
21
|
-
belongs_to :user, :async_counter_cache => true
|
22
|
-
belongs_to :post, :async_counter_cache => "count_of_comments"
|
23
|
-
end
|
24
|
-
|
25
|
-
Notice, you may specify the name of the counter cache column just as you can
|
26
|
-
with the normal belongs_to :counter_cache option. You also may not use both
|
27
|
-
the :async_counter_cache and :counter_cache options in the same belongs_to
|
28
|
-
call.
|
29
|
-
|
30
|
-
All you should need to do is require this gem in your project that uses
|
31
|
-
ActiveRecord and you should be good to go;
|
32
|
-
|
33
|
-
e.g. In your Gemfile:
|
34
|
-
|
35
|
-
gem 'ar-async-counter-cache', '0.0.1'
|
36
|
-
|
37
|
-
and then in RAILS_ROOT/config/environment.rb somewhere:
|
38
|
-
|
39
|
-
require 'ar-async-counter-cache'
|
40
|
-
|
41
|
-
This gem has built-in support for Resque (http://github.com/defunkt/resque).
|
42
|
-
Al you need is resque in your loadpath:
|
43
|
-
|
44
|
-
e.g. In your Gemfile:
|
45
|
-
|
46
|
-
gem 'resque', '1.9.4'
|
47
|
-
|
48
|
-
By default, the Resque job is placed on the :counter_caches queue:
|
49
|
-
|
50
|
-
@queue = :counter_caches
|
51
|
-
|
52
|
-
However, you can change this:
|
53
|
-
|
54
|
-
in RAILS_ROOT/config/environment.rb somewhere:
|
55
|
-
|
56
|
-
ArAsyncCounterCache.resque_job_queue = :low_priority
|
57
|
-
|
58
|
-
If you don't want to use Resque, you must define one instance method for your
|
59
|
-
models using ar-async-counter-cache:
|
60
|
-
|
61
|
-
e.g.:
|
62
|
-
|
63
|
-
Comment#async_increment_counters
|
64
|
-
|
65
|
-
That method should pass a message onto a queue for a background processor to
|
66
|
-
handle later. That background processor should call:
|
67
|
-
|
68
|
-
Comment#update_async_counters(:increment)
|
@@ -1,26 +0,0 @@
|
|
1
|
-
module ArAsyncCounterCache
|
2
|
-
|
3
|
-
def self.resque_job_queue=(sym)
|
4
|
-
ArAsyncCounterCache::IncrementCountersJob.class_eval do
|
5
|
-
@queue = sym
|
6
|
-
end
|
7
|
-
end
|
8
|
-
|
9
|
-
class IncrementCountersJob
|
10
|
-
@queue = :counter_caches
|
11
|
-
|
12
|
-
# Take advantage of resque-retry if possible.
|
13
|
-
begin
|
14
|
-
require 'resque-retry'
|
15
|
-
require 'active_record/base'
|
16
|
-
extend Resque::Plugins::ExponentialBackoff
|
17
|
-
@retry_exceptions = [::ActiveRecord::RecordNotFound]
|
18
|
-
@backoff_strategy = [0, 10, 100]
|
19
|
-
rescue LoadError
|
20
|
-
end
|
21
|
-
|
22
|
-
def self.perform(klass_name, id)
|
23
|
-
Object.const_get(klass_name).find(id).update_async_counters(:increment)
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
Binary file
|