identity_cache 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,15 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: ba4de54fb34aecd2a44702ea1bb950c718cdc715
4
- data.tar.gz: 752b53a7d7399f5273fd7083152c4b28ee5cbdeb
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ YmQyZjllMTVlOTNiMmUxMDkxMzJlNzYzYjY0ODZlMmQ4Y2E0YTNkNA==
5
+ data.tar.gz: !binary |-
6
+ OGQ2MDc5Zjk2NmY3MTcwMmIxNDkzYjczNTZjMmJhNjMwMWI1ZjJjYg==
5
7
  SHA512:
6
- metadata.gz: ffa0486c327789a3d975ea768ffe63ed73878f4916aa54b24fa06c1771ece940b4e8bd67a2b8838e224d996a9088b2bd6696237209040608b30c10ed28bb0438
7
- data.tar.gz: 903fc321ed4092d797d230302cfb8a09bfb3a4f47e58f0e67b605391ca0ea6dbc5916dd801ed06ee58e65ffae8793e6f25d8f7e94a875d2dd35d1fa427e62825
8
+ metadata.gz: !binary |-
9
+ MmM4NmJmMTY2MDRkNDdkYzRjMDg3NTQ4OTRlMzgyOWJiYzY4Y2RjMDQxZDQ0
10
+ NDA2NzYwZGIyN2UzMjVkNGU1MTI1ZTViNDU4YThkNTAxNGYyY2UxMWU5Y2U4
11
+ MzhjN2JjMDkyMWVmYjhkMWRmMmIyMmVmZjI4NzJkYTExZGZiYmM=
12
+ data.tar.gz: !binary |-
13
+ Yjg4MTdkOGI0OTJmY2ZkMWEwMDY0NjViMDE2MjdiMDg4NTkzZTc0MmU1YjEy
14
+ NzI1Nzc0ZjQ0ODk1YWY0M2ExMDdjNWNjMjQ5OGY3Y2YyMTcyNzQ3MjA3OTY3
15
+ YWMxMTlkZTgxNjQ0OTgyYmY3YzY5OWYzY2QyY2NlZjZkNzUwYTQ=
data/.travis.yml CHANGED
@@ -2,10 +2,12 @@ language: ruby
2
2
  rvm:
3
3
  - 1.9.3
4
4
  - 2.0.0
5
-
5
+ - jruby-19mode
6
6
  services:
7
7
  - memcache
8
8
  - mysql
9
-
9
+ matrix:
10
+ allow_failures:
11
+ - rvm: jruby-19mode
10
12
  before_script:
11
13
  - mysql -e 'create database identity_cache_test'
data/CHANGELOG CHANGED
@@ -1,3 +1,8 @@
1
+ 0.0.4
2
+ * Fix: only marshal attributes, embedded associations and normalized association IDs
3
+ * Add cache version number to cache keys
4
+ * Add test case to ensure version number is updated when the marshalled format changes
5
+
1
6
  0.0.3
2
7
  * Fix: memoization for multi hits actually work
3
8
  * Fix: quotes SELECT projection elements on cache misses
data/README.md CHANGED
@@ -11,6 +11,7 @@ Add this line to your application's Gemfile:
11
11
 
12
12
  ```ruby
13
13
  gem 'identity_cache'
14
+ gem 'cityhash' # optional, for faster hashing (C-Ruby only)
14
15
  ```
15
16
 
16
17
  And then execute:
@@ -205,12 +206,18 @@ class ApplicationController < ActionController::Base
205
206
  end
