ngmoco-cache-money 0.2.23 → 0.2.24.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.
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