counter-cache 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +10 -0
  5. data/Gemfile +4 -0
  6. data/Guardfile +9 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +29 -0
  9. data/Rakefile +6 -0
  10. data/counter-cache.gemspec +30 -0
  11. data/lib/counter/cache.rb +28 -0
  12. data/lib/counter/cache/active_record_updater.rb +23 -0
  13. data/lib/counter/cache/config.rb +12 -0
  14. data/lib/counter/cache/counters/buffer_counter.rb +25 -0
  15. data/lib/counter/cache/counters/buffer_counter/enqueuer.rb +27 -0
  16. data/lib/counter/cache/counters/buffer_counter/key.rb +23 -0
  17. data/lib/counter/cache/counters/buffer_counter/relation_finder.rb +29 -0
  18. data/lib/counter/cache/counters/buffer_counter/saver.rb +70 -0
  19. data/lib/counter/cache/counters/buffer_counter/updater.rb +54 -0
  20. data/lib/counter/cache/options_parser.rb +69 -0
  21. data/lib/counter/cache/redis.rb +40 -0
  22. data/lib/counter/cache/version.rb +5 -0
  23. data/spec/features/counter_spec.rb +95 -0
  24. data/spec/lib/counter/cache/active_record_updater_spec.rb +26 -0
  25. data/spec/lib/counter/cache/buffer_counter/enqueuer_spec.rb +53 -0
  26. data/spec/lib/counter/cache/buffer_counter/key_spec.rb +13 -0
  27. data/spec/lib/counter/cache/buffer_counter/relation_finder_spec.rb +49 -0
  28. data/spec/lib/counter/cache/buffer_counter/saver_spec.rb +84 -0
  29. data/spec/lib/counter/cache/buffer_counter/updater_spec.rb +96 -0
  30. data/spec/lib/counter/cache/buffer_counter_spec.rb +27 -0
  31. data/spec/lib/counter/cache/config_spec.rb +36 -0
  32. data/spec/lib/counter/cache/options_parser_spec.rb +176 -0
  33. data/spec/lib/counter/cache/redis_spec.rb +44 -0
  34. data/spec/lib/counter/cache_spec.rb +14 -0
  35. data/spec/spec_helper.rb +93 -0
  36. data/spec/support/models.rb +87 -0
  37. data/spec/support/worker_adapter.rb +8 -0
  38. metadata +208 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 381fdac0060611f59b5314096aa953310c927a98