206
207
  ```
207
208
 
209
+ ## Versioning
210
+
211
+ Cache keys include a version number by default, specified in `IdentityCache::CACHE_VERSION`. This version number is updated whenever the storage format for cache values is modified. If you modify the cache value format, you must run `rake update_serialization_format` in order to pass the unit tests, and include the modified `test/fixtures/serialized_record` file in your pull request.
212
+
208
213
  ## Caveats
209
214
 
210
215
  A word of warning. Some versions of rails will silently rescue all exceptions in `after_commit` hooks. If an `after_commit` fails before the cache expiry `after_commit` the cache will not be expired and you will be left with stale data.
211
216
 
212
217
  Since everything is being marshalled and unmarshalled from Memcached changing Ruby or Rails versions could mean your objects cannot be unmarshalled from Memcached. There are a number of ways to get around this such as namespacing keys when you upgrade or rescuing marshal load errors and treating it as a cache miss. Just something to be aware of if you are using IdentityCache and upgrade Ruby or Rails.
213
218
 
219
+ IdentityCache is also very much _opt-in_ by deliberate design. This means IdentityCache does not mess with the way normal Rails associations work, and including it in a model won't change any clients of that model until you switch them to use `fetch` instead of `find`. This is because there is no way IdentityCache is ever going to be 100% consistent. Processes die, execeptions happen, and network blips occur, which means there is a chance that some database transaction might commit but the corresponding memcached DEL operation does not make it. This means that you need to think carefully about when you use `fetch` and when you use `find`. For example, at Shopify, we never use any `fetch`ers on the path which moves money around, because IdentityCache could simply be wrong, and we want to charge people the right amount of money. We do however use the fetchers on performance critical paths where absolute correctness isn't the most important thing, and this is what IdentityCache is intended for.
220
+
214
221
  ## Contributing
215
222
 
216
223
  Caching is hard. Chances are that if some feature was left out, it was left out on purpose because it didn't make sense to cache in that way. This is used in production at Shopify so we are very opinionated about the types of features we're going to add. Please start the discussion early, before even adding code, so that we can talk about the feature you are proposing and decide if it makes sense in IdentityCache.
@@ -236,4 +243,5 @@ Tom Burns (@boourns)
236
243
  Harry Brundage (@hornairs)
237
244
  Dylan Smith (@dylanahsmith)
238
245
  Tobias Lütke (@tobi)
239
- John Duff (@jduff)
246
+ John Duff (@jduff)
247
+ Francis Bogsany (@fbogsany)
data/Rakefile CHANGED
@@ -15,6 +15,11 @@ Rake::TestTask.new(:test) do |t|
15
15
  t.verbose = true
16
16
  end
17
17
 
18
+ desc 'Update serialization format test fixture.'
19
+ task :update_serialization_format do
20
+ ruby './test/helpers/update_serialization_format.rb'
21
+ end
22
+
18
23
  namespace :benchmark do
19
24
  desc "Run the identity cache CPU benchmark"
20
25
  task :cpu do
@@ -32,4 +37,3 @@ namespace :profile do
32
37
  ruby "./performance/profile.rb"
33
38
  end
34
39
  end
35
-
@@ -17,13 +17,21 @@ Gem::Specification.new do |gem|
17
17
 
18
18
 
19
19
  gem.add_dependency('ar_transaction_changes', '0.0.1')
20
- gem.add_dependency('activerecord', '3.2.13')
21
- gem.add_dependency('activesupport', '3.2.13')
22
- gem.add_dependency('cityhash', '0.6.0')
20
+ gem.add_dependency('activerecord', '~> 3.2.12')
21
+ gem.add_dependency('activesupport', '~> 3.2.12')
22
+
23
23
  gem.add_development_dependency('memcache-client')
24
24
  gem.add_development_dependency('rake')
25
25
  gem.add_development_dependency('mocha', '0.14.0')
26
- gem.add_development_dependency('mysql2')
27
- gem.add_development_dependency('debugger')
28
- gem.add_development_dependency('ruby-prof')
26
+
27
+ if RUBY_PLATFORM == 'java'
28
+ gem.add_development_dependency 'jruby-openssl'
29
+ gem.add_development_dependency 'activerecord-jdbcmysql-adapter'
30
+ gem.add_development_dependency 'jdbc-mysql'
31
+ else
32
+ gem.add_development_dependency('debugger')
33
+ gem.add_development_dependency('ruby-prof')
34
+ gem.add_development_dependency('cityhash', '0.6.0')
35
+ gem.add_development_dependency('mysql2')
36
+ end
29
37
  end
@@ -1,5 +1,6 @@
1
- require 'cityhash'
1
+ require 'active_record'
2
2
  require 'ar_transaction_changes'
3
+
3
4
  require "identity_cache/version"
4
5
  require 'identity_cache/memoized_cache_proxy'
5
6
  require 'identity_cache/belongs_to_caching'
@@ -7,6 +8,7 @@ require 'identity_cache/cache_key_generation'
7
8
  require 'identity_cache/configuration_dsl'
8
9
  require 'identity_cache/parent_model_expiration'
9
10
  require 'identity_cache/query_api'
11
+ require "identity_cache/cache_hash"
10
12
 
11
13
  module IdentityCache
12
14
  CACHED_NIL = :idc_cached_nil
@@ -19,9 +21,20 @@ module IdentityCache
19
21
  end
20
22
 
21
23
  class << self
24
+ include IdentityCache::CacheHash
25
+
26
+ attr_accessor :readonly
27
+ attr_writer :logger
22
28
 
23
- attr_accessor :logger, :readonly
24
- attr_reader :cache
29
+ def included(base) #:nodoc:
30
+ raise AlreadyIncludedError if base.respond_to? :cache_indexes
31
+
32
+ base.send(:include, ArTransactionChanges) unless base.include?(ArTransactionChanges)
33
+ base.send(:include, IdentityCache::BelongsToCaching)
34
+ base.send(:include, IdentityCache::CacheKeyGeneration)
35
+ base.send(:include, IdentityCache::ConfigurationDSL)
36
+ base.send(:include, IdentityCache::QueryAPI)
37
+ end
25
38
 
26
39
  # Sets the cache adaptor IdentityCache will be using
27
40
  #
@@ -30,7 +43,7 @@ module IdentityCache
30
43
  # +cache_adaptor+ - A ActiveSupport::Cache::Store
31
44
  #
32
45
  def cache_backend=(cache_adaptor)
33
- cache.memcache = cache_adaptor
46
+ cache.cache_backend = cache_adaptor
34
47
  end
35
48
 
36
49
  def cache
@@ -73,7 +86,6 @@ module IdentityCache
73
86
  value.nil? ? IdentityCache::CACHED_NIL : value
74
87
  end
75
88
 
76
-
77
89
  def unmap_cached_nil_for(value)
78
90
  value == IdentityCache::CACHED_NIL ? nil : value
79
91
  end
@@ -113,34 +125,5 @@ module IdentityCache
113
125
 
114
126
  result
115
127
  end
116
-
117
- def schema_to_string(columns)
118
- columns.sort_by(&:name).map{|c| "#{c.name}:#{c.type}"}.join(',')
119
- end
120
-
121
- def denormalized_schema_hash(klass)
122
- schema_string = schema_to_string(klass.columns)
123
- if klass.respond_to?(:all_cached_associations_needing_population) && !(embeded_associations = klass.all_cached_associations_needing_population).empty?
124
- embedded_schema = embeded_associations.map do |name, options|
125
- "#{name}:(#{denormalized_schema_hash(options[:association_class])})"
126
- end.sort.join(',')
127
- schema_string << "," << embedded_schema
128
- end
129
- IdentityCache.memcache_hash(schema_string)
130
- end
131
-
132
- def included(base) #:nodoc:
133
- raise AlreadyIncludedError if base.respond_to? :cache_indexes
134
-
135
- base.send(:include, ArTransactionChanges) unless base.include?(ArTransactionChanges)
136
- base.send(:include, IdentityCache::BelongsToCaching)
137
- base.send(:include, IdentityCache::CacheKeyGeneration)
138
- base.send(:include, IdentityCache::ConfigurationDSL)
139
- base.send(:include, IdentityCache::QueryAPI)
140
- end
141
-
142
- def memcache_hash(key) #:nodoc:
143
- CityHash.hash64(key)
144
- end
145
128
  end
146
129
  end
@@ -0,0 +1,36 @@
1
+ # Use CityHash for fast hashing if it is available; use Digest::MD5 otherwise
2
+ begin
3
+ require 'cityhash'
4
+ rescue LoadError
5
+ unless RUBY_PLATFORM == 'java'
6
+ warn <<-NOTICE
7
+ ** Notice: CityHash was not loaded. **
8
+
9
+ For optimal performance, use of the cityhash gem is recommended.
10
+
11
+ Run the following command, or add it to your Gemfile:
12
+
13
+ gem install cityhash
14
+ NOTICE
15
+ end
16
+
17
+ require 'digest/md5'
18
+ end
19
+
20
+ module IdentityCache
21
+ module CacheHash
22
+
23
+ if defined?(CityHash)
24
+
25
+ def memcache_hash(key) #:nodoc:
26
+ CityHash.hash64(key)
27
+ end
28
+ else
29
+
30
+ def memcache_hash(key) #:nodoc:
31
+ a = Digest::MD5.digest(key).unpack('LL')
32
+ (a[0] << 32) | a[1]
33
+ end
34
+ end
35
+ end
36
+ end
@@ -1,26 +1,54 @@
1
1
  module IdentityCache
2
2
  module CacheKeyGeneration
3
3
  extend ActiveSupport::Concern
4
+ DEFAULT_NAMESPACE = "IDC:#{CACHE_VERSION}:".freeze
5
+
6
+ def self.schema_to_string(columns)
7
+ columns.sort_by(&:name).map{|c| "#{c.name}:#{c.type}"}.join(',')
8
+ end
9
+
10
+ def self.denormalized_schema_hash(klass)
11
+ schema_string = schema_to_string(klass.columns)
12
+ if !(associations = embedded_associations(klass)).empty?
13
+ embedded_schema = associations.map do |name, options|
14
+ "#{name}:(#{denormalized_schema_hash(options[:association_class])})"
15
+ end.sort.join(',')
16
+ schema_string << "," << embedded_schema
17
+ end
18
+ IdentityCache.memcache_hash(schema_string)
19
+ end
20
+
21
+ def self.embedded_associations(klass)
22
+ if klass.respond_to?(:all_cached_associations_needing_population)
23
+ klass.all_cached_associations_needing_population
24
+ else
25
+ {}
26
+ end
27
+ end
4
28
 
5
29
  module ClassMethods
6
30
  def rails_cache_key(id)
7
- rails_cache_key_prefix + id.to_s
31
+ "#{rails_cache_key_prefix}#{id}"
8
32
  end
9
33
 
10
34
  def rails_cache_key_prefix
11
- @rails_cache_key_prefix ||= begin
12
- "IDC:blob:#{base_class.name}:#{IdentityCache.denormalized_schema_hash(self)}:"
13
- end
35
+ @rails_cache_key_prefix ||= IdentityCache::CacheKeyGeneration.denormalized_schema_hash(self)
36
+ "#{rails_cache_key_namespace}blob:#{base_class.name}:#{@rails_cache_key_prefix}:"
14
37
  end
15
38
 
16
39
  def rails_cache_index_key_for_fields_and_values(fields, values)
17
- "IDC:index:#{base_class.name}:#{rails_cache_string_for_fields_and_values(fields, values)}"
40
+ "#{rails_cache_key_namespace}index:#{base_class.name}:#{rails_cache_string_for_fields_and_values(fields, values)}"
18
41
  end
19
42
 
20
43
  def rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, values)
21
- "IDC:attribute:#{base_class.name}:#{attribute}:#{rails_cache_string_for_fields_and_values(fields, values)}"
44
+ "#{rails_cache_key_namespace}attribute:#{base_class.name}:#{attribute}:#{rails_cache_string_for_fields_and_values(fields, values)}"
45
+ end
46
+
47
+ def rails_cache_key_namespace
48
+ DEFAULT_NAMESPACE
22
49
  end
23
50
 
51
+ private
24
52
  def rails_cache_string_for_fields_and_values(fields, values)
25
53
  "#{fields.join('/')}:#{IdentityCache.memcache_hash(values.join('/'))}"
26
54
  end
@@ -183,7 +183,7 @@ module IdentityCache
183
183
  end
184
184
 
185
185
  def identity_cache_single_value_dynamic_fetcher(fields, values) # :nodoc:
186
- sql_on_miss = "SELECT `id` FROM `#{table_name}` WHERE #{identity_cache_sql_conditions(fields, values)} LIMIT 1"
186
+ sql_on_miss = "SELECT #{quoted_primary_key} FROM #{quoted_table_name} WHERE #{identity_cache_sql_conditions(fields, values)} LIMIT 1"
187
187
  cache_key = rails_cache_index_key_for_fields_and_values(fields, values)
188
188
  id = IdentityCache.fetch(cache_key) { connection.select_value(sql_on_miss) }
189
189
  unless id.nil?
@@ -195,7 +195,7 @@ module IdentityCache
195
195
  end
196
196
 
197
197
  def identity_cache_multiple_value_dynamic_fetcher(fields, values) # :nodoc
198
- sql_on_miss = "SELECT `id` FROM `#{table_name}` WHERE #{identity_cache_sql_conditions(fields, values)}"
198
+ sql_on_miss = "SELECT #{quoted_primary_key} FROM #{quoted_table_name} WHERE #{identity_cache_sql_conditions(fields, values)}"
199
199
  cache_key = rails_cache_index_key_for_fields_and_values(fields, values)
200
200
  ids = IdentityCache.fetch(cache_key) { connection.select_values(sql_on_miss) }
201
201
 
@@ -267,8 +267,7 @@ module IdentityCache
267
267
 
268
268
  def attribute_dynamic_fetcher(attribute, fields, values) #:nodoc:
269
269
  cache_key = rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, values)
270
- sql_on_miss = "SELECT `#{attribute}` FROM `#{table_name}` WHERE #{identity_cache_sql_conditions(fields, values)} LIMIT 1"
271
-
270
+ sql_on_miss = "SELECT #{connection.quote_column_name(attribute)} FROM #{quoted_table_name} WHERE #{identity_cache_sql_conditions(fields, values)} LIMIT 1"
272
271
  IdentityCache.fetch(cache_key) { connection.select_value(sql_on_miss) }
