shanboli-cache-money 0.2.6

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,17 @@
1
+ TOP PRIORITY
2
+
3
+ REFACTOR
4
+ * Clarify terminology around cache/key/index, etc.
5
+
6
+ INFRASTRUCTURE
7
+
8
+ NEW FEATURES
9
+ * transactional get multi isn't really multi
10
+
11
+ BUGS
12
+ * Handle append strategy (using add rather than set?) to avoid race condition
13
+
14
+ MISSING TESTS:
15
+ * missing tests for Klass.transaction do ... end
16
+ * non "id" pks work but lack test coverage
17
+ * 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
@@ -0,0 +1,12 @@
1
+ ActiveRecord::Schema.define(:version => 2) do
2
+ create_table "stories", :force => true do |t|
3
+ t.string "title", "subtitle"
4
+ t.string "type"
5
+ t.boolean "published"
6
+ end
7
+
8
+ create_table "characters", :force => true do |t|
9
+ t.integer "story_id"
10
+ t.string "name"
11
+ end
12
+ end
@@ -0,0 +1,54 @@
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
+ options.assert_valid_keys(:ttl, :repository, :version)
30
+ include Cash
31
+ Config.create(self, options)
32
+ end
33
+ end
34
+
35
+ module Cash
36
+ def self.included(active_record_class)
37
+ active_record_class.class_eval do
38
+ include Config, Accessor, WriteThrough, Finders
39
+ extend ClassMethods
40
+ end
41
+ end
42
+
43
+ module ClassMethods
44
+ def self.extended(active_record_class)
45
+ class << active_record_class
46
+ alias_method_chain :transaction, :cache_transaction
47
+ end
48
+ end
49
+
50
+ def transaction_with_cache_transaction(&block)
51
+ repository.transaction { transaction_without_cache_transaction(&block) }
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,79 @@
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
+ if repository.add(cache_key(key), value, options[:ttl] || 0, options[:raw]) == "NOT_STORED\r\n"
42
+ yield
43
+ end
44
+ end
45
+
46
+ def set(key, value, options = {})
47
+ repository.set(cache_key(key), value, options[:ttl] || 0, options[:raw])
48
+ end
49
+
50
+ def incr(key, delta = 1, ttl = 0)
51
+ repository.incr(cache_key = cache_key(key), delta) || begin
52
+ repository.add(cache_key, (result = yield).to_s, ttl, true) { repository.incr(cache_key) }
53
+ result
54
+ end
55
+ end
56
+
57
+ def decr(key, delta = 1, ttl = 0)
58
+ repository.decr(cache_key = cache_key(key), delta) || begin
59
+ repository.add(cache_key, (result = yield).to_s, ttl, true) { repository.decr(cache_key) }
60
+ result
61
+ end
62
+ end
63
+
64
+ def expire(key)
65
+ repository.delete(cache_key(key))
66
+ end
67
+
68
+ def cache_key(key)
69
+ "#{name}:#{cache_config.version}/#{key.to_s.gsub(' ', '+')}"
70
+ end
71
+ end
72
+
73
+ module InstanceMethods
74
+ def expire
75
+ self.class.expire(id)
76
+ end
77
+ end
78
+ end
79
+ 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,71 @@
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
+ class << a_class
13
+ attr_reader :cache_config
14
+ delegate :repository, :indices, :to => :@cache_config
15
+ alias_method_chain :inherited, :cache_config
16
+ end
17
+ end
18
+
19
+ def inherited_with_cache_config(subclass)
20
+ inherited_without_cache_config(subclass)
21
+ @cache_config.inherit(subclass)
22
+ end
23
+
24
+ def index(attributes, options = {})
25
+ options.assert_valid_keys(:ttl, :order, :limit, :buffer)
26
+ (@cache_config.indices.unshift(Index.new(@cache_config, self, attributes, options))).uniq!
27
+ end
28
+
29
+ def version(number)
30
+ @cache_config.options[:version] = number
31
+ end
32
+
33
+ def cache_config=(config)
34
+ @cache_config = config
35
+ end
36
+ end
37
+
38
+ class Config
39
+ attr_reader :active_record, :options
40
+
41
+ def self.create(active_record, options, indices = [])
42
+ active_record.cache_config = new(active_record, options)
43
+ indices.each { |i| active_record.index i.attributes, i.options }
44
+ end
45
+
46
+ def initialize(active_record, options = {})
47
+ @active_record, @options = active_record, options
48
+ end
49
+
50
+ def repository
51
+ @options[:repository]
52
+ end
53
+
54
+ def ttl
55
+ @options[:ttl]
56
+ end
57
+
58
+ def version
59
+ @options[:version] || 1
60
+ end
61
+
62
+ def indices
63
+ @indices ||= active_record == ActiveRecord::Base ? [] : [Index.new(self, active_record, active_record.primary_key)]
64
+ end
65
+
66
+ def inherit(active_record)
67
+ self.class.create(active_record, @options, indices)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,38 @@
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
+ class << active_record_class
12
+ alias_method_chain :find_every, :cache
13
+ alias_method_chain :find_from_ids, :cache
14
+ alias_method_chain :calculate, :cache
15
+ end
16
+ end
17
+
18
+ def without_cache(&block)
19
+ with_scope(:find => {:readonly => true}, &block)
20
+ end
21
+
22
+ # User.find(:first, ...), User.find_by_foo(...), User.find(:all, ...), User.find_all_by_foo(...)
23
+ def find_every_with_cache(options)
24
+ Query::Select.perform(self, options, scope(:find))
25
+ end
26
+
27
+ # User.find(1), User.find(1, 2, 3), User.find([1, 2, 3]), User.find([])
28
+ def find_from_ids_with_cache(ids, options)
29
+ Query::PrimaryKey.perform(self, ids, options, scope(:find))
30
+ end
31
+
32
+ # User.count(:all), User.count, User.sum(...)
33
+ def calculate_with_cache(operation, column_name, options = {})
34
+ Query::Calculation.perform(self, operation, column_name, options, scope(:find))
35
+ end
36
+ end
37
+ end
38
+ end
@@ -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
+ limit && limit + 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