artemk-cache-money 0.2.13.2

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.
Files changed (48) hide show
  1. data/LICENSE +201 -0
  2. data/README +210 -0
  3. data/README.markdown +210 -0
  4. data/TODO +17 -0
  5. data/UNSUPPORTED_FEATURES +12 -0
  6. data/config/environment.rb +16 -0
  7. data/config/memcached.yml +6 -0
  8. data/db/schema.rb +18 -0
  9. data/init.rb +1 -0
  10. data/lib/cache_money.rb +86 -0
  11. data/lib/cash/accessor.rb +83 -0
  12. data/lib/cash/buffered.rb +129 -0
  13. data/lib/cash/config.rb +82 -0
  14. data/lib/cash/fake.rb +83 -0
  15. data/lib/cash/finders.rb +38 -0
  16. data/lib/cash/index.rb +214 -0
  17. data/lib/cash/local.rb +76 -0
  18. data/lib/cash/lock.rb +63 -0
  19. data/lib/cash/mock.rb +154 -0
  20. data/lib/cash/query/abstract.rb +197 -0
  21. data/lib/cash/query/calculation.rb +45 -0
  22. data/lib/cash/query/primary_key.rb +50 -0
  23. data/lib/cash/query/select.rb +16 -0
  24. data/lib/cash/request.rb +3 -0
  25. data/lib/cash/transactional.rb +43 -0
  26. data/lib/cash/util/active_record.rb +5 -0
  27. data/lib/cash/util/array.rb +9 -0
  28. data/lib/cash/util/marshal.rb +19 -0
  29. data/lib/cash/write_through.rb +69 -0
  30. data/lib/mem_cached_session_store.rb +50 -0
  31. data/lib/mem_cached_support_store.rb +141 -0
  32. data/lib/memcached_wrapper.rb +261 -0
  33. data/spec/cash/accessor_spec.rb +186 -0
  34. data/spec/cash/active_record_spec.rb +224 -0
  35. data/spec/cash/buffered_spec.rb +9 -0
  36. data/spec/cash/calculations_spec.rb +67 -0
  37. data/spec/cash/finders_spec.rb +408 -0
  38. data/spec/cash/local_buffer_spec.rb +9 -0
  39. data/spec/cash/local_spec.rb +9 -0
  40. data/spec/cash/lock_spec.rb +108 -0
  41. data/spec/cash/marshal_spec.rb +60 -0
  42. data/spec/cash/order_spec.rb +172 -0
  43. data/spec/cash/transactional_spec.rb +578 -0
  44. data/spec/cash/window_spec.rb +195 -0
  45. data/spec/cash/write_through_spec.rb +245 -0
  46. data/spec/memcached_wrapper_test.rb +209 -0
  47. data/spec/spec_helper.rb +68 -0
  48. metadata +168 -0
