generation_cacheable 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|