4
+ data.tar.gz: 94cfd0f2f7f91c8e7fd3e6c2f2579d26fee90904
5
+ SHA512:
6
+ metadata.gz: 41ac6baff5ade8a503c71244efcbfd584982e0ec9ea00bde813c0797100f30044e9304c922e3f20afbab0a3e82b3c5e4c9efb5cc90125cadab6d52901b85e0b8
7
+ data.tar.gz: aa2e8b95531190a0cddcc33d5f88f4a7a332a2646f8ff39784db5ecaea567e431a6fb7af23ab06375172b613d6f28c24081cc6f5b9b043bd7f65bf7066cbd041
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
@@ -0,0 +1,10 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.1
4
+ - 2.1.0
5
+ - 2.0.0
6
+ - 1.9.3
7
+ notifications:
8
+ email:
9
+ on_success: never
10
+ webhooks: "http://ci.wanelo.com/projects/2f4c3438-a0bc-434e-a9fb-30c0c335b432/status"
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in counter-cache.gemspec
4
+ gemspec
@@ -0,0 +1,9 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard :rspec do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+ end
9
+
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 James Hart & Matt Camuto
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,29 @@
1
+ # Counter::Cache
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'counter-cache'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install counter-cache
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it ( https://github.com/[my-github-username]/counter-cache/fork )
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create a new Pull Request
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'counter/cache/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "counter-cache"
8
+ spec.version = Counter::Cache::VERSION
9
+ spec.authors = ["Paul Henry & Matt Camuto"]
10
+ spec.email = ["dev@wanelo.com"]
11
+ spec.summary = %q{Counting is hard.}
12
+ spec.description = %q{This makes it easier.}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "activerecord", ">= 3.0"
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.6"
24
+ spec.add_development_dependency "rake"
25
+ spec.add_development_dependency "rspec", ">= 3.0"
26
+ spec.add_development_dependency "guard"
27
+ spec.add_development_dependency "guard-rspec"
28
+ spec.add_development_dependency "fakeredis"
29
+ spec.add_development_dependency "sqlite3"
30
+ end
@@ -0,0 +1,28 @@
1
+ require "counter/cache/version"
2
+ require "counter/cache/active_record_updater"
3
+ require "counter/cache/options_parser"
4
+ require "counter/cache/config"
5
+ require "counter/cache/counters/buffer_counter"
6
+ require "counter/cache/redis"
7
+
8
+ module Counter
9
+
10
+ module Cache
11
+ def self.configure
12
+ yield configuration
13
+ end
14
+
15
+ def self.configuration
16
+ @configuration ||= Counter::Cache::Config.new
17
+ end
18
+
19
+ def self.included(base)
20
+ base.instance_eval do
21
+ def counter_cache_on(options)
22
+ after_create ActiveRecordUpdater.new(options)
23
+ after_destroy ActiveRecordUpdater.new(options)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,23 @@
1
+ module Counter
2
+ module Cache
3
+ class ActiveRecordUpdater < Struct.new(:options)
4
+ def after_create(record)
5
+ counter_for(record).update(:incr)
6
+ end
7
+
8
+ def after_destroy(record)
9
+ counter_for(record).update(:decr)
10
+ end
11
+
12
+ private
13
+
14
+ def counter_for(object)
15
+ counter_class.new(object, options)
16
+ end
17
+
18
+ def counter_class
19
+ options[:counter_class] || Counter::Cache::Counters::BufferCounter
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,12 @@
1
+ module Counter
2
+ module Cache
3
+ class Config
4
+ # TODO:: Confer with paul/kig about adapting the counting data store
5
+ attr_accessor :default_worker_adapter, :recalculation_delay, :redis_pool, :counting_data_store
6
+
7
+ def initialize
8
+ self.counting_data_store = Counter::Cache::Redis.new
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,25 @@
1
+ require 'counter/cache/counters/buffer_counter/updater'
2
+ require 'counter/cache/counters/buffer_counter/saver'
3
+
4
+ module Counter
5
+ module Cache
6
+ module Counters
7
+ class BufferCounter
8
+ attr_accessor :source_object, :options
9
+
10
+ def initialize(source_object, options)
11
+ @options = Counter::Cache::OptionsParser.new(options)
12
+ @source_object = source_object
13
+ end
14
+
15
+ def update(direction)
16
+ Updater.new(source_object, options, self.class.name).update!(direction)
17
+ end
18
+
19
+ def save!(&block)
20
+ Saver.new(options).save!(&block)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,27 @@
1
+ module Counter
2
+ module Cache
3
+ module Counters
4
+ class BufferCounter
5
+ class Enqueuer < Struct.new(:options, :source_object_class_name, :relation_id, :relation_class, :counter_class_name)
6
+ def enqueue!(source_object)
7
+ create_and_enqueue(options.wait(source_object), options.cached?)
8
+ create_and_enqueue(options.recalculation_delay, false) if options.recalculation?
9
+ end
10
+
11
+ private
12
+
13
+ def create_and_enqueue(delay, cached)
14
+ options.worker_adapter.enqueue(delay,
15
+ source_object_class_name,
16
+ { relation_class_name: relation_class,
17
+ relation_id: relation_id,
18
+ column: options.column,
19
+ method: options.method,
20
+ cache: cached,
21
+ counter: counter_class_name })
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,23 @@
1
+ module Counter
2
+ module Cache
3
+ module Counters
4
+ class BufferCounter
5
+ class Key < Struct.new(:source_object, :options)
6
+ def to_s
7
+ "cc:#{relation_finder.relation_class.to_s[0..1]}:#{relation_finder.relation_id}:#{column}"
8
+ end
9
+
10
+ protected
11
+
12
+ def column
13
+ options.column.to_s.gsub(/_count/, '')
14
+ end
15
+
16
+ def relation_finder
17
+ RelationFinder.new(source_object, options)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,29 @@
1
+ module Counter
2
+ module Cache
3
+ module Counters
4
+ class BufferCounter
5
+ class RelationFinder < Struct.new(:source_object, :options)
6
+ def relation_class
7
+ return options.relation_class_name if options.relation_class_name
8
+ return polymorphic_type if options.polymorphic?
9
+ reflection_type
10
+ end
11
+
12
+ def relation_id
13
+ options.relation_id || source_object.send("#{options.relation}_id")
14
+ end
15
+
16
+ private
17
+
18
+ def polymorphic_type
19
+ source_object.send("#{options.relation}_type")
20
+ end
21
+
22
+ def reflection_type
23
+ source_object.reflections[options.relation.to_sym].class_name.to_s.camelize # let AR give us the correct class name :)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,70 @@
1
+ module Counter
2
+ module Cache
3
+ module Counters
4
+ class BufferCounter
5
+ class Saver < Struct.new(:options)
6
+ def save!
7
+ return unless relation_object
8
+
9
+ old_value = current_column_value
10
+ new_value = calculate_new_value
11
+
12
+ save_new_value!(new_value)
13
+
14
+ yield old_value, new_value, relation_object, options.column if block_given?
15
+ end
16
+
17
+ private
18
+
19
+ def save_new_value!(value)
20
+ relation_object.send("#{options.column}=", value)
21
+ relation_object.save!(validate: false)
22
+ end
23
+
24
+ def calculate_new_value
25
+ return current_column_value + counter_value.to_i if options.cached? && counter_value
26
+ non_cached_count.to_i
27
+ end
28
+
29
+ def non_cached_count
30
+ method = options.method
31
+ return relation_object.send(method) if method && relation_object.respond_to?(method)
32
+ relation_object.send(options.source_object_class_name.table_name).count
33
+ end
34
+
35
+ def current_column_value
36
+ relation_object.send(options.column).to_i
37
+ end
38
+
39
+ def counter_value
40
+ @counter_value ||= get.tap { |v| reset if v }
41
+ end
42
+
43
+ def get
44
+ counting_data_store.get(key)
45
+ end
46
+
47
+ def reset
48
+ counting_data_store.del(key)
49
+ end
50
+
51
+ def key
52
+ Key.new(nil, options).to_s
53
+ end
54
+
55
+ def counting_data_store
56
+ Counter::Cache.configuration.counting_data_store
57
+ end
58
+
59
+ def relation_object
60
+ @relation_object ||= constantized_relation.find_by_id(options.relation_id)
61
+ end
62
+
63
+ def constantized_relation
64
+ Object.const_get(options.relation_class_name)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,54 @@
1
+ require 'counter/cache/counters/buffer_counter/relation_finder'
2
+ require 'counter/cache/counters/buffer_counter/enqueuer'
3
+ require 'counter/cache/counters/buffer_counter/key'
4
+
5
+ module Counter
6
+ module Cache
7
+ module Counters
8
+ class BufferCounter
9
+ class Updater < Struct.new(:source_object, :options, :counter_class_name)
10
+ def update!(direction)
11
+ return unless valid?
12
+ incr if direction == :incr
13
+ decr if direction == :decr
14
+ enqueue
15
+ end
16
+
17
+ private
18
+
19
+ def enqueue
20
+ Enqueuer.new(options,
21
+ source_object.class.name,
22
+ relation_finder.relation_id,
23
+ relation_finder.relation_class,
24
+ counter_class_name).enqueue!(source_object)
25
+ end
26
+
27
+ def relation_finder
28
+ RelationFinder.new(source_object, options)
29
+ end
30
+
31
+ def valid?
32
+ (!options.if_value || options.if_value.call(source_object)) && !relation_finder.relation_id.nil?
33
+ end
34
+
35
+ def incr
36
+ redis.incr(key)
37
+ end
38
+
39
+ def decr
40
+ redis.decr(key)
41
+ end
42
+
43
+ def key
44
+ Key.new(source_object, options).to_s
45
+ end
46
+
47
+ def redis
48
+ Counter::Cache::Redis.new
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end