nkallen-cache-money 0.2.1

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/README ADDED
@@ -0,0 +1 @@
1
+ README.markdown
data/TODO ADDED
@@ -0,0 +1,20 @@
1
+ TOP PRIORITY
2
+ * cache_fu adapter
3
+
4
+ REFACTOR
5
+ * Reorganize transactional spec
6
+ * Clarify terminology around cache/key/index, etc.
7
+
8
+ INFRASTRUCTURE
9
+
10
+ NEW FEATURES
11
+ * transactional get multi isn't really multi
12
+ * add on "not stored" should yield
13
+
14
+ BUGS
15
+ * Handle append strategy (using add rather than set?) to avoid race condition
16
+
17
+ MISSING TESTS:
18
+ * missing tests for Klass.transaction do ... end
19
+ * non "id" pks work but lack test coverage
20
+ * expire_cache
@@ -0,0 +1,14 @@
1
+ * does not work with :dependent => nullify because
2
+ def nullify_has_many_dependencies(record, reflection_name, association_class, primary_key_name, dependent_conditions)
3
+ association_class.update_all("#{primary_key_name} = NULL", dependent_conditions)
4
+ end
5
+ This does not trigger callbacks
6
+ * update_all, delete, update_counter, increment_counter, decrement_counter, counter_caches in general - counter caches are replaced by this gem, bear that in mind.
7
+ * attr_readonly - no technical obstacle, just not yet supported
8
+ * attributes before typecast behave unpredictably - hard to support
9
+ * ActiveRecord::Rollback is unsupported - the exception gets swallowed so there isn't an opportunity to rollback the cache transaction - not hard to support
10
+ * Named bind variables :conditions => ["name = :name", { :name => "37signals!" }] - not hard to support
11
+ * printf style binds: :conditions => ["name = '%s'", "37signals!"] - not too hard to support
12
+ * objects as attributes that are serialized. story.title = {:foo => :bar}; customer.balance = Money.new(...) - these could be coerced using Column#type_cast?
13
+
14
+ With a lot of these features the issue is not technical but performance. Every special case costs some overhead.
@@ -0,0 +1,6 @@
1
+ require 'activerecord'
2
+
3
+ ActiveRecord::Base.establish_connection(
4
+ :adapter => 'sqlite3',
5
+ :dbfile => ':memory:'
6
+ )
@@ -0,0 +1,6 @@
1
+ test:
2
+ ttl: 604800
3
+ namespace: cache
4
+ sessions: false
5
+ debug: false
6
+ servers: localhost:11211
data/db/schema.rb ADDED
@@ -0,0 +1,11 @@
1
+ ActiveRecord::Schema.define(:version => 1) do
2
+ create_table "stories", :force => true do |t|
3
+ t.string "title", "subtitle"
4
+ t.string "type"
5
+ end
6
+
7
+ create_table "characters", :force => true do |t|
8
+ t.integer "story_id"
9
+ t.string "name"
10
+ end
11
+ end
data/lib/cash.rb ADDED
@@ -0,0 +1,53 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+
3
+ require 'rubygems'
4
+ require 'activesupport'
5
+ require 'activerecord'
6
+
7
+ require 'cash/lock'
8
+ require 'cash/transactional'
9
+ require 'cash/write_through'
10
+ require 'cash/finders'
11
+ require 'cash/buffered'
12
+ require 'cash/index'
13
+ require 'cash/config'
14
+ require 'cash/accessor'
15
+
16
+ require 'cash/request'
17
+ require 'cash/mock'
18
+ require 'cash/local'
19
+
20
+ require 'cash/query/abstract'
21
+ require 'cash/query/select'
22
+ require 'cash/query/primary_key'
23
+ require 'cash/query/calculation'
24
+
25
+ require 'cash/util/array'
26
+
27
+ class ActiveRecord::Base
28
+ def self.is_cached(options = {})
29
+ include Cash
30
+ Config.create(self, options)
31
+ end
32
+ end
33
+
34
+ module Cash
35
+ def self.included(active_record_class)
36
+ active_record_class.class_eval do
37
+ include Config, Accessor, WriteThrough, Finders
38
+ extend ClassMethods
39
+ end
40
+ end
41
+
42
+ module ClassMethods
43
+ def self.extended(active_record_class)
44
+ class << active_record_class
45
+ alias_method_chain :transaction, :cache_transaction
46
+ end
47
+ end
48
+
49
+ def transaction_with_cache_transaction(&block)
50
+ repository.transaction { transaction_without_cache_transaction(&block) }
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,78 @@
1
+ module Cash
2
+ module Accessor
3
+ def self.included(a_module)
4
+ a_module.module_eval do
5
+ extend ClassMethods
6
+ include InstanceMethods
7
+ end
8
+ end
9
+
10
+ module ClassMethods
11
+ def fetch(keys, options = {}, &block)
12
+ case keys
13
+ when Array
14
+ keys = keys.collect { |key| cache_key(key) }
15
+ hits = repository.get_multi(keys)
16
+ if (missed_keys = keys - hits.keys).any?
17
+ missed_values = block.call(missed_keys)
18
+ hits.merge!(missed_keys.zip(Array(missed_values)).to_hash)
19
+ end
20
+ hits
21
+ else
22
+ repository.get(cache_key(keys), options[:raw]) || (block ? block.call : nil)
23
+ end
24
+ end
25
+
26
+ def get(keys, options = {}, &block)
27
+ case keys
28
+ when Array
29
+ fetch(keys, options, &block)
30
+ else
31
+ fetch(keys, options) do
32
+ if block_given?
33
+ add(keys, result = yield(keys), options)
34
+ result
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ def add(key, value, options = {})
41
+ repository.add(cache_key(key), value, options[:ttl] || 0, options[:raw])
42
+ end
43
+
44
+ def set(key, value, options = {})
45
+ repository.set(cache_key(key), value, options[:ttl] || 0, options[:raw])
46
+ end
47
+
48
+ def incr(key, delta = 1, ttl = 0)
49
+ repository.incr(cache_key(key), delta) || begin
50
+ repository.add(cache_key(key), (result = yield).to_s, ttl, true)
51
+ result
52
+ end
53
+ end
54
+
55
+ def decr(key, delta = 1, ttl = 0)
56
+ repository.decr(cache_key(key), delta) || begin
57
+ repository.add(cache_key(key), (result = yield).to_s, ttl, true)
58
+ result
59
+ end
60
+ end
61
+
62
+ def expire(key)
63
+ repository.delete(cache_key(key))
64
+ end
65
+
66
+ def cache_key(key)
67
+ "#{name}/#{key.to_s.gsub(' ', '+')}"
68
+ end
69
+ end
70
+
71
+ module InstanceMethods
72
+ def expire
73
+ self.class.expire(id)
74
+ end
75
+ alias_method :expire_cache, :expire
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,126 @@
1
+ module Cash
2
+ class Buffered
3
+ def self.push(cache, lock)
4
+ if cache.is_a?(Buffered)
5
+ cache.push
6
+ else
7
+ Buffered.new(cache, lock)
8
+ end
9
+ end
10
+
11
+ def initialize(memcache, lock)
12
+ @buffer = {}
13
+ @commands = []
14
+ @cache = memcache
15
+ @lock = lock
16
+ end
17
+
18
+ def pop
19
+ @cache
20
+ end
21
+
22
+ def push
23
+ NestedBuffered.new(self, @lock)
24
+ end
25
+
26
+ def get(key, *options)
27
+ if @buffer.has_key?(key)
28
+ @buffer[key]
29
+ else
30
+ @buffer[key] = @cache.get(key, *options)
31
+ end
32
+ end
33
+
34
+ def set(key, value, *options)
35
+ @buffer[key] = value
36
+ buffer_command Command.new(:set, key, value, *options)
37
+ end
38
+
39
+ def incr(key, amount = 1)
40
+ return unless value = get(key, true)
41
+
42
+ @buffer[key] = value.to_i + amount
43
+ buffer_command Command.new(:incr, key, amount)
44
+ @buffer[key]
45
+ end
46
+
47
+ def decr(key, amount = 1)
48
+ return unless value = get(key, true)
49
+
50
+ @buffer[key] = [value.to_i - amount, 0].max
51
+ buffer_command Command.new(:decr, key, amount)
52
+ @buffer[key]
53
+ end
54
+
55
+ def add(key, value, *options)
56
+ @buffer[key] = value
57
+ buffer_command Command.new(:add, key, value, *options)
58
+ end
59
+
60
+ def delete(key, *options)
61
+ @buffer[key] = nil
62
+ buffer_command Command.new(:delete, key, *options)
63
+ end
64
+
65
+ def get_multi(keys)
66
+ values = keys.collect { |key| get(key) }
67
+ keys.zip(values).to_hash
68
+ end
69
+
70
+ def flush
71
+ sorted_keys = @commands.select(&:requires_lock?).collect(&:key).uniq.sort
72
+ sorted_keys.each do |key|
73
+ @lock.acquire_lock(key)
74
+ end
75
+ perform_commands
76
+ ensure
77
+ @buffer = {}
78
+ sorted_keys.each do |key|
79
+ @lock.release_lock(key)
80
+ end
81
+ end
82
+
83
+ def method_missing(method, *args, &block)
84
+ @cache.send(method, *args, &block)
85
+ end
86
+
87
+ def respond_to?(method)
88
+ @cache.respond_to?(method)
89
+ end
90
+
91
+ protected
92
+ def perform_commands
93
+ @commands.each do |command|
94
+ command.call(@cache)
95
+ end
96
+ end
97
+
98
+ def buffer_command(command)
99
+ @commands << command
100
+ end
101
+ end
102
+
103
+ class NestedBuffered < Buffered
104
+ def flush
105
+ perform_commands
106
+ end
107
+ end
108
+
109
+ class Command
110
+ attr_accessor :key
111
+
112
+ def initialize(name, key, *args)
113
+ @name = name
114
+ @key = key
115
+ @args = args
116
+ end
117
+
118
+ def requires_lock?
119
+ @name == :set
120
+ end
121
+
122
+ def call(cache)
123
+ cache.send @name, @key, *@args
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,64 @@
1
+ module Cash
2
+ module Config
3
+ def self.included(a_module)
4
+ a_module.module_eval do
5
+ extend ClassMethods
6
+ delegate :repository, :to => "self.class"
7
+ end
8
+ end
9
+
10
+ module ClassMethods
11
+ def self.extended(a_class)
12
+ a_class.class_eval do
13
+ class << self
14
+ delegate :repository, :indices, :to => :@cache_config
15
+ alias_method_chain :inherited, :cache_config
16
+ end
17
+ end
18
+ end
19
+
20
+ def inherited_with_cache_config(subclass)
21
+ inherited_without_cache_config(subclass)
22
+ @cache_config.inherit(subclass)
23
+ end
24
+
25
+ def index(attributes, options = {})
26
+ options.assert_valid_keys(:ttl, :order, :limit, :buffer)
27
+ (@cache_config.indices.unshift(Index.new(@cache_config, self, attributes, options))).uniq!
28
+ end
29
+
30
+ def cache_config=(config)
31
+ @cache_config = config
32
+ end
33
+ end
34
+
35
+ class Config
36
+ attr_reader :active_record, :options
37
+
38
+ def self.create(active_record, options, indices = [])
39
+ active_record.cache_config = new(active_record, options)
40
+ indices.each { |i| active_record.index i.attributes, i.options }
41
+ end
42
+
43
+ def initialize(active_record, options = {})
44
+ @active_record, @options = active_record, options
45
+ end
46
+
47
+ def repository
48
+ @options[:repository]
49
+ end
50
+
51
+ def ttl
52
+ @options[:ttl]
53
+ end
54
+
55
+ def indices
56
+ @indices ||= active_record == ActiveRecord::Base ? [] : [Index.new(self, active_record, active_record.primary_key)]
57
+ end
58
+
59
+ def inherit(active_record)
60
+ self.class.create(active_record, @options, indices)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,40 @@
1
+ module Cash
2
+ module Finders
3
+ def self.included(active_record_class)
4
+ active_record_class.class_eval do
5
+ extend ClassMethods
6
+ end
7
+ end
8
+
9
+ module ClassMethods
10
+ def self.extended(active_record_class)
11
+ active_record_class.class_eval do
12
+ class << self
13
+ alias_method_chain :find_every, :cache
14
+ alias_method_chain :find_from_ids, :cache
15
+ alias_method_chain :calculate, :cache
16
+ end
17
+ end
18
+ end
19
+
20
+ def without_cache(&block)
21
+ User.with_scope(:find => {:readonly => true}, &block)
22
+ end
23
+
24
+ # User.find(:first, ...), User.find_by_foo(...), User.find(:all, ...), User.find_all_by_foo(...)
25
+ def find_every_with_cache(options)
26
+ Query::Select.perform(self, options, scope(:find))
27
+ end
28
+
29
+ # User.find(1), User.find(1, 2, 3), User.find([1, 2, 3]), User.find([])
30
+ def find_from_ids_with_cache(ids, options)
31
+ Query::PrimaryKey.perform(self, ids, options, scope(:find))
32
+ end
33
+
34
+ # User.count(:all), User.count, User.sum(...)
35
+ def calculate_with_cache(operation, column_name, options = {})
36
+ Query::Calculation.perform(self, operation, column_name, options, scope(:find))
37
+ end
38
+ end
39
+ end
40
+ end
data/lib/cash/index.rb ADDED
@@ -0,0 +1,207 @@
1
+ module Cash
2
+ class Index
3
+ attr_reader :attributes, :options
4
+ delegate :each, :hash, :to => :@attributes
5
+ delegate :get, :set, :expire, :find_every_without_cache, :calculate_without_cache, :calculate_with_cache, :incr, :decr, :primary_key, :to => :@active_record
6
+
7
+ DEFAULT_OPTIONS = { :ttl => 1.day }
8
+
9
+ def initialize(config, active_record, attributes, options = {})
10
+ @config, @active_record, @attributes, @options = config, active_record, Array(attributes).collect(&:to_s).sort, DEFAULT_OPTIONS.merge(options)
11
+ end
12
+
13
+ def ==(other)
14
+ case other
15
+ when Index
16
+ attributes == other.attributes
17
+ else
18
+ attributes == Array(other)
19
+ end
20
+ end
21
+ alias_method :eql?, :==
22
+
23
+ module Commands
24
+ def add(object)
25
+ clone = object.shallow_clone
26
+ _, new_attribute_value_pairs = old_and_new_attribute_value_pairs(object)
27
+ add_to_index_with_minimal_network_operations(new_attribute_value_pairs, clone)
28
+ end
29
+
30
+ def update(object)
31
+ clone = object.shallow_clone
32
+ old_attribute_value_pairs, new_attribute_value_pairs = old_and_new_attribute_value_pairs(object)
33
+ update_index_with_minimal_network_operations(old_attribute_value_pairs, new_attribute_value_pairs, clone)
34
+ end
35
+
36
+ def remove(object)
37
+ old_attribute_value_pairs, _ = old_and_new_attribute_value_pairs(object)
38
+ remove_from_index_with_minimal_network_operations(old_attribute_value_pairs, object)
39
+ end
40
+
41
+ def delete(object)
42
+ old_attribute_value_pairs, _ = old_and_new_attribute_value_pairs(object)
43
+ key = cache_key(old_attribute_value_pairs)
44
+ expire(key)
45
+ end
46
+ end
47
+ include Commands
48
+
49
+ module Attributes
50
+ def ttl
51
+ @ttl ||= options[:ttl] || config.ttl
52
+ end
53
+
54
+ def order
55
+ @order ||= options[:order] || :asc
56
+ end
57
+
58
+ def limit
59
+ options[:limit]
60
+ end
61
+
62
+ def buffer
63
+ options[:buffer]
64
+ end
65
+
66
+ def window
67
+ options[:limit] && options[:limit] + options[:buffer]
68
+ end
69
+ end
70
+ include Attributes
71
+
72
+ def serialize_object(object)
73
+ primary_key? ? object : object.id
74
+ end
75
+
76
+ def matches?(query)
77
+ query.calculation? ||
78
+ (query.order == ['id', order] &&
79
+ (!limit || (query.limit && query.limit + query.offset <= limit)))
80
+ end
81
+
82
+ private
83
+ def old_and_new_attribute_value_pairs(object)
84
+ old_attribute_value_pairs = []
85
+ new_attribute_value_pairs = []
86
+ @attributes.each do |name|
87
+ new_value = object.attributes[name]
88
+ original_value = object.send("#{name}_was")
89
+ old_attribute_value_pairs << [name, original_value]
90
+ new_attribute_value_pairs << [name, new_value]
91
+ end
92
+ [old_attribute_value_pairs, new_attribute_value_pairs]
93
+ end
94
+
95
+ def add_to_index_with_minimal_network_operations(attribute_value_pairs, object)
96
+ if primary_key?
97
+ add_object_to_primary_key_cache(attribute_value_pairs, object)
98
+ else
99
+ add_object_to_cache(attribute_value_pairs, object)
100
+ end
101
+ end
102
+
103
+ def primary_key?
104
+ @attributes.size == 1 && @attributes.first == primary_key
105
+ end
106
+
107
+ def add_object_to_primary_key_cache(attribute_value_pairs, object)
108
+ set(cache_key(attribute_value_pairs), [object], :ttl => ttl)
109
+ end
110
+
111
+ def cache_key(attribute_value_pairs)
112
+ attribute_value_pairs.flatten.join('/')
113
+ end
114
+
115
+ def add_object_to_cache(attribute_value_pairs, object, overwrite = true)
116
+ return if invalid_cache_key?(attribute_value_pairs)
117
+
118
+ key, cache_value, cache_hit = get_key_and_value_at_index(attribute_value_pairs)
119
+ if !cache_hit || overwrite
120
+ object_to_add = serialize_object(object)
121
+ objects = (cache_value + [object_to_add]).sort do |a, b|
122
+ (a <=> b) * (order == :asc ? 1 : -1)
123
+ end.uniq
124
+ objects = truncate_if_necessary(objects)
125
+ set(key, objects, :ttl => ttl)
126
+ incr("#{key}/count") { calculate_at_index(:count, attribute_value_pairs) }
127
+ end
128
+ end
129
+
130
+ def invalid_cache_key?(attribute_value_pairs)
131
+ attribute_value_pairs.collect { |_,value| value }.any? { |x| x.nil? }
132
+ end
133
+
134
+ def get_key_and_value_at_index(attribute_value_pairs)
135
+ key = cache_key(attribute_value_pairs)
136
+ cache_hit = true
137
+ cache_value = get(key) do
138
+ cache_hit = false
139
+ conditions = attribute_value_pairs.to_hash
140
+ find_every_without_cache(:select => primary_key, :conditions => conditions, :limit => window).collect do |object|
141
+ serialize_object(object)
142
+ end
143
+ end
144
+ [key, cache_value, cache_hit]
145
+ end
146
+
147
+ def truncate_if_necessary(objects)
148
+ objects.slice(0, window || objects.size)
149
+ end
150
+
151
+ def calculate_at_index(operation, attribute_value_pairs)
152
+ conditions = attribute_value_pairs.to_hash
153
+ calculate_without_cache(operation, :all, :conditions => conditions)
154
+ end
155
+
156
+ def update_index_with_minimal_network_operations(old_attribute_value_pairs, new_attribute_value_pairs, object)
157
+ if index_is_stale?(old_attribute_value_pairs, new_attribute_value_pairs)
158
+ remove_object_from_cache(old_attribute_value_pairs, object)
159
+ add_object_to_cache(new_attribute_value_pairs, object)
160
+ elsif primary_key?
161
+ add_object_to_primary_key_cache(new_attribute_value_pairs, object)
162
+ else
163
+ add_object_to_cache(new_attribute_value_pairs, object, false)
164
+ end
165
+ end
166
+
167
+ def index_is_stale?(old_attribute_value_pairs, new_attribute_value_pairs)
168
+ old_attribute_value_pairs != new_attribute_value_pairs
169
+ end
170
+
171
+ def remove_from_index_with_minimal_network_operations(attribute_value_pairs, object)
172
+ if primary_key?
173
+ remove_object_from_primary_key_cache(attribute_value_pairs, object)
174
+ else
175
+ remove_object_from_cache(attribute_value_pairs, object)
176
+ end
177
+ end
178
+
179
+ def remove_object_from_primary_key_cache(attribute_value_pairs, object)
180
+ set(cache_key(attribute_value_pairs), [], :ttl => ttl)
181
+ end
182
+
183
+ def remove_object_from_cache(attribute_value_pairs, object)
184
+ return if invalid_cache_key?(attribute_value_pairs)
185
+
186
+ key, cache_value, _ = get_key_and_value_at_index(attribute_value_pairs)
187
+ object_to_remove = serialize_object(object)
188
+ objects = cache_value - [object_to_remove]
189
+ objects = resize_if_necessary(attribute_value_pairs, objects)
190
+ set(key, objects, :ttl => ttl)
191
+ end
192
+
193
+ def resize_if_necessary(attribute_value_pairs, objects)
194
+ conditions = attribute_value_pairs.to_hash
195
+ key = cache_key(attribute_value_pairs)
196
+ count = decr("#{key}/count") { calculate_at_index(:count, attribute_value_pairs) }
197
+
198
+ if limit && objects.size < limit && objects.size < count
199
+ find_every_without_cache(:select => :id, :conditions => conditions).collect do |object|
200
+ serialize_object(object)
201
+ end
202
+ else
203
+ objects
204
+ end
205
+ end
206
+ end
207
+ end