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/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,12 @@
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
+ * printf style binds: :conditions => ["name = '%s'", "37signals!"] - not too hard to support
10
+ * objects as attributes that are serialized. story.title = {:foo => :bar}; customer.balance = Money.new(...) - these could be coerced using Column#type_cast?
11
+
12
+ With a lot of these features the issue is not technical but performance. Every special case costs some overhead.
@@ -0,0 +1,16 @@
1
+ require 'rubygems'
2
+ gem 'activesupport', '~> 2.3.0'
3
+ gem 'activerecord', '~> 2.3.0'
4
+ gem 'actionpack', '~> 2.3.0'
5
+ gem 'rspec', '>= 1.3.0'
6
+ gem 'jeweler', '~> 1.4.0'
7
+
8
+ require 'action_controller'
9
+ require 'active_record'
10
+ require 'active_record/session_store'
11
+ require 'jeweler'
12
+
13
+ ActiveRecord::Base.establish_connection(
14
+ :adapter => 'sqlite3',
15
+ :database => ':memory:'
16
+ )
@@ -0,0 +1,6 @@
1
+ test:
2
+ ttl: 604800
3
+ namespace: cache
4
+ sessions: false
5
+ debug: false
6
+ servers: localhost:11211
data/db/schema.rb ADDED
@@ -0,0 +1,18 @@
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
+
13
+ create_table :sessions, :force => true do |t|
14
+ t.string :session_id
15
+ t.text :data
16
+ t.timestamps
17
+ end
18
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'cache_money'
@@ -0,0 +1,86 @@
1
+ require 'active_support'
2
+ require 'active_record'
3
+
4
+ require 'cash/lock'
5
+ require 'cash/transactional'
6
+ require 'cash/write_through'
7
+ require 'cash/finders'
8
+ require 'cash/buffered'
9
+ require 'cash/index'
10
+ require 'cash/config'
11
+ require 'cash/accessor'
12
+
13
+ require 'cash/request'
14
+ require 'cash/fake'
15
+ require 'cash/local'
16
+
17
+ require 'cash/query/abstract'
18
+ require 'cash/query/select'
19
+ require 'cash/query/primary_key'
20
+ require 'cash/query/calculation'
21
+
22
+ require 'cash/util/array'
23
+ require 'cash/util/marshal'
24
+
25
+ class ActiveRecord::Base
26
+ def self.is_cached(options = {})
27
+ if options == false
28
+ include NoCash
29
+ else
30
+ options.assert_valid_keys(:ttl, :repository, :version)
31
+ include Cash unless ancestors.include?(Cash)
32
+ Cash::Config.create(self, options)
33
+ end
34
+ end
35
+
36
+ def <=>(other)
37
+ if self.id == other.id then
38
+ 0
39
+ else
40
+ self.id < other.id ? -1 : 1
41
+ end
42
+ end
43
+ end
44
+
45
+ module Cash
46
+ def self.included(active_record_class)
47
+ active_record_class.class_eval do
48
+ include Config, Accessor, WriteThrough, Finders
49
+ extend ClassMethods
50
+ end
51
+ end
52
+
53
+ module ClassMethods
54
+ def self.extended(active_record_class)
55
+ class << active_record_class
56
+ alias_method_chain :transaction, :cache_transaction
57
+ end
58
+ end
59
+
60
+ def transaction_with_cache_transaction(&block)
61
+ if cache_config
62
+ transaction_without_cache_transaction do
63
+ repository.transaction(&block)
64
+ end
65
+ else
66
+ transaction_without_cache_transaction(&block)
67
+ end
68
+ end
69
+
70
+ def cacheable?(*args)
71
+ true
72
+ end
73
+ end
74
+ end
75
+ module NoCash
76
+ def self.included(active_record_class)
77
+ active_record_class.class_eval do
78
+ extend ClassMethods
79
+ end
80
+ end
81
+ module ClassMethods
82
+ def cachable?(*args)
83
+ false
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,83 @@
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
+ return {} if keys.empty?
15
+
16
+ keys = keys.collect { |key| cache_key(key) }
17
+ hits = repository.get_multi(*keys)
18
+ if (missed_keys = keys - hits.keys).any?
19
+ missed_values = block.call(missed_keys)
20
+ hits.merge!(missed_keys.zip(Array(missed_values)).to_hash_without_nils)
21
+ end
22
+ hits
23
+ else
24
+ repository.get(cache_key(keys), options[:raw]) || (block ? block.call : nil)
25
+ end
26
+ end
27
+
28
+ def get(keys, options = {}, &block)
29
+ case keys
30
+ when Array
31
+ fetch(keys, options, &block)
32
+ else
33
+ fetch(keys, options) do
34
+ if block_given?
35
+ add(keys, result = yield(keys), options)
36
+ result
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ def add(key, value, options = {})
43
+ if repository.add(cache_key(key), value, options[:ttl] || cache_config.ttl, options[:raw]) == "NOT_STORED\r\n"
44
+ yield if block_given?
45
+ end
46
+ end
47
+
48
+ def set(key, value, options = {})
49
+ repository.set(cache_key(key), value, options[:ttl] || cache_config.ttl, options[:raw])
50
+ end
51
+
52
+ def incr(key, delta = 1, ttl = nil)
53
+ ttl ||= cache_config.ttl
54
+ repository.incr(cache_key = cache_key(key), delta) || begin
55
+ repository.add(cache_key, (result = yield).to_s, ttl, true) { repository.incr(cache_key) }
56
+ result
57
+ end
58
+ end
59
+
60
+ def decr(key, delta = 1, ttl = nil)
61
+ ttl ||= cache_config.ttl
62
+ repository.decr(cache_key = cache_key(key), delta) || begin
63
+ repository.add(cache_key, (result = yield).to_s, ttl, true) { repository.decr(cache_key) }
64
+ result
65
+ end
66
+ end
67
+
68
+ def expire(key)
69
+ repository.delete(cache_key(key))
70
+ end
71
+
72
+ def cache_key(key)
73
+ "#{name}:#{cache_config.version}/#{key.to_s.gsub(' ', '+')}"
74
+ end
75
+ end
76
+
77
+ module InstanceMethods
78
+ def expire
79
+ self.class.expire(id)
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,129 @@
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_without_nils
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 respond_to?(method)
84
+ @cache.respond_to?(method)
85
+ end
86
+
87
+ protected
88
+
89
+ def perform_commands
90
+ @commands.each do |command|
91
+ command.call(@cache)
92
+ end
93
+ end
94
+
95
+ def buffer_command(command)
96
+ @commands << command
97
+ end
98
+
99
+ private
100
+
101
+ def method_missing(method, *args, &block)
102
+ @cache.send(method, *args, &block)
103
+ end
104
+ end
105
+
106
+ class NestedBuffered < Buffered
107
+ def flush
108
+ perform_commands
109
+ end
110
+ end
111
+
112
+ class Command
113
+ attr_accessor :key
114
+
115
+ def initialize(name, key, *args)
116
+ @name = name
117
+ @key = key
118
+ @args = args
119
+ end
120
+
121
+ def requires_lock?
122
+ @name == :set
123
+ end
124
+
125
+ def call(cache)
126
+ cache.send @name, @key, *@args
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,82 @@
1
+ module Cash
2
+ module Config
3
+ def self.create(active_record, options, indices = [])
4
+ active_record.cache_config = Cash::Config::Config.new(active_record, options)
5
+ indices.each { |i| active_record.index i.attributes, i.options }
6
+ end
7
+
8
+ def self.included(a_module)
9
+ a_module.module_eval do
10
+ extend ClassMethods
11
+ delegate :repository, :to => "self.class"
12
+ end
13
+ end
14
+
15
+ module ClassMethods
16
+ def self.extended(a_class)
17
+ class << a_class
18
+ def cache_config
19
+ @cache_config ? @cache_config : superclass.cache_config
20
+ end
21
+
22
+ delegate :repository, :indices, :to => :cache_config
23
+ alias_method_chain :inherited, :cache_config
24
+ end
25
+ end
26
+
27
+ def inherited_with_cache_config(subclass)
28
+ inherited_without_cache_config(subclass)
29
+ @cache_config.inherit(subclass)
30
+ end
31
+
32
+ def index(attributes, options = {})
33
+ options.assert_valid_keys(:ttl, :order, :limit, :buffer, :order_column)
34
+ (@cache_config.indices.unshift(Index.new(@cache_config, self, attributes, options))).uniq!
35
+ end
36
+
37
+ def version(number)
38
+ @cache_config.options[:version] = number
39
+ end
40
+
41
+ def cache_config=(config)
42
+ @cache_config = config
43
+ end
44
+ end
45
+
46
+ class Config
47
+ attr_reader :active_record, :options
48
+
49
+ def initialize(active_record, options = {})
50
+ @active_record, @options = active_record, options
51
+ end
52
+
53
+ def repository
54
+ @options[:repository]
55
+ end
56
+
57
+ def ttl
58
+ @ttl ||= @options[:ttl] || default_ttl
59
+ end
60
+
61
+ def version
62
+ @options[:version] || 1
63
+ end
64
+
65
+ def indices
66
+ @indices ||= active_record == ActiveRecord::Base ? [] : [Index.new(self, active_record, active_record.primary_key)]
67
+ end
68
+
69
+ def inherit(active_record)
70
+ Cash::Config.create(active_record, @options, indices)
71
+ end
72
+
73
+ private
74
+
75
+ def default_ttl
76
+ default = repository.default_ttl if repository.respond_to?(:default_ttl)
77
+ default ||= 1.day
78
+ default
79
+ end
80
+ end
81
+ end
82
+ end