273
272
  end
274
273
 
@@ -278,7 +277,6 @@ module IdentityCache
278
277
  raise InverseAssociationError unless child_association
279
278
  foreign_key = child_association.association_foreign_key
280
279
  parent_class ||= self.name
281
- new_parent = options[:inverse_name]
282
280
 
283
281
  child_class.send(:include, ArTransactionChanges) unless child_class.include?(ArTransactionChanges)
284
282
  child_class.send(:include, ParentModelExpiration) unless child_class.include?(ParentModelExpiration)
@@ -294,7 +292,7 @@ module IdentityCache
294
292
  end
295
293
 
296
294
  def identity_cache_sql_conditions(fields, values)
297
- fields.each_with_index.collect { |f, i| "`#{f}` = #{quote_value(values[i])}" }.join(" AND ")
295
+ fields.each_with_index.collect { |f, i| "#{connection.quote_column_name(f)} = #{quote_value(values[i])}" }.join(" AND ")
298
296
  end
299
297
  end
300
298
  end
@@ -2,10 +2,10 @@ require 'monitor'
2
2
 
3
3
  module IdentityCache
4
4
  class MemoizedCacheProxy
5
- attr_writer :memcache
5
+ attr_accessor :cache_backend
6
6
 
7
- def initialize(memcache = nil)
8
- @memcache = memcache || Rails.cache
7
+ def initialize(cache_backend = nil)
8
+ @cache_backend = cache_backend || Rails.cache
9
9
  @key_value_maps = Hash.new {|h, k| h[k] = {} }
10
10
  end
11
11
 
@@ -23,27 +23,27 @@ module IdentityCache
23
23
 
24
24
  def write(key, value)
25
25
  memoized_key_values[key] = value if memoizing?
26
- @memcache.write(key, value)
26
+ @cache_backend.write(key, value)
27
27
  end
28
28
 
29
29
  def read(key)
30
- used_memcached = true
30
+ used_cache_backend = true
31
31
 
32
32
  result = if memoizing?
33
- used_memcached = false
33
+ used_cache_backend = false
34
34
  mkv = memoized_key_values
35
35
 
36
36
  mkv.fetch(key) do
37
- used_memcached = true
38
- mkv[key] = @memcache.read(key)
37
+ used_cache_backend = true
38
+ mkv[key] = @cache_backend.read(key)
39
39
  end
40
40
 
41
41
  else
42
- @memcache.read(key)
42
+ @cache_backend.read(key)
43
43
  end
44
44
 
45
45
  if result
46
- IdentityCache.logger.debug { "[IdentityCache] #{ used_memcached ? '(memcache)' : '(memoized)' } cache hit for #{key}" }
46
+ IdentityCache.logger.debug { "[IdentityCache] #{ used_cache_backend ? '(cache_backend)' : '(memoized)' } cache hit for #{key}" }
47
47
  else
