sreeix-cache-money 0.2.24.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/LICENSE +201 -0
- data/README +210 -0
- data/README.markdown +210 -0
- data/TODO +17 -0
- data/UNSUPPORTED_FEATURES +13 -0
- data/config/environment.rb +16 -0
- data/config/memcached.yml +6 -0
- data/db/schema.rb +18 -0
- data/init.rb +1 -0
- data/lib/cache_money.rb +91 -0
- data/lib/cash/accessor.rb +83 -0
- data/lib/cash/buffered.rb +129 -0
- data/lib/cash/config.rb +75 -0
- data/lib/cash/fake.rb +83 -0
- data/lib/cash/finders.rb +38 -0
- data/lib/cash/index.rb +214 -0
- data/lib/cash/local.rb +76 -0
- data/lib/cash/lock.rb +63 -0
- data/lib/cash/mock.rb +154 -0
- data/lib/cash/query/abstract.rb +210 -0
- data/lib/cash/query/calculation.rb +45 -0
- data/lib/cash/query/primary_key.rb +50 -0
- data/lib/cash/query/select.rb +16 -0
- data/lib/cash/request.rb +3 -0
- data/lib/cash/transactional.rb +43 -0
- data/lib/cash/util/array.rb +9 -0
- data/lib/cash/util/marshal.rb +19 -0
- data/lib/cash/write_through.rb +69 -0
- data/lib/mem_cached_session_store.rb +49 -0
- data/lib/mem_cached_support_store.rb +135 -0
- data/lib/memcached_wrapper.rb +263 -0
- data/rails/init.rb +38 -0
- data/spec/cash/accessor_spec.rb +159 -0
- data/spec/cash/active_record_spec.rb +224 -0
- data/spec/cash/buffered_spec.rb +9 -0
- data/spec/cash/calculations_spec.rb +78 -0
- data/spec/cash/finders_spec.rb +430 -0
- data/spec/cash/local_buffer_spec.rb +9 -0
- data/spec/cash/local_spec.rb +9 -0
- data/spec/cash/lock_spec.rb +108 -0
- data/spec/cash/marshal_spec.rb +60 -0
- data/spec/cash/order_spec.rb +138 -0
- data/spec/cash/shared.rb +62 -0
- data/spec/cash/transactional_spec.rb +578 -0
- data/spec/cash/window_spec.rb +195 -0
- data/spec/cash/write_through_spec.rb +245 -0
- data/spec/memcached_wrapper_test.rb +209 -0
- data/spec/spec_helper.rb +60 -0
- metadata +151 -0
@@ -0,0 +1,210 @@
|
|
1
|
+
module Cash
|
2
|
+
module Query
|
3
|
+
class Abstract
|
4
|
+
delegate :with_exclusive_scope, :get, :table_name, :indices, :find_from_ids_without_cache, :cache_key, :columns_hash, :logger, :to => :@active_record
|
5
|
+
|
6
|
+
def self.perform(*args)
|
7
|
+
new(*args).perform
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(active_record, options1, options2)
|
11
|
+
@active_record, @options1, @options2 = active_record, options1, options2 || {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def perform(find_options = {}, get_options = {})
|
15
|
+
if cache_config = cacheable?(@options1, @options2, find_options)
|
16
|
+
cache_keys, index = cache_keys(cache_config[0]), cache_config[1]
|
17
|
+
|
18
|
+
misses, missed_keys, objects = hit_or_miss(cache_keys, index, get_options)
|
19
|
+
format_results(cache_keys, choose_deserialized_objects_if_possible(missed_keys, cache_keys, misses, objects))
|
20
|
+
else
|
21
|
+
logger.debug(" \e[1;4;31mUNCACHEABLE\e[0m #{table_name} - #{find_options.inspect} - #{get_options.inspect} - #{@options1.inspect} - #{@options2.inspect}") if logger
|
22
|
+
uncacheable
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
DESC = /DESC/i
|
27
|
+
|
28
|
+
def order
|
29
|
+
@order ||= begin
|
30
|
+
if order_sql = @options1[:order] || @options2[:order]
|
31
|
+
matched, table_name, column_name, direction = *(ORDER.match(order_sql.to_s))
|
32
|
+
[column_name, direction =~ DESC ? :desc : :asc]
|
33
|
+
else
|
34
|
+
['id', :asc]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
rescue TypeError
|
38
|
+
['id', :asc]
|
39
|
+
end
|
40
|
+
|
41
|
+
def limit
|
42
|
+
@limit ||= @options1[:limit] || @options2[:limit]
|
43
|
+
end
|
44
|
+
|
45
|
+
def offset
|
46
|
+
@offset ||= @options1[:offset] || @options2[:offset] || 0
|
47
|
+
end
|
48
|
+
|
49
|
+
def calculation?
|
50
|
+
false
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
def cacheable?(*optionss)
|
55
|
+
if @active_record.respond_to?(:cacheable?) && ! @active_record.cacheable?(*optionss)
|
56
|
+
if logger
|
57
|
+
if @active_record.respond_to?(:cacheable?)
|
58
|
+
logger.debug(" \e[1;4;31mUNCACHEABLE CLASS\e[0m #{table_name}")
|
59
|
+
else
|
60
|
+
logger.debug(" \e[1;4;31mUNCACHEABLE INSTANCE\e[0m #{table_name} - #{optionss.inspect}")
|
61
|
+
end
|
62
|
+
end
|
63
|
+
return false
|
64
|
+
end
|
65
|
+
optionss.each do |options|
|
66
|
+
unless safe_options_for_cache?(options)
|
67
|
+
logger.debug(" \e[1;4;31mUNCACHEABLE UNSAFE\e[0m #{table_name} - #{options.inspect}") if logger
|
68
|
+
return false
|
69
|
+
end
|
70
|
+
end
|
71
|
+
partial_indices = optionss.collect { |options| attribute_value_pairs_for_conditions(options[:conditions]) }
|
72
|
+
return if partial_indices.include?(nil)
|
73
|
+
attribute_value_pairs = partial_indices.sum.sort { |x, y| x[0] <=> y[0] }
|
74
|
+
|
75
|
+
# attribute_value_pairs.each do |attribute_value_pair|
|
76
|
+
# return false if attribute_value_pair.last.is_a?(Array)
|
77
|
+
# end
|
78
|
+
|
79
|
+
if index = indexed_on?(attribute_value_pairs.collect { |pair| pair[0] })
|
80
|
+
if index.matches?(self)
|
81
|
+
[attribute_value_pairs, index]
|
82
|
+
else
|
83
|
+
logger.debug(" \e[1;4;31mUNCACHEABLE NO MATCHING INDEX\e[0m #{table_name} - #{index.order_column.inspect} #{index.order.inspect} #{index.limit.inspect}") if logger
|
84
|
+
false
|
85
|
+
end
|
86
|
+
else
|
87
|
+
logger.debug(" \e[1;4;31mUNCACHEABLE NOT INDEXED\e[0m #{table_name} - #{attribute_value_pairs.collect { |pair| pair[0] }.inspect}") if logger
|
88
|
+
false
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def hit_or_miss(cache_keys, index, options)
|
93
|
+
misses, missed_keys = nil, nil
|
94
|
+
objects = @active_record.get(cache_keys, options.merge(:ttl => index.ttl)) do |missed_keys|
|
95
|
+
misses = miss(missed_keys, @options1.merge(:limit => index.window))
|
96
|
+
serialize_objects(index, misses)
|
97
|
+
end
|
98
|
+
[misses, missed_keys, objects]
|
99
|
+
end
|
100
|
+
|
101
|
+
def cache_keys(attribute_value_pairs)
|
102
|
+
attribute_value_pairs.flatten.join('/')
|
103
|
+
end
|
104
|
+
|
105
|
+
def safe_options_for_cache?(options)
|
106
|
+
return false unless options.kind_of?(Hash)
|
107
|
+
options.except(:conditions, :readonly, :limit, :offset, :order).values.compact.empty? && !options[:readonly]
|
108
|
+
end
|
109
|
+
|
110
|
+
def attribute_value_pairs_for_conditions(conditions)
|
111
|
+
case conditions
|
112
|
+
when Hash
|
113
|
+
conditions.to_a.collect { |key, value| [key.to_s, value] }
|
114
|
+
when String
|
115
|
+
parse_indices_from_condition(conditions.gsub('1 = 1 AND ', '')) #ignore unnecessary conditions
|
116
|
+
when Array
|
117
|
+
parse_indices_from_condition(*conditions)
|
118
|
+
when NilClass
|
119
|
+
[]
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
AND = /\s+AND\s+/i
|
124
|
+
TABLE_AND_COLUMN = /(?:(?:`|")?(\w+)(?:`|")?\.)?(?:`|")?(\w+)(?:`|")?/ # Matches: `users`.id, `users`.`id`, users.id, id
|
125
|
+
VALUE = /'?(\d+|\?|(?:(?:[^']|'')*))'?/ # Matches: 123, ?, '123', '12''3'
|
126
|
+
KEY_EQ_VALUE = /^\(?#{TABLE_AND_COLUMN}\s+=\s+#{VALUE}\)?$/ # Matches: KEY = VALUE, (KEY = VALUE)
|
127
|
+
ORDER = /^#{TABLE_AND_COLUMN}\s*(ASC|DESC)?$/i # Matches: COLUMN ASC, COLUMN DESC, COLUMN
|
128
|
+
|
129
|
+
def parse_indices_from_condition(conditions = '', *values)
|
130
|
+
values = values.dup
|
131
|
+
conditions.split(AND).inject([]) do |indices, condition|
|
132
|
+
matched, table_name, column_name, sql_value = *(KEY_EQ_VALUE.match(condition))
|
133
|
+
if matched
|
134
|
+
# value = sql_value == '?' ? values.shift : columns_hash[column_name].type_cast(sql_value)
|
135
|
+
if sql_value == '?'
|
136
|
+
value = values.shift
|
137
|
+
else
|
138
|
+
column = columns_hash[column_name]
|
139
|
+
raise "could not find column #{column_name} in columns #{columns_hash.keys.join(',')}" if column.nil?
|
140
|
+
if sql_value[0..0] == ':' && values && values.count > 0 && values[0].is_a?(Hash)
|
141
|
+
symb = sql_value[1..-1].to_sym
|
142
|
+
value = column.type_cast(values[0][symb])
|
143
|
+
else
|
144
|
+
value = column.type_cast(sql_value)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
indices << [column_name, value]
|
148
|
+
else
|
149
|
+
return nil
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def indexed_on?(attributes)
|
155
|
+
indices.detect { |index| index == attributes }
|
156
|
+
rescue NoMethodError
|
157
|
+
nil
|
158
|
+
end
|
159
|
+
alias_method :index_for, :indexed_on?
|
160
|
+
|
161
|
+
def format_results(cache_keys, objects)
|
162
|
+
return objects if objects.blank?
|
163
|
+
|
164
|
+
objects = convert_to_array(cache_keys, objects)
|
165
|
+
objects = apply_limits_and_offsets(objects, @options1)
|
166
|
+
deserialize_objects(objects)
|
167
|
+
end
|
168
|
+
|
169
|
+
def choose_deserialized_objects_if_possible(missed_keys, cache_keys, misses, objects)
|
170
|
+
missed_keys == cache_keys ? misses : objects
|
171
|
+
end
|
172
|
+
|
173
|
+
def serialize_objects(index, objects)
|
174
|
+
Array(objects).collect { |missed| index.serialize_object(missed) }
|
175
|
+
end
|
176
|
+
|
177
|
+
def convert_to_array(cache_keys, object)
|
178
|
+
if object.kind_of?(Hash)
|
179
|
+
cache_keys.collect { |key| object[cache_key(key)] }.flatten.compact
|
180
|
+
else
|
181
|
+
Array(object)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def apply_limits_and_offsets(results, options)
|
186
|
+
results.slice((options[:offset] || 0), (options[:limit] || results.length))
|
187
|
+
end
|
188
|
+
|
189
|
+
def deserialize_objects(objects)
|
190
|
+
if objects.first.kind_of?(ActiveRecord::Base)
|
191
|
+
objects
|
192
|
+
else
|
193
|
+
cache_keys = objects.collect { |id| "id/#{id}" }
|
194
|
+
with_exclusive_scope(:find => {}) { objects = get(cache_keys, &method(:find_from_keys)) }
|
195
|
+
convert_to_array(cache_keys, objects)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def find_from_keys(*missing_keys)
|
200
|
+
missing_ids = Array(missing_keys).flatten.collect { |key| key.split('/')[2].to_i }
|
201
|
+
options = {}
|
202
|
+
order_sql = @options1[:order] || @options2[:order]
|
203
|
+
options[:order] = order_sql if order_sql
|
204
|
+
results = find_from_ids_without_cache(missing_ids, options)
|
205
|
+
results.each {|o| @active_record.add_to_caches(o) } if results && results.is_a?(Array)
|
206
|
+
results
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Cash
|
2
|
+
module Query
|
3
|
+
class Calculation < Abstract
|
4
|
+
delegate :calculate_without_cache, :incr, :to => :@active_record
|
5
|
+
|
6
|
+
def initialize(active_record, operation, column, options1, options2)
|
7
|
+
super(active_record, options1, options2)
|
8
|
+
@operation, @column = operation, column
|
9
|
+
end
|
10
|
+
|
11
|
+
def perform
|
12
|
+
super({}, :raw => true)
|
13
|
+
end
|
14
|
+
|
15
|
+
def calculation?
|
16
|
+
true
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
def miss(_, __)
|
21
|
+
calculate_without_cache(@operation, @column, @options1)
|
22
|
+
end
|
23
|
+
|
24
|
+
def uncacheable
|
25
|
+
calculate_without_cache(@operation, @column, @options1)
|
26
|
+
end
|
27
|
+
|
28
|
+
def format_results(_, objects)
|
29
|
+
objects.to_i
|
30
|
+
end
|
31
|
+
|
32
|
+
def serialize_objects(_, objects)
|
33
|
+
objects.to_s
|
34
|
+
end
|
35
|
+
|
36
|
+
def cacheable?(*optionss)
|
37
|
+
@column == :all && super(*optionss)
|
38
|
+
end
|
39
|
+
|
40
|
+
def cache_keys(attribute_value_pairs)
|
41
|
+
"#{super(attribute_value_pairs)}/#{@operation}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Cash
|
2
|
+
module Query
|
3
|
+
class PrimaryKey < Abstract
|
4
|
+
def initialize(active_record, ids, options1, options2)
|
5
|
+
super(active_record, options1, options2)
|
6
|
+
@expects_array = ids.first.kind_of?(Array)
|
7
|
+
@original_ids = ids
|
8
|
+
@ids = ids.flatten.compact.uniq.collect do |object|
|
9
|
+
object.respond_to?(:quoted_id) ? object.quoted_id : object.to_i
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def perform
|
14
|
+
return [] if @expects_array && @ids.empty?
|
15
|
+
raise ActiveRecord::RecordNotFound if @ids.empty?
|
16
|
+
|
17
|
+
super(:conditions => { :id => @ids.first })
|
18
|
+
end
|
19
|
+
|
20
|
+
protected
|
21
|
+
def deserialize_objects(objects)
|
22
|
+
convert_to_active_record_collection(super(objects))
|
23
|
+
end
|
24
|
+
|
25
|
+
def cache_keys(attribute_value_pairs)
|
26
|
+
@ids.collect { |id| "id/#{id}" }
|
27
|
+
end
|
28
|
+
|
29
|
+
def miss(missing_keys, options)
|
30
|
+
find_from_keys(*missing_keys)
|
31
|
+
end
|
32
|
+
|
33
|
+
def uncacheable
|
34
|
+
find_from_ids_without_cache(@original_ids, @options1)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
def convert_to_active_record_collection(objects)
|
39
|
+
case objects.size
|
40
|
+
when 0
|
41
|
+
raise ActiveRecord::RecordNotFound
|
42
|
+
when 1
|
43
|
+
@expects_array ? objects : objects.first
|
44
|
+
else
|
45
|
+
objects
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Cash
|
2
|
+
module Query
|
3
|
+
class Select < Abstract
|
4
|
+
delegate :find_every_without_cache, :to => :@active_record
|
5
|
+
|
6
|
+
protected
|
7
|
+
def miss(_, miss_options)
|
8
|
+
find_every_without_cache(miss_options)
|
9
|
+
end
|
10
|
+
|
11
|
+
def uncacheable
|
12
|
+
find_every_without_cache(@options1)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/cash/request.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
module Cash
|
2
|
+
class Transactional
|
3
|
+
attr_reader :memcache
|
4
|
+
|
5
|
+
def initialize(memcache, lock)
|
6
|
+
@memcache, @cache = [memcache, memcache]
|
7
|
+
@lock = lock
|
8
|
+
end
|
9
|
+
|
10
|
+
def transaction
|
11
|
+
exception_was_raised = false
|
12
|
+
begin_transaction
|
13
|
+
result = yield
|
14
|
+
rescue Object => e
|
15
|
+
exception_was_raised = true
|
16
|
+
raise
|
17
|
+
ensure
|
18
|
+
begin
|
19
|
+
@cache.flush unless exception_was_raised
|
20
|
+
ensure
|
21
|
+
end_transaction
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def respond_to?(method)
|
26
|
+
@cache.respond_to?(method)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def method_missing(method, *args, &block)
|
32
|
+
@cache.send(method, *args, &block)
|
33
|
+
end
|
34
|
+
|
35
|
+
def begin_transaction
|
36
|
+
@cache = Buffered.push(@cache, @lock)
|
37
|
+
end
|
38
|
+
|
39
|
+
def end_transaction
|
40
|
+
@cache = @cache.pop
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
class Array
|
2
|
+
alias_method :count, :size
|
3
|
+
|
4
|
+
def to_hash_without_nils
|
5
|
+
keys_and_values_without_nils = reject { |key, value| value.nil? }
|
6
|
+
shallow_flattened_keys_and_values_without_nils = keys_and_values_without_nils.inject([]) { |result, pair| result += pair }
|
7
|
+
Hash[*shallow_flattened_keys_and_values_without_nils]
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Marshal
|
2
|
+
class << self
|
3
|
+
def constantize(name)
|
4
|
+
name.constantize
|
5
|
+
end
|
6
|
+
|
7
|
+
def load_with_constantize(value)
|
8
|
+
begin
|
9
|
+
Marshal.load_without_constantize value
|
10
|
+
rescue ArgumentError => e
|
11
|
+
_, class_name = *(/undefined class\/module ([\w:]*\w)/.match(e.message))
|
12
|
+
raise if !class_name
|
13
|
+
constantize(class_name)
|
14
|
+
Marshal.load value
|
15
|
+
end
|
16
|
+
end
|
17
|
+
alias_method_chain :load, :constantize
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Cash
|
2
|
+
module WriteThrough
|
3
|
+
DEFAULT_TTL = 12.hours
|
4
|
+
|
5
|
+
def self.included(active_record_class)
|
6
|
+
active_record_class.class_eval do
|
7
|
+
include InstanceMethods
|
8
|
+
extend ClassMethods
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module InstanceMethods
|
13
|
+
def self.included(active_record_class)
|
14
|
+
active_record_class.class_eval do
|
15
|
+
after_create :add_to_caches
|
16
|
+
after_update :update_caches
|
17
|
+
after_destroy :remove_from_caches
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def add_to_caches
|
22
|
+
InstanceMethods.unfold(self.class, :add_to_caches, self)
|
23
|
+
end
|
24
|
+
|
25
|
+
def update_caches
|
26
|
+
InstanceMethods.unfold(self.class, :update_caches, self)
|
27
|
+
end
|
28
|
+
|
29
|
+
def remove_from_caches
|
30
|
+
return if new_record?
|
31
|
+
InstanceMethods.unfold(self.class, :remove_from_caches, self)
|
32
|
+
end
|
33
|
+
|
34
|
+
def expire_caches
|
35
|
+
InstanceMethods.unfold(self.class, :expire_caches, self)
|
36
|
+
end
|
37
|
+
|
38
|
+
def shallow_clone
|
39
|
+
self.class.send(:instantiate, instance_variable_get(:@attributes))
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
def self.unfold(klass, operation, object)
|
44
|
+
while klass < ActiveRecord::Base && klass.ancestors.include?(WriteThrough)
|
45
|
+
klass.send(operation, object)
|
46
|
+
klass = klass.superclass
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
module ClassMethods
|
52
|
+
def add_to_caches(object)
|
53
|
+
indices.each { |index| index.add(object) } if cache_config
|
54
|
+
end
|
55
|
+
|
56
|
+
def update_caches(object)
|
57
|
+
indices.each { |index| index.update(object) } if cache_config
|
58
|
+
end
|
59
|
+
|
60
|
+
def remove_from_caches(object)
|
61
|
+
indices.each { |index| index.remove(object) } if cache_config
|
62
|
+
end
|
63
|
+
|
64
|
+
def expire_caches(object)
|
65
|
+
indices.each { |index| index.delete(object) } if cache_config
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# begin
|
2
|
+
require 'memcached'
|
3
|
+
|
4
|
+
class MemCachedSessionStore < ActionController::Session::AbstractStore
|
5
|
+
def initialize(app, options = {})
|
6
|
+
# Support old :expires option
|
7
|
+
options[:expire_after] ||= options[:expires]
|
8
|
+
|
9
|
+
super
|
10
|
+
|
11
|
+
@default_options = {
|
12
|
+
:namespace => 'rack:session',
|
13
|
+
:servers => 'localhost:11211'
|
14
|
+
}.merge(@default_options)
|
15
|
+
|
16
|
+
@default_options[:prefix_key] ||= @default_options[:namespace]
|
17
|
+
|
18
|
+
@pool = options[:cache] || Memcached.new(@default_options[:servers], @default_options)
|
19
|
+
# unless @pool.servers.any? { |s| s.alive? }
|
20
|
+
# raise "#{self} unable to find server during initialization."
|
21
|
+
# end
|
22
|
+
@mutex = Mutex.new
|
23
|
+
|
24
|
+
super
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
def get_session(env, sid)
|
29
|
+
sid ||= generate_sid
|
30
|
+
begin
|
31
|
+
session = @pool.get(sid) || {}
|
32
|
+
rescue Memcached::NotFound, MemCache::MemCacheError, Errno::ECONNREFUSED
|
33
|
+
session = {}
|
34
|
+
end
|
35
|
+
[sid, session]
|
36
|
+
end
|
37
|
+
|
38
|
+
def set_session(env, sid, session_data)
|
39
|
+
options = env['rack.session.options']
|
40
|
+
expiry = options[:expire_after] || 0
|
41
|
+
@pool.set(sid, session_data, expiry)
|
42
|
+
return true
|
43
|
+
rescue Memcached::NotStored, MemCache::MemCacheError, Errno::ECONNREFUSED
|
44
|
+
return false
|
45
|
+
end
|
46
|
+
end
|
47
|
+
# rescue LoadError
|
48
|
+
# # Memcached wasn't available so neither can the store be
|
49
|
+
# end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'memcached'
|
2
|
+
|
3
|
+
# A cache store implementation which stores data in Memcached:
|
4
|
+
# http://www.danga.com/memcached/
|
5
|
+
#
|
6
|
+
# This is currently the most popular cache store for production websites.
|
7
|
+
#
|
8
|
+
# Special features:
|
9
|
+
# - Clustering and load balancing. One can specify multiple memcached servers,
|
10
|
+
# and MemCacheStore will load balance between all available servers. If a
|
11
|
+
# server goes down, then MemCacheStore will ignore it until it goes back
|
12
|
+
# online.
|
13
|
+
# - Time-based expiry support. See #write and the +:expires_in+ option.
|
14
|
+
# - Per-request in memory cache for all communication with the MemCache server(s).
|
15
|
+
class MemCachedSupportStore < ActiveSupport::Cache::Store
|
16
|
+
|
17
|
+
attr_reader :addresses
|
18
|
+
|
19
|
+
# Creates a new MemCacheStore object, with the given memcached server
|
20
|
+
# addresses. Each address is either a host name, or a host-with-port string
|
21
|
+
# in the form of "host_name:port". For example:
|
22
|
+
#
|
23
|
+
# ActiveSupport::Cache::MemCacheStore.new("localhost", "server-downstairs.localnetwork:8229")
|
24
|
+
#
|
25
|
+
# If no addresses are specified, then MemCacheStore will connect to
|
26
|
+
# localhost port 11211 (the default memcached port).
|
27
|
+
def initialize(*addresses)
|
28
|
+
addresses = addresses.flatten
|
29
|
+
options = addresses.extract_options!
|
30
|
+
options[:prefix_key] ||= options[:namespace]
|
31
|
+
addresses = ["localhost"] if addresses.empty?
|
32
|
+
@addresses = addresses
|
33
|
+
@data = Memcached.new(addresses, options)
|
34
|
+
|
35
|
+
extend ActiveSupport::Cache::Strategy::LocalCache
|
36
|
+
end
|
37
|
+
|
38
|
+
def read(key, options = nil) # :nodoc:
|
39
|
+
super
|
40
|
+
@data.get(key, marshal?(options))
|
41
|
+
rescue Memcached::NotFound
|
42
|
+
nil
|
43
|
+
rescue Memcached::Error => e
|
44
|
+
logger.error("MemcachedError (#{e}): #{e.message}")
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
|
48
|
+
# Writes a value to the cache.
|
49
|
+
#
|
50
|
+
# Possible options:
|
51
|
+
# - +:unless_exist+ - set to true if you don't want to update the cache
|
52
|
+
# if the key is already set.
|
53
|
+
# - +:expires_in+ - the number of seconds that this value may stay in
|
54
|
+
# the cache. See ActiveSupport::Cache::Store#write for an example.
|
55
|
+
def write(key, value, options = nil)
|
56
|
+
super
|
57
|
+
method = options && options[:unless_exist] ? :add : :set
|
58
|
+
# memcache-client will break the connection if you send it an integer
|
59
|
+
# in raw mode, so we convert it to a string to be sure it continues working.
|
60
|
+
@data.send(method, key, value, expires_in(options), marshal?(options))
|
61
|
+
true
|
62
|
+
rescue Memcached::NotStored
|
63
|
+
false
|
64
|
+
rescue Memcached::NotFound
|
65
|
+
false
|
66
|
+
rescue Memcached::Error => e
|
67
|
+
logger.error("MemcachedError (#{e}): #{e.message}")
|
68
|
+
false
|
69
|
+
end
|
70
|
+
|
71
|
+
def delete(key, options = nil) # :nodoc:
|
72
|
+
super
|
73
|
+
@data.delete(key)
|
74
|
+
true
|
75
|
+
rescue Memcached::NotFound
|
76
|
+
false
|
77
|
+
rescue Memcached::Error => e
|
78
|
+
logger.error("MemcachedError (#{e}): #{e.message}")
|
79
|
+
false
|
80
|
+
end
|
81
|
+
|
82
|
+
def exist?(key, options = nil) # :nodoc:
|
83
|
+
# Doesn't call super, cause exist? in memcache is in fact a read
|
84
|
+
# But who cares? Reading is very fast anyway
|
85
|
+
# Local cache is checked first, if it doesn't know then memcache itself is read from
|
86
|
+
!read(key, options).nil?
|
87
|
+
end
|
88
|
+
|
89
|
+
def increment(key, amount = 1) # :nodoc:
|
90
|
+
log("incrementing", key, amount)
|
91
|
+
|
92
|
+
@data.incr(key, amount)
|
93
|
+
response
|
94
|
+
rescue Memcached::NotFound
|
95
|
+
nil
|
96
|
+
rescue Memcached::Error
|
97
|
+
nil
|
98
|
+
end
|
99
|
+
|
100
|
+
def decrement(key, amount = 1) # :nodoc:
|
101
|
+
log("decrement", key, amount)
|
102
|
+
@data.decr(key, amount)
|
103
|
+
response
|
104
|
+
rescue Memcached::NotFound
|
105
|
+
nil
|
106
|
+
rescue Memcached::Error
|
107
|
+
nil
|
108
|
+
end
|
109
|
+
|
110
|
+
def delete_matched(matcher, options = nil) # :nodoc:
|
111
|
+
# don't do any local caching at present, just pass
|
112
|
+
# through and let the error happen
|
113
|
+
super
|
114
|
+
raise "Not supported by Memcache"
|
115
|
+
end
|
116
|
+
|
117
|
+
def clear
|
118
|
+
@data.flush
|
119
|
+
rescue Memcached::NotFound
|
120
|
+
end
|
121
|
+
|
122
|
+
def stats
|
123
|
+
@data.stats
|
124
|
+
rescue Memcached::NotFound
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
def expires_in(options)
|
129
|
+
(options && options[:expires_in] && options[:expires_in].to_i) || 0
|
130
|
+
end
|
131
|
+
|
132
|
+
def marshal?(options)
|
133
|
+
!(options && options[:raw])
|
134
|
+
end
|
135
|
+
end
|