data/lib/cash/mock.rb ADDED
@@ -0,0 +1,154 @@
1
+ module Cash
2
+ class Mock < HashWithIndifferentAccess
3
+ attr_accessor :servers
4
+
5
+ class CacheEntry
6
+ attr_reader :value
7
+
8
+ def self.default_ttl
9
+ 1_000_000
10
+ end
11
+
12
+ def self.now
13
+ Time.now
14
+ end
15
+
16
+ def initialize(value, raw, ttl)
17
+ if raw
18
+ @value = value.to_s
19
+ else
20
+ @value = Marshal.dump(value)
21
+ end
22
+
23
+ if ttl.zero?
24
+ @ttl = self.class.default_ttl
25
+ else
26
+ @ttl = ttl
27
+ end
28
+
29
+ @expires_at = self.class.now + @ttl
30
+ end
31
+
32
+
33
+ def expired?
34
+ self.class.now > @expires_at
35
+ end
36
+
37
+ def increment(amount = 1)
38
+ @value = (@value.to_i + amount).to_s
39
+ end
40
+
41
+ def decrement(amount = 1)
42
+ @value = (@value.to_i - amount).to_s
43
+ end
44
+
45
+ def unmarshal
46
+ Marshal.load(@value)
47
+ end
48
+
49
+ def to_i
50
+ @value.to_i
51
+ end
52
+ end
53
+
54
+ attr_accessor :logging
55
+
56
+ def initialize
57
+ @logging = false
58
+ end
59
+
60
+ def get_multi(keys)
61
+ slice(*keys).collect { |k,v| [k, v.unmarshal] }.to_hash_without_nils
62
+ end
63
+
64
+ def set(key, value, ttl = CacheEntry.default_ttl, raw = false)
65
+ log "< set #{key} #{ttl}"
66
+ self[key] = CacheEntry.new(value, raw, ttl)
67
+ log('> STORED')
68
+ end
69
+
70
+ def get(key, raw = false)
71
+ log "< get #{key}"
72
+ unless self.has_unexpired_key?(key)
73
+ log('> END')
74
+ return nil
75
+ end
76
+
77
+ log("> sending key #{key}")
78
+ log('> END')
79
+ if raw
80
+ self[key].value
81
+ else
82
+ self[key].unmarshal
83
+ end
84
+ end
85
+
86
+ def delete(key, options = {})
87
+ log "< delete #{key}"
88
+ if self.has_unexpired_key?(key)
89
+ log "> DELETED"
90
+ super(key)
91
+ else
92
+ log "> NOT FOUND"
93
+ end
94
+ end
95
+
96
+ def incr(key, amount = 1)
97
+ if self.has_unexpired_key?(key)
98
+ self[key].increment(amount)
99
+ self[key].to_i
100
+ end
101
+ end
102
+
103
+ def decr(key, amount = 1)
104
+ if self.has_unexpired_key?(key)
105
+ self[key].decrement(amount)
106
+ self[key].to_i
107
+ end
108
+ end
109
+
110
+ def add(key, value, ttl = CacheEntry.default_ttl, raw = false)
111
+ if self.has_unexpired_key?(key)
112
+ "NOT_STORED\r\n"
113
+ else
114
+ set(key, value, ttl, raw)
115
+ "STORED\r\n"
116
+ end
117
+ end
118
+
119
+ def append(key, value)
120
+ set(key, get(key, true).to_s + value.to_s, nil, true)
121
+ end
122
+
123
+ def namespace
124
+ nil
125
+ end
126
+
127
+ def flush_all
128
+ log('< flush_all')
129
+ clear
130
+ end
131
+
132
+ def stats
133
+ {}
134
+ end
135
+
136
+ def reset_runtime
137
+ [0, Hash.new(0)]
138
+ end
139
+
140
+ def has_unexpired_key?(key)
141
+ self.has_key?(key) && !self[key].expired?
142
+ end
143
+
144
+ def log(message)
145
+ return unless logging
146
+ logger.debug(message)
147
+ end
148
+
149
+ def logger
150
+ @logger ||= ActiveSupport::BufferedLogger.new(Rails.root.join('log/cash_mock.log'))
151
+ end
152
+
153
+ end
154
+ end
@@ -0,0 +1,197 @@
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
+
13
+ # if @options2.empty? and active_record.base_class != active_record
14
+ # @options2 = { :conditions => { active_record.inheritance_column => active_record.to_s }}
15
+ # end
16
+ # if active_record.base_class != active_record
17
+ # @options2[:conditions] = active_record.merge_conditions(
18
+ # @options2[:conditions], { active_record.inheritance_column => active_record.to_s }
19
+ # )
20
+ # end
21
+ end
22
+
23
+ def perform(find_options = {}, get_options = {})
24
+ if cache_config = cacheable?(@options1, @options2, find_options)
25
+ cache_keys, index = cache_keys(cache_config[0]), cache_config[1]
26
+
27
+ misses, missed_keys, objects = hit_or_miss(cache_keys, index, get_options)
28
+ format_results(cache_keys, choose_deserialized_objects_if_possible(missed_keys, cache_keys, misses, objects))
29
+ else
30
+ logger.debug(" \e[1;4;31mUNCACHEABLE\e[0m #{table_name} - #{find_options.inspect} - #{get_options.inspect} - #{@options1.inspect} - #{@options2.inspect}") if logger
31
+ uncacheable
32
+ end
33
+ end
34
+
35
+ DESC = /DESC/i
36
+
37
+ def order
38
+ @order ||= begin
39
+ if order_sql = @options1[:order] || @options2[:order]
40
+ matched, table_name, column_name, direction = *(ORDER.match(order_sql.to_s))
41
+ [column_name, direction =~ DESC ? :desc : :asc]
42
+ else
43
+ ['id', :asc]
44
+ end
45
+ end
46
+ rescue TypeError
47
+ ['id', :asc]
48
+ end
49
+
50
+ def limit
51
+ @limit ||= @options1[:limit] || @options2[:limit]
52
+ end
53
+
54
+ def offset
55
+ @offset ||= @options1[:offset] || @options2[:offset] || 0
56
+ end
57
+
58
+ def calculation?
59
+ false
60
+ end
61
+
62
+ private
63
+ def cacheable?(*optionss)
64
+ return false if @active_record.respond_to?(:cachable?) && ! @active_record.cachable?(*optionss)
65
+ optionss.each { |options| return unless safe_options_for_cache?(options) }
66
+ partial_indices = optionss.collect { |options| attribute_value_pairs_for_conditions(options[:conditions]) }
67
+ return if partial_indices.include?(nil)
68
+ attribute_value_pairs = partial_indices.sum.sort { |x, y| x[0] <=> y[0] }
69
+
70
+ # attribute_value_pairs.each do |attribute_value_pair|
71
+ # return false if attribute_value_pair.last.is_a?(Array)
72
+ # end
73
+
74
+ if index = indexed_on?(attribute_value_pairs.collect { |pair| pair[0] })
75
+ if index.matches?(self)
76
+ [attribute_value_pairs, index]
77
+ end
78
+ end
79
+ end
80
+
81
+ def hit_or_miss(cache_keys, index, options)
82
+ misses, missed_keys = nil, nil
83
+ objects = @active_record.get(cache_keys, options.merge(:ttl => index.ttl)) do |missed_keys|
84
+ misses = miss(missed_keys, @options1.merge(:limit => index.window))
85
+ serialize_objects(index, misses)
86
+ end
87
+ [misses, missed_keys, objects]
88
+ end
89
+
90
+ def cache_keys(attribute_value_pairs)
91
+ attribute_value_pairs.flatten.join('/')
92
+ end
93
+
94
+ def safe_options_for_cache?(options)
95
+ return false unless options.kind_of?(Hash)
96
+ options.except(:conditions, :readonly, :limit, :offset, :order).values.compact.empty? && !options[:readonly]
97
+ end
98
+
99
+ def attribute_value_pairs_for_conditions(conditions)
100
+ case conditions
101
+ when Hash
102
+ conditions.to_a.collect { |key, value| [key.to_s, value] }
103
+ when String
104
+ parse_indices_from_condition(conditions.gsub('1 = 1 AND ', '')) #ignore unnecessary conditions
105
+ when Array
106
+ parse_indices_from_condition(*conditions)
107
+ when NilClass
108
+ []
109
+ end
110
+ end
111
+
112
+ AND = /\s+AND\s+/i
113
+ TABLE_AND_COLUMN = /(?:(?:`|")?(\w+)(?:`|")?\.)?(?:`|")?(\w+)(?:`|")?/ # Matches: `users`.id, `users`.`id`, users.id, id
114
+ VALUE = /'?(\d+|\?|(?:(?:[^']|'')*))'?/ # Matches: 123, ?, '123', '12''3'
115
+ KEY_EQ_VALUE = /^\(?#{TABLE_AND_COLUMN}\s+=\s+#{VALUE}\)?$/ # Matches: KEY = VALUE, (KEY = VALUE)
116
+ ORDER = /^#{TABLE_AND_COLUMN}\s*(ASC|DESC)?$/i # Matches: COLUMN ASC, COLUMN DESC, COLUMN
117
+
118
+ def parse_indices_from_condition(conditions = '', *values)
119
+ values = values.dup
120
+ conditions.split(AND).inject([]) do |indices, condition|
121
+ matched, table_name, column_name, sql_value = *(KEY_EQ_VALUE.match(condition))
122
+ if matched
123
+ # value = sql_value == '?' ? values.shift : columns_hash[column_name].type_cast(sql_value)
124
+ if sql_value == '?'
125
+ value = values.shift
126
+ else
127
+ column = columns_hash[column_name]
128
+ raise "could not find column #{column_name} in columns #{columns_hash.keys.join(',')}" if column.nil?
129
+ if sql_value[0..0] == ':' && values && values.count > 0 && values[0].is_a?(Hash)
130
+ symb = sql_value[1..-1].to_sym
131
+ value = column.type_cast(values[0][symb])
132
+ else
133
+ value = column.type_cast(sql_value)
134
+ end
135
+ end
136
+ indices << [column_name, value]
137
+ else
138
+ return nil
139
+ end
140
+ end
141
+ end
142
+
143
+ def indexed_on?(attributes)
144
+ indices.detect { |index| index == attributes }
145
+ rescue NoMethodError
146
+ nil
147
+ end
148
+ alias_method :index_for, :indexed_on?
149
+
150
+ def format_results(cache_keys, objects)
151
+ return objects if objects.blank?
152
+
153
+ objects = convert_to_array(cache_keys, objects)
154
+ objects = apply_limits_and_offsets(objects, @options1)
155
+ deserialize_objects(objects)
156
+ end
157
+
158
+ def choose_deserialized_objects_if_possible(missed_keys, cache_keys, misses, objects)
159
+ missed_keys == cache_keys ? misses : objects
160
+ end
161
+
162
+ def serialize_objects(index, objects)
163
+ Array(objects).collect { |missed| index.serialize_object(missed) }
164
+ end
165
+
166
+ def convert_to_array(cache_keys, object)
167
+ if object.kind_of?(Hash)
168
+ cache_keys.collect { |key| object[cache_key(key)] }.flatten.compact
169
+ else
170
+ Array(object)
171
+ end
172
+ end
173
+
174
+ def apply_limits_and_offsets(results, options)
175
+ results.slice((options[:offset] || 0), (options[:limit] || results.length))
176
+ end
177
+
178
+ def deserialize_objects(objects)
179
+ if objects.first.kind_of?(ActiveRecord::Base)
180
+ objects
181
+ else
182
+ cache_keys = objects.collect { |id| "id/#{id}" }
183
+ objects = get(cache_keys, &method(:find_from_keys))
184
+ convert_to_array(cache_keys, objects)
185
+ end
186
+ end
187
+
188
+ def find_from_keys(*missing_keys)
189
+ missing_ids = Array(missing_keys).flatten.collect { |key| key.split('/')[2].to_i }
190
+ options = {}
191
+ order_sql = @options1[:order] || @options2[:order]
192
+ options[:order] = order_sql if order_sql
193
+ with_exclusive_scope { find_from_ids_without_cache(missing_ids, options) }
194
+ end
195
+ end
196
+ end
197
+ 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