ngmoco-cache-money 0.2.23 → 0.2.24.2

Sign up to get free protection for your applications and to get access to all the features.
data/README CHANGED
@@ -117,9 +117,7 @@ Nested transactions are fully supported, with partial rollback and (apparent) pa
117
117
 
118
118
  ### Mocks ###
119
119
 
120
- For your unit tests, it is faster to use a Memcached mock than the real deal. Just place this in your initializer for your test environment:
121
-
122
- $memcache = Cash::Mock.new
120
+ For your unit tests, it is faster to use a Memcached mock than the real deal. In your test environment, initialize the repository with an instance of Cash::Mock.
123
121
 
124
122
  ### Locks ###
125
123
 
@@ -133,39 +131,41 @@ In most cases locks are unnecessary; the transactional Memcached client will tak
133
131
 
134
132
  Sometimes your code will request the same cache key twice in one request. You can avoid a round trip to the Memcached server by using a local, per-request cache. Add this to your initializer:
135
133
 
136
- $local = Cash::Local.new($memcache)
137
- $cache = Cash::Transactional.new($local, $lock)
138
-
134
+ $memcache = MemcachedWrapper.new(config[:servers].gsub(' ', '').split(','), config)
135
+ $local = Cash::Local.new($memcache)
136
+ $lock = Cash::Lock.new($memcache)
137
+ $cache = Cash::Transactional.new($local, $lock)
138
+
139
139
  ## Installation ##
140
140
 
141
- #### Step 0: Install MemCached
142
-
143
141
  #### Step 1: Get the GEM ####
144
142
 
145
- % gem sources -a http://gems.github.com
146
143
  % sudo gem install ngmoco-cache-money
147
144
 
148
- #### Step 2: Configure MemCached.
145
+ Add the gem you your Gemfile:
146
+ gem 'ngmoco-cache-money', :lib => 'cache_money'
147
+
148
+ #### Step 2: Configure cache client
149
+
150
+ In your environment, create a cache client instance configured for your cache servers.
151
+
152
+ $memcached = Memcached.new( ...servers..., ...options...)
149
153
 
150
- Place a YAML file in `config/memcached.yml` with contents like:
154
+ Currently supported cache clients are: memcached, memcache-client
151
155
 
152
- test:
153
- ttl: 604800
154
- namespace: ...
155
- sessions: false
156
- debug: false
157
- servers: localhost:11211
158
- cache_money: true
156
+ #### Step 3: Configure Caching
159
157
 
160
- development:
161
- ....
158
+ Add the following to an initializer:
162
159
 
163
- #### Step 3: `config/environment.rb` ####
164
- config.gem "ngmoco-cache-money",
165
- :lib => "cache_money",
166
- :source => 'http://gems.github.com',
167
- :version => '0.2.9'
160
+ Cash.configure :repository => $memcached, :adapter => :memcached
168
161
 
162
+ Supported adapters are :memcache_client, :memcached. :memcached is assumed and is only compatible with Memcached clients.
163
+ Local or transactional semantics may be disabled by setting :local => false or :transactional => false.
164
+
165
+ Caching can be disabled on a per-environment basis in the environment's initializer:
166
+
167
+ Cash.enabled = false
168
+
169
169
  #### Step 4: Add indices to your ActiveRecord models ####
170
170
 
171
171
  Queries like `User.find(1)` will use the cache automatically. For more complex queries you must add indices on the attributes that you will query on. For example, a query like `User.find(:all, :conditions => {:name => 'bob'})` will require an index like:
@@ -187,24 +187,20 @@ There may be times where you only want to cache some of your models instead of e
187
187
  In that case, you can omit the following from your `config/initializers/cache_money.rb`
188
188
 
189
189
  class ActiveRecord::Base
190
- is_cached :repository => $cache
190
+ is_cached
191
191
  end
192
192
 
193
193
  After that is removed, you can simple put this at the top of your models you wish to cache:
194
194
 
195
- is_cached :repository => $cache
196
-
197
- Just make sure that you put that line before any of your index directives.
198
-
199
- ## Version ##
195
+ is_cached
200
196
 
