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.
Files changed (37) hide show
  1. data/.gitignore +2 -0
  2. data/.rvmrc +71 -0
  3. data/Gemfile +3 -0
  4. data/Gemfile.lock +55 -0
  5. data/README.md +130 -0
  6. data/Rakefile +20 -0
  7. data/lib/ultracache.rb +29 -0
  8. data/lib/ultracache/cached.rb +102 -0
  9. data/lib/ultracache/configurations.rb +79 -0
  10. data/lib/ultracache/macro.rb +40 -0
  11. data/lib/ultracache/models/mongoid_extension.rb +9 -0
  12. data/lib/ultracache/railtie.rb +16 -0
  13. data/lib/ultracache/relationship.rb +24 -0
  14. data/lib/ultracache/relationship/belongs_as_cached_queue.rb +96 -0
  15. data/lib/ultracache/relationship/has_cached_attribute.rb +51 -0
  16. data/lib/ultracache/relationship/has_cached_queue.rb +31 -0
  17. data/lib/ultracache/relationships.rb +41 -0
  18. data/lib/ultracache/serializer/base.rb +19 -0
  19. data/lib/ultracache/serializer/json_serializer.rb +15 -0
  20. data/lib/ultracache/storage/redis.rb +63 -0
  21. data/lib/ultracache/version.rb +3 -0
  22. data/spec/models/admin.rb +13 -0
  23. data/spec/models/customer.rb +7 -0
  24. data/spec/models/order.rb +11 -0
  25. data/spec/models/person.rb +15 -0
  26. data/spec/models/post.rb +9 -0
  27. data/spec/spec_helper.rb +15 -0
  28. data/spec/unit/cached_spec.rb +51 -0
  29. data/spec/unit/configurations_spec.rb +49 -0
  30. data/spec/unit/relationship/belongs_as_cached_queue_spec.rb +18 -0
  31. data/spec/unit/relationship/has_cached_attribute_spec.rb +20 -0
  32. data/spec/unit/relationship/has_cached_queue_spec.rb +20 -0
  33. data/spec/unit/relationship_spec.rb +12 -0
  34. data/spec/unit/relationships_spec.rb +72 -0
  35. data/spec/unit/serializer/json_serializer_spec.rb +24 -0
  36. data/ultracache.gemspec +30 -0
  37. 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,9 @@
1
+ module Ultracache
2
+ module Models
3
+ module MongoidExtension
4
+ def cached_queue_score
5
+ self.id.to_s[0..6].to_i(16)
6
+ end
7
+ end
8
+ end
9
+ 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
@@ -0,0 +1,3 @@
1
+ module Ultracache
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,13 @@
1
+ require 'models/person'
2
+
3
+ class Admin < Person
4
+ include Ultracache::Cached
5
+
6
+ has_cached_attribute :cached_permissions do |admin|
7
+ admin.permission
8
+ end
9
+
10
+ def name
11
+ 'admin'
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ class Customer
2
+ include Mongoid::Document
3
+ include Ultracache::Cached
4
+
5
+ has_many :orders
6
+ has_cached_queue :cached_orders, :class => 'Order'
7
+ end