Skip to content
This repository was archived by the owner on Dec 1, 2023. It is now read-only.

Repository files navigation

Counter::Cache

Gem VersionBuild StatusCode Climate

Counting things is hard, counting them at scale is even harder, so control when things are counted.

Any time your application performs pagination, the underlying library probably issues a select count(*) from ... request to the database, because all paginators need to know how many pages there are. This works on a small-to-medium dataset, and in an application with relatively low web traffic. But at high traffic volume, live counts saturate CPU on the database server. This is because sorting typically happens on CPU of the database server, using a small amount of heap RAM or even worse — using temp files, which grinds the disk IO to a hault. Web requests become slower and slower, start to pile up in various queues, and eventually saturate all of the app servers. There you are, the site is down.

This gem provides a solution that works at scale, and will help you keep your site up.

This library is battle-tested at Wanelo, where it has been running for several years.

Overview

Rails Counter Caches are a convenient way to keep counters on models that have many children. Without them, you always do live counts, which do not scale. But at high scale, Rails counter caches create update contention on single models, especially for social sites where any single model might become extremely popular. Many web requests trying to update the same row creates database deadlocks and kills performance due to locking and an uncontrollable increase in iops.

This library provides all the benefits of rails counter cache, without the penalty of the contention on updates, by serializing, buffering, and delaying updates via a queue. Counts becoming slightly less realtime, but with a guarantee that single models will never be updated more than once in certain time periods.

Counter Cache Flow

By default, a Buffer Counter is used which implements two modes of counting. The two modes are deferred and recalculation.

IMPORTANT: If Sidekiq is to be used as the delayed job framework, using sidekiq-unique-jobs is essential: https://github.com/mhenrixon/sidekiq-unique-jobs

Mode: Deferred

Initial mode that is used to provide roughly realtime counters.

This mode is meant to provide very reasonably up to date counters using values buffered into Redis, without asking the database for the count at all. An example of how this works is described:

Scenario: User has many posts. We want to keep track of the number of posts on the user model (posts_count column).

When a post is created:

  1. Increment a key in Redis that corresponds to the field and user that relates to the post.
  2. Enqueue a delayed job that will later reconcile the counter column based on the key in redis.
  3. When the job runs, it picks up the value from redis (which can be zero or more) and adds the value to user.posts_count column on the associated model.
user=User.find_by_id(100)user.posts_count# 10user.posts.create(...)# => Job is enqueueduser.posts.create(...)# => Job is already enqueued# come back later (after a delay)user=User.find_by_id(100)user.posts_count# 12

Mode: Recalculation

Runs later and ensures values are completely up to date.

This mode is used to compensate for transient errors that may cause the deferred counters to drift from the actual values. The exact reasons this happens are undefined, redis could hang, go away, the universe could skip ahead in time, who knows.

Using the same scenario as above:

Scenario: User has many posts. We want to keep track of the number of posts on the user model (posts_count column).

  1. Enqueue a job that is delayed by many hours (customizable)
  2. When the job runs, run a full count query to find the true count from the database and save the value to the database.
user=User.find_by_id(100)user.posts_count# 10user.posts.create(...)user.posts.create(...)# redis crashes, world explodes, etc.. we miss on deferred update.user=User.find_by_id(100)user.posts_count# 11, due to only one deferred update having run.# come back later in a couple hoursuser=User.find_by_id(100)user.posts_count# 12

Installation

Add this line to your application's Gemfile:

gem 'counter-cache'

And then execute:

$ bundle

Or install it yourself as:

$ gem install counter-cache

Usage

Counter caches are configured on the models from the perspective of the child model to the parent that contains the counter.

Basic Counter with recalculation:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count# This is a method on the user.end

To allow a timestamp to be updated:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.touch_column: :posts_updated_atend

To increment dynamically:

classReviewincludeCounter::Cachecounter_cache_oncolumn: :reviews_sum,relation: :product,relation_class_name: "Product",increment_by: ->(review){review.score},method: :recalculate_reviews_sum,# This is a method on the product.end

To control when recalculation happens:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.recalculation: true|false,# whether to ever recalculate this counter.recalculation_delay: 10.seconds# Only a hard value that defines when to perform a full recalculation.end

To control when the deferred job runs:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.wait: 10.seconds# This can be a hard valuecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.wait: ->(user){user.posts_count * 10}# .. or a proc, in this case, the more posts a user has, the less frequently it will be updated.end

To control if an update should even happen:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.if: ->(post){post.public? ? false : true}end

Polymorphism (because YAY)

Setting polymorphic: true, will ask ActiveRecord what the class is (User, Store), based on followee_type, and update the appropriate model. So if a user is followed, then that users followers_count will increment.

classUserattr_accessible:followers_countendclassStoreattr_accessible:followers_countendclassFollowattr_accessible:user_id,:followee_id,:followee_typebelongs_to:followee,polymorphic: trueincludeCounter::Cachecounter_cache_oncolumn: :followers_count,relation: :followee,polymorphic: trueend

Configuration

In an initializer such as config/initializers/counter_cache.rb, write the configuration as:

Counter::Cache.configuredo |c|
c.default_worker_adapter=MyCustomWorkAdapterc.recalculation_delay=6.hours# Default delay for recalculationsc.redis_pool=Redis.newc.counting_data_store=MyCustomDataStore# Default is Counter::Cache::Redisend

default_worker_adapter

The worker adapter allows you to control how jobs are delayed/enqueued for later execution. Three options are passed:

  • delay: This is the delay in seconds that the execution should be delayed. Can be ignored or adjusted. We pass this to sidekiq.
  • base_class: This is the class name of the source object.
  • options: This will be a hash of options that should be passed to the instance of the counter.

An example of a dummy adapter is like so:

classTestWorkerAdapterdefenqueue(delay,base_class,options)options[:source_object_class_name]=base_class.constantizecounter_class=options[:counter].constantize# options[:counter] is the class name of the counter that called the adapter.counter=counter_class.new(nil,options)counter.save!endend

An example of a dummy adapter that uses Sidekiq is like so:

classCounterWorkerincludeSidekiq::Workerdefperform(base_class,options)options.symbolize_keys!# From ActiveSupport, Sidekiq looses symbol information from hashes.options[:source_object_class_name]=base_class.constantizecounter_class=options[:counter].constantize# options[:counter] is the class name of the counter that called the adapter.counter=counter_class.new(nil,options)counter.save!enddefself.enqueue(delay,base_class,options)perform_in(delay,base_class,options)endend

recalculation_delay

This should be set to the default delay for recalculations, in seconds.

redis_pool