201
- WARNING: This is currently a RELEASE CANDIDATE. A version of this code is in production use at Twitter but the extraction and refactoring process may have introduced bugs and/or performance problems. There are no known major defects at this point, but still.
197
+ Just make sure that you put that line before any of your index directives. Note that all subclasses of a cached model are also cached.
202
198
 
203
199
  ## Acknowledgments ##
204
200
 
205
201
  Thanks to
206
202
 
207
203
  * Twitter for commissioning the development of this library and supporting the effort to open-source it.
208
- * Sam Luckenbill for pairing with me on most of the hard stuff.
204
+ * Sam Luckenbill for pairing with Nick on most of the hard stuff.
209
205
  * Matthew and Chris for pairing a few days, offering useful feedback on the readability of the code, and the initial implementation of the Memcached mock.
210
- * Evan Weaver for helping to reason-through software and testing strategies to deal with replication lag, and the initial implementation of the Memcached lock.
206
+ * Evan Weaver for helping to reason-through software and testing strategies to deal with replication lag, and the initial implementation of the Memcached lock.
@@ -117,9 +117,7 @@ Nested transactions are fully supported, with partial rollback and (apparent) pa
117
117
 
118
118
  ### Mocks ###
119
119
 
120
- For your unit tests, it is faster to use a Memcached mock than the real deal. Just place this in your initializer for your test environment:
121
-
122
- $memcache = Cash::Mock.new
120
+ For your unit tests, it is faster to use a Memcached mock than the real deal. In your test environment, initialize the repository with an instance of Cash::Mock.
123
121
 
124
122
  ### Locks ###
125
123
 
@@ -133,39 +131,41 @@ In most cases locks are unnecessary; the transactional Memcached client will tak
133
131
 
134
132
  Sometimes your code will request the same cache key twice in one request. You can avoid a round trip to the Memcached server by using a local, per-request cache. Add this to your initializer:
135
133
 
136
- $local = Cash::Local.new($memcache)
137
- $cache = Cash::Transactional.new($local, $lock)
138
-
134
+ $memcache = MemcachedWrapper.new(config[:servers].gsub(' ', '').split(','), config)
135
+ $local = Cash::Local.new($memcache)
136
+ $lock = Cash::Lock.new($memcache)
137
+ $cache = Cash::Transactional.new($local, $lock)
138
+
139
139
  ## Installation ##
140
140
 
141
- #### Step 0: Install MemCached
142
-
143
141
  #### Step 1: Get the GEM ####
144
142
 
145
- % gem sources -a http://gems.github.com
146
143
  % sudo gem install ngmoco-cache-money
147
144
 
148
- #### Step 2: Configure MemCached.
145
+ Add the gem you your Gemfile:
146
+ gem 'ngmoco-cache-money', :lib => 'cache_money'
147
+
148
+ #### Step 2: Configure cache client
149
+
150
+ In your environment, create a cache client instance configured for your cache servers.
151
+
152
+ $memcached = Memcached.new( ...servers..., ...options...)
149
153
 
150
- Place a YAML file in `config/memcached.yml` with contents like:
154
+ Currently supported cache clients are: memcached, memcache-client
151
155
 
152
- test:
153
- ttl: 604800
154
- namespace: ...
155
- sessions: false
156
- debug: false
157
- servers: localhost:11211
158
- cache_money: true
156
+ #### Step 3: Configure Caching
159
157
 
160
- development:
161
- ....
158
+ Add the following to an initializer:
162
159
 
163
- #### Step 3: `config/environment.rb` ####
164
- config.gem "ngmoco-cache-money",
165
- :lib => "cache_money",
166
- :source => 'http://gems.github.com',
167
- :version => '0.2.9'
160
+ Cash.configure :repository => $memcached, :adapter => :memcached
168
161
 
162
+ Supported adapters are :memcache_client, :memcached. :memcached is assumed and is only compatible with Memcached clients.
163
+ Local or transactional semantics may be disabled by setting :local => false or :transactional => false.
164
+
165
+ Caching can be disabled on a per-environment basis in the environment's initializer:
166
+
167
+ Cash.enabled = false
168
+
169
169
  #### Step 4: Add indices to your ActiveRecord models ####
