README.md in counter-cache-0.0.1 vs README.md in counter-cache-0.0.2

- old
+ new

@@ -1,9 +1,85 @@ # Counter::Cache -TODO: Write a gem description +[![Build Status](https://travis-ci.org/wanelo/counter-cache.svg?branch=master)](https://travis-ci.org/wanelo/counter-cache) +Counting things is hard, counting them at scale is even harder, so control when things are counted. + +[Rails Counter Caches](http://railscasts.com/episodes/23-counter-cache-column) 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 singe 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](doc/counter-cache-flow.png) + +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. + +```ruby + user = User.find_by_id(100) + user.posts_count # 10 + user.posts.create(...) # => Job is enqueued + user.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. + +```ruby + user = User.find_by_id(100) + user.posts_count # 10 + user.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 hours + user = User.find_by_id(100) + user.posts_count # 12 +``` + ## Installation Add this line to your application's Gemfile: gem 'counter-cache' @@ -16,14 +92,182 @@ $ gem install counter-cache ## Usage -TODO: Write usage instructions here +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: + +```ruby +class Post + include Counter::Cache + + counter_cache_on column: :posts_count, # users.posts_count + relation: :user, + relation_class_name: "User", + method: :calculate_posts_count, # This is a method on the user. +end +``` + +#### To control when recalculation happens: + +```ruby +class Post + include Counter::Cache + + counter_cache_on column: :posts_count, # users.posts_count + relation: :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: + +```ruby +class Post + include Counter::Cache + + counter_cache_on column: :posts_count, # users.posts_count + relation: :user, + relation_class_name: "User", + method: :calculate_posts_count, # This is a method on the user. + wait: 10.seconds # This can be a hard value + + counter_cache_on column: :posts_count, # users.posts_count + relation: :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: + +```ruby +class Post + include Counter::Cache + + counter_cache_on column: :posts_count, # users.posts_count + relation: :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. + +```ruby +class User + attr_accessible :followers_count +end + +class Store + attr_accessible :followers_count +end + +class Follow + attr_accessible :user_id, :followee_id, :followee_type + + belongs_to :followee, polymorphic: true + + include Counter::Cache + + counter_cache_on column: :followers_count, + relation: :followee, + polymorphic: true +end +``` + +## Configuration + +In an initializer such as `config/initializers/counter_cache.rb`, write the configuration as: + +```ruby +Counter::Cache.configure do |c| + c.default_worker_adapter = MyCustomWorkAdapter + c.recalculation_delay = 6.hours # Default delay for recalculations + c.redis_pool = Redis.new + c.counting_data_store = MyCustomDataStore # Default is Counter::Cache::Redis +end +``` + +### 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: + +```ruby +class TestWorkerAdapter + def enqueue(delay, base_class, options) + options[:source_object_class_name] = base_class.constantize + counter_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! + end +end +``` + +An example of a dummy adapter that uses Sidekiq is like so: + +```ruby +class CounterWorker + include Sidekiq::Worker + + def perform(base_class, options) + options.symbolize_keys! # From ActiveSupport, Sidekiq looses symbol information from hashes. + options[:source_object_class_name] = base_class.constantize + counter_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! + end + + def self.enqueue(delay, base_class, options) + perform_in(delay, base_class, options) + end +end +``` + +### 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