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 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,10 @@
1
+ begin
2
+ require 'resque'
3
+ require 'ar_async_counter_cache/resque_job'
4
+ rescue LoadError
5
+ end
6
+
7
+ require 'active_record'
8
+ require 'ar_async_counter_cache/active_record'
9
+
10
+ ActiveRecord::Base.send(:include, ArAsyncCounterCache::ActiveRecord)
@@ -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
@@ -0,0 +1,13 @@
1
+ daemonize yes
2
+ pidfile ./redis-test.pid
3
+ port 9736
4
+ timeout 300
5
+ save 900 1
6
+ save 300 10
7
+ save 60 10000
8
+ dbfilename ./dump.rdb
9
+ dir .
10
+ loglevel debug
11
+ logfile stdout
12
+ databases 16
13
+ glueoutputbuf yes
@@ -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