ultracache 0.1.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.
- data/.gitignore +2 -0
- data/.rvmrc +71 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +55 -0
- data/README.md +130 -0
- data/Rakefile +20 -0
- data/lib/ultracache.rb +29 -0
- data/lib/ultracache/cached.rb +102 -0
- data/lib/ultracache/configurations.rb +79 -0
- data/lib/ultracache/macro.rb +40 -0
- data/lib/ultracache/models/mongoid_extension.rb +9 -0
- data/lib/ultracache/railtie.rb +16 -0
- data/lib/ultracache/relationship.rb +24 -0
- data/lib/ultracache/relationship/belongs_as_cached_queue.rb +96 -0
- data/lib/ultracache/relationship/has_cached_attribute.rb +51 -0
- data/lib/ultracache/relationship/has_cached_queue.rb +31 -0
- data/lib/ultracache/relationships.rb +41 -0
- data/lib/ultracache/serializer/base.rb +19 -0
- data/lib/ultracache/serializer/json_serializer.rb +15 -0
- data/lib/ultracache/storage/redis.rb +63 -0
- data/lib/ultracache/version.rb +3 -0
- data/spec/models/admin.rb +13 -0
- data/spec/models/customer.rb +7 -0
- data/spec/models/order.rb +11 -0
- data/spec/models/person.rb +15 -0
- data/spec/models/post.rb +9 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/unit/cached_spec.rb +51 -0
- data/spec/unit/configurations_spec.rb +49 -0
- data/spec/unit/relationship/belongs_as_cached_queue_spec.rb +18 -0
- data/spec/unit/relationship/has_cached_attribute_spec.rb +20 -0
- data/spec/unit/relationship/has_cached_queue_spec.rb +20 -0
- data/spec/unit/relationship_spec.rb +12 -0
- data/spec/unit/relationships_spec.rb +72 -0
- data/spec/unit/serializer/json_serializer_spec.rb +24 -0
- data/ultracache.gemspec +30 -0
- metadata +184 -0
@@ -0,0 +1,40 @@
|
|
1
|
+
module Ultracache
|
2
|
+
module Macro
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
def cached_queue_in(another, options={}, &block)
|
7
|
+
name = options[:as] ? options[:as] : self.to_s.underscore
|
8
|
+
relationship = BelongsAsCachedQueue.new name, block, :self_class => self,
|
9
|
+
:need_update => options[:need_update], :unless => options[:unless],
|
10
|
+
:associated_class => another
|
11
|
+
|
12
|
+
self.relationships(:strict => true).add(relationship)
|
13
|
+
end
|
14
|
+
|
15
|
+
def has_cached_queue(another, options={})
|
16
|
+
name = options[:as] ? options[:as] : another
|
17
|
+
relationship = HasCachedQueue.new name, :self_class => self,
|
18
|
+
:associated_class => options[:class] ? options[:class] : another
|
19
|
+
|
20
|
+
self.relationships(:strict => true).add(relationship)
|
21
|
+
|
22
|
+
define_method name do |*options|
|
23
|
+
read_cache(name, options.first || {})
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def has_cached_attribute(name, options={}, &block)
|
28
|
+
relationship = HasCachedAttribute.new name, block, :self_class => self,
|
29
|
+
:serializer => options[:serializer]
|
30
|
+
|
31
|
+
rs = self.relationships(:strict => true)
|
32
|
+
self.relationships(:strict => true).add(relationship)
|
33
|
+
|
34
|
+
define_method name do |*options|
|
35
|
+
read_cache(name, options.first || {})
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'rails'
|
2
|
+
|
3
|
+
begin; require 'mongoid'; rescue LoadError; end
|
4
|
+
|
5
|
+
module Rails
|
6
|
+
module Ultracache
|
7
|
+
class Railtie < Rails::Railtie
|
8
|
+
initializer 'ultracache' do |app|
|
9
|
+
if defined? ::Mongoid
|
10
|
+
require File.join(File.dirname(__FILE__), 'models/mongoid_extension.rb')
|
11
|
+
::Mongoid::Document.send :include, Ultracache::Models::MongoidExtension
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Ultracache
|
2
|
+
class Relationship
|
3
|
+
attr_reader :name
|
4
|
+
|
5
|
+
def initialize(name, options={})
|
6
|
+
@name = name
|
7
|
+
@self_class = options[:self_class].to_s
|
8
|
+
@associated_class = options[:associated_class].to_s
|
9
|
+
end
|
10
|
+
|
11
|
+
def self_class
|
12
|
+
@self_class.camelize.constantize
|
13
|
+
end
|
14
|
+
|
15
|
+
def associated_class
|
16
|
+
@associated_class.camelize.constantize
|
17
|
+
end
|
18
|
+
|
19
|
+
def read_cache(obj, options = {}); end
|
20
|
+
def save_cache(obj); end
|
21
|
+
def destroy_cache(obj); end
|
22
|
+
def update_cache(obj); end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module Ultracache
|
2
|
+
class BelongsAsCachedQueue < Relationship
|
3
|
+
def initialize(name, block, options={})
|
4
|
+
super(name, options)
|
5
|
+
@serializer_block = block
|
6
|
+
@alias = options[:as]
|
7
|
+
@need_update = options[:need_update]
|
8
|
+
@unless = options[:unless]
|
9
|
+
|
10
|
+
@storage = Ultracache::Configurations.storage
|
11
|
+
@serializer = Ultracache::Configurations.serializer
|
12
|
+
end
|
13
|
+
|
14
|
+
# Saves serialized form of object into cache queue which the object
|
15
|
+
# has a relationship to.
|
16
|
+
#
|
17
|
+
# The first parameter, `obj`, is the object which will be stored into
|
18
|
+
# cache queue. `storage` parameter is
|
19
|
+
#
|
20
|
+
# @return Serialized form of the object
|
21
|
+
def save_cache(obj)
|
22
|
+
return if @unless && obj.send(@unless)
|
23
|
+
|
24
|
+
value = if @serializer_block
|
25
|
+
@serializer_block.call obj
|
26
|
+
else
|
27
|
+
JSON.generate(obj.as_json)
|
28
|
+
end
|
29
|
+
|
30
|
+
@storage.put_queue(key(obj), score_of(obj), value)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Destroys serialized cache from associated cache queue. In some cases
|
34
|
+
# like caching serialized documents of MongoDB, two or more cached objects
|
35
|
+
# may have the same score with other documents. To remove cache we want to
|
36
|
+
# delete only, we should deal with this problem.
|
37
|
+
#
|
38
|
+
# If two or more documents are fetched with the computed score,
|
39
|
+
# `destroy_cache` deserializes the cached documents in order to find one
|
40
|
+
# cache having the same identifier.
|
41
|
+
def destroy_cache(obj)
|
42
|
+
score = score_of(obj)
|
43
|
+
key = key(obj)
|
44
|
+
|
45
|
+
docs = @storage.get_queue(key, :from => score, :to => score)
|
46
|
+
|
47
|
+
if docs.count == 1
|
48
|
+
# Only one document is fetched from queue, and it is okay to remove
|
49
|
+
@storage.remove_from_queue_by_range(key, :from => score, :to => score)
|
50
|
+
elsif docs.count > 1
|
51
|
+
# We should deserialize fetched documents to find the document having
|
52
|
+
# the same id with `obj`
|
53
|
+
docs.each do |doc|
|
54
|
+
deserialized = @serializer.deserialize(doc)
|
55
|
+
_id = deserialized["id"]
|
56
|
+
_id = deserialized["_id"] unless _id
|
57
|
+
|
58
|
+
if _id == obj.id
|
59
|
+
@storage.remove_from_queue(key, doc)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Updates object cache in queue. To update cache, we should destroy the
|
66
|
+
# document and regenerate it with updated information.
|
67
|
+
def update_cache(obj)
|
68
|
+
return unless @need_update
|
69
|
+
delete_cache(obj)
|
70
|
+
save_cache(obj)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Returns key of cache queue which cache of the object will be stored into
|
74
|
+
def key(obj)
|
75
|
+
associated = @associated_class.to_s.underscore
|
76
|
+
|
77
|
+
namespace = @name
|
78
|
+
parent_id = obj.send("#{associated}_id")
|
79
|
+
|
80
|
+
"#{@associated_class.to_s.underscore}:#{parent_id}:#{namespace}"
|
81
|
+
end
|
82
|
+
|
83
|
+
protected
|
84
|
+
def score_of(obj)
|
85
|
+
obj.respond_to?(:cached_queue_score) ? obj.cached_queue_score : obj.id
|
86
|
+
end
|
87
|
+
|
88
|
+
def serialize(obj)
|
89
|
+
# Is it okay to refer configurations object here?
|
90
|
+
@serializer.serialize(obj) do |obj|
|
91
|
+
@serializer_block.call(obj)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Ultracache
|
2
|
+
class HasCachedAttribute < Relationship
|
3
|
+
def initialize(name, block, options={})
|
4
|
+
super(name, options)
|
5
|
+
@serializer_method = options[:serializer]
|
6
|
+
@serializing_block = block if block_given?
|
7
|
+
|
8
|
+
@serializer = Ultracache::Configurations.serializer
|
9
|
+
@storage = Ultracache::Configurations.storage
|
10
|
+
end
|
11
|
+
|
12
|
+
def key(obj)
|
13
|
+
"#{@self_class.to_s.underscore}:#{obj.id}:#{@name}"
|
14
|
+
end
|
15
|
+
|
16
|
+
# Save serialized attribute to cache. If serializer method or serialization
|
17
|
+
# block is not given, Ultracache tries to serialize the object by
|
18
|
+
# serializeing hash returned by its `as_json` method.
|
19
|
+
def save_cache(obj)
|
20
|
+
value = if @serializer_method
|
21
|
+
@serializer.serialize(obj.send(@serializer_method))
|
22
|
+
elsif @serializing_block
|
23
|
+
@serializer.serialize(@serializing_block.call(obj))
|
24
|
+
else
|
25
|
+
@serializer.serialize(obj.as_json)
|
26
|
+
end
|
27
|
+
|
28
|
+
@storage.set(key(obj), value)
|
29
|
+
value
|
30
|
+
end
|
31
|
+
|
32
|
+
# Read stored cache from storage. If the cache is not found, it tries to
|
33
|
+
# save corresponding cache to storage and return its value
|
34
|
+
def read_cache(obj, options = {})
|
35
|
+
k = key(obj)
|
36
|
+
|
37
|
+
@storage.get(k) || save_cache(obj)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Destroys cache from storage
|
41
|
+
def destroy_cache(obj)
|
42
|
+
@storage.del(key(obj))
|
43
|
+
end
|
44
|
+
|
45
|
+
# Updates value of existing cache. Its behavior is same with that of
|
46
|
+
# `save_cache`.
|
47
|
+
def update_cache(obj)
|
48
|
+
save_cache(obj)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Ultracache
|
2
|
+
class HasCachedQueue < Relationship
|
3
|
+
attr_reader :fetch_by
|
4
|
+
|
5
|
+
def initialize(name, options={})
|
6
|
+
@fetch_by = options[:fetch_by]
|
7
|
+
super(name, options)
|
8
|
+
|
9
|
+
@storage = Ultracache::Configurations.storage
|
10
|
+
end
|
11
|
+
|
12
|
+
def key(obj)
|
13
|
+
"#{@self_class.to_s.underscore}:#{obj.id}:#{@name}"
|
14
|
+
end
|
15
|
+
|
16
|
+
# Reads caches stored in corresponding queue. Caches can be fetched by
|
17
|
+
# their score or their rank.
|
18
|
+
def read_cache(obj, options = {})
|
19
|
+
k = self.key(obj)
|
20
|
+
|
21
|
+
fetch_by = @fetch_by || options[:fetch_by]
|
22
|
+
if fetch_by && fetch_by == :rank
|
23
|
+
@storage.get_queue_by_rank(k, options)
|
24
|
+
elsif options[:per_page]
|
25
|
+
@storage.get_queue_paged(k, options)
|
26
|
+
else
|
27
|
+
@storage.get_queue(k, options)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Ultracache
|
2
|
+
class Relationships
|
3
|
+
attr_reader :klass
|
4
|
+
attr_reader :relationships
|
5
|
+
|
6
|
+
def initialize(klass, rs = {})
|
7
|
+
@relationships = rs
|
8
|
+
@klass = klass
|
9
|
+
end
|
10
|
+
|
11
|
+
def add(relationship)
|
12
|
+
@relationships[relationship.name] = relationship
|
13
|
+
end
|
14
|
+
|
15
|
+
def keys
|
16
|
+
@relationships.keys
|
17
|
+
end
|
18
|
+
|
19
|
+
def find(name)
|
20
|
+
@relationships[name]
|
21
|
+
end
|
22
|
+
alias [] find
|
23
|
+
|
24
|
+
def each
|
25
|
+
@relationships.each do |k,v|
|
26
|
+
yield k, v
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def merge(another)
|
31
|
+
klass = if another.klass >= @klass
|
32
|
+
@klass
|
33
|
+
else
|
34
|
+
another.klass
|
35
|
+
end
|
36
|
+
|
37
|
+
Relationships.new(klass, @relationships.merge(another.relationships))
|
38
|
+
end
|
39
|
+
alias | merge
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Ultracache
|
2
|
+
# Base class
|
3
|
+
module Serializer
|
4
|
+
class Base
|
5
|
+
|
6
|
+
# Serializes model object. The Object should be converted into
|
7
|
+
# a hash containing an id field and required attributes.
|
8
|
+
#
|
9
|
+
# @param [Hash] obj A hash containing information of an object
|
10
|
+
#
|
11
|
+
# @return [String] Serialized object
|
12
|
+
def serialize(obj, &block); obj.to_s; end
|
13
|
+
|
14
|
+
# Deserializes a string which an object is serialized into to a
|
15
|
+
# hash.
|
16
|
+
def deserialize(serialized_object); end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Ultracache
|
4
|
+
module Serializer
|
5
|
+
class JsonSerializer < Serializer::Base
|
6
|
+
def serialize(obj, &block)
|
7
|
+
JSON.generate(obj)
|
8
|
+
end
|
9
|
+
|
10
|
+
def deserialize(serialized_object)
|
11
|
+
JSON.parse(serialized_object)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'redis'
|
2
|
+
require 'redis/distributed'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Ultracache
|
6
|
+
module Storage
|
7
|
+
class Redis
|
8
|
+
include Storage
|
9
|
+
|
10
|
+
def initialize(options = {})
|
11
|
+
@urls = options[:urls] || 'localhost'
|
12
|
+
end
|
13
|
+
|
14
|
+
def connection
|
15
|
+
@connection ||= ::Redis::Distributed.new(@urls)
|
16
|
+
end
|
17
|
+
|
18
|
+
def set(id, doc)
|
19
|
+
connection.set(id, doc)
|
20
|
+
end
|
21
|
+
|
22
|
+
def get(id)
|
23
|
+
connection.get(id)
|
24
|
+
end
|
25
|
+
|
26
|
+
def multi_get(*ids)
|
27
|
+
connection.mget(ids)
|
28
|
+
end
|
29
|
+
|
30
|
+
def del(id)
|
31
|
+
connection.del(id)
|
32
|
+
end
|
33
|
+
|
34
|
+
def get_queue(id, opts={})
|
35
|
+
connection.zrevrangebyscore(id, opts[:to] || "+inf", opts[:from] || "-inf")
|
36
|
+
end
|
37
|
+
|
38
|
+
def get_queue_paged(id, opts={})
|
39
|
+
per_page = opts[:per_page] || 20
|
40
|
+
page = opts[:page] || 1
|
41
|
+
offset = per_page.to_i * (page.to_i - 1)
|
42
|
+
|
43
|
+
connection.zrevrangebyscore(id, opts[:to] || "+inf", opts[:from] || "-inf", :limit => [offset, per_page])
|
44
|
+
end
|
45
|
+
|
46
|
+
def remove_from_queue_by_range(id, opts={})
|
47
|
+
connection.zremrangebyscore(id, opts[:from], opts[:to])
|
48
|
+
end
|
49
|
+
|
50
|
+
def remove_from_queue(id, val)
|
51
|
+
connection.zrem(id, val)
|
52
|
+
end
|
53
|
+
|
54
|
+
def get_queue_by_rank(id, opts={})
|
55
|
+
connection.zrevrange(id, opts[:from] || 0, opts[:to] || -1)
|
56
|
+
end
|
57
|
+
|
58
|
+
def put_queue(id, key, entry)
|
59
|
+
connection.zadd(id, key, entry)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|