ar-async-counter-cache 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README +0 -0
- data/Rakefile +15 -0
- data/VERSION +1 -0
- data/ar-async-counter-cache.gemspec +57 -0
- data/lib/ar-async-counter-cache.rb +10 -0
- data/lib/ar_async_counter_cache/active_record.rb +89 -0
- data/lib/ar_async_counter_cache/resque_job.rb +25 -0
- data/spec/ar_async_counter_cache/active_record_spec.rb +23 -0
- data/spec/integration_spec.rb +72 -0
- data/spec/models.rb +42 -0
- data/spec/redis-test.conf +13 -0
- data/spec/spec_helper.rb +47 -0
- metadata +96 -0
data/README
ADDED
File without changes
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
begin
|
2
|
+
require 'jeweler'
|
3
|
+
Jeweler::Tasks.new do |gemspec|
|
4
|
+
gemspec.name = "ar-async-counter-cache"
|
5
|
+
gemspec.summary = "Increment ActiveRecord's counter cache column asynchronously (using Resque)."
|
6
|
+
gemspec.description = "Increment ActiveRecord's counter cache column asynchronously (using Resque)."
|
7
|
+
gemspec.email = "aaron.gibralter@gmail.com"
|
8
|
+
gemspec.homepage = "http://github.com/agibralter/ar-async-counter-cache"
|
9
|
+
gemspec.authors = ["Aaron Gibralter"]
|
10
|
+
gemspec.add_dependency("activerecord", ">= 2.3.5")
|
11
|
+
end
|
12
|
+
Jeweler::GemcutterTasks.new
|
13
|
+
rescue LoadError
|
14
|
+
puts "Jeweler not available. Install it with: gem install jeweler"
|
15
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
@@ -0,0 +1,57 @@
|
|
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-async-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-06-16}
|
13
|
+
s.description = %q{Increment ActiveRecord's counter cache column asynchronously (using Resque).}
|
14
|
+
s.email = %q{aaron.gibralter@gmail.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"README"
|
17
|
+
]
|
18
|
+
s.files = [
|
19
|
+
"README",
|
20
|
+
"Rakefile",
|
21
|
+
"VERSION",
|
22
|
+
"ar-async-counter-cache.gemspec",
|
23
|
+
"lib/ar-async-counter-cache.rb",
|
24
|
+
"lib/ar_async_counter_cache/active_record.rb",
|
25
|
+
"lib/ar_async_counter_cache/resque_job.rb",
|
26
|
+
"spec/ar_async_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-async-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).}
|
37
|
+
s.test_files = [
|
38
|
+
"spec/ar_async_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
|
+
else
|
51
|
+
s.add_dependency(%q<activerecord>, [">= 2.3.5"])
|
52
|
+
end
|
53
|
+
else
|
54
|
+
s.add_dependency(%q<activerecord>, [">= 2.3.5"])
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module ArAsyncCounterCache
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
|
5
|
+
def async_increment_counters
|
6
|
+
raise %{
|
7
|
+
Since you don't have resque installed, please define #{self.class.to_s}#async_increment_counters.
|
8
|
+
Basically it should queue a job that calls #{self.class.to_s}#update_async_counters(:increment).
|
9
|
+
}.gsub(/\s+/, ' ').strip
|
10
|
+
end
|
11
|
+
|
12
|
+
def async_decrement_counters
|
13
|
+
raise %{
|
14
|
+
Since you don't have resque installed, please define #{self.class.to_s}#async_decrement_counters.
|
15
|
+
Basically it should queue a job that calls #{self.class.to_s}#update_async_counters(:decrement).
|
16
|
+
}.gsub(/\s+/, ' ').strip
|
17
|
+
end
|
18
|
+
|
19
|
+
def update_async_counters(dir, *parent_types)
|
20
|
+
(parent_types.empty? ? self.class.async_counter_types.keys : parent_types).each do |parent_type|
|
21
|
+
if (col = self.class.async_counter_types[parent_type]) && (parent = send(parent_type))
|
22
|
+
parent.class.send("#{dir}_counter", col, parent.id)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
module ClassMethods
|
28
|
+
|
29
|
+
def belongs_to(association_id, options={})
|
30
|
+
if col = async_counter_cache_column(options.delete(:async_counter_cache))
|
31
|
+
# Add callbacks only once: update_async_counters updates all the
|
32
|
+
# model's belongs_to counter caches; we do this because there's no
|
33
|
+
# need to add excessive messages on the async queue.
|
34
|
+
add_callbacks if self.async_counter_types.empty?
|
35
|
+
# Store async counter so that update_async_counters knows which
|
36
|
+
# counters to update.
|
37
|
+
self.async_counter_types[association_id] = col
|
38
|
+
# Let's make the column read-only like the normal belongs_to
|
39
|
+
# counter_cache.
|
40
|
+
parrent_class = association_id.to_s.classify.constantize
|
41
|
+
parrent_class.send(:attr_readonly, col.to_sym) if defined?(parrent_class) && parrent_class.respond_to?(:attr_readonly)
|
42
|
+
raise "Please don't use both async_counter_cache and counter_cache." if options[:counter_cache]
|
43
|
+
end
|
44
|
+
super(association_id, options)
|
45
|
+
end
|
46
|
+
|
47
|
+
def async_counter_types
|
48
|
+
@async_counter_types ||= {}
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def add_callbacks
|
54
|
+
# Define after_create callback method.
|
55
|
+
method_name = "belongs_to_async_counter_cache_after_create".to_sym
|
56
|
+
if defined?(ArAsyncCounterCache::UpdateCountersJob)
|
57
|
+
define_method(method_name) do
|
58
|
+
Resque.enqueue(ArAsyncCounterCache::UpdateCountersJob, self.class.to_s, self.id, :increment)
|
59
|
+
end
|
60
|
+
else
|
61
|
+
define_method(method_name) do
|
62
|
+
self.async_increment_counters
|
63
|
+
end
|
64
|
+
end
|
65
|
+
after_create(method_name)
|
66
|
+
# Define before_destroy callback method.
|
67
|
+
method_name = "belongs_to_async_counter_cache_before_destroy".to_sym
|
68
|
+
if defined?(ArAsyncCounterCache::UpdateCountersJob)
|
69
|
+
define_method(method_name) do
|
70
|
+
Resque.enqueue(ArAsyncCounterCache::UpdateCountersJob, self.class.to_s, self.id, :decrement)
|
71
|
+
end
|
72
|
+
else
|
73
|
+
define_method(method_name) do
|
74
|
+
self.async_decrement_counters
|
75
|
+
end
|
76
|
+
end
|
77
|
+
before_destroy(method_name)
|
78
|
+
end
|
79
|
+
|
80
|
+
def async_counter_cache_column(opt)
|
81
|
+
opt === true ? "#{self.table_name}_count" : opt
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.included(base)
|
86
|
+
base.extend(ClassMethods)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module ArAsyncCounterCache
|
2
|
+
|
3
|
+
def self.resque_job_queue=(sym)
|
4
|
+
ArAsyncCounterCache::UpdateCountersJob.class_eval do
|
5
|
+
@queue = sym
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class UpdateCountersJob
|
10
|
+
@queue = :default
|
11
|
+
|
12
|
+
# Take advantage of resque-retry if possible.
|
13
|
+
begin
|
14
|
+
require 'resque-retry'
|
15
|
+
extend Resque::Plugins::ExponentialBackoff
|
16
|
+
@retry_exceptions = [ActiveRecord::RecordNotFound]
|
17
|
+
@backoff_strategy = [0, 10, 100]
|
18
|
+
rescue LoadError
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.perform(klass_name, id, dir)
|
22
|
+
Object.const_get(klass_name).find(id).update_async_counters(dir)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ArAsyncCounterCache::ActiveRecord do
|
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
|
+
context "callbacks" do
|
13
|
+
|
14
|
+
before(:each) do
|
15
|
+
@user = User.create(:name => "Susan")
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should queue job" do
|
19
|
+
Resque.should_receive(:enqueue).with(ArAsyncCounterCache::UpdateCountersJob, "Post", an_instance_of(Fixnum), :increment)
|
20
|
+
@user.posts.create(:body => "I have a cat!")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,72 @@
|
|
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
|
+
# Models...
|
9
|
+
@user1 = User.create(:name => "Susan")
|
10
|
+
@user2 = User.create(:name => "Bob")
|
11
|
+
@post1 = @user1.posts.create(:body => "I have a cat!")
|
12
|
+
@post2 = @user1.posts.create(:body => "I have a mouse!")
|
13
|
+
@comment1 = @post1.comments.create(:body => "Your cat sucks!", :user => @user2)
|
14
|
+
@comment2 = @post1.comments.create(:body => "No it doesn't!", :user => @user1)
|
15
|
+
@comment3 = @post2.comments.create(:body => "Your mouse also sucks!", :user => @user2)
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should set countercache asynchronously" do
|
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
|
+
end
|
26
|
+
|
27
|
+
it "should perform one at a time" do
|
28
|
+
perform_next_job
|
29
|
+
@user1.reload.posts_count.should == 1
|
30
|
+
@user1.reload.comments_count.should == 0
|
31
|
+
@user2.reload.posts_count.should == 0
|
32
|
+
@user2.reload.comments_count.should == 0
|
33
|
+
@post1.reload.count_of_comments.should == 0
|
34
|
+
@post2.reload.count_of_comments.should == 0
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should update all counters once the jobs are done" do
|
38
|
+
perform_all_jobs
|
39
|
+
@user1.reload.posts_count.should == 2
|
40
|
+
@user1.reload.comments_count.should == 1
|
41
|
+
@user2.reload.posts_count.should == 0
|
42
|
+
@user2.reload.comments_count.should == 2
|
43
|
+
@post1.reload.count_of_comments.should == 2
|
44
|
+
@post2.reload.count_of_comments.should == 1
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should decrement too" do
|
48
|
+
@comment1.destroy
|
49
|
+
@comment2.destroy
|
50
|
+
@comment3.destroy
|
51
|
+
perform_all_jobs
|
52
|
+
@user1.reload.posts_count.should == 2
|
53
|
+
@user1.reload.comments_count.should == 0
|
54
|
+
@user2.reload.posts_count.should == 0
|
55
|
+
@user2.reload.comments_count.should == 0
|
56
|
+
@post1.reload.count_of_comments.should == 0
|
57
|
+
@post2.reload.count_of_comments.should == 0
|
58
|
+
end
|
59
|
+
|
60
|
+
def perform_next_job
|
61
|
+
return unless job = @worker.reserve
|
62
|
+
@worker.perform(job)
|
63
|
+
@worker.done_working
|
64
|
+
end
|
65
|
+
|
66
|
+
def perform_all_jobs
|
67
|
+
while job = @worker.reserve
|
68
|
+
@worker.perform(job)
|
69
|
+
@worker.done_working
|
70
|
+
end
|
71
|
+
end
|
72
|
+
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,47 @@
|
|
1
|
+
spec_dir = File.expand_path(File.dirname(__FILE__))
|
2
|
+
|
3
|
+
$: << File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
|
4
|
+
$: << spec_dir
|
5
|
+
|
6
|
+
require 'rubygems'
|
7
|
+
# Ensure resque for tests.
|
8
|
+
require 'resque'
|
9
|
+
require 'ar-async-counter-cache'
|
10
|
+
|
11
|
+
require 'spec'
|
12
|
+
require 'models'
|
13
|
+
|
14
|
+
cwd = Dir.getwd
|
15
|
+
Dir.chdir(spec_dir)
|
16
|
+
|
17
|
+
if !system("which redis-server")
|
18
|
+
puts '', "** can't find `redis-server` in your path"
|
19
|
+
abort ''
|
20
|
+
end
|
21
|
+
|
22
|
+
at_exit do
|
23
|
+
if (pid = `cat redis-test.pid`.strip) =~ /^\d+$/
|
24
|
+
puts "Killing test redis server with pid #{pid}..."
|
25
|
+
`rm -f dump.rdb`
|
26
|
+
`rm -f redis-test.pid`
|
27
|
+
Process.kill("KILL", pid.to_i)
|
28
|
+
Dir.chdir(cwd)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
puts "Starting redis for testing at localhost:9736..."
|
33
|
+
`redis-server #{spec_dir}/redis-test.conf`
|
34
|
+
Resque.redis = '127.0.0.1:9736'
|
35
|
+
|
36
|
+
Spec::Runner.configure do |config|
|
37
|
+
config.before(:all) do
|
38
|
+
ArAsyncCounterCache.resque_job_queue = :testing
|
39
|
+
end
|
40
|
+
config.before(:each) do
|
41
|
+
ActiveRecord::Base.silence { CreateModelsForTest.migrate(:up) }
|
42
|
+
Resque.redis.flushall
|
43
|
+
end
|
44
|
+
config.after(:each) do
|
45
|
+
ActiveRecord::Base.silence { CreateModelsForTest.migrate(:down) }
|
46
|
+
end
|
47
|
+
end
|
metadata
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ar-async-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-06-16 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
|
+
description: Increment ActiveRecord's counter cache column asynchronously (using Resque).
|
38
|
+
email: aaron.gibralter@gmail.com
|
39
|
+
executables: []
|
40
|
+
|
41
|
+
extensions: []
|
42
|
+
|
43
|
+
extra_rdoc_files:
|
44
|
+
- README
|
45
|
+
files:
|
46
|
+
- README
|
47
|
+
- Rakefile
|
48
|
+
- VERSION
|
49
|
+
- ar-async-counter-cache.gemspec
|
50
|
+
- lib/ar-async-counter-cache.rb
|
51
|
+
- lib/ar_async_counter_cache/active_record.rb
|
52
|
+
- lib/ar_async_counter_cache/resque_job.rb
|
53
|
+
- spec/ar_async_counter_cache/active_record_spec.rb
|
54
|
+
- spec/integration_spec.rb
|
55
|
+
- spec/models.rb
|
56
|
+
- spec/redis-test.conf
|
57
|
+
- spec/spec_helper.rb
|
58
|
+
has_rdoc: true
|
59
|
+
homepage: http://github.com/agibralter/ar-async-counter-cache
|
60
|
+
licenses: []
|
61
|
+
|
62
|
+
post_install_message:
|
63
|
+
rdoc_options:
|
64
|
+
- --charset=UTF-8
|
65
|
+
require_paths:
|
66
|
+
- lib
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
68
|
+
none: false
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
hash: 3
|
73
|
+
segments:
|
74
|
+
- 0
|
75
|
+
version: "0"
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
77
|
+
none: false
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
hash: 3
|
82
|
+
segments:
|
83
|
+
- 0
|
84
|
+
version: "0"
|
85
|
+
requirements: []
|
86
|
+
|
87
|
+
rubyforge_project:
|
88
|
+
rubygems_version: 1.3.7
|
89
|
+
signing_key:
|
90
|
+
specification_version: 3
|
91
|
+
summary: Increment ActiveRecord's counter cache column asynchronously (using Resque).
|
92
|
+
test_files:
|
93
|
+
- spec/ar_async_counter_cache/active_record_spec.rb
|
94
|
+
- spec/integration_spec.rb
|
95
|
+
- spec/models.rb
|
96
|
+
- spec/spec_helper.rb
|