170
170
 
171
171
  Queries like `User.find(1)` will use the cache automatically. For more complex queries you must add indices on the attributes that you will query on. For example, a query like `User.find(:all, :conditions => {:name => 'bob'})` will require an index like:
@@ -187,24 +187,20 @@ There may be times where you only want to cache some of your models instead of e
187
187
  In that case, you can omit the following from your `config/initializers/cache_money.rb`
188
188
 
189
189
  class ActiveRecord::Base
190
- is_cached :repository => $cache
190
+ is_cached
191
191
  end
192
192
 
193
193
  After that is removed, you can simple put this at the top of your models you wish to cache:
194
194
 
195
- is_cached :repository => $cache
196
-
197
- Just make sure that you put that line before any of your index directives.
198
-
199
- ## Version ##
195
+ is_cached
200
196
 
201
- WARNING: This is currently a RELEASE CANDIDATE. A version of this code is in production use at Twitter but the extraction and refactoring process may have introduced bugs and/or performance problems. There are no known major defects at this point, but still.
197
+ Just make sure that you put that line before any of your index directives. Note that all subclasses of a cached model are also cached.
202
198
 
203
199
  ## Acknowledgments ##
204
200
 
205
201
  Thanks to
206
202
 
207
203
  * Twitter for commissioning the development of this library and supporting the effort to open-source it.
208
- * Sam Luckenbill for pairing with me on most of the hard stuff.
204
+ * Sam Luckenbill for pairing with Nick on most of the hard stuff.
209
205
  * Matthew and Chris for pairing a few days, offering useful feedback on the readability of the code, and the initial implementation of the Memcached mock.
210
- * Evan Weaver for helping to reason-through software and testing strategies to deal with replication lag, and the initial implementation of the Memcached lock.
206
+ * Evan Weaver for helping to reason-through software and testing strategies to deal with replication lag, and the initial implementation of the Memcached lock.
@@ -1,14 +1,6 @@
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
1
  require 'action_controller'
9
2
  require 'active_record'
10
3
  require 'active_record/session_store'
11
- require 'jeweler'
12
4
 
13
5
  ActiveRecord::Base.establish_connection(
14
6
  :adapter => 'sqlite3',
@@ -1,6 +1,4 @@
1
1
  test:
2
2
  ttl: 604800
3
3
  namespace: cache
4
- sessions: false
5
- debug: false
6
4
  servers: localhost:11211
data/init.rb CHANGED
@@ -1 +1 @@
1
- require File.join(File.basedir(__FILE__),'rails','init')
1
+ require 'rails/init'
@@ -1,6 +1,7 @@
1
1
  require 'active_support'
2
2
  require 'active_record'
3
3
 
4
+ require 'cash/version'
4
5
  require 'cash/lock'
5
6
  require 'cash/transactional'
6
7
  require 'cash/write_through'
@@ -22,27 +23,31 @@ require 'cash/query/calculation'
22
23
  require 'cash/util/array'
23
24
  require 'cash/util/marshal'
24
25
 
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
26
+ module Cash
27
+ mattr_accessor :enabled
28
+ self.enabled = true
29
+
30
+ mattr_accessor :repository
31
+
32
+ def self.configure(options = {})
33
+ options.assert_valid_keys(:repository, :local, :transactional, :adapter, :default_ttl)
34
+ cache = options[:repository] || raise(":repository is a required option")
35
+
36
+ adapter = options.fetch(:adapter, :memcached)
37
+
38
+ if adapter
39
+ require "cash/adapter/#{adapter.to_s}"
40
+ klass = "Cash::Adapter::#{adapter.to_s.camelize}".constantize
41
+ cache = klass.new(cache, :logger => Rails.logger, :default_ttl => options.fetch(:default_ttl, 1.day.to_i))
41
42
  end
43
+
44
+ lock = Cash::Lock.new(cache)
45
+ cache = Cash::Local.new(cache) if options.fetch(:local, true)
46
+ cache = Cash::Transactional.new(cache, lock) if options.fetch(:transactional, true)
47
+
48
+ self.repository = cache
42
49
  end
43
- end
44
-
45
- module Cash
50
+
46
51
  def self.included(active_record_class)
47
52
  active_record_class.class_eval do
48
53
  include Config, Accessor, WriteThrough, Finders
@@ -50,6 +55,12 @@ module Cash
50
55
  end
51
56
  end
52
57
 
58
+ private
59
+
60
+ def self.repository
61
+ @@repository || raise("Cash.configure must be called when Cash.enabled is true")
62
+ end
63
+
53
64
  module ClassMethods
54
65
  def self.extended(active_record_class)
55
66
  class << active_record_class
@@ -57,30 +68,38 @@ module Cash
57
68
  end
58
69
  end
59
70
 
60
- def transaction_with_cache_transaction(*args)
61
- if cache_config
62
- transaction_without_cache_transaction(*args) do
63
- repository.transaction { yield }
71
+ def transaction_with_cache_transaction(*args, &block)
72
+ if Cash.enabled
73
+ # Wrap both the db and cache transaction in another cache transaction so that the cache
74
+ # gets written only after the database commit but can still flush the inner cache
75
+ # transaction if an AR::Rollback is issued.
76
+ Cash.repository.transaction do
77
+ transaction_without_cache_transaction(*args) do
78
+ Cash.repository.transaction { block.call }
79
+ end
64
80
  end
65
81
  else
66
- transaction_without_cache_transaction(*args)
82
+ transaction_without_cache_transaction(*args, &block)
67
83
  end
68
84
  end
69
-
70
- def cacheable?(*args)
71
- true
72
- end
73
85
  end
74
86
  end
75
- module NoCash
76
- def self.included(active_record_class)
77
- active_record_class.class_eval do
78
- extend ClassMethods
79
- end
87
+
88
+ class ActiveRecord::Base
89
+ include Cash
90
+
91
+ def self.is_cached(options = {})
92
+ options.assert_valid_keys(:ttl, :repository, :version)
93
+ opts = options.dup
94
+ opts[:repository] = Cash.repository unless opts.has_key?(:repository)
95
+ Cash::Config.create(self, opts)
80
96
  end
81
- module ClassMethods
82
- def cacheable?(*args)
83
- false
97
+
98
+ def <=>(other)
99
+ if self.id == other.id then
100
+ 0
101
+ else
102
+ self.id < other.id ? -1 : 1
84
103
  end
85
104
  end
86
105
  end
@@ -0,0 +1,36 @@
1
+ require 'memcache'
2
+
3
+ module Cash
4
+ module Adapter
5
+ class MemcacheClient
6
+ def initialize(repository, options = {})
7
+ @repository = repository
8
+ @logger = options[:logger]
9
+ @default_ttl = options[:default_ttl] || raise(":default_ttl is a required option")
10
+ end
11
+
12
+ def add(key, value, ttl=nil, raw=false)
13
+ @repository.add(key, value, ttl || @default_ttl, raw)
14
+ end
15
+
16
+ def set(key, value, ttl=nil, raw=false)
17
+ @repository.set(key, value, ttl || @default_ttl, raw)
18
+ end
19
+
20
+ def exception_classes
21
+ MemCache::MemCacheError
22
+ end
23
+
24
+ def respond_to?(method)
25
+ super || @repository.respond_to?(method)
26
+ end
27
+
28
+ private
29
+
30
+ def method_missing(*args, &block)
31
+ @repository.send(*args, &block)
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,131 @@
1
+ require 'memcached'
2
+
3
+ # Maps memcached methods and semantics to those of memcache-client
4
+ module Cash
5
+ module Adapter
6
+ class Memcached
7
+ def initialize(repository, options = {})
8
+ @repository = repository
9
+ @logger = options[:logger]
10
+ @default_ttl = options[:default_ttl] || raise(":default_ttl is a required option")
11
+ end
12
+
13
+ def add(key, value, ttl=nil, raw=false)
14
+ wrap(key, not_stored) do
15
+ logger.debug("Memcached add: #{key.inspect}") if debug_logger?
16
+ @repository.add(key, raw ? value.to_s : value, ttl || @default_ttl, !raw)
17
+ logger.debug("Memcached hit: #{key.inspect}") if debug_logger?
18
+ stored
19
+ end
20
+ end
21
+
22
+ # Wraps Memcached#get so that it doesn't raise. This has the side-effect of preventing you from
23
+ # storing <tt>nil</tt> values.
24
+ def get(key, raw=false)
25
+ wrap(key) do
26
+ logger.debug("Memcached get: #{key.inspect}") if debug_logger?
27
+ value = wrap(key) { @repository.get(key, !raw) }
28
+ logger.debug("Memcached hit: #{key.inspect}") if debug_logger?
29
+ value
30
+ end
31
+ end
32
+
33
+ def get_multi(*keys)
34
+ wrap(keys, {}) do
35
+ begin
36
+ keys.flatten!
37
+ logger.debug("Memcached get_multi: #{keys.inspect}") if debug_logger?
38
+ values = @repository.get(keys, true)
39
+ logger.debug("Memcached hit: #{keys.inspect}") if debug_logger?
40
+ values
41
+ rescue TypeError
42
+ log_error($!) if logger
43
+ keys.each { |key| delete(key) }
44
+ logger.debug("Memcached deleted: #{keys.inspect}") if debug_logger?
45
+ {}
46
+ end
47
+ end
48
+ end
49
+
50
+ def set(key, value, ttl=nil, raw=false)
51
+ wrap(key, not_stored) do
52
+ logger.debug("Memcached set: #{key.inspect}") if debug_logger?
53
+ @repository.set(key, raw ? value.to_s : value, ttl || @default_ttl, !raw)
54
+ logger.debug("Memcached hit: #{key.inspect}") if debug_logger?
55
+ stored
56
+ end
57
+ end
58
+
59
+ def delete(key)
60
+ wrap(key, not_found) do
61
+ logger.debug("Memcached delete: #{key.inspect}") if debug_logger?
62
+ @repository.delete(key)
63
+ logger.debug("Memcached hit: #{key.inspect}") if debug_logger?
64
+ deleted
65
+ end
66
+ end
67
+
68
+ def get_server_for_key(key)
69
+ wrap(key) { @repository.server_by_key(key) }
70
+ end
71
+
72
+ def incr(key, value = 1)
73
+ wrap(key) { @repository.incr(key, value) }
74
+ end
75
+
76
+ def decr(key, value = 1)
77
+ wrap(key) { @repository.decr(key, value) }
78
+ end
79
+
80
+ def flush_all
81
+ @repository.flush
82
+ end
83
+
84
+ def exception_classes
85
+ ::Memcached::Error
86
+ end
87
+
88
+ private
89
+
90
+ def logger
91
+ @logger
92
+ end
93
+
94
+ def debug_logger?
95
+ logger && logger.respond_to?(:debug?) && logger.debug?
96
+ end
97
+
98
+ def wrap(key, error_value = nil, options = {})
99
+ yield
100
+ rescue ::Memcached::NotStored
101
+ logger.debug("Memcached miss: #{key.inspect}") if debug_logger?
102
+ error_value
103
+ rescue ::Memcached::Error
104
+ log_error($!) if logger
105
+ raise if options[:reraise_error]
106
+ error_value
107
+ end
108
+
109
+ def stored
110
+ "STORED\r\n"
111
+ end
112
+
113
+ def deleted
114
+ "DELETED\r\n"
115
+ end
116
+
117
+ def not_stored
118
+ "NOT_STORED\r\n"
119
+ end
120
+
121
+ def not_found
122
+ "NOT_FOUND\r\n"
123
+ end
124
+
125
+ def log_error(err)
126
+ #logger.error("#{err}: \n\t#{err.backtrace.join("\n\t")}") if logger
127
+ logger.error("Memcached ERROR, #{err.class}: #{err}") if logger
128
+ end
129
+ end
130
+ end
131
+ end