ar-async-counter-cache 0.0.3 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|