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 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