nkallen-cash 0.1.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.
@@ -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,36 @@
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
+ # User.find(:first, ...), User.find_by_foo(...), User.find(:all, ...), User.find_all_by_foo(...)
21
+ def find_every_with_cache(options)
22
+ Query::Select.perform(self, options, scope(:find))
23
+ end
24
+
25
+ # User.find(1), User.find(1, 2, 3), User.find([1, 2, 3]), User.find([])
26
+ def find_from_ids_with_cache(ids, options)
27
+ Query::PrimaryKey.perform(self, ids, options, scope(:find))
28
+ end
29
+
30
+ # User.count(:all), User.count, User.sum(...)
31
+ def calculate_with_cache(operation, column_name, options = {})
32
+ Query::Calculation.perform(self, operation, column_name, options, scope(:find))
33
+ end
34
+ end
35
+ end
36
+ end
data/lib/cash/index.rb ADDED
@@ -0,0 +1,200 @@
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
+ def add(object)
24
+ clone = object.shallow_clone
25
+ _, new_attribute_value_pairs = old_and_new_attribute_value_pairs(object)
26
+ add_to_index_with_minimal_network_operations(new_attribute_value_pairs, clone)
27
+ end
28
+
29
+ def update(object)
30
+ clone = object.shallow_clone
31
+ old_attribute_value_pairs, new_attribute_value_pairs = old_and_new_attribute_value_pairs(object)
32
+ update_index_with_minimal_network_operations(old_attribute_value_pairs, new_attribute_value_pairs, clone)
33
+ end
34
+
35
+ def remove(object)
36
+ old_attribute_value_pairs, _ = old_and_new_attribute_value_pairs(object)
37
+ remove_from_index_with_minimal_network_operations(old_attribute_value_pairs, object)
38
+ end
39
+
40
+ def delete(object)
41
+ old_attribute_value_pairs, _ = old_and_new_attribute_value_pairs(object)
42
+ key = cache_key(old_attribute_value_pairs)
43
+ expire(key)
44
+ end
45
+
46
+ def ttl
47
+ @ttl ||= options[:ttl] || config.ttl
48
+ end
49
+
50
+ def order
51
+ @order ||= options[:order] || :asc
52
+ end
53
+
54
+ def limit
55
+ options[:limit]
56
+ end
57
+
58
+ def buffer
59
+ options[:buffer]
60
+ end
61
+
62
+ def window
63
+ options[:limit] && options[:limit] + options[:buffer]
64
+ end
65
+
66
+ def serialize_object(object)
67
+ primary_key? ? object : object.id
68
+ end
69
+
70
+ def matches?(query)
71
+ query.calculation? ||
72
+ (query.order == ['id', order] &&
73
+ (!limit || (query.limit && query.limit + query.offset <= limit)))
74
+ end
75
+
76
+ private
77
+ def old_and_new_attribute_value_pairs(object)
78
+ old_attribute_value_pairs = []
79
+ new_attribute_value_pairs = []
80
+ @attributes.each do |name|
81
+ new_value = object.attributes[name]
82
+ original_value = object.send("#{name}_was")
83
+ old_attribute_value_pairs << [name, original_value]
84
+ new_attribute_value_pairs << [name, new_value]
85
+ end
86
+ [old_attribute_value_pairs, new_attribute_value_pairs]
87
+ end
88
+
89
+ def add_to_index_with_minimal_network_operations(attribute_value_pairs, object)
90
+ if primary_key?
91
+ add_object_to_primary_key_cache(attribute_value_pairs, object)
92
+ else
93
+ add_object_to_cache(attribute_value_pairs, object)
94
+ end
95
+ end
96
+
97
+ def primary_key?
98
+ @attributes.size == 1 && @attributes.first == primary_key
99
+ end
100
+
101
+ def add_object_to_primary_key_cache(attribute_value_pairs, object)
102
+ set(cache_key(attribute_value_pairs), [object], :ttl => ttl)
103
+ end
104
+
105
+ def cache_key(attribute_value_pairs)
106
+ attribute_value_pairs.flatten.join('/')
107
+ end
108
+
109
+ def add_object_to_cache(attribute_value_pairs, object, overwrite = true)
110
+ return if invalid_cache_key?(attribute_value_pairs)
111
+
112
+ key, cache_value, cache_hit = get_key_and_value_at_index(attribute_value_pairs)
113
+ if !cache_hit || overwrite
114
+ object_to_add = serialize_object(object)
115
+ objects = (cache_value + [object_to_add]).sort do |a, b|
116
+ (a <=> b) * (order == :asc ? 1 : -1)
117
+ end.uniq
118
+ objects = truncate_if_necessary(objects)
119
+ set(key, objects, :ttl => ttl)
120
+ incr("#{key}/count") { calculate_at_index(:count, attribute_value_pairs) }
121
+ end
122
+ end
123
+
124
+ def invalid_cache_key?(attribute_value_pairs)
125
+ attribute_value_pairs.collect { |_,value| value }.any? { |x| x.nil? }
126
+ end
127
+
128
+ def get_key_and_value_at_index(attribute_value_pairs)
129
+ key = cache_key(attribute_value_pairs)
130
+ cache_hit = true
131
+ cache_value = get(key) do
132
+ cache_hit = false
133
+ conditions = Hash[*attribute_value_pairs.flatten]
134
+ find_every_without_cache(:select => primary_key, :conditions => conditions, :limit => window).collect do |object|
135
+ serialize_object(object)
136
+ end
137
+ end
138
+ [key, cache_value, cache_hit]
139
+ end
140
+
141
+ def truncate_if_necessary(objects)
142
+ objects.slice(0, window || objects.size)
143
+ end
144
+
145
+ def calculate_at_index(operation, attribute_value_pairs)
146
+ conditions = Hash[*attribute_value_pairs.flatten]
147
+ calculate_without_cache(operation, :all, :conditions => conditions)
148
+ end
149
+
150
+ def update_index_with_minimal_network_operations(old_attribute_value_pairs, new_attribute_value_pairs, object)
151
+ if index_is_stale?(old_attribute_value_pairs, new_attribute_value_pairs)
152
+ remove_object_from_cache(old_attribute_value_pairs, object)
153
+ add_object_to_cache(new_attribute_value_pairs, object)
154
+ elsif primary_key?
155
+ add_object_to_primary_key_cache(new_attribute_value_pairs, object)
156
+ else
157
+ add_object_to_cache(new_attribute_value_pairs, object, false)
158
+ end
159
+ end
160
+
161
+ def index_is_stale?(old_attribute_value_pairs, new_attribute_value_pairs)
162
+ old_attribute_value_pairs != new_attribute_value_pairs
163
+ end
164
+
165
+ def remove_from_index_with_minimal_network_operations(attribute_value_pairs, object)
166
+ if primary_key?
167
+ remove_object_from_primary_key_cache(attribute_value_pairs, object)
168
+ else
169
+ remove_object_from_cache(attribute_value_pairs, object)
170
+ end
171
+ end
172
+
173
+ def remove_object_from_primary_key_cache(attribute_value_pairs, object)
174
+ set(cache_key(attribute_value_pairs), [], :ttl => ttl)
175
+ end
176
+
177
+ def remove_object_from_cache(attribute_value_pairs, object)
178
+ return if invalid_cache_key?(attribute_value_pairs)
179
+
180
+ key, cache_value, _ = get_key_and_value_at_index(attribute_value_pairs)
181
+ object_to_remove = serialize_object(object)
182
+ objects = cache_value - [object_to_remove]
183
+ objects = resize_if_necessary(attribute_value_pairs, objects)
184
+ set(key, objects, :ttl => ttl)
185
+ end
186
+
187
+ def resize_if_necessary(attribute_value_pairs, objects)
188
+ conditions = Hash[*attribute_value_pairs.flatten]
189
+ key = cache_key(attribute_value_pairs)
190
+ count = decr("#{key}/count") { calculate_at_index(:count, attribute_value_pairs) }
191
+ if limit && objects.size < limit && objects.size < count
192
+ find_every_without_cache(:select => :id, :conditions => conditions).collect do |object|
193
+ serialize_object(object)
194
+ end
195
+ else
196
+ objects
197
+ end
198
+ end
199
+ end
200
+ end
data/lib/cash/local.rb ADDED
@@ -0,0 +1,59 @@
1
+ module Cash
2
+ class Local
3
+ delegate :respond_to?, :to => :@remote_cache
4
+
5
+ def initialize(remote_cache)
6
+ @remote_cache = remote_cache
7
+ end
8
+
9
+ def cache_locally
10
+ @remote_cache = LocalBuffer.new(original_cache = @remote_cache)
11
+ yield
12
+ ensure
13
+ @remote_cache = original_cache
14
+ end
15
+
16
+ def method_missing(method, *args, &block)
17
+ @remote_cache.send(method, *args, &block)
18
+ end
19
+ end
20
+
21
+ class LocalBuffer
22
+ delegate :respond_to?, :to => :@remote_cache
23
+
24
+ def initialize(remote_cache)
25
+ @local_cache = {}
26
+ @remote_cache = remote_cache
27
+ end
28
+
29
+ def get(key, *options)
30
+ if @local_cache.has_key?(key)
31
+ @local_cache[key]
32
+ else
33
+ @local_cache[key] = @remote_cache.get(key, *options)
34
+ end
35
+ end
36
+
37
+ def set(key, value, *options)
38
+ @remote_cache.set(key, value, *options)
39
+ @local_cache[key] = value
40
+ end
41
+
42
+ def add(key, value, *options)
43
+ result = @remote_cache.add(key, value, *options)
44
+ if result == "STORED\r\n"
45
+ @local_cache[key] = value
46
+ end
47
+ result
48
+ end
49
+
50
+ def delete(key, *options)
51
+ @remote_cache.delete(key, *options)
52
+ @local_cache.delete(key)
53
+ end
54
+
55
+ def method_missing(method, *args, &block)
56
+ @remote_cache.send(method, *args, &block)
57
+ end
58
+ end
59
+ end
data/lib/cash/lock.rb ADDED
@@ -0,0 +1,52 @@
1
+ module Cash
2
+ class Lock
3
+ class Error < RuntimeError; end
4
+
5
+ DEFAULT_RETRY = 5
6
+ DEFAULT_EXPIRY = 30
7
+
8
+ def initialize(cache)
9
+ @cache = cache
10
+ end
11
+
12
+ def synchronize(key, lock_expiry = DEFAULT_EXPIRY, retries = DEFAULT_RETRY)
13
+ if recursive_lock?(key)
14
+ yield
15
+ else
16
+ acquire_lock(key, lock_expiry, retries)
17
+ begin
18
+ yield
19
+ ensure
20
+ release_lock(key)
21
+ end
22
+ end
23
+ end
24
+
25
+ def acquire_lock(key, lock_expiry = DEFAULT_EXPIRY, retries = DEFAULT_RETRY)
26
+ retries.times do |count|
27
+ begin
28
+ response = @cache.add("lock/#{key}", Process.pid, lock_expiry)
29
+ return if response == "STORED\r\n"
30
+ raise Error if count == retries - 1
31
+ end
32
+ exponential_sleep(count) unless count == retries - 1
33
+ end
34
+ raise Error, "Couldn't acquire memcache lock for: #{key}"
35
+ end
36
+
37
+ def release_lock(key)
38
+ @cache.delete("lock/#{key}")
39
+ end
40
+
41
+ def exponential_sleep(count)
42
+ @runtime += Benchmark::measure { sleep((2**count) / 2.0) }
43
+ end
44
+
45
+ private
46
+
47
+ def recursive_lock?(key)
48
+ @cache.get("lock/#{key}") == Process.pid
49
+ end
50
+
51
+ end
52
+ end
data/lib/cash/mock.rb ADDED
@@ -0,0 +1,82 @@
1
+ module Cash
2
+ class Mock < HashWithIndifferentAccess
3
+ attr_accessor :servers
4
+
5
+ def get_multi(*values)
6
+ reject { |k,v| !values.include? k }
7
+ end
8
+
9
+ def []=(key, value)
10
+ super(key, deep_clone(value))
11
+ end
12
+
13
+ def [](key)
14
+ deep_clone(super(key))
15
+ end
16
+
17
+ def set(key, value, *options)
18
+ self[key] = value
19
+ end
20
+
21
+ def get(key, *args)
22
+ self[key]
23
+ end
24
+
25
+ def incr(key, amount = 1)
26
+ self[key] ||= 0
27
+ self[key] += amount
28
+ end
29
+
30
+ def decr(key, amount = 1)
31
+ self[key] ||= 0
32
+ self[key] -= amount
33
+ end
34
+
35
+ def add(key, value, *options)
36
+ if self[key]
37
+ "NOT_STORED\r\n"
38
+ else
39
+ self[key] = value
40
+ "STORED\r\n"
41
+ end
42
+ end
43
+
44
+ def delete(key, *options)
45
+ self[key] = nil
46
+ end
47
+
48
+ def append(key, value)
49
+ set(key, get(key).to_s + value.to_s)
50
+ end
51
+
52
+ def namespace
53
+ nil
54
+ end
55
+
56
+ def flush_all
57
+ clear
58
+ end
59
+
60
+ def stats
61
+ {}
62
+ end
63
+
64
+ def reset_runtime
65
+ [0, Hash.new(0)]
66
+ end
67
+
68
+ private
69
+
70
+ def marshal(obj)
71
+ Marshal.dump(obj)
72
+ end
73
+
74
+ def unmarshal(marshaled_obj)
75
+ Marshal.load(marshaled_obj)
76
+ end
77
+
78
+ def deep_clone(obj)
79
+ unmarshal(marshal(obj))
80
+ end
81
+ end
82
+ end