This can either be a single redis connection or a ConnectionPool instance (https://github.com/mperham/connection_pool).

counting_data_store

This defaults to Counter::Cache::Redis but can be set to anything. The Redis store describes what the API would be.

Contributing

  1. Fork it ( https://github.com/[my-github-username]/counter-cache/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Running specs:

Appraisal is used to test against multiple versions of activerecord. 3.2, 4.0, and 4.1 are currently supported.

To install dependencies:

$ bundle exec appraisal install

To run specs across versions:

$ bundle exec appraisal rspec

About

Counting at scale is hard. Let's do it good.

Resources

Stars

144 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - wanelo/counter-cache: Counting at scale is hard. Let's do it good. · GitHub
Skip to content
This repository was archived by the owner on Dec 1, 2023. It is now read-only.

Repository files navigation

Counter::Cache

Gem VersionBuild StatusCode Climate

Counting things is hard, counting them at scale is even harder, so control when things are counted.

Any time your application performs pagination, the underlying library probably issues a select count(*) from ... request to the database, because all paginators need to know how many pages there are. This works on a small-to-medium dataset, and in an application with relatively low web traffic. But at high traffic volume, live counts saturate CPU on the database server. This is because sorting typically happens on CPU of the database server, using a small amount of heap RAM or even worse — using temp files, which grinds the disk IO to a hault. Web requests become slower and slower, start to pile up in various queues, and eventually saturate all of the app servers. There you are, the site is down.

This gem provides a solution that works at scale, and will help you keep your site up.

This library is battle-tested at Wanelo, where it has been running for several years.

Overview

Rails Counter Caches are a convenient way to keep counters on models that have many children. Without them, you always do live counts, which do not scale. But at high scale, Rails counter caches create update contention on single models, especially for social sites where any single model might become extremely popular. Many web requests trying to update the same row creates database deadlocks and kills performance due to locking and an uncontrollable increase in iops.

This library provides all the benefits of rails counter cache, without the penalty of the contention on updates, by serializing, buffering, and delaying updates via a queue. Counts becoming slightly less realtime, but with a guarantee that single models will never be updated more than once in certain time periods.

Counter Cache Flow

By default, a Buffer Counter is used which implements two modes of counting. The two modes are deferred and recalculation.

IMPORTANT: If Sidekiq is to be used as the delayed job framework, using sidekiq-unique-jobs is essential: https://github.com/mhenrixon/sidekiq-unique-jobs

Mode: Deferred

Initial mode that is used to provide roughly realtime counters.

This mode is meant to provide very reasonably up to date counters using values buffered into Redis, without asking the database for the count at all. An example of how this works is described:

Scenario: User has many posts. We want to keep track of the number of posts on the user model (posts_count column).

When a post is created:

  1. Increment a key in Redis that corresponds to the field and user that relates to the post.
  2. Enqueue a delayed job that will later reconcile the counter column based on the key in redis.
  3. When the job runs, it picks up the value from redis (which can be zero or more) and adds the value to user.posts_count column on the associated model.
user=User.find_by_id(100)user.posts_count# 10user.posts.create(...)# => Job is enqueueduser.posts.create(...)# => Job is already enqueued# come back later (after a delay)user=User.find_by_id(100)user.posts_count# 12

Mode: Recalculation

Runs later and ensures values are completely up to date.

This mode is used to compensate for transient errors that may cause the deferred counters to drift from the actual values. The exact reasons this happens are undefined, redis could hang, go away, the universe could skip ahead in time, who knows.

Using the same scenario as above:

Scenario: User has many posts. We want to keep track of the number of posts on the user model (posts_count column).

  1. Enqueue a job that is delayed by many hours (customizable)
  2. When the job runs, run a full count query to find the true count from the database and save the value to the database.
user=User.find_by_id(100)user.posts_count# 10user.posts.create(...)user.posts.create(...)# redis crashes, world explodes, etc.. we miss on deferred update.user=User.find_by_id(100)user.posts_count# 11, due to only one deferred update having run.# come back later in a couple hoursuser=User.find_by_id(100)user.posts_count# 12

Installation

Add this line to your application's Gemfile:

gem 'counter-cache'

And then execute:

$ bundle

Or install it yourself as:

$ gem install counter-cache

Usage

Counter caches are configured on the models from the perspective of the child model to the parent that contains the counter.

Basic Counter with recalculation:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count# This is a method on the user.end

To allow a timestamp to be updated:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.touch_column: :posts_updated_atend

To increment dynamically:

classReviewincludeCounter::Cachecounter_cache_oncolumn: :reviews_sum,relation: :product,relation_class_name: "Product",increment_by: ->(review){review.score},method: :recalculate_reviews_sum,# This is a method on the product.end

To control when recalculation happens:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.recalculation: true|false,# whether to ever recalculate this counter.recalculation_delay: 10.seconds# Only a hard value that defines when to perform a full recalculation.end

To control when the deferred job runs:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.wait: 10.seconds# This can be a hard valuecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.wait: ->(user){user.posts_count * 10}# .. or a proc, in this case, the more posts a user has, the less frequently it will be updated.end

To control if an update should even happen:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.if: ->(post){post.public? ? false : true}end

Polymorphism (because YAY)

Setting polymorphic: true, will ask ActiveRecord what the class is (User, Store), based on followee_type, and update the appropriate model. So if a user is followed, then that users followers_count will increment.

classUserattr_accessible:followers_countendclassStoreattr_accessible:followers_countendclassFollowattr_accessible:user_id,:followee_id,:followee_typebelongs_to:followee,polymorphic: trueincludeCounter::Cachecounter_cache_oncolumn: :followers_count,relation: :followee,polymorphic: trueend

Configuration

In an initializer such as config/initializers/counter_cache.rb, write the configuration as:

Counter::Cache.configuredo |c|
c.default_worker_adapter=MyCustomWorkAdapterc.recalculation_delay=6.hours# Default delay for recalculationsc.redis_pool=Redis.newc.counting_data_store=MyCustomDataStore# Default is Counter::Cache::Redisend

default_worker_adapter

The worker adapter allows you to control how jobs are delayed/enqueued for later execution. Three options are passed:

  • delay: This is the delay in seconds that the execution should be delayed. Can be ignored or adjusted. We pass this to sidekiq.
  • base_class: This is the class name of the source object.
  • options: This will be a hash of options that should be passed to the instance of the counter.

An example of a dummy adapter is like so:

classTestWorkerAdapterdefenqueue(delay,base_class,options)options[:source_object_class_name]=base_class.constantizecounter_class=options[:counter].constantize# options[:counter] is the class name of the counter that called the adapter.counter=counter_class.new(nil,options)counter.save!endend

An example of a dummy adapter that uses Sidekiq is like so:

classCounterWorkerincludeSidekiq::Workerdefperform(base_class,options)options.symbolize_keys!# From ActiveSupport, Sidekiq looses symbol information from hashes.options[:source_object_class_name]=base_class.constantizecounter_class=options[:counter].constantize# options[:counter] is the class name of the counter that called the adapter.counter=counter_class.new(nil,options)counter.save!enddefself.enqueue(delay,base_class,options)perform_in(delay,base_class,options)endend

recalculation_delay

This should be set to the default delay for recalculations, in seconds.

redis_pool

This can either be a single redis connection or a ConnectionPool instance (https://github.com/mperham/connection_pool).

counting_data_store

This defaults to Counter::Cache::Redis but can be set to anything. The Redis store describes what the API would be.

Contributing

  1. Fork it ( https://github.com/[my-github-username]/counter-cache/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Running specs:

Appraisal is used to test against multiple versions of activerecord. 3.2, 4.0, and 4.1 are currently supported.

To install dependencies:

$ bundle exec appraisal install

To run specs across versions:

$ bundle exec appraisal rspec

About

Counting at scale is hard. Let's do it good.

Resources

Stars

144 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - wanelo/counter-cache: Counting at scale is hard. Let's do it good. · GitHub
Skip to content
This repository was archived by the owner on Dec 1, 2023. It is now read-only.

Repository files navigation

Counter::Cache

Gem VersionBuild StatusCode Climate

Counting things is hard, counting them at scale is even harder, so control when things are counted.

Any time your application performs pagination, the underlying library probably issues a select count(*) from ... request to the database, because all paginators need to know how many pages there are. This works on a small-to-medium dataset, and in an application with relatively low web traffic. But at high traffic volume, live counts saturate CPU on the database server. This is because sorting typically happens on CPU of the database server, using a small amount of heap RAM or even worse — using temp files, which grinds the disk IO to a hault. Web requests become slower and slower, start to pile up in various queues, and eventually saturate all of the app servers. There you are, the site is down.

This gem provides a solution that works at scale, and will help you keep your site up.

This library is battle-tested at Wanelo, where it has been running for several years.

Overview

Rails Counter Caches are a convenient way to keep counters on models that have many children. Without them, you always do live counts, which do not scale. But at high scale, Rails counter caches create update contention on single models, especially for social sites where any single model might become extremely popular. Many web requests trying to update the same row creates database deadlocks and kills performance due to locking and an uncontrollable increase in iops.

This library provides all the benefits of rails counter cache, without the penalty of the contention on updates, by serializing, buffering, and delaying updates via a queue. Counts becoming slightly less realtime, but with a guarantee that single models will never be updated more than once in certain time periods.

Counter Cache Flow

By default, a Buffer Counter is used which implements two modes of counting. The two modes are deferred and recalculation.

IMPORTANT: If Sidekiq is to be used as the delayed job framework, using sidekiq-unique-jobs is essential: https://github.com/mhenrixon/sidekiq-unique-jobs

Mode: Deferred

Initial mode that is used to provide roughly realtime counters.

This mode is meant to provide very reasonably up to date counters using values buffered into Redis, without asking the database for the count at all. An example of how this works is described:

Scenario: User has many posts. We want to keep track of the number of posts on the user model (posts_count column).

When a post is created:

  1. Increment a key in Redis that corresponds to the field and user that relates to the post.
  2. Enqueue a delayed job that will later reconcile the counter column based on the key in redis.
  3. When the job runs, it picks up the value from redis (which can be zero or more) and adds the value to user.posts_count column on the associated model.
user=User.find_by_id(100)user.posts_count# 10user.posts.create(...)# => Job is enqueueduser.posts.create(...)# => Job is already enqueued# come back later (after a delay)user=User.find_by_id(100)user.posts_count# 12

Mode: Recalculation

Runs later and ensures values are completely up to date.

This mode is used to compensate for transient errors that may cause the deferred counters to drift from the actual values. The exact reasons this happens are undefined, redis could hang, go away, the universe could skip ahead in time, who knows.

Using the same scenario as above:

Scenario: User has many posts. We want to keep track of the number of posts on the user model (posts_count column).

  1. Enqueue a job that is delayed by many hours (customizable)
  2. When the job runs, run a full count query to find the true count from the database and save the value to the database.
user=User.find_by_id(100)user.posts_count# 10user.posts.create(...)user.posts.create(...)# redis crashes, world explodes, etc.. we miss on deferred update.user=User.find_by_id(100)user.posts_count# 11, due to only one deferred update having run.# come back later in a couple hoursuser=User.find_by_id(100)user.posts_count# 12

Installation

Add this line to your application's Gemfile:

gem 'counter-cache'

And then execute:

$ bundle

Or install it yourself as:

$ gem install counter-cache

Usage

Counter caches are configured on the models from the perspective of the child model to the parent that contains the counter.

Basic Counter with recalculation:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count# This is a method on the user.end

To allow a timestamp to be updated:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.touch_column: :posts_updated_atend

To increment dynamically:

classReviewincludeCounter::Cachecounter_cache_oncolumn: :reviews_sum,relation: :product,relation_class_name: "Product",increment_by: ->(review){review.score},method: :recalculate_reviews_sum,# This is a method on the product.end

To control when recalculation happens:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.recalculation: true|false,# whether to ever recalculate this counter.recalculation_delay: 10.seconds# Only a hard value that defines when to perform a full recalculation.end

To control when the deferred job runs:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.wait: 10.seconds# This can be a hard valuecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.wait: ->(user){user.posts_count * 10}# .. or a proc, in this case, the more posts a user has, the less frequently it will be updated.end

To control if an update should even happen:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.if: ->(post){post.public? ? false : true}end

Polymorphism (because YAY)

Setting polymorphic: true, will ask ActiveRecord what the class is (User, Store), based on followee_type, and update the appropriate model. So if a user is followed, then that users followers_count will increment.

classUserattr_accessible:followers_countendclassStoreattr_accessible:followers_countendclassFollowattr_accessible:user_id,:followee_id,:followee_typebelongs_to:followee,polymorphic: trueincludeCounter::Cachecounter_cache_oncolumn: :followers_count,relation: :followee,polymorphic: trueend

Configuration

In an initializer such as config/initializers/counter_cache.rb, write the configuration as:

Counter::Cache.configuredo |c|
c.default_worker_adapter=MyCustomWorkAdapterc.recalculation_delay=6.hours# Default delay for recalculationsc.redis_pool=Redis.newc.counting_data_store=MyCustomDataStore# Default is Counter::Cache::Redisend

default_worker_adapter

The worker adapter allows you to control how jobs are delayed/enqueued for later execution. Three options are passed:

  • delay: This is the delay in seconds that the execution should be delayed. Can be ignored or adjusted. We pass this to sidekiq.
  • base_class: This is the class name of the source object.
  • options: This will be a hash of options that should be passed to the instance of the counter.

An example of a dummy adapter is like so:

classTestWorkerAdapterdefenqueue(delay,base_class,options)options[:source_object_class_name]=base_class.constantizecounter_class=options[:counter].constantize# options[:counter] is the class name of the counter that called the adapter.counter=counter_class.new(nil,options)counter.save!endend

An example of a dummy adapter that uses Sidekiq is like so:

classCounterWorkerincludeSidekiq::Workerdefperform(base_class,options)options.symbolize_keys!# From ActiveSupport, Sidekiq looses symbol information from hashes.options[:source_object_class_name]=base_class.constantizecounter_class=options[:counter].constantize# options[:counter] is the class name of the counter that called the adapter.counter=counter_class.new(nil,options)counter.save!enddefself.enqueue(delay,base_class,options)perform_in(delay,base_class,options)endend

recalculation_delay

This should be set to the default delay for recalculations, in seconds.

redis_pool

This can either be a single redis connection or a ConnectionPool instance (https://github.com/mperham/connection_pool).

counting_data_store

This defaults to Counter::Cache::Redis but can be set to anything. The Redis store describes what the API would be.

Contributing

  1. Fork it ( https://github.com/[my-github-username]/counter-cache/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Running specs:

Appraisal is used to test against multiple versions of activerecord. 3.2, 4.0, and 4.1 are currently supported.

To install dependencies:

$ bundle exec appraisal install

To run specs across versions:

$ bundle exec appraisal rspec

About

Counting at scale is hard. Let's do it good.

Resources

Stars

144 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - wanelo/counter-cache: Counting at scale is hard. Let's do it good. · GitHub
Skip to content
This repository was archived by the owner on Dec 1, 2023. It is now read-only.

Repository files navigation

Counter::Cache

Gem VersionBuild StatusCode Climate

Counting things is hard, counting them at scale is even harder, so control when things are counted.

Any time your application performs pagination, the underlying library probably issues a select count(*) from ... request to the database, because all paginators need to know how many pages there are. This works on a small-to-medium dataset, and in an application with relatively low web traffic. But at high traffic volume, live counts saturate CPU on the database server. This is because sorting typically happens on CPU of the database server, using a small amount of heap RAM or even worse — using temp files, which grinds the disk IO to a hault. Web requests become slower and slower, start to pile up in various queues, and eventually saturate all of the app servers. There you are, the site is down.

This gem provides a solution that works at scale, and will help you keep your site up.

This library is battle-tested at Wanelo, where it has been running for several years.

Overview

Rails Counter Caches are a convenient way to keep counters on models that have many children. Without them, you always do live counts, which do not scale. But at high scale, Rails counter caches create update contention on single models, especially for social sites where any single model might become extremely popular. Many web requests trying to update the same row creates database deadlocks and kills performance due to locking and an uncontrollable increase in iops.

This library provides all the benefits of rails counter cache, without the penalty of the contention on updates, by serializing, buffering, and delaying updates via a queue. Counts becoming slightly less realtime, but with a guarantee that single models will never be updated more than once in certain time periods.

Counter Cache Flow

By default, a Buffer Counter is used which implements two modes of counting. The two modes are deferred and recalculation.

IMPORTANT: If Sidekiq is to be used as the delayed job framework, using sidekiq-unique-jobs is essential: https://github.com/mhenrixon/sidekiq-unique-jobs

Mode: Deferred

Initial mode that is used to provide roughly realtime counters.

This mode is meant to provide very reasonably up to date counters using values buffered into Redis, without asking the database for the count at all. An example of how this works is described:

Scenario: User has many posts. We want to keep track of the number of posts on the user model (posts_count column).

When a post is created:

  1. Increment a key in Redis that corresponds to the field and user that relates to the post.
  2. Enqueue a delayed job that will later reconcile the counter column based on the key in redis.
  3. When the job runs, it picks up the value from redis (which can be zero or more) and adds the value to user.posts_count column on the associated model.
user=User.find_by_id(100)user.posts_count# 10user.posts.create(...)# => Job is enqueueduser.posts.create(...)# => Job is already enqueued# come back later (after a delay)user=User.find_by_id(100)user.posts_count# 12

Mode: Recalculation

Runs later and ensures values are completely up to date.

This mode is used to compensate for transient errors that may cause the deferred counters to drift from the actual values. The exact reasons this happens are undefined, redis could hang, go away, the universe could skip ahead in time, who knows.

Using the same scenario as above:

Scenario: User has many posts. We want to keep track of the number of posts on the user model (posts_count column).

  1. Enqueue a job that is delayed by many hours (customizable)
  2. When the job runs, run a full count query to find the true count from the database and save the value to the database.
user=User.find_by_id(100)user.posts_count# 10user.posts.create(...)user.posts.create(...)# redis crashes, world explodes, etc.. we miss on deferred update.user=User.find_by_id(100)user.posts_count# 11, due to only one deferred update having run.# come back later in a couple hoursuser=User.find_by_id(100)user.posts_count# 12

Installation

Add this line to your application's Gemfile:

gem 'counter-cache'

And then execute:

$ bundle

Or install it yourself as:

$ gem install counter-cache

Usage

Counter caches are configured on the models from the perspective of the child model to the parent that contains the counter.

Basic Counter with recalculation:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count# This is a method on the user.end

To allow a timestamp to be updated:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.touch_column: :posts_updated_atend

To increment dynamically:

classReviewincludeCounter::Cachecounter_cache_oncolumn: :reviews_sum,relation: :product,relation_class_name: "Product",increment_by: ->(review){review.score},method: :recalculate_reviews_sum,# This is a method on the product.end

To control when recalculation happens:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.recalculation: true|false,# whether to ever recalculate this counter.recalculation_delay: 10.seconds# Only a hard value that defines when to perform a full recalculation.end

To control when the deferred job runs:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.wait: 10.seconds# This can be a hard valuecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.wait: ->(user){user.posts_count * 10}# .. or a proc, in this case, the more posts a user has, the less frequently it will be updated.end

To control if an update should even happen:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.if: ->(post){post.public? ? false : true}end

Polymorphism (because YAY)

Setting polymorphic: true, will ask ActiveRecord what the class is (User, Store), based on followee_type, and update the appropriate model. So if a user is followed, then that users followers_count will increment.

classUserattr_accessible:followers_countendclassStoreattr_accessible:followers_countendclassFollowattr_accessible:user_id,:followee_id,:followee_typebelongs_to:followee,polymorphic: trueincludeCounter::Cachecounter_cache_oncolumn: :followers_count,relation: :followee,polymorphic: trueend

Configuration

In an initializer such as config/initializers/counter_cache.rb, write the configuration as:

Counter::Cache.configuredo |c|
c.default_worker_adapter=MyCustomWorkAdapterc.recalculation_delay=6.hours# Default delay for recalculationsc.redis_pool=Redis.newc.counting_data_store=MyCustomDataStore# Default is Counter::Cache::Redisend

default_worker_adapter

The worker adapter allows you to control how jobs are delayed/enqueued for later execution. Three options are passed:

  • delay: This is the delay in seconds that the execution should be delayed. Can be ignored or adjusted. We pass this to sidekiq.
  • base_class: This is the class name of the source object.
  • options: This will be a hash of options that should be passed to the instance of the counter.

An example of a dummy adapter is like so:

classTestWorkerAdapterdefenqueue(delay,base_class,options)options[:source_object_class_name]=base_class.constantizecounter_class=options[:counter].constantize# options[:counter] is the class name of the counter that called the adapter.counter=counter_class.new(nil,options)counter.save!endend

An example of a dummy adapter that uses Sidekiq is like so:

classCounterWorkerincludeSidekiq::Workerdefperform(base_class,options)options.symbolize_keys!# From ActiveSupport, Sidekiq looses symbol information from hashes.options[:source_object_class_name]=base_class.constantizecounter_class=options[:counter].constantize# options[:counter] is the class name of the counter that called the adapter.counter=counter_class.new(nil,options)counter.save!enddefself.enqueue(delay,base_class,options)perform_in(delay,base_class,options)endend

recalculation_delay

This should be set to the default delay for recalculations, in seconds.

redis_pool

This can either be a single redis connection or a ConnectionPool instance (https://github.com/mperham/connection_pool).

counting_data_store

This defaults to Counter::Cache::Redis but can be set to anything. The Redis store describes what the API would be.

Contributing

  1. Fork it ( https://github.com/[my-github-username]/counter-cache/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Running specs:

Appraisal is used to test against multiple versions of activerecord. 3.2, 4.0, and 4.1 are currently supported.

To install dependencies:

$ bundle exec appraisal install

To run specs across versions:

$ bundle exec appraisal rspec

About

Counting at scale is hard. Let's do it good.

Resources

Stars

144 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - wanelo/counter-cache: Counting at scale is hard. Let's do it good. · GitHub
Skip to content
This repository was archived by the owner on Dec 1, 2023. It is now read-only.

Repository files navigation

Counter::Cache

Gem VersionBuild StatusCode Climate

Counting things is hard, counting them at scale is even harder, so control when things are counted.

Any time your application performs pagination, the underlying library probably issues a select count(*) from ... request to the database, because all paginators need to know how many pages there are. This works on a small-to-medium dataset, and in an application with relatively low web traffic. But at high traffic volume, live counts saturate CPU on the database server. This is because sorting typically happens on CPU of the database server, using a small amount of heap RAM or even worse — using temp files, which grinds the disk IO to a hault. Web requests become slower and slower, start to pile up in various queues, and eventually saturate all of the app servers. There you are, the site is down.

This gem provides a solution that works at scale, and will help you keep your site up.

This library is battle-tested at Wanelo, where it has been running for several years.

Overview

Rails Counter Caches are a convenient way to keep counters on models that have many children. Without them, you always do live counts, which do not scale. But at high scale, Rails counter caches create update contention on single models, especially for social sites where any single model might become extremely popular. Many web requests trying to update the same row creates database deadlocks and kills performance due to locking and an uncontrollable increase in iops.

This library provides all the benefits of rails counter cache, without the penalty of the contention on updates, by serializing, buffering, and delaying updates via a queue. Counts becoming slightly less realtime, but with a guarantee that single models will never be updated more than once in certain time periods.

Counter Cache Flow

By default, a Buffer Counter is used which implements two modes of counting. The two modes are deferred and recalculation.

IMPORTANT: If Sidekiq is to be used as the delayed job framework, using sidekiq-unique-jobs is essential: https://github.com/mhenrixon/sidekiq-unique-jobs

Mode: Deferred

Initial mode that is used to provide roughly realtime counters.

This mode is meant to provide very reasonably up to date counters using values buffered into Redis, without asking the database for the count at all. An example of how this works is described:

Scenario: User has many posts. We want to keep track of the number of posts on the user model (posts_count column).

When a post is created:

  1. Increment a key in Redis that corresponds to the field and user that relates to the post.
  2. Enqueue a delayed job that will later reconcile the counter column based on the key in redis.
  3. When the job runs, it picks up the value from redis (which can be zero or more) and adds the value to user.posts_count column on the associated model.
user=User.find_by_id(100)user.posts_count# 10user.posts.create(...)# => Job is enqueueduser.posts.create(...)# => Job is already enqueued# come back later (after a delay)user=User.find_by_id(100)user.posts_count# 12

Mode: Recalculation

Runs later and ensures values are completely up to date.

This mode is used to compensate for transient errors that may cause the deferred counters to drift from the actual values. The exact reasons this happens are undefined, redis could hang, go away, the universe could skip ahead in time, who knows.

Using the same scenario as above:

Scenario: User has many posts. We want to keep track of the number of posts on the user model (posts_count column).

  1. Enqueue a job that is delayed by many hours (customizable)
  2. When the job runs, run a full count query to find the true count from the database and save the value to the database.
user=User.find_by_id(100)user.posts_count# 10user.posts.create(...)user.posts.create(...)# redis crashes, world explodes, etc.. we miss on deferred update.user=User.find_by_id(100)user.posts_count# 11, due to only one deferred update having run.# come back later in a couple hoursuser=User.find_by_id(100)user.posts_count# 12

Installation

Add this line to your application's Gemfile:

gem 'counter-cache'

And then execute:

$ bundle

Or install it yourself as:

$ gem install counter-cache

Usage

Counter caches are configured on the models from the perspective of the child model to the parent that contains the counter.

Basic Counter with recalculation:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count# This is a method on the user.end

To allow a timestamp to be updated:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.touch_column: :posts_updated_atend

To increment dynamically:

classReviewincludeCounter::Cachecounter_cache_oncolumn: :reviews_sum,relation: :product,relation_class_name: "Product",increment_by: ->(review){review.score},method: :recalculate_reviews_sum,# This is a method on the product.end

To control when recalculation happens:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.recalculation: true|false,# whether to ever recalculate this counter.recalculation_delay: 10.seconds# Only a hard value that defines when to perform a full recalculation.end

To control when the deferred job runs:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.wait: 10.seconds# This can be a hard valuecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.wait: ->(user){user.posts_count * 10}# .. or a proc, in this case, the more posts a user has, the less frequently it will be updated.end

To control if an update should even happen:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.if: ->(post){post.public? ? false : true}end

Polymorphism (because YAY)

Setting polymorphic: true, will ask ActiveRecord what the class is (User, Store), based on followee_type, and update the appropriate model. So if a user is followed, then that users followers_count will increment.

classUserattr_accessible:followers_countendclassStoreattr_accessible:followers_countendclassFollowattr_accessible:user_id,:followee_id,:followee_typebelongs_to:followee,polymorphic: trueincludeCounter::Cachecounter_cache_oncolumn: :followers_count,relation: :followee,polymorphic: trueend

Configuration

In an initializer such as config/initializers/counter_cache.rb, write the configuration as:

Counter::Cache.configuredo |c|
c.default_worker_adapter=MyCustomWorkAdapterc.recalculation_delay=6.hours# Default delay for recalculationsc.redis_pool=Redis.newc.counting_data_store=MyCustomDataStore# Default is Counter::Cache::Redisend

default_worker_adapter

The worker adapter allows you to control how jobs are delayed/enqueued for later execution. Three options are passed:

  • delay: This is the delay in seconds that the execution should be delayed. Can be ignored or adjusted. We pass this to sidekiq.
  • base_class: This is the class name of the source object.
  • options: This will be a hash of options that should be passed to the instance of the counter.

An example of a dummy adapter is like so:

classTestWorkerAdapterdefenqueue(delay,base_class,options)options[:source_object_class_name]=base_class.constantizecounter_class=options[:counter].constantize# options[:counter] is the class name of the counter that called the adapter.counter=counter_class.new(nil,options)counter.save!endend

An example of a dummy adapter that uses Sidekiq is like so:

classCounterWorkerincludeSidekiq::Workerdefperform(base_class,options)options.symbolize_keys!# From ActiveSupport, Sidekiq looses symbol information from hashes.options[:source_object_class_name]=base_class.constantizecounter_class=options[:counter].constantize# options[:counter] is the class name of the counter that called the adapter.counter=counter_class.new(nil,options)counter.save!enddefself.enqueue(delay,base_class,options)perform_in(delay,base_class,options)endend

recalculation_delay

This should be set to the default delay for recalculations, in seconds.

redis_pool

This can either be a single redis connection or a ConnectionPool instance (https://github.com/mperham/connection_pool).

counting_data_store

This defaults to Counter::Cache::Redis but can be set to anything. The Redis store describes what the API would be.

Contributing

  1. Fork it ( https://github.com/[my-github-username]/counter-cache/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Running specs:

Appraisal is used to test against multiple versions of activerecord. 3.2, 4.0, and 4.1 are currently supported.

To install dependencies:

$ bundle exec appraisal install

To run specs across versions:

$ bundle exec appraisal rspec

About

Counting at scale is hard. Let's do it good.

Resources

Stars

144 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - wanelo/counter-cache: Counting at scale is hard. Let's do it good. · GitHub
Skip to content
This repository was archived by the owner on Dec 1, 2023. It is now read-only.

Repository files navigation

Counter::Cache

Gem VersionBuild StatusCode Climate

Counting things is hard, counting them at scale is even harder, so control when things are counted.

Any time your application performs pagination, the underlying library probably issues a select count(*) from ... request to the database, because all paginators need to know how many pages there are. This works on a small-to-medium dataset, and in an application with relatively low web traffic. But at high traffic volume, live counts saturate CPU on the database server. This is because sorting typically happens on CPU of the database server, using a small amount of heap RAM or even worse — using temp files, which grinds the disk IO to a hault. Web requests become slower and slower, start to pile up in various queues, and eventually saturate all of the app servers. There you are, the site is down.

This gem provides a solution that works at scale, and will help you keep your site up.

This library is battle-tested at Wanelo, where it has been running for several years.

Overview

Rails Counter Caches are a convenient way to keep counters on models that have many children. Without them, you always do live counts, which do not scale. But at high scale, Rails counter caches create update contention on single models, especially for social sites where any single model might become extremely popular. Many web requests trying to update the same row creates database deadlocks and kills performance due to locking and an uncontrollable increase in iops.

This library provides all the benefits of rails counter cache, without the penalty of the contention on updates, by serializing, buffering, and delaying updates via a queue. Counts becoming slightly less realtime, but with a guarantee that single models will never be updated more than once in certain time periods.

Counter Cache Flow

By default, a Buffer Counter is used which implements two modes of counting. The two modes are deferred and recalculation.

IMPORTANT: If Sidekiq is to be used as the delayed job framework, using sidekiq-unique-jobs is essential: https://github.com/mhenrixon/sidekiq-unique-jobs

Mode: Deferred

Initial mode that is used to provide roughly realtime counters.

This mode is meant to provide very reasonably up to date counters using values buffered into Redis, without asking the database for the count at all. An example of how this works is described:

Scenario: User has many posts. We want to keep track of the number of posts on the user model (posts_count column).

When a post is created:

  1. Increment a key in Redis that corresponds to the field and user that relates to the post.
  2. Enqueue a delayed job that will later reconcile the counter column based on the key in redis.
  3. When the job runs, it picks up the value from redis (which can be zero or more) and adds the value to user.posts_count column on the associated model.
user=User.find_by_id(100)user.posts_count# 10user.posts.create(...)# => Job is enqueueduser.posts.create(...)# => Job is already enqueued# come back later (after a delay)user=User.find_by_id(100)user.posts_count# 12

Mode: Recalculation

Runs later and ensures values are completely up to date.

This mode is used to compensate for transient errors that may cause the deferred counters to drift from the actual values. The exact reasons this happens are undefined, redis could hang, go away, the universe could skip ahead in time, who knows.

Using the same scenario as above:

Scenario: User has many posts. We want to keep track of the number of posts on the user model (posts_count column).

  1. Enqueue a job that is delayed by many hours (customizable)
  2. When the job runs, run a full count query to find the true count from the database and save the value to the database.
user=User.find_by_id(100)user.posts_count# 10user.posts.create(...)user.posts.create(...)# redis crashes, world explodes, etc.. we miss on deferred update.user=User.find_by_id(100)user.posts_count# 11, due to only one deferred update having run.# come back later in a couple hoursuser=User.find_by_id(100)user.posts_count# 12

Installation

Add this line to your application's Gemfile:

gem 'counter-cache'

And then execute:

$ bundle

Or install it yourself as:

$ gem install counter-cache

Usage

Counter caches are configured on the models from the perspective of the child model to the parent that contains the counter.

Basic Counter with recalculation:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count# This is a method on the user.end

To allow a timestamp to be updated:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.touch_column: :posts_updated_atend

To increment dynamically:

classReviewincludeCounter::Cachecounter_cache_oncolumn: :reviews_sum,relation: :product,relation_class_name: "Product",increment_by: ->(review){review.score},method: :recalculate_reviews_sum,# This is a method on the product.end

To control when recalculation happens:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.recalculation: true|false,# whether to ever recalculate this counter.recalculation_delay: 10.seconds# Only a hard value that defines when to perform a full recalculation.end

To control when the deferred job runs:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.wait: 10.seconds# This can be a hard valuecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.wait: ->(user){user.posts_count * 10}# .. or a proc, in this case, the more posts a user has, the less frequently it will be updated.end

To control if an update should even happen:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.if: ->(post){post.public? ? false : true}end

Polymorphism (because YAY)

Setting polymorphic: true, will ask ActiveRecord what the class is (User, Store), based on followee_type, and update the appropriate model. So if a user is followed, then that users followers_count will increment.

classUserattr_accessible:followers_countendclassStoreattr_accessible:followers_countendclassFollowattr_accessible:user_id,:followee_id,:followee_typebelongs_to:followee,polymorphic: trueincludeCounter::Cachecounter_cache_oncolumn: :followers_count,relation: :followee,polymorphic: trueend

Configuration

In an initializer such as config/initializers/counter_cache.rb, write the configuration as:

Counter::Cache.configuredo |c|
c.default_worker_adapter=MyCustomWorkAdapterc.recalculation_delay=6.hours# Default delay for recalculationsc.redis_pool=Redis.newc.counting_data_store=MyCustomDataStore# Default is Counter::Cache::Redisend

default_worker_adapter

The worker adapter allows you to control how jobs are delayed/enqueued for later execution. Three options are passed:

  • delay: This is the delay in seconds that the execution should be delayed. Can be ignored or adjusted. We pass this to sidekiq.
  • base_class: This is the class name of the source object.
  • options: This will be a hash of options that should be passed to the instance of the counter.

An example of a dummy adapter is like so:

classTestWorkerAdapterdefenqueue(delay,base_class,options)options[:source_object_class_name]=base_class.constantizecounter_class=options[:counter].constantize# options[:counter] is the class name of the counter that called the adapter.counter=counter_class.new(nil,options)counter.save!endend

An example of a dummy adapter that uses Sidekiq is like so:

classCounterWorkerincludeSidekiq::Workerdefperform(base_class,options)options.symbolize_keys!# From ActiveSupport, Sidekiq looses symbol information from hashes.options[:source_object_class_name]=base_class.constantizecounter_class=options[:counter].constantize# options[:counter] is the class name of the counter that called the adapter.counter=counter_class.new(nil,options)counter.save!enddefself.enqueue(delay,base_class,options)perform_in(delay,base_class,options)endend

recalculation_delay

This should be set to the default delay for recalculations, in seconds.

redis_pool

This can either be a single redis connection or a ConnectionPool instance (https://github.com/mperham/connection_pool).

counting_data_store

This defaults to Counter::Cache::Redis but can be set to anything. The Redis store describes what the API would be.

Contributing

  1. Fork it ( https://github.com/[my-github-username]/counter-cache/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Running specs:

Appraisal is used to test against multiple versions of activerecord. 3.2, 4.0, and 4.1 are currently supported.

To install dependencies:

$ bundle exec appraisal install

To run specs across versions:

$ bundle exec appraisal rspec

About

Counting at scale is hard. Let's do it good.

Resources

Stars

144 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - wanelo/counter-cache: Counting at scale is hard. Let's do it good. · GitHub
Skip to content
This repository was archived by the owner on Dec 1, 2023. It is now read-only.

Repository files navigation

Counter::Cache

Gem VersionBuild StatusCode Climate

Counting things is hard, counting them at scale is even harder, so control when things are counted.

Any time your application performs pagination, the underlying library probably issues a select count(*) from ... request to the database, because all paginators need to know how many pages there are. This works on a small-to-medium dataset, and in an application with relatively low web traffic. But at high traffic volume, live counts saturate CPU on the database server. This is because sorting typically happens on CPU of the database server, using a small amount of heap RAM or even worse — using temp files, which grinds the disk IO to a hault. Web requests become slower and slower, start to pile up in various queues, and eventually saturate all of the app servers. There you are, the site is down.

This gem provides a solution that works at scale, and will help you keep your site up.

This library is battle-tested at Wanelo, where it has been running for several years.

Overview

Rails Counter Caches are a convenient way to keep counters on models that have many children. Without them, you always do live counts, which do not scale. But at high scale, Rails counter caches create update contention on single models, especially for social sites where any single model might become extremely popular. Many web requests trying to update the same row creates database deadlocks and kills performance due to locking and an uncontrollable increase in iops.

This library provides all the benefits of rails counter cache, without the penalty of the contention on updates, by serializing, buffering, and delaying updates via a queue. Counts becoming slightly less realtime, but with a guarantee that single models will never be updated more than once in certain time periods.

Counter Cache Flow

By default, a Buffer Counter is used which implements two modes of counting. The two modes are deferred and recalculation.

IMPORTANT: If Sidekiq is to be used as the delayed job framework, using sidekiq-unique-jobs is essential: https://github.com/mhenrixon/sidekiq-unique-jobs

Mode: Deferred

Initial mode that is used to provide roughly realtime counters.

This mode is meant to provide very reasonably up to date counters using values buffered into Redis, without asking the database for the count at all. An example of how this works is described:

Scenario: User has many posts. We want to keep track of the number of posts on the user model (posts_count column).

When a post is created:

  1. Increment a key in Redis that corresponds to the field and user that relates to the post.
  2. Enqueue a delayed job that will later reconcile the counter column based on the key in redis.
  3. When the job runs, it picks up the value from redis (which can be zero or more) and adds the value to user.posts_count column on the associated model.
user=User.find_by_id(100)user.posts_count# 10user.posts.create(...)# => Job is enqueueduser.posts.create(...)# => Job is already enqueued# come back later (after a delay)user=User.find_by_id(100)user.posts_count# 12

Mode: Recalculation

Runs later and ensures values are completely up to date.

This mode is used to compensate for transient errors that may cause the deferred counters to drift from the actual values. The exact reasons this happens are undefined, redis could hang, go away, the universe could skip ahead in time, who knows.

Using the same scenario as above:

Scenario: User has many posts. We want to keep track of the number of posts on the user model (posts_count column).

  1. Enqueue a job that is delayed by many hours (customizable)
  2. When the job runs, run a full count query to find the true count from the database and save the value to the database.
user=User.find_by_id(100)user.posts_count# 10user.posts.create(...)user.posts.create(...)# redis crashes, world explodes, etc.. we miss on deferred update.user=User.find_by_id(100)user.posts_count# 11, due to only one deferred update having run.# come back later in a couple hoursuser=User.find_by_id(100)user.posts_count# 12

Installation

Add this line to your application's Gemfile:

gem 'counter-cache'

And then execute:

$ bundle

Or install it yourself as:

$ gem install counter-cache

Usage

Counter caches are configured on the models from the perspective of the child model to the parent that contains the counter.

Basic Counter with recalculation:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count# This is a method on the user.end

To allow a timestamp to be updated:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.touch_column: :posts_updated_atend

To increment dynamically:

classReviewincludeCounter::Cachecounter_cache_oncolumn: :reviews_sum,relation: :product,relation_class_name: "Product",increment_by: ->(review){review.score},method: :recalculate_reviews_sum,# This is a method on the product.end

To control when recalculation happens:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.recalculation: true|false,# whether to ever recalculate this counter.recalculation_delay: 10.seconds# Only a hard value that defines when to perform a full recalculation.end

To control when the deferred job runs:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.wait: 10.seconds# This can be a hard valuecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.wait: ->(user){user.posts_count * 10}# .. or a proc, in this case, the more posts a user has, the less frequently it will be updated.end

To control if an update should even happen:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.if: ->(post){post.public? ? false : true}end

Polymorphism (because YAY)

Setting polymorphic: true, will ask ActiveRecord what the class is (User, Store), based on followee_type, and update the appropriate model. So if a user is followed, then that users followers_count will increment.

classUserattr_accessible:followers_countendclassStoreattr_accessible:followers_countendclassFollowattr_accessible:user_id,:followee_id,:followee_typebelongs_to:followee,polymorphic: trueincludeCounter::Cachecounter_cache_oncolumn: :followers_count,relation: :followee,polymorphic: trueend

Configuration

In an initializer such as config/initializers/counter_cache.rb, write the configuration as:

Counter::Cache.configuredo |c|
c.default_worker_adapter=MyCustomWorkAdapterc.recalculation_delay=6.hours# Default delay for recalculationsc.redis_pool=Redis.newc.counting_data_store=MyCustomDataStore# Default is Counter::Cache::Redisend

default_worker_adapter

The worker adapter allows you to control how jobs are delayed/enqueued for later execution. Three options are passed:

  • delay: This is the delay in seconds that the execution should be delayed. Can be ignored or adjusted. We pass this to sidekiq.
  • base_class: This is the class name of the source object.
  • options: This will be a hash of options that should be passed to the instance of the counter.

An example of a dummy adapter is like so:

classTestWorkerAdapterdefenqueue(delay,base_class,options)options[:source_object_class_name]=base_class.constantizecounter_class=options[:counter].constantize# options[:counter] is the class name of the counter that called the adapter.counter=counter_class.new(nil,options)counter.save!endend

An example of a dummy adapter that uses Sidekiq is like so:

classCounterWorkerincludeSidekiq::Workerdefperform(base_class,options)options.symbolize_keys!# From ActiveSupport, Sidekiq looses symbol information from hashes.options[:source_object_class_name]=base_class.constantizecounter_class=options[:counter].constantize# options[:counter] is the class name of the counter that called the adapter.counter=counter_class.new(nil,options)counter.save!enddefself.enqueue(delay,base_class,options)perform_in(delay,base_class,options)endend

recalculation_delay

This should be set to the default delay for recalculations, in seconds.

redis_pool

This can either be a single redis connection or a ConnectionPool instance (https://github.com/mperham/connection_pool).

counting_data_store

This defaults to Counter::Cache::Redis but can be set to anything. The Redis store describes what the API would be.

Contributing

  1. Fork it ( https://github.com/[my-github-username]/counter-cache/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Running specs:

Appraisal is used to test against multiple versions of activerecord. 3.2, 4.0, and 4.1 are currently supported.

To install dependencies:

$ bundle exec appraisal install

To run specs across versions:

$ bundle exec appraisal rspec

About

Counting at scale is hard. Let's do it good.

Resources

Stars

144 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); GitHub - wanelo/counter-cache: Counting at scale is hard. Let's do it good. · GitHub
Skip to content
This repository was archived by the owner on Dec 1, 2023. It is now read-only.

Repository files navigation

Counter::Cache

Gem VersionBuild StatusCode Climate

Counting things is hard, counting them at scale is even harder, so control when things are counted.

Any time your application performs pagination, the underlying library probably issues a select count(*) from ... request to the database, because all paginators need to know how many pages there are. This works on a small-to-medium dataset, and in an application with relatively low web traffic. But at high traffic volume, live counts saturate CPU on the database server. This is because sorting typically happens on CPU of the database server, using a small amount of heap RAM or even worse — using temp files, which grinds the disk IO to a hault. Web requests become slower and slower, start to pile up in various queues, and eventually saturate all of the app servers. There you are, the site is down.

This gem provides a solution that works at scale, and will help you keep your site up.

This library is battle-tested at Wanelo, where it has been running for several years.

Overview

Rails Counter Caches are a convenient way to keep counters on models that have many children. Without them, you always do live counts, which do not scale. But at high scale, Rails counter caches create update contention on single models, especially for social sites where any single model might become extremely popular. Many web requests trying to update the same row creates database deadlocks and kills performance due to locking and an uncontrollable increase in iops.

This library provides all the benefits of rails counter cache, without the penalty of the contention on updates, by serializing, buffering, and delaying updates via a queue. Counts becoming slightly less realtime, but with a guarantee that single models will never be updated more than once in certain time periods.

Counter Cache Flow

By default, a Buffer Counter is used which implements two modes of counting. The two modes are deferred and recalculation.

IMPORTANT: If Sidekiq is to be used as the delayed job framework, using sidekiq-unique-jobs is essential: https://github.com/mhenrixon/sidekiq-unique-jobs

Mode: Deferred

Initial mode that is used to provide roughly realtime counters.

This mode is meant to provide very reasonably up to date counters using values buffered into Redis, without asking the database for the count at all. An example of how this works is described:

Scenario: User has many posts. We want to keep track of the number of posts on the user model (posts_count column).

When a post is created:

  1. Increment a key in Redis that corresponds to the field and user that relates to the post.
  2. Enqueue a delayed job that will later reconcile the counter column based on the key in redis.
  3. When the job runs, it picks up the value from redis (which can be zero or more) and adds the value to user.posts_count column on the associated model.
user=User.find_by_id(100)user.posts_count# 10user.posts.create(...)# => Job is enqueueduser.posts.create(...)# => Job is already enqueued# come back later (after a delay)user=User.find_by_id(100)user.posts_count# 12

Mode: Recalculation

Runs later and ensures values are completely up to date.

This mode is used to compensate for transient errors that may cause the deferred counters to drift from the actual values. The exact reasons this happens are undefined, redis could hang, go away, the universe could skip ahead in time, who knows.

Using the same scenario as above:

Scenario: User has many posts. We want to keep track of the number of posts on the user model (posts_count column).

  1. Enqueue a job that is delayed by many hours (customizable)
  2. When the job runs, run a full count query to find the true count from the database and save the value to the database.
user=User.find_by_id(100)user.posts_count# 10user.posts.create(...)user.posts.create(...)# redis crashes, world explodes, etc.. we miss on deferred update.user=User.find_by_id(100)user.posts_count# 11, due to only one deferred update having run.# come back later in a couple hoursuser=User.find_by_id(100)user.posts_count# 12

Installation

Add this line to your application's Gemfile:

gem 'counter-cache'

And then execute:

$ bundle

Or install it yourself as:

$ gem install counter-cache

Usage

Counter caches are configured on the models from the perspective of the child model to the parent that contains the counter.

Basic Counter with recalculation:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count# This is a method on the user.end

To allow a timestamp to be updated:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.touch_column: :posts_updated_atend

To increment dynamically:

classReviewincludeCounter::Cachecounter_cache_oncolumn: :reviews_sum,relation: :product,relation_class_name: "Product",increment_by: ->(review){review.score},method: :recalculate_reviews_sum,# This is a method on the product.end

To control when recalculation happens:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.recalculation: true|false,# whether to ever recalculate this counter.recalculation_delay: 10.seconds# Only a hard value that defines when to perform a full recalculation.end

To control when the deferred job runs:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.wait: 10.seconds# This can be a hard valuecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.wait: ->(user){user.posts_count * 10}# .. or a proc, in this case, the more posts a user has, the less frequently it will be updated.end

To control if an update should even happen:

classPostincludeCounter::Cachecounter_cache_oncolumn: :posts_count,# users.posts_countrelation: :user,relation_class_name: "User",method: :calculate_posts_count,# This is a method on the user.if: ->(post){post.public? ? false : true}end

Polymorphism (because YAY)

Setting polymorphic: true, will ask ActiveRecord what the class is (User, Store), based on followee_type, and update the appropriate model. So if a user is followed, then that users followers_count will increment.

classUserattr_accessible:followers_countendclassStoreattr_accessible:followers_countendclassFollowattr_accessible:user_id,:followee_id,:followee_typebelongs_to:followee,polymorphic: trueincludeCounter::Cachecounter_cache_oncolumn: :followers_count,relation: :followee,polymorphic: trueend

Configuration

In an initializer such as config/initializers/counter_cache.rb, write the configuration as:

Counter::Cache.configuredo |c|
c.default_worker_adapter=MyCustomWorkAdapterc.recalculation_delay=6.hours# Default delay for recalculationsc.redis_pool=Redis.newc.counting_data_store=MyCustomDataStore# Default is Counter::Cache::Redisend

default_worker_adapter

The worker adapter allows you to control how jobs are delayed/enqueued for later execution. Three options are passed:

  • delay: This is the delay in seconds that the execution should be delayed. Can be ignored or adjusted. We pass this to sidekiq.
  • base_class: This is the class name of the source object.
  • options: This will be a hash of options that should be passed to the instance of the counter.

An example of a dummy adapter is like so:

classTestWorkerAdapterdefenqueue(delay,base_class,options)options[:source_object_class_name]=base_class.constantizecounter_class=options[:counter].constantize# options[:counter] is the class name of the counter that called the adapter.counter=counter_class.new(nil,options)counter.save!endend

An example of a dummy adapter that uses Sidekiq is like so:

classCounterWorkerincludeSidekiq::Workerdefperform(base_class,options)options.symbolize_keys!# From ActiveSupport, Sidekiq looses symbol information from hashes.options[:source_object_class_name]=base_class.constantizecounter_class=options[:counter].constantize# options[:counter] is the class name of the counter that called the adapter.counter=counter_class.new(nil,options)counter.save!enddefself.enqueue(delay,base_class,options)perform_in(delay,base_class,options)endend

recalculation_delay

This should be set to the default delay for recalculations, in seconds.

redis_pool

This can either be a single redis connection or a ConnectionPool instance (https://github.com/mperham/connection_pool).

counting_data_store

This defaults to Counter::Cache::Redis but can be set to anything. The Redis store describes what the API would be.

Contributing

  1. Fork it ( https://github.com/[my-github-username]/counter-cache/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Running specs:

Appraisal is used to test against multiple versions of activerecord. 3.2, 4.0, and 4.1 are currently supported.

To install dependencies:

$ bundle exec appraisal install

To run specs across versions:

$ bundle exec appraisal rspec

About

Counting at scale is hard. Let's do it good.

Resources

Stars

144 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages