activesupport 3.0.0.beta3 → 3.0.0.beta4

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activesupport might be problematic. Click here for more details.

Files changed (63) hide show
  1. data/CHANGELOG +57 -0
  2. data/lib/active_support/builder.rb +6 -0
  3. data/lib/active_support/cache.rb +428 -70
  4. data/lib/active_support/cache/compressed_mem_cache_store.rb +6 -15
  5. data/lib/active_support/cache/file_store.rb +139 -41
  6. data/lib/active_support/cache/mem_cache_store.rb +115 -76
  7. data/lib/active_support/cache/memory_store.rb +127 -27
  8. data/lib/active_support/cache/strategy/local_cache.rb +109 -57
  9. data/lib/active_support/cache/synchronized_memory_store.rb +2 -38
  10. data/lib/active_support/callbacks.rb +27 -27
  11. data/lib/active_support/configurable.rb +19 -18
  12. data/lib/active_support/core_ext/array/conversions.rb +30 -26
  13. data/lib/active_support/core_ext/array/random_access.rb +19 -5
  14. data/lib/active_support/core_ext/benchmark.rb +0 -12
  15. data/lib/active_support/core_ext/class/attribute.rb +1 -4
  16. data/lib/active_support/core_ext/class/inheritable_attributes.rb +3 -0
  17. data/lib/active_support/core_ext/date/calculations.rb +27 -8
  18. data/lib/active_support/core_ext/date/conversions.rb +1 -0
  19. data/lib/active_support/core_ext/date_time/conversions.rb +9 -3
  20. data/lib/active_support/core_ext/file.rb +1 -0
  21. data/lib/active_support/core_ext/hash/conversions.rb +14 -137
  22. data/lib/active_support/core_ext/kernel/debugger.rb +1 -1
  23. data/lib/active_support/core_ext/kernel/reporting.rb +2 -1
  24. data/lib/active_support/core_ext/load_error.rb +1 -0
  25. data/lib/active_support/core_ext/logger.rb +1 -1
  26. data/lib/active_support/core_ext/module/attr_internal.rb +2 -2
  27. data/lib/active_support/core_ext/object/to_param.rb +2 -2
  28. data/lib/active_support/core_ext/object/with_options.rb +2 -0
  29. data/lib/active_support/core_ext/string.rb +1 -0
  30. data/lib/active_support/core_ext/string/conversions.rb +35 -1
  31. data/lib/active_support/core_ext/string/encoding.rb +11 -0
  32. data/lib/active_support/core_ext/string/filters.rb +29 -0
  33. data/lib/active_support/core_ext/string/inflections.rb +0 -11
  34. data/lib/active_support/core_ext/string/interpolation.rb +1 -0
  35. data/lib/active_support/core_ext/string/multibyte.rb +16 -19
  36. data/lib/active_support/core_ext/time/calculations.rb +7 -6
  37. data/lib/active_support/core_ext/uri.rb +8 -3
  38. data/lib/active_support/dependencies.rb +33 -1
  39. data/lib/active_support/duration.rb +1 -0
  40. data/lib/active_support/hash_with_indifferent_access.rb +5 -1
  41. data/lib/active_support/i18n.rb +7 -2
  42. data/lib/active_support/inflector/transliterate.rb +58 -38
  43. data/lib/active_support/json/encoding.rb +28 -5
  44. data/lib/active_support/lazy_load_hooks.rb +14 -4
  45. data/lib/active_support/locale/en.yml +4 -1
  46. data/lib/active_support/message_verifier.rb +4 -4
  47. data/lib/active_support/multibyte.rb +1 -19
  48. data/lib/active_support/multibyte/chars.rb +143 -427
  49. data/lib/active_support/multibyte/unicode.rb +393 -0
  50. data/lib/active_support/notifications/fanout.rb +15 -5
  51. data/lib/active_support/notifications/instrumenter.rb +10 -4
  52. data/lib/active_support/railtie.rb +36 -0
  53. data/lib/active_support/rescuable.rb +1 -0
  54. data/lib/active_support/ruby/shim.rb +1 -0
  55. data/lib/active_support/testing/declarative.rb +1 -1
  56. data/lib/active_support/testing/isolation.rb +2 -1
  57. data/lib/active_support/testing/setup_and_teardown.rb +3 -0
  58. data/lib/active_support/values/time_zone.rb +20 -30
  59. data/lib/active_support/values/unicode_tables.dat +0 -0
  60. data/lib/active_support/version.rb +1 -1
  61. data/lib/active_support/xml_mini.rb +126 -1
  62. metadata +8 -61
  63. data/lib/active_support/multibyte/unicode_database.rb +0 -71
