ar-async-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 +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
|