generation_cacheable 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +2 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/ChangeLog +2 -0
  7. data/Gemfile +16 -0
  8. data/README.md +33 -0
  9. data/Rakefile +8 -0
  10. data/generation_cacheable.gemspec +23 -0
  11. data/lib/gen_cache.rb +32 -0
  12. data/lib/gen_cache/cache_io/fetching.rb +78 -0
  13. data/lib/gen_cache/cache_io/formatting.rb +75 -0
  14. data/lib/gen_cache/cache_io/parsing.rb +66 -0
  15. data/lib/gen_cache/cache_types/association_cache.rb +50 -0
  16. data/lib/gen_cache/cache_types/attribute_cache.rb +30 -0
  17. data/lib/gen_cache/cache_types/class_method_cache.rb +23 -0
  18. data/lib/gen_cache/cache_types/key_cache.rb +14 -0
  19. data/lib/gen_cache/cache_types/method_cache.rb +28 -0
  20. data/lib/gen_cache/caches.rb +15 -0
  21. data/lib/gen_cache/expiry.rb +37 -0
  22. data/lib/gen_cache/keys.rb +70 -0
  23. data/lib/gen_cache/version.rb +3 -0
  24. data/spec/gen_cache/cache_io/fetching_spec.rb +41 -0
  25. data/spec/gen_cache/cache_io/formatting_spec.rb +59 -0
  26. data/spec/gen_cache/cache_io/parsing_spec.rb +47 -0
  27. data/spec/gen_cache/cache_types/association_cache_spec.rb +313 -0
  28. data/spec/gen_cache/cache_types/attribute_cache_spec.rb +98 -0
  29. data/spec/gen_cache/cache_types/class_method_cache_spec.rb +72 -0
  30. data/spec/gen_cache/cache_types/key_cache_spec.rb +34 -0
  31. data/spec/gen_cache/cache_types/method_cache_spec.rb +108 -0
  32. data/spec/gen_cache/expiry_spec.rb +182 -0
  33. data/spec/gen_cache/keys_spec.rb +66 -0
  34. data/spec/gen_cache_spec.rb +76 -0
  35. data/spec/models/account.rb +7 -0
  36. data/spec/models/comment.rb +9 -0
  37. data/spec/models/descendant.rb +20 -0
  38. data/spec/models/group.rb +6 -0
  39. data/spec/models/image.rb +11 -0
  40. data/spec/models/location.rb +3 -0
  41. data/spec/models/post.rb +31 -0
  42. data/spec/models/tag.rb +6 -0
  43. data/spec/models/user.rb +54 -0
  44. data/spec/spec_helper.rb +96 -0
  45. metadata +149 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: add01c39075f3847dfc1e750ece37f1329de16f5
4
+ data.tar.gz: 2f5c3410a8f11824e9d77a774ea5f57ef849b4e5
5
+ SHA512:
6
+ metadata.gz: 0ba9aade08e246c5a995c00636f2ea483533b043ade43b1e6739430951c291d723d540939bfa37ee8fd8d13288d6022ed29f7ad9009debd05c14439a775bc008
7
+ data.tar.gz: 8d975869ee3d4922c3c9557d08dc453bc14b7f195ba6f13b08062d80d3ecc2b666ca5e44a8f5789d23a3027b165ef229355d0176c9d76cdf516cb268569115fe
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ pkg/
4
+ bin/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format nested
2
+ --color
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ generation_cacheable
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.0.0
data/ChangeLog ADDED
@@ -0,0 +1,2 @@
1
+ 0.0.1
2
+ * Initial implementation
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in cacheable.gemspec
4
+ gemspec
5
+
6
+ platforms :ruby do
7
+ gem "sqlite3"
8
+ gem "memcached"
9
+ gem "cityhash"
10
+ end
11
+
12
+ platforms :jruby do
13
+ gem "activerecord-jdbc-adapter"
14
+ gem "activerecord-jdbcsqlite3-adapter"
15
+ gem "jruby-memcached"
16
+ end
data/README.md ADDED
@@ -0,0 +1,33 @@
1
+ Generation Cacheable
2
+ ====================
3
+
4
+ A rails cache implementation that began as a fork of [simple cacheable](https://github.com/flyerhzm/simple_cacheable) and incorporated some ideas from [identity cache](https://github.com/Shopify/identity_cache) as well.
5
+
6
+ Usage
7
+ =====
8
+
9
+ ```ruby
10
+ class User < ActiveRecord::Base
11
+ include GenCache
12
+
13
+ has_one :profile
14
+ has_many :friends
15
+
16
+ model_cache do
17
+ with_key # => User.find_cached(1)
18
+ with_attribute :name # => User.find_cached_by_name("Pathouse")
19
+ with_method :meaning_of_life # => User.cached_meaning_of_life
20
+ with_class_method :population # => User.cached_population
21
+ with_association :profile, :friends # => User.cached_profile User.cached_friends
22
+ end
23
+
24
+ def self.population
25
+ all.count
26
+ end
27
+
28
+ def meaning_of_life
29
+ 42
30
+ end
31
+ end
32
+ ```
33
+
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task :default => :spec
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "gen_cache/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "generation_cacheable"
7
+ s.version = GenerationCacheable::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Pat McGee"]
10
+ s.email = ["patmcgee331@gmail.com"]
11
+ s.homepage = "https://github.com/pathouse/generation-cacheable"
12
+ s.summary = %q{a simple cache implementation with attribute based expiry}
13
+ s.description = %q{a simple cache implementation with attribute based expiry}
14
+
15
+ s.add_dependency("rails", ">= 3.0.0")
16
+ s.add_development_dependency("rspec", "2.8")
17
+ s.add_development_dependency("mocha", "0.10.5")
18
+
19
+ s.files = `git ls-files`.split("\n")
20
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
21
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
22
+ s.require_paths = ["lib"]
23
+ end
data/lib/gen_cache.rb ADDED
@@ -0,0 +1,32 @@
1
+ require 'uri'
2
+ require 'cityhash'
3
+ require "gen_cache/caches"
4
+ require "gen_cache/keys"
5
+ require "gen_cache/expiry"
6
+ require "gen_cache/cache_io/fetching"
7
+ require "gen_cache/cache_io/formatting"
8
+ require "gen_cache/cache_io/parsing"
9
+
10
+ module GenCache
11
+
12
+ def self.included(base)
13
+ base.extend(GenCache::Caches)
14
+ base.extend(GenCache::ClassMethods)
15
+
16
+ base.class_eval do
17
+ class_attribute :cached_key,
18
+ :cached_indices,
19
+ :cached_methods,
20
+ :cached_class_methods,
21
+ :cached_associations
22
+ after_commit :expire_all
23
+ end
24
+ end
25
+
26
+ module ClassMethods
27
+ def model_cache(&block)
28
+ instance_exec &block
29
+ end
30
+ end
31
+
32
+ end
@@ -0,0 +1,78 @@
1
+ module GenCache
2
+
3
+ def self.fetch(key_blob, options={}, &block)
4
+ unless key_blob.is_a?(Array)
5
+ single_fetch(key_blob, options) { yield if block_given? }
6
+ else
7
+ multiple_fetch(key_blob) { yield if block_given? }
8
+ end
9
+ end
10
+
11
+ def self.single_fetch(key_blob, options, &block)
12
+ result = read_from_cache(key_blob)
13
+ method_args = symbolize_args(options[:args])
14
+ should_write = false
15
+
16
+ if block_given?
17
+ if method_args != :no_args && (result.nil? || result[method_args].nil?)
18
+ result ||= {}
19
+ result[method_args] = yield
20
+ should_write = true
21
+ elsif method_args == :no_args && result.nil?
22
+ result = yield
23
+ should_write = true
24
+ end
25
+ end
26
+
27
+ write_to_cache(key_blob, result) if should_write
28
+
29
+ result = (method_args == :no_args) ? result : result[method_args]
30
+ end
31
+
32
+ def self.multiple_fetch(key_blobs, &block)
33
+ results = read_multi_from_cache(key_blobs)
34
+ if results.nil?
35
+ if block_given?
36
+ results = yield
37
+ write_multi_to_cache(results)
38
+ end
39
+ end
40
+ results.values
41
+ end
42
+
43
+ ##
44
+ ## READING FROM THE CACHE
45
+ ##
46
+
47
+ def self.read_from_cache(key_blob)
48
+ result = Rails.cache.read key_blob[:key]
49
+ return result if result.nil?
50
+ parse_with_key(result, key_blob[:type])
51
+ end
52
+
53
+ def self.read_multi_from_cache(key_blobs)
54
+ keys = key_blobs.map { |blob| blob[:key] }
55
+ results = Rails.cache.read_multi(*keys)
56
+ return nil if results.values.all?(&:nil?)
57
+ results.each do |key, value|
58
+ type = key_blobs.select {|kb| kb.has_value?(key) }.first[:type]
59
+ results[key] = parse_with_key(value, type)
60
+ end
61
+ results
62
+ end
63
+
64
+ ###
65
+ ### WRITING TO THE CACHE
66
+ ###
67
+
68
+ def self.write_multi_to_cache(keys_and_results)
69
+ keys_and_results.each do |key, result|
70
+ write_to_cache(key, result)
71
+ end
72
+ end
73
+
74
+ def self.write_to_cache(key_blob, result)
75
+ formatted_result = format_with_key(result, key_blob[:type])
76
+ Rails.cache.write key_blob[:key], formatted_result
77
+ end
78
+ end
@@ -0,0 +1,75 @@
1
+ module GenCache
2
+
3
+ def self.format_with_key(result, key_type)
4
+ return if result.nil?
5
+ if key_type == :association
6
+ result
7
+ elsif key_type == :object
8
+ formatted_result = format_object(result)
9
+ else
10
+ formatted_result = format_method(result)
11
+ end
12
+ end
13
+
14
+ ## OBJECT FORMATTING ##
15
+
16
+ def self.format_object(object)
17
+ if object.is_a?(Array)
18
+ object.map { |obj| coder_from_record(obj) }
19
+ else
20
+ coder_from_record(object)
21
+ end
22
+ end
23
+
24
+ def self.coder_from_record(record)
25
+ unless record.nil?
26
+ coder = { :class => record.class }
27
+ record.encode_with(coder)
28
+ coder
29
+ end
30
+ end
31
+
32
+ ## METHOD FORMATTING ##
33
+ def self.symbolize_args(args)
34
+ return :no_args if args.nil? || args.empty?
35
+ args.map do |arg|
36
+ if arg.is_a?(Hash)
37
+ arg.map {|k,v| "#{k}:#{v}"}.join(",")
38
+ elsif arg.is_a?(Array)
39
+ arg.join(",")
40
+ else
41
+ arg.to_s.split(" ").join("_")
42
+ end
43
+ end.join("+").to_sym
44
+ end
45
+
46
+ def self.escape_punctuation(string)
47
+ string.sub(/\?\Z/, '_query').sub(/!\Z/, '_bang')
48
+ end
49
+
50
+ def self.detect_object(data)
51
+ data.is_a?(ActiveRecord::Base) ||
52
+ (data.is_a?(Array) && data[0].is_a?(ActiveRecord::Base))
53
+ end
54
+
55
+ def self.format_method(result)
56
+ if detect_object(result)
57
+ format_object(result)
58
+ elsif result.is_a?(Hash)
59
+ result.each do |arg_key, value|
60
+ result[arg_key] = format_data(value)
61
+ end
62
+ else
63
+ format_data(result)
64
+ end
65
+ end
66
+
67
+ def self.format_data(result)
68
+ if detect_object(result)
69
+ format_object(result)
70
+ else
71
+ result
72
+ end
73
+ end
74
+
75
+ end
@@ -0,0 +1,66 @@
1
+ module GenCache
2
+
3
+ def self.parse_with_key(result, key_type)
4
+ return if result.nil?
5
+ if key_type == :association
6
+ result
7
+ elsif key_type == :object
8
+ object_parse(result)
9
+ else
10
+ method_parse(result)
11
+ end
12
+ end
13
+
14
+ ## OBJECT PARSING ##
15
+
16
+ def self.object_parse(result)
17
+ if result.is_a?(Array)
18
+ result.map {|obj| record_from_coder(obj)}
19
+ else
20
+ record_from_coder(result)
21
+ end
22
+ end
23
+
24
+ def self.record_from_coder(coder)
25
+ record = coder[:class].allocate
26
+ record.init_with(coder)
27
+ record
28
+ end
29
+
30
+ ## METHOD PARSING ##
31
+ #
32
+ ## METHOD STORE FORMATTING
33
+ #
34
+ # { args.to_string.to_symbol => answer }
35
+
36
+ def self.detect_coder(data)
37
+ (data.is_a?(Hash) && hash_inspect(data)) ||
38
+ (data.is_a?(Array) && data[0].is_a?(Hash) && hash_inspect(data[0]))
39
+ end
40
+
41
+ def self.hash_inspect(hash)
42
+ hash.has_key?(:class) && hash.has_key?('attributes')
43
+ end
44
+
45
+ def self.method_parse(result)
46
+ if detect_coder(result)
47
+ object_parse(result)
48
+ elsif result.is_a?(Hash)
49
+ result.each do |k,v|
50
+ result[k] = data_parse(v)
51
+ end
52
+ else
53
+ data_parse(result)
54
+ end
55
+ end
56
+
57
+ ## DATA PARSING ##
58
+
59
+ def self.data_parse(result)
60
+ if detect_coder(result)
61
+ object_parse(result)
62
+ else
63
+ result
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,50 @@
1
+ module GenCache
2
+ module AssocationCache
3
+
4
+ def with_association(*association_names)
5
+ self.cached_associations ||= []
6
+ self.cached_associations += association_names
7
+
8
+ association_names.each do |assoc_name|
9
+ cached_assoc_methods(assoc_name)
10
+ end
11
+ end
12
+
13
+ def cached_assoc_methods(name)
14
+ method_name = "cached_#{name}"
15
+ define_method(method_name) do
16
+ cache_key = GenCache.association_key(self, name)
17
+
18
+ # an object's association cache holds a collection of the instance keys
19
+ # for the objects returned by a call to that association. These are read first
20
+ if instance_variable_get("@#{method_name}").nil?
21
+ instance_keys = GenCache.fetch(cache_key) do
22
+ Array.wrap(self.send(name)).map { |obj| GenCache.instance_key(obj.class, obj.id) }
23
+ end
24
+
25
+ # all of the instance keys are read in a single multi_read
26
+ association = GenCache.fetch(instance_keys) do |key_blobs|
27
+ result = {}
28
+ instance_keys.each do |ik|
29
+ key_parts = ik[:key].scan(/(^.*)\/(.*)\/(.*$)/).flatten
30
+ result[ik] = Object.const_get(key_parts.first.singularize.capitalize).send(:find, key_parts.last.to_i)
31
+ end
32
+ result
33
+ end
34
+
35
+ # plural associations expect arrays, singular associations expect single objects
36
+ association = if association.size == 0
37
+ name.to_s.pluralize == name.to_s ? [] : nil
38
+ elsif association.size == 1
39
+ name.to_s.pluralize == name.to_s ? association : association.first
40
+ else
41
+ association
42
+ end
43
+
44
+ instance_variable_set("@#{method_name}", association)
45
+ end
46
+ instance_variable_get("@#{method_name}")
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,30 @@
1
+ module GenCache
2
+ module AttributeCache
3
+ def with_attribute(*attributes)
4
+ self.cached_indices ||= {}
5
+ self.cached_indices = self.cached_indices.merge(attributes.each_with_object({}) {
6
+ |attribute, indices| indices[attribute] = {}
7
+ })
8
+
9
+ attributes.each do |attribute|
10
+ define_singleton_method("find_cached_by_#{attribute}") do |value|
11
+ self.cached_indices["#{attribute}"] ||= []
12
+ self.cached_indices["#{attribute}"] << value
13
+ cache_key = GenCache.attribute_key(self, attribute, value)
14
+ GenCache.fetch(cache_key) do
15
+ self.send("find_by_#{attribute}", value)
16
+ end
17
+ end
18
+
19
+ define_singleton_method("find_cached_all_by_#{attribute}") do |value|
20
+ self.cached_indices["#{attribute}"] ||= []
21
+ self.cached_indices["#{attribute}"] << value
22
+ cache_key = GenCache.attribute_key(self, attribute, value, all: true)
23
+ GenCache.fetch(cache_key) do
24
+ self.send("find_all_by_#{attribute}", value)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end