@@ -1,21 +1,12 @@
1
- require 'active_support/gzip'
2
-
3
1
  module ActiveSupport
4
2
  module Cache
5
3
  class CompressedMemCacheStore < MemCacheStore
6
- def read(name, options = nil)
7
- if value = super(name, (options || {}).merge(:raw => true))
8
- if raw?(options)
9
- value
10
- else
11
- Marshal.load(ActiveSupport::Gzip.decompress(value))
12
- end
13
- end
14
- end
15
-
16
- def write(name, value, options = nil)
17
- value = ActiveSupport::Gzip.compress(Marshal.dump(value)) unless raw?(options)
18
- super(name, value, (options || {}).merge(:raw => true))
4
+ def initialize(*args)
5
+ ActiveSupport::Deprecation.warn('ActiveSupport::Cache::CompressedMemCacheStore has been deprecated in favor of ActiveSupport::Cache::MemCacheStore(:compress => true).', caller)
6
+ addresses = args.dup
7
+ options = addresses.extract_options!
8
+ args = addresses + [options.merge(:compress => true)]
9
+ super(*args)
19
10
  end
20
11
  end
21
12
  end
@@ -3,73 +3,171 @@ require 'active_support/core_ext/file/atomic'
3
3
  module ActiveSupport
4
4
  module Cache
5
5
  # A cache store implementation which stores everything on the filesystem.
6
+ #
7
+ # FileStore implements the Strategy::LocalCache strategy which implements
8
+ # an in memory cache inside of a block.
6
9
  class FileStore < Store
7
10
  attr_reader :cache_path
8
11
 
9
- def initialize(cache_path)
12
+ DIR_FORMATTER = "%03X"
13
+ ESCAPE_FILENAME_CHARS = /[^a-z0-9_.-]/i
14
+ UNESCAPE_FILENAME_CHARS = /%[0-9A-F]{2}/
15
+
16
+ def initialize(cache_path, options = nil)
17
+ super(options)
10
18
  @cache_path = cache_path
19
+ extend Strategy::LocalCache
11
20
  end
12
21
 
13
- # Reads a value from the cache.
14
- #
15
- # Possible options:
16
- # - +:expires_in+ - the number of seconds that this value may stay in
17
- # the cache.
18
- def read(name, options = nil)
19
- super do
20
- file_name = real_file_path(name)
21
- expires = expires_in(options)
22
-
23
- if File.exist?(file_name) && (expires <= 0 || Time.now - File.mtime(file_name) < expires)
24
- File.open(file_name, 'rb') { |f| Marshal.load(f) }
25
- end
22
+ def clear(options = nil)
23
+ root_dirs = Dir.entries(cache_path).reject{|f| ['.', '..'].include?(f)}
24
+ FileUtils.rm_r(root_dirs.collect{|f| File.join(cache_path, f)})
25
+ end
26
+
27
+ def cleanup(options = nil)
28
+ options = merged_options(options)
29
+ each_key(options) do |key|
30
+ entry = read_entry(key, options)
31
+ delete_entry(key, options) if entry && entry.expired?
26
32
  end
27
33
  end
28
34
 
29
- # Writes a value to the cache.
30
- def write(name, value, options = nil)
31
- super do
32
- ensure_cache_path(File.dirname(real_file_path(name)))
33
- File.atomic_write(real_file_path(name), cache_path) { |f| Marshal.dump(value, f) }
34
- value
35
+ def increment(name, amount = 1, options = nil)
36
+ file_name = key_file_path(namespaced_key(name, options))
37
+ lock_file(file_name) do
38
+ options = merged_options(options)
39
+ if num = read(name, options)
40
+ num = num.to_i + amount
41
+ write(name, num, options)
42
+ num
43
+ else
44
+ nil
45
+ end
35
46
  end
36
- rescue => e
37
- logger.error "Couldn't create cache directory: #{name} (#{e.message})" if logger
38
47
  end
39
48
 
40
- def delete(name, options = nil)
41
- super do
42
- File.delete(real_file_path(name))
49
+ def decrement(name, amount = 1, options = nil)
50
+ file_name = key_file_path(namespaced_key(name, options))
51
+ lock_file(file_name) do
52
+ options = merged_options(options)
53
+ if num = read(name, options)
54
+ num = num.to_i - amount
55
+ write(name, num, options)
56
+ num
57
+ else
58
+ nil
59
+ end
43
60
  end
44
- rescue SystemCallError => e
45
- # If there's no cache, then there's nothing to complain about
46
61
  end
47
62
 
48
63
  def delete_matched(matcher, options = nil)
49
- super do
50
- search_dir(@cache_path) do |f|
51
- if f =~ matcher
52
- begin
53
- File.delete(f)
54
- rescue SystemCallError => e
55
- # If there's no cache, then there's nothing to complain about
64
+ options = merged_options(options)
65
+ instrument(:delete_matched, matcher.inspect) do
66
+ matcher = key_matcher(matcher, options)
67
+ search_dir(cache_path) do |path|
68
+ key = file_path_key(path)
69
+ delete_entry(key, options) if key.match(matcher)
70
+ end
71
+ end
72
+ end
73
+
74
+ protected
75
+
76
+ def read_entry(key, options)
77
+ file_name = key_file_path(key)
78
+ if File.exist?(file_name)
79
+ entry = File.open(file_name) { |f| Marshal.load(f) }
80
+ if entry && !entry.expired? && !entry.expires_in && !self.options[:expires_in]
81
+ # Check for deprecated use of +:expires_in+ option from versions < 3.0
82
+ deprecated_expires_in = options[:expires_in]
83
+ if deprecated_expires_in
84
+ ActiveSupport::Deprecation.warn('Setting :expires_in on read has been deprecated in favor of setting it on write.', caller)
85
+ if entry.created_at + deprecated_expires_in.to_f <= Time.now.to_f
86
+ delete_entry(key, options)
87
+ entry = nil
88
+ end
56
89
  end
57
90
  end
91
+ entry
58
92
  end
93
+ rescue
94
+ nil
59
95
  end
60
- end
61
96
 
62
- def exist?(name, options = nil)
63
- super do
64
- File.exist?(real_file_path(name))
97
+ def write_entry(key, entry, options)
98
+ file_name = key_file_path(key)
99
+ ensure_cache_path(File.dirname(file_name))
100
+ File.atomic_write(file_name, cache_path) {|f| Marshal.dump(entry, f)}
101
+ true
102
+ end
103
+
104
+ def delete_entry(key, options)
105
+ file_name = key_file_path(key)
106
+ if File.exist?(file_name)
107
+ begin
108
+ File.delete(file_name)
109
+ delete_empty_directories(File.dirname(file_name))
110
+ true
111
+ rescue => e
112
+ # Just in case the error was caused by another process deleting the file first.
113
+ raise e if File.exist?(file_name)
114
+ false
115
+ end
116
+ end
65
117
  end
66
- end
67
118
 
68
119
  private
69
- def real_file_path(name)
70
- '%s/%s.cache' % [@cache_path, name.gsub('?', '.').gsub(':', '.')]
120
+ # Lock a file for a block so only one process can modify it at a time.
121
+ def lock_file(file_name, &block) # :nodoc:
122
+ if File.exist?(file_name)
123
+ File.open(file_name, 'r') do |f|
124
+ begin
125
+ f.flock File::LOCK_EX
126
+ yield
127
+ ensure
128
+ f.flock File::LOCK_UN
129
+ end
130
+ end
131
+ else
132
+ yield
133
+ end
134
+ end
135
+
136
+ # Translate a key into a file path.
137
+ def key_file_path(key)
138
+ fname = key.to_s.gsub(ESCAPE_FILENAME_CHARS){|match| "%#{match.ord.to_s(16).upcase}"}
139
+ hash = Zlib.adler32(fname)
140
+ hash, dir_1 = hash.divmod(0x1000)
141
+ dir_2 = hash.modulo(0x1000)
142
+ fname_paths = []
143
+ # Make sure file name is < 255 characters so it doesn't exceed file system limits.
144
+ if fname.size <= 255
145
+ fname_paths << fname
146
+ else
147
+ while fname.size <= 255
148
+ fname_path << fname[0, 255]
149
+ fname = fname[255, -1]
150
+ end
151
+ end
152
+ File.join(cache_path, DIR_FORMATTER % dir_1, DIR_FORMATTER % dir_2, *fname_paths)
153
+ end
154
+
155
+ # Translate a file path into a key.
156
+ def file_path_key(path)
157
+ fname = path[cache_path.size, path.size].split(File::SEPARATOR, 4).last
158
+ fname.gsub(UNESCAPE_FILENAME_CHARS){|match| $1.ord.to_s(16)}
159
+ end
160
+
161
+ # Delete empty directories in the cache.
162
+ def delete_empty_directories(dir)
163
+ return if dir == cache_path
164
+ if Dir.entries(dir).reject{|f| ['.', '..'].include?(f)}.empty?
165
+ File.delete(dir) rescue nil
166
+ delete_empty_directories(File.dirname(dir))
167
+ end
71
168
  end
72
169
 
170
+ # Make sure a file path's directories exist.
73
171
  def ensure_cache_path(path)
74
172
  FileUtils.makedirs(path) unless File.exist?(path)
75
173
  end
@@ -1,5 +1,10 @@
1
- require 'memcache'
2
- require 'active_support/core_ext/array/extract_options'
1
+ begin
2
+ require 'memcache'
3
+ rescue LoadError => e
4
+ $stderr.puts "You don't have memcache installed in your application. Please add it to your Gemfile and run bundle install"
5
+ raise e
6
+ end
7
+ require 'digest/md5'
3
8
 
4
9
  module ActiveSupport
5
10
  module Cache
@@ -13,8 +18,9 @@ module ActiveSupport
13
18
  # and MemCacheStore will load balance between all available servers. If a
14
19
  # server goes down, then MemCacheStore will ignore it until it goes back
15
20
  # online.
16
- # - Time-based expiry support. See #write and the <tt>:expires_in</tt> option.
17
- # - Per-request in memory cache for all communication with the MemCache server(s).
21
+ #
22
+ # MemCacheStore implements the Strategy::LocalCache strategy which implements
23
+ # an in memory cache inside of a block.
18
24
  class MemCacheStore < Store
19
25
  module Response # :nodoc:
20
26
  STORED = "STORED\r\n"
@@ -24,6 +30,8 @@ module ActiveSupport
24
30
  DELETED = "DELETED\r\n"
25
31
  end
26
32
 
33
+ ESCAPE_KEY_CHARS = /[\x00-\x20%\x7F-\xFF]/
34
+
27
35
  def self.build_mem_cache(*addresses)
28
36
  addresses = addresses.flatten
29
37
  options = addresses.extract_options!
@@ -45,108 +53,139 @@ module ActiveSupport
45
53
  # require 'memcached' # gem install memcached; uses C bindings to libmemcached
46
54
  # ActiveSupport::Cache::MemCacheStore.new(Memcached::Rails.new("localhost:11211"))
47
55
  def initialize(*addresses)
56
+ addresses = addresses.flatten
57
+ options = addresses.extract_options!
58
+ super(options)
59
+
48
60
  if addresses.first.respond_to?(:get)
49
61
  @data = addresses.first
50
62
  else
51
- @data = self.class.build_mem_cache(*addresses)
63
+ mem_cache_options = options.dup
64
+ UNIVERSAL_OPTIONS.each{|name| mem_cache_options.delete(name)}
65
+ @data = self.class.build_mem_cache(*(addresses + [mem_cache_options]))
52
66
  end
53
67
 
54
68
  extend Strategy::LocalCache
69
+ extend LocalCacheWithRaw
55
70
  end
56
71
 
57
- # Reads multiple keys from the cache.
58
- def read_multi(*keys)
59
- @data.get_multi keys
60
- end
61
-
62
- def read(key, options = nil) # :nodoc:
63
- super do
64
- @data.get(key, raw?(options))
65
- end
66
- rescue MemCache::MemCacheError => e
67
- logger.error("MemCacheError (#{e}): #{e.message}") if logger
68
- nil
69
- end
70
-
71
- # Writes a value to the cache.
72
- #
73
- # Possible options:
74
- # - <tt>:unless_exist</tt> - set to true if you don't want to update the cache
75
- # if the key is already set.
76
- # - <tt>:expires_in</tt> - the number of seconds that this value may stay in
77
- # the cache. See ActiveSupport::Cache::Store#write for an example.
78
- def write(key, value, options = nil)
79
- super do
80
- method = options && options[:unless_exist] ? :add : :set
81
- # memcache-client will break the connection if you send it an integer
82
- # in raw mode, so we convert it to a string to be sure it continues working.
83
- value = value.to_s if raw?(options)
84
- response = @data.send(method, key, value, expires_in(options), raw?(options))
85
- response == Response::STORED
86
- end
87
- rescue MemCache::MemCacheError => e
88
- logger.error("MemCacheError (#{e}): #{e.message}") if logger
89
- false
90
- end
91
-
92
- def delete(key, options = nil) # :nodoc:
93
- super do
94
- response = @data.delete(key)
95
- response == Response::DELETED
72
+ # Reads multiple keys from the cache using a single call to the
73
+ # servers for all keys. Options can be passed in the last argument.
74
+ def read_multi(*names)
75
+ options = names.extract_options!
76
+ options = merged_options(options)
77
+ keys_to_names = names.inject({}){|map, name| map[escape_key(namespaced_key(name, options))] = name; map}
78
+ raw_values = @data.get_multi(keys_to_names.keys, :raw => true)
79
+ values = {}
80
+ raw_values.each do |key, value|
81
+ entry = deserialize_entry(value)
82
+ values[keys_to_names[key]] = entry.value unless entry.expired?
96
83
  end
97
- rescue MemCache::MemCacheError => e
98
- logger.error("MemCacheError (#{e}): #{e.message}") if logger
99
- false
84
+ values
100
85
  end
101
86
 
102
- def exist?(key, options = nil) # :nodoc:
103
- # Doesn't call super, cause exist? in memcache is in fact a read
104
- # But who cares? Reading is very fast anyway
105
- # Local cache is checked first, if it doesn't know then memcache itself is read from
106
- super do
107
- !read(key, options).nil?
87
+ # Increment a cached value. This method uses the memcached incr atomic
88
+ # operator and can only be used on values written with the :raw option.
89
+ # Calling it on a value not stored with :raw will initialize that value
90
+ # to zero.
91
+ def increment(name, amount = 1, options = nil) # :nodoc:
92
+ options = merged_options(options)
93
+ response = instrument(:increment, name, :amount => amount) do
94
+ @data.incr(escape_key(namespaced_key(name, options)), amount)
108
95
  end
109
- end
110
-
111
- def increment(key, amount = 1) # :nodoc:
112
- response = instrument(:increment, key, :amount => amount) do
113
- @data.incr(key, amount)
114
- end
115
-
116
- response == Response::NOT_FOUND ? nil : response
96
+ response == Response::NOT_FOUND ? nil : response.to_i
117
97
  rescue MemCache::MemCacheError
118
98
  nil
119
99
  end
120
100
 
121
- def decrement(key, amount = 1) # :nodoc:
122
- response = instrument(:decrement, key, :amount => amount) do
123
- @data.decr(key, amount)
101
+ # Decrement a cached value. This method uses the memcached decr atomic
102
+ # operator and can only be used on values written with the :raw option.
103
+ # Calling it on a value not stored with :raw will initialize that value
104
+ # to zero.
105
+ def decrement(name, amount = 1, options = nil) # :nodoc:
106
+ options = merged_options(options)
107
+ response = instrument(:decrement, name, :amount => amount) do
108
+ @data.decr(escape_key(namespaced_key(name, options)), amount)
124
109
  end
125
-
126
- response == Response::NOT_FOUND ? nil : response
110
+ response == Response::NOT_FOUND ? nil : response.to_i
127
111
  rescue MemCache::MemCacheError
128
112
  nil
129
113
  end
130
114
 
131
- def delete_matched(matcher, options = nil) # :nodoc:
132
- # don't do any local caching at present, just pass
133
- # through and let the error happen
134
- super
135
- raise "Not supported by Memcache"
136
- end
137
-
138
- def clear
115
+ # Clear the entire cache on all memcached servers. This method should
116
+ # be used with care when using a shared cache.
117
+ def clear(options = nil)
139
118
  @data.flush_all
140
119
  end
141
120
 
121
+ # Get the statistics from the memcached servers.
142
122
  def stats
143
123
  @data.stats
144
124
  end
145
125
 
126
+ protected
127
+ # Read an entry from the cache.
128
+ def read_entry(key, options) # :nodoc:
129
+ deserialize_entry(@data.get(escape_key(key), true))
130
+ rescue MemCache::MemCacheError => e
131
+ logger.error("MemCacheError (#{e}): #{e.message}") if logger
132
+ nil
133
+ end
134
+
135
+ # Write an entry to the cache.
136
+ def write_entry(key, entry, options) # :nodoc:
137
+ method = options && options[:unless_exist] ? :add : :set
138
+ value = options[:raw] ? entry.value.to_s : entry
139
+ expires_in = options[:expires_in].to_i
140
+ if expires_in > 0 && !options[:raw]
141
+ # Set the memcache expire a few minutes in the future to support race condition ttls on read
142
+ expires_in += 5.minutes
143
+ end
144
+ response = @data.send(method, escape_key(key), value, expires_in, options[:raw])
145
+ response == Response::STORED
146
+ rescue MemCache::MemCacheError => e
147
+ logger.error("MemCacheError (#{e}): #{e.message}") if logger
148
+ false
149
+ end
150
+
151
+ # Delete an entry from the cache.
152
+ def delete_entry(key, options) # :nodoc:
153
+ response = @data.delete(escape_key(key))
154
+ response == Response::DELETED
155
+ rescue MemCache::MemCacheError => e
156
+ logger.error("MemCacheError (#{e}): #{e.message}") if logger
157
+ false
158
+ end
159
+
146
160
  private
147
- def raw?(options)
148
- options && options[:raw]
161
+ def escape_key(key)
162
+ key = key.to_s.gsub(ESCAPE_KEY_CHARS){|match| "%#{match[0].to_s(16).upcase}"}
163
+ key = "#{key[0, 213]}:md5:#{Digest::MD5.hexdigest(key)}" if key.size > 250
164
+ key
149
165
  end
166
+
167
+ def deserialize_entry(raw_value)
168
+ if raw_value
169
+ entry = Marshal.load(raw_value) rescue raw_value
170
+ entry.is_a?(Entry) ? entry : Entry.new(entry)
171
+ else
172
+ nil
173
+ end
174
+ end
175
+
176
+ # Provide support for raw values in the local cache strategy.
177
+ module LocalCacheWithRaw # :nodoc:
178
+ protected
179
+ def write_entry(key, entry, options) # :nodoc:
180
+ retval = super
181
+ if options[:raw] && local_cache && retval
182
+ raw_entry = Entry.new(entry.value.to_s)
183
+ raw_entry.expires_at = entry.expires_at
184
+ local_cache.write_entry(key, raw_entry, options)
185
+ end
186
+ retval
187
+ end
188
+ end
150
189
  end
151
190
  end
152
191
  end