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 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 (using Resque)."
6
- gemspec.description = "Increment ActiveRecord's counter cache column asynchronously (using Resque)."
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", ">= 2.3.5")
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.0.3
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.0.3"
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-17}
13
- s.description = %q{Increment ActiveRecord's counter cache column asynchronously (using Resque).}
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/increment_counters_job.rb",
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.6}
38
- s.summary = %q{Increment ActiveRecord's counter cache column asynchronously (using Resque).}
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::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
51
- s.add_runtime_dependency(%q<activerecord>, [">= 2.3.5"])
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>, [">= 2.3.5"])
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>, [">= 2.3.5"])
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 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 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
- if col = async_counter_cache_column(options.delete(:async_counter_cache))
24
- # Add callbacks only once: update_async_counters updates all the
25
- # model's belongs_to counter caches; we do this because there's no
26
- # need to add excessive messages on the async queue.
27
- add_callbacks if self.async_counter_types.empty?
28
- # Store async counter so that update_async_counters knows which
29
- # counters to update.
30
- self.async_counter_types[association_id] = col
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
- parrent_class = association_id.to_s.classify.constantize
34
- parrent_class.send(:attr_readonly, col.to_sym) if defined?(parrent_class) && parrent_class.respond_to?(:attr_readonly)
35
- raise "Please don't use both async_counter_cache and counter_cache." if options[:counter_cache]
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 = "belongs_to_async_counter_cache_after_create".to_sym
49
- if defined?(ArAsyncCounterCache::IncrementCountersJob)
50
- define_method(method_name) do
51
- Resque.enqueue(ArAsyncCounterCache::IncrementCountersJob, self.class.to_s, self.id)
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 = "belongs_to_async_counter_cache_before_destroy".to_sym
54
+ method_name = "#{base_method_name}_before_destroy".to_sym
61
55
  define_method(method_name) do
62
- update_async_counters(:decrement)
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
- before(:each) do
15
- @user = User.create(:name => "Susan")
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 queue job" do
19
- Resque.should_receive(:enqueue).with(ArAsyncCounterCache::IncrementCountersJob, "Post", an_instance_of(Fixnum))
20
- @user.posts.create(:body => "I have a cat!")
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
@@ -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 set countercache asynchronously" do
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
- it "should perform one at a time" do
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 == 1
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
- it "should update all counters once the jobs are done" do
38
- perform_all_jobs
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
- it "should decrement synchronously" do
48
- perform_all_jobs
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
- - 0
8
- - 3
9
- version: 0.0.3
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-17 00:00:00 -04:00
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
- description: Increment ActiveRecord's counter cache column asynchronously (using Resque).
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/increment_counters_job.rb
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.6
121
+ rubygems_version: 1.3.7
84
122
  signing_key:
85
123
  specification_version: 3
86
- summary: Increment ActiveRecord's counter cache column asynchronously (using Resque).
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