netshade-cache-money 0.2.5.2

Sign up to get free protection for your applications and to get access to all the features.
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 is_cached(options = {})
29
+ options.assert_valid_keys(:ttl, :repository, :version)
30
+ include Cash
31
+ Cash::Config::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
+ extend Cash::ClassMethods
39
+ include Cash::Config, Cash::Accessor, Cash::WriteThrough, Cash::Finders
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,78 @@
1
+ module Cash
2
+ module Config
3
+ def self.included(a_module)
4
+ a_module.module_eval do
5
+ extend ClassMethods
6
+ end
7
+ end
8
+
9
+ module ClassMethods
10
+ def self.extended(a_class)
11
+ class << a_class
12
+ delegate :repository, :indices, :to => :cache_config
13
+ alias_method_chain :inherited, :cache_config
14
+ end
15
+ end
16
+
17
+ def cache_config
18
+ unless c = @cache_config
19
+ if self.superclass.respond_to?(:cache_config) && (c = self.superclass.cache_config)
20
+ c.inherit(self)
21
+ end
22
+ end
23
+ @cache_config
24
+ end
25
+
26
+ def inherited_with_cache_config(subclass)
27
+ inherited_without_cache_config(subclass)
28
+ cache_config.inherit(subclass)
29
+ end
30
+
31
+ def index(attributes, options = {})
32
+ options.assert_valid_keys(:ttl, :order, :limit, :buffer)
33
+ (cache_config.indices.unshift(Index.new(cache_config, self, attributes, options))).uniq!
34
+ end
35
+
36
+ def version(number)
37
+ cache_config.options[:version] = number
38
+ end
39
+
40
+ def cache_config=(config)
41
+ @cache_config = config
42
+ end
43
+ end
44
+
45
+ class Config
46
+ attr_reader :active_record, :options
47
+
48
+ def self.create(active_record, options, indices = [])
49
+ active_record.cache_config = new(active_record, options)
50
+ indices.each { |i| active_record.index i.attributes, i.options }
51
+ end
52
+
53
+ def initialize(active_record, options = {})
54
+ @active_record, @options = active_record, options
55
+ end
56
+
57
+ def repository
58
+ @options[:repository]
59
+ end
60
+
61
+ def ttl
62
+ @options[:ttl]
63
+ end
64
+
65
+ def version
66
+ @options[:version] || 1
67
+ end
68
+
69
+ def indices
70
+ @indices ||= active_record == ActiveRecord::Base ? [] : [Index.new(self, active_record, active_record.primary_key)]
71
+ end
72
+
73
+ def inherit(active_record)
74
+ self.class.create(active_record, @options, indices)
75
+ end
76
+ end
77
+ end
78
+ 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