48
48
  IdentityCache.logger.debug { "[IdentityCache] cache miss for #{key}" }
49
49
  end
@@ -53,13 +53,15 @@ module IdentityCache
53
53
 
54
54
  def delete(key)
55
55
  memoized_key_values.delete(key) if memoizing?
56
- @memcache.delete(key)
56
+ result = @cache_backend.delete(key)
57
+ IdentityCache.logger.debug { "[IdentityCache] delete #{ result ? 'hit' : 'miss' } for #{key}" }
58
+ result
57
59
  end
58
60
 
59
61
  def read_multi(*keys)
60
62
 
61
63
  if IdentityCache.logger.debug?
62
- memoized_keys , memcache_keys = [], []
64
+ memoized_keys , cache_backend_keys = [], []
63
65
  end
64
66
 
65
67
  result = if memoizing?
@@ -75,7 +77,7 @@ module IdentityCache
75
77
  end
76
78
  end
77
79
 
78
- hits = missing_keys.empty? ? {} : @memcache.read_multi(*missing_keys)
80
+ hits = missing_keys.empty? ? {} : @cache_backend.read_multi(*missing_keys)
79
81
 
80
82
  missing_keys.each do |key|
81
83
  hit = hits[key]
@@ -84,17 +86,17 @@ module IdentityCache
84
86
  end
85
87
  hash
86
88
  else
87
- @memcache.read_multi(*keys)
89
+ @cache_backend.read_multi(*keys)
88
90
  end
89
91
 
90
92
  if IdentityCache.logger.debug?
91
93
 
92
94
  result.each do |k, v|
93
- memcache_keys << k if !v.nil? && !memoized_keys.include?(k)
95
+ cache_backend_keys << k if !v.nil? && !memoized_keys.include?(k)
94
96
  end
95
97
 
96
98
  memoized_keys.each{ |k| IdentityCache.logger.debug "[IdentityCache] (memoized) cache hit for #{k} (multi)" }
97
- memcache_keys.each{ |k| IdentityCache.logger.debug "[IdentityCache] (memcache) cache hit for #{k} (multi)" }
99
+ cache_backend_keys.each{ |k| IdentityCache.logger.debug "[IdentityCache] (cache_backend) cache hit for #{k} (multi)" }
98
100
  end
99
101
 
100
102
  result
@@ -102,7 +104,7 @@ module IdentityCache
102
104
 
103
105
  def clear
104
106
  clear_memoization
105
- @memcache.clear
107
+ @cache_backend.clear
106
108
  end
107
109
 
108
110
  private