generation_cacheable 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/ChangeLog +2 -0
- data/Gemfile +16 -0
- data/README.md +33 -0
- data/Rakefile +8 -0
- data/generation_cacheable.gemspec +23 -0
- data/lib/gen_cache.rb +32 -0
- data/lib/gen_cache/cache_io/fetching.rb +78 -0
- data/lib/gen_cache/cache_io/formatting.rb +75 -0
- data/lib/gen_cache/cache_io/parsing.rb +66 -0
- data/lib/gen_cache/cache_types/association_cache.rb +50 -0
- data/lib/gen_cache/cache_types/attribute_cache.rb +30 -0
- data/lib/gen_cache/cache_types/class_method_cache.rb +23 -0
- data/lib/gen_cache/cache_types/key_cache.rb +14 -0
- data/lib/gen_cache/cache_types/method_cache.rb +28 -0
- data/lib/gen_cache/caches.rb +15 -0
- data/lib/gen_cache/expiry.rb +37 -0
- data/lib/gen_cache/keys.rb +70 -0
- data/lib/gen_cache/version.rb +3 -0
- data/spec/gen_cache/cache_io/fetching_spec.rb +41 -0
- data/spec/gen_cache/cache_io/formatting_spec.rb +59 -0
- data/spec/gen_cache/cache_io/parsing_spec.rb +47 -0
- data/spec/gen_cache/cache_types/association_cache_spec.rb +313 -0
- data/spec/gen_cache/cache_types/attribute_cache_spec.rb +98 -0
- data/spec/gen_cache/cache_types/class_method_cache_spec.rb +72 -0
- data/spec/gen_cache/cache_types/key_cache_spec.rb +34 -0
- data/spec/gen_cache/cache_types/method_cache_spec.rb +108 -0
- data/spec/gen_cache/expiry_spec.rb +182 -0
- data/spec/gen_cache/keys_spec.rb +66 -0
- data/spec/gen_cache_spec.rb +76 -0
- data/spec/models/account.rb +7 -0
- data/spec/models/comment.rb +9 -0
- data/spec/models/descendant.rb +20 -0
- data/spec/models/group.rb +6 -0
- data/spec/models/image.rb +11 -0
- data/spec/models/location.rb +3 -0
- data/spec/models/post.rb +31 -0
- data/spec/models/tag.rb +6 -0
- data/spec/models/user.rb +54 -0
- data/spec/spec_helper.rb +96 -0
- 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
data/.rspec
ADDED
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
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,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
|