activesupport 5.2.0 → 6.0.3.2

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 (160) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +479 -330
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +4 -3
  5. data/lib/active_support.rb +2 -1
  6. data/lib/active_support/actionable_error.rb +48 -0
  7. data/lib/active_support/backtrace_cleaner.rb +27 -1
  8. data/lib/active_support/cache.rb +104 -84
  9. data/lib/active_support/cache/file_store.rb +29 -30
  10. data/lib/active_support/cache/mem_cache_store.rb +14 -19
  11. data/lib/active_support/cache/memory_store.rb +15 -9
  12. data/lib/active_support/cache/null_store.rb +8 -3
  13. data/lib/active_support/cache/redis_cache_store.rb +73 -34
  14. data/lib/active_support/cache/strategy/local_cache.rb +23 -23
  15. data/lib/active_support/callbacks.rb +16 -8
  16. data/lib/active_support/concern.rb +31 -4
  17. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +18 -0
  18. data/lib/active_support/concurrency/share_lock.rb +0 -1
  19. data/lib/active_support/configurable.rb +7 -11
  20. data/lib/active_support/core_ext/array.rb +1 -1
  21. data/lib/active_support/core_ext/array/access.rb +18 -6
  22. data/lib/active_support/core_ext/array/conversions.rb +5 -5
  23. data/lib/active_support/core_ext/array/extract.rb +21 -0
  24. data/lib/active_support/core_ext/array/prepend_and_append.rb +2 -6
  25. data/lib/active_support/core_ext/class/attribute.rb +11 -16
  26. data/lib/active_support/core_ext/class/subclasses.rb +1 -1
  27. data/lib/active_support/core_ext/date/calculations.rb +6 -5
  28. data/lib/active_support/core_ext/date_and_time/calculations.rb +24 -47
  29. data/lib/active_support/core_ext/date_and_time/zones.rb +0 -1
  30. data/lib/active_support/core_ext/date_time/calculations.rb +1 -1
  31. data/lib/active_support/core_ext/date_time/conversions.rb +0 -1
  32. data/lib/active_support/core_ext/digest.rb +3 -0
  33. data/lib/active_support/core_ext/enumerable.rb +97 -68
  34. data/lib/active_support/core_ext/file/atomic.rb +1 -1
  35. data/lib/active_support/core_ext/hash.rb +1 -2
  36. data/lib/active_support/core_ext/hash/compact.rb +2 -26
  37. data/lib/active_support/core_ext/hash/conversions.rb +1 -1
  38. data/lib/active_support/core_ext/hash/deep_transform_values.rb +46 -0
  39. data/lib/active_support/core_ext/hash/except.rb +1 -1
  40. data/lib/active_support/core_ext/hash/keys.rb +0 -29
  41. data/lib/active_support/core_ext/hash/slice.rb +3 -25
  42. data/lib/active_support/core_ext/hash/transform_values.rb +2 -29
  43. data/lib/active_support/core_ext/integer/multiple.rb +1 -1
  44. data/lib/active_support/core_ext/kernel.rb +0 -1
  45. data/lib/active_support/core_ext/load_error.rb +1 -1
  46. data/lib/active_support/core_ext/module.rb +0 -1
  47. data/lib/active_support/core_ext/module/attribute_accessors.rb +7 -10
  48. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +13 -19
  49. data/lib/active_support/core_ext/module/delegation.rb +41 -8
  50. data/lib/active_support/core_ext/module/introspection.rb +38 -13
  51. data/lib/active_support/core_ext/module/reachable.rb +1 -6
  52. data/lib/active_support/core_ext/module/redefine_method.rb +8 -17
  53. data/lib/active_support/core_ext/numeric.rb +0 -1
  54. data/lib/active_support/core_ext/numeric/conversions.rb +124 -128
  55. data/lib/active_support/core_ext/numeric/inquiry.rb +2 -25
  56. data/lib/active_support/core_ext/object/blank.rb +1 -2
  57. data/lib/active_support/core_ext/object/duplicable.rb +7 -114
  58. data/lib/active_support/core_ext/object/json.rb +1 -0
  59. data/lib/active_support/core_ext/object/to_query.rb +5 -2
  60. data/lib/active_support/core_ext/object/try.rb +17 -7
  61. data/lib/active_support/core_ext/object/with_options.rb +1 -1
  62. data/lib/active_support/core_ext/range.rb +1 -1
  63. data/lib/active_support/core_ext/range/compare_range.rb +76 -0
  64. data/lib/active_support/core_ext/range/conversions.rb +31 -29
  65. data/lib/active_support/core_ext/range/each.rb +0 -1
  66. data/lib/active_support/core_ext/range/include_range.rb +6 -22
  67. data/lib/active_support/core_ext/range/include_time_with_zone.rb +2 -2
  68. data/lib/active_support/core_ext/regexp.rb +0 -4
  69. data/lib/active_support/core_ext/securerandom.rb +23 -3
  70. data/lib/active_support/core_ext/string/access.rb +8 -0
  71. data/lib/active_support/core_ext/string/filters.rb +42 -1
  72. data/lib/active_support/core_ext/string/inflections.rb +7 -2
  73. data/lib/active_support/core_ext/string/multibyte.rb +4 -3
  74. data/lib/active_support/core_ext/string/output_safety.rb +63 -6
  75. data/lib/active_support/core_ext/string/strip.rb +3 -1
  76. data/lib/active_support/core_ext/time/calculations.rb +31 -2
  77. data/lib/active_support/core_ext/uri.rb +2 -4
  78. data/lib/active_support/current_attributes.rb +8 -0
  79. data/lib/active_support/dependencies.rb +77 -18
  80. data/lib/active_support/dependencies/zeitwerk_integration.rb +117 -0
  81. data/lib/active_support/deprecation.rb +1 -1
  82. data/lib/active_support/deprecation/behaviors.rb +5 -1
  83. data/lib/active_support/deprecation/method_wrappers.rb +20 -13
  84. data/lib/active_support/deprecation/proxy_wrappers.rb +28 -5
  85. data/lib/active_support/deprecation/reporting.rb +1 -1
  86. data/lib/active_support/descendants_tracker.rb +55 -9
  87. data/lib/active_support/duration.rb +19 -16
  88. data/lib/active_support/duration/iso8601_parser.rb +2 -4
  89. data/lib/active_support/duration/iso8601_serializer.rb +3 -5
  90. data/lib/active_support/encrypted_configuration.rb +1 -5
  91. data/lib/active_support/encrypted_file.rb +4 -3
  92. data/lib/active_support/evented_file_update_checker.rb +39 -10
  93. data/lib/active_support/execution_wrapper.rb +1 -0
  94. data/lib/active_support/file_update_checker.rb +0 -1
  95. data/lib/active_support/gem_version.rb +4 -4
  96. data/lib/active_support/hash_with_indifferent_access.rb +36 -18
  97. data/lib/active_support/i18n.rb +1 -0
  98. data/lib/active_support/i18n_railtie.rb +18 -2
  99. data/lib/active_support/inflector/inflections.rb +1 -5
  100. data/lib/active_support/inflector/methods.rb +18 -29
  101. data/lib/active_support/inflector/transliterate.rb +47 -18
  102. data/lib/active_support/json/decoding.rb +23 -24
  103. data/lib/active_support/json/encoding.rb +6 -2
  104. data/lib/active_support/key_generator.rb +0 -32
  105. data/lib/active_support/lazy_load_hooks.rb +5 -2
  106. data/lib/active_support/locale/en.rb +33 -0
  107. data/lib/active_support/log_subscriber.rb +31 -9
  108. data/lib/active_support/logger.rb +1 -16
  109. data/lib/active_support/logger_silence.rb +28 -12
  110. data/lib/active_support/logger_thread_safe_level.rb +28 -5
  111. data/lib/active_support/message_encryptor.rb +4 -6
  112. data/lib/active_support/message_verifier.rb +5 -5
  113. data/lib/active_support/messages/metadata.rb +3 -2
  114. data/lib/active_support/messages/rotator.rb +4 -4
  115. data/lib/active_support/multibyte/chars.rb +29 -49
  116. data/lib/active_support/multibyte/unicode.rb +44 -282
  117. data/lib/active_support/notifications.rb +41 -4
  118. data/lib/active_support/notifications/fanout.rb +100 -15
  119. data/lib/active_support/notifications/instrumenter.rb +80 -9
  120. data/lib/active_support/number_helper.rb +11 -0
  121. data/lib/active_support/number_helper/number_converter.rb +4 -5
  122. data/lib/active_support/number_helper/number_to_currency_converter.rb +9 -10
  123. data/lib/active_support/number_helper/number_to_delimited_converter.rb +3 -2
  124. data/lib/active_support/number_helper/number_to_human_converter.rb +3 -2
  125. data/lib/active_support/number_helper/number_to_human_size_converter.rb +3 -2
  126. data/lib/active_support/number_helper/number_to_percentage_converter.rb +3 -1
  127. data/lib/active_support/number_helper/number_to_phone_converter.rb +2 -1
  128. data/lib/active_support/number_helper/number_to_rounded_converter.rb +5 -4
  129. data/lib/active_support/option_merger.rb +21 -3
  130. data/lib/active_support/ordered_hash.rb +1 -1
  131. data/lib/active_support/ordered_options.rb +5 -1
  132. data/lib/active_support/parameter_filter.rb +128 -0
  133. data/lib/active_support/rails.rb +0 -6
  134. data/lib/active_support/reloader.rb +4 -5
  135. data/lib/active_support/security_utils.rb +1 -1
  136. data/lib/active_support/string_inquirer.rb +0 -1
  137. data/lib/active_support/subscriber.rb +65 -22
  138. data/lib/active_support/tagged_logging.rb +13 -4
  139. data/lib/active_support/test_case.rb +92 -1
  140. data/lib/active_support/testing/assertions.rb +15 -1
  141. data/lib/active_support/testing/deprecation.rb +0 -1
  142. data/lib/active_support/testing/file_fixtures.rb +2 -0
  143. data/lib/active_support/testing/isolation.rb +2 -2
  144. data/lib/active_support/testing/method_call_assertions.rb +28 -1
  145. data/lib/active_support/testing/parallelization.rb +134 -0
  146. data/lib/active_support/testing/setup_and_teardown.rb +5 -9
  147. data/lib/active_support/testing/stream.rb +1 -2
  148. data/lib/active_support/testing/time_helpers.rb +7 -9
  149. data/lib/active_support/time_with_zone.rb +15 -5
  150. data/lib/active_support/values/time_zone.rb +14 -8
  151. data/lib/active_support/xml_mini.rb +2 -10
  152. data/lib/active_support/xml_mini/jdom.rb +2 -3
  153. data/lib/active_support/xml_mini/libxml.rb +2 -2
  154. data/lib/active_support/xml_mini/libxmlsax.rb +4 -4
  155. data/lib/active_support/xml_mini/nokogiri.rb +2 -2
  156. data/lib/active_support/xml_mini/nokogirisax.rb +3 -3
  157. data/lib/active_support/xml_mini/rexml.rb +2 -2
  158. metadata +42 -13
  159. data/lib/active_support/core_ext/kernel/agnostics.rb +0 -13
  160. data/lib/active_support/values/unicode_tables.dat +0 -0
@@ -18,7 +18,6 @@ module ActiveSupport
18
18
  DIR_FORMATTER = "%03X"
19
19
  FILENAME_MAX_SIZE = 228 # max filename size on file system is 255, minus room for timestamp and random characters appended by Tempfile (used by atomic write)
20
20
  FILEPATH_MAX_SIZE = 900 # max is 1024, plus some room
21
- EXCLUDED_DIRS = [".", ".."].freeze
22
21
  GITKEEP_FILES = [".gitkeep", ".keep"].freeze
23
22
 
24
23
  def initialize(cache_path, options = nil)
@@ -26,21 +25,26 @@ module ActiveSupport
26
25
  @cache_path = cache_path.to_s
27
26
  end
28
27
 
28
+ # Advertise cache versioning support.
29
+ def self.supports_cache_versioning?
30
+ true
31
+ end
32
+
29
33
  # Deletes all items from the cache. In this case it deletes all the entries in the specified
30
34
  # file store directory except for .keep or .gitkeep. Be careful which directory is specified in your
31
35
  # config file when using +FileStore+ because everything in that directory will be deleted.
32
36
  def clear(options = nil)
33
- root_dirs = exclude_from(cache_path, EXCLUDED_DIRS + GITKEEP_FILES)
37
+ root_dirs = (Dir.children(cache_path) - GITKEEP_FILES)
34
38
  FileUtils.rm_r(root_dirs.collect { |f| File.join(cache_path, f) })
35
- rescue Errno::ENOENT
39
+ rescue Errno::ENOENT, Errno::ENOTEMPTY
36
40
  end
37
41
 
38
42
  # Preemptively iterates through all stored keys and removes the ones which have expired.
39
43
  def cleanup(options = nil)
40
44
  options = merged_options(options)
41
45
  search_dir(cache_path) do |fname|
42
- entry = read_entry(fname, options)
43
- delete_entry(fname, options) if entry && entry.expired?
46
+ entry = read_entry(fname, **options)
47
+ delete_entry(fname, **options) if entry && entry.expired?
44
48
  end
45
49
  end
46
50
 
@@ -62,14 +66,13 @@ module ActiveSupport
62
66
  matcher = key_matcher(matcher, options)
63
67
  search_dir(cache_path) do |path|
64
68
  key = file_path_key(path)
65
- delete_entry(path, options) if key.match(matcher)
69
+ delete_entry(path, **options) if key.match(matcher)
66
70
  end
67
71
  end
68
72
  end
69
73
 
70
74
  private
71
-
72
- def read_entry(key, options)
75
+ def read_entry(key, **options)
73
76
  if File.exist?(key)
74
77
  File.open(key) { |f| Marshal.load(f) }
75
78
  end
@@ -78,14 +81,14 @@ module ActiveSupport
78
81
  nil
79
82
  end
80
83
 
81
- def write_entry(key, entry, options)
84
+ def write_entry(key, entry, **options)
82
85
  return false if options[:unless_exist] && File.exist?(key)
83
86
  ensure_cache_path(File.dirname(key))
84
87
  File.atomic_write(key, cache_path) { |f| Marshal.dump(entry, f) }
85
88
  true
86
89
  end
87
90
 
88
- def delete_entry(key, options)
91
+ def delete_entry(key, **options)
89
92
  if File.exist?(key)
90
93
  begin
91
94
  File.delete(key)
@@ -103,12 +106,10 @@ module ActiveSupport
103
106
  def lock_file(file_name, &block)
104
107
  if File.exist?(file_name)
105
108
  File.open(file_name, "r+") do |f|
106
- begin
107
- f.flock File::LOCK_EX
108
- yield
109
- ensure
110
- f.flock File::LOCK_UN
111
- end
109
+ f.flock File::LOCK_EX
110
+ yield
111
+ ensure
112
+ f.flock File::LOCK_UN
112
113
  end
113
114
  else
114
115
  yield
@@ -127,15 +128,19 @@ module ActiveSupport
127
128
  hash = Zlib.adler32(fname)
128
129
  hash, dir_1 = hash.divmod(0x1000)
129
130
  dir_2 = hash.modulo(0x1000)
130
- fname_paths = []
131
131
 
132
132
  # Make sure file name doesn't exceed file system limits.
133
- begin
134
- fname_paths << fname[0, FILENAME_MAX_SIZE]
135
- fname = fname[FILENAME_MAX_SIZE..-1]
136
- end until fname.blank?
133
+ if fname.length < FILENAME_MAX_SIZE
134
+ fname_paths = fname
135
+ else
136
+ fname_paths = []
137
+ begin
138
+ fname_paths << fname[0, FILENAME_MAX_SIZE]
139
+ fname = fname[FILENAME_MAX_SIZE..-1]
140
+ end until fname.blank?
141
+ end
137
142
 
138
- File.join(cache_path, DIR_FORMATTER % dir_1, DIR_FORMATTER % dir_2, *fname_paths)
143
+ File.join(cache_path, DIR_FORMATTER % dir_1, DIR_FORMATTER % dir_2, fname_paths)
139
144
  end
140
145
 
141
146
  # Translate a file path into a key.
@@ -147,7 +152,7 @@ module ActiveSupport
147
152
  # Delete empty directories in the cache.
148
153
  def delete_empty_directories(dir)
149
154
  return if File.realpath(dir) == File.realpath(cache_path)
150
- if exclude_from(dir, EXCLUDED_DIRS).empty?
155
+ if Dir.children(dir).empty?
151
156
  Dir.delete(dir) rescue nil
152
157
  delete_empty_directories(File.dirname(dir))
153
158
  end
@@ -160,8 +165,7 @@ module ActiveSupport
160
165
 
161
166
  def search_dir(dir, &callback)
162
167
  return if !File.exist?(dir)
163
- Dir.foreach(dir) do |d|
164
- next if EXCLUDED_DIRS.include?(d)
168
+ Dir.each_child(dir) do |d|
165
169
  name = File.join(dir, d)
166
170
  if File.directory?(name)
167
171
  search_dir(name, &callback)
@@ -186,11 +190,6 @@ module ActiveSupport
186
190
  end
187
191
  end
188
192
  end
189
-
190
- # Exclude entries from source directory
191
- def exclude_from(source, excludes)
192
- Dir.entries(source).reject { |f| excludes.include?(f) }
193
- end
194
193
  end
195
194
  end
196
195
  end
@@ -7,7 +7,6 @@ rescue LoadError => e
7
7
  raise e
8
8
  end
9
9
 
10
- require "active_support/core_ext/marshal"
11
10
  require "active_support/core_ext/array/extract_options"
12
11
 
13
12
  module ActiveSupport
@@ -28,25 +27,22 @@ module ActiveSupport
28
27
  # Provide support for raw values in the local cache strategy.
29
28
  module LocalCacheWithRaw # :nodoc:
30
29
  private
31
- def read_entry(key, options)
32
- entry = super
33
- if options[:raw] && local_cache && entry
34
- entry = deserialize_entry(entry.value)
35
- end
36
- entry
37
- end
38
-
39
- def write_entry(key, entry, options)
30
+ def write_entry(key, entry, **options)
40
31
  if options[:raw] && local_cache
41
32
  raw_entry = Entry.new(entry.value.to_s)
42
33
  raw_entry.expires_at = entry.expires_at
43
- super(key, raw_entry, options)
34
+ super(key, raw_entry, **options)
44
35
  else
45
36
  super
46
37
  end
47
38
  end
48
39
  end
49
40
 
41
+ # Advertise cache versioning support.
42
+ def self.supports_cache_versioning?
43
+ true
44
+ end
45
+
50
46
  prepend Strategy::LocalCache
51
47
  prepend LocalCacheWithRaw
52
48
 
@@ -137,12 +133,12 @@ module ActiveSupport
137
133
 
138
134
  private
139
135
  # Read an entry from the cache.
140
- def read_entry(key, options)
136
+ def read_entry(key, **options)
141
137
  rescue_error_with(nil) { deserialize_entry(@data.with { |c| c.get(key, options) }) }
142
138
  end
143
139
 
144
140
  # Write an entry to the cache.
145
- def write_entry(key, entry, options)
141
+ def write_entry(key, entry, **options)
146
142
  method = options && options[:unless_exist] ? :add : :set
147
143
  value = options[:raw] ? entry.value.to_s : entry
148
144
  expires_in = options[:expires_in].to_i
@@ -151,12 +147,12 @@ module ActiveSupport
151
147
  expires_in += 5.minutes
152
148
  end
153
149
  rescue_error_with false do
154
- @data.with { |c| c.send(method, key, value, expires_in, options) }
150
+ @data.with { |c| c.send(method, key, value, expires_in, **options) }
155
151
  end
156
152
  end
157
153
 
158
154
  # Reads multiple entries from the cache implementation.
159
- def read_multi_entries(names, options)
155
+ def read_multi_entries(names, **options)
160
156
  keys_to_names = Hash[names.map { |name| [normalize_key(name, options), name] }]
161
157
 
162
158
  raw_values = @data.with { |c| c.get_multi(keys_to_names.keys) }
@@ -174,7 +170,7 @@ module ActiveSupport
174
170
  end
175
171
 
176
172
  # Delete an entry from the cache.
177
- def delete_entry(key, options)
173
+ def delete_entry(key, **options)
178
174
  rescue_error_with(false) { @data.with { |c| c.delete(key) } }
179
175
  end
180
176
 
@@ -189,9 +185,8 @@ module ActiveSupport
189
185
  key
190
186
  end
191
187
 
192
- def deserialize_entry(raw_value)
193
- if raw_value
194
- entry = Marshal.load(raw_value) rescue raw_value
188
+ def deserialize_entry(entry)
189
+ if entry
195
190
  entry.is_a?(Entry) ? entry : Entry.new(entry)
196
191
  end
197
192
  end
@@ -30,6 +30,11 @@ module ActiveSupport
30
30
  @pruning = false
31
31
  end
32
32
 
33
+ # Advertise cache versioning support.
34
+ def self.supports_cache_versioning?
35
+ true
36
+ end
37
+
33
38
  # Delete all data stored in a given cache store.
34
39
  def clear(options = nil)
35
40
  synchronize do
@@ -46,7 +51,7 @@ module ActiveSupport
46
51
  keys = synchronize { @data.keys }
47
52
  keys.each do |key|
48
53
  entry = @data[key]
49
- delete_entry(key, options) if entry && entry.expired?
54
+ delete_entry(key, **options) if entry && entry.expired?
50
55
  end
51
56
  end
52
57
  end
@@ -57,13 +62,13 @@ module ActiveSupport
57
62
  return if pruning?
58
63
  @pruning = true
59
64
  begin
60
- start_time = Time.now
65
+ start_time = Concurrent.monotonic_time
61
66
  cleanup
62
67
  instrument(:prune, target_size, from: @cache_size) do
63
68
  keys = synchronize { @key_access.keys.sort { |a, b| @key_access[a].to_f <=> @key_access[b].to_f } }
64
69
  keys.each do |key|
65
- delete_entry(key, options)
66
- return if @cache_size <= target_size || (max_time && Time.now - start_time > max_time)
70
+ delete_entry(key, **options)
71
+ return if @cache_size <= target_size || (max_time && Concurrent.monotonic_time - start_time > max_time)
67
72
  end
68
73
  end
69
74
  ensure
@@ -93,7 +98,7 @@ module ActiveSupport
93
98
  matcher = key_matcher(matcher, options)
94
99
  keys = synchronize { @data.keys }
95
100
  keys.each do |key|
96
- delete_entry(key, options) if key.match(matcher)
101
+ delete_entry(key, **options) if key.match(matcher)
97
102
  end
98
103
  end
99
104
  end
@@ -109,17 +114,18 @@ module ActiveSupport
109
114
  end
110
115
 
111
116
  private
112
-
113
117
  PER_ENTRY_OVERHEAD = 240
114
118
 
115
119
  def cached_size(key, entry)
116
120
  key.to_s.bytesize + entry.size + PER_ENTRY_OVERHEAD
117
121
  end
118
122
 
119
- def read_entry(key, options)
123
+ def read_entry(key, **options)
120
124
  entry = @data[key]
121
125
  synchronize do
122
126
  if entry
127
+ entry = entry.dup
128
+ entry.dup_value!
123
129
  @key_access[key] = Time.now.to_f
124
130
  else
125
131
  @key_access.delete(key)
@@ -128,7 +134,7 @@ module ActiveSupport
128
134
  entry
129
135
  end
130
136
 
131
- def write_entry(key, entry, options)
137
+ def write_entry(key, entry, **options)
132
138
  entry.dup_value!
133
139
  synchronize do
134
140
  old_entry = @data[key]
@@ -145,7 +151,7 @@ module ActiveSupport
145
151
  end
146
152
  end
147
153
 
148
- def delete_entry(key, options)
154
+ def delete_entry(key, **options)
149
155
  synchronize do
150
156
  @key_access.delete(key)
151
157
  entry = @data.delete(key)
@@ -12,6 +12,11 @@ module ActiveSupport
12
12
  class NullStore < Store
13
13
  prepend Strategy::LocalCache
14
14
 
15
+ # Advertise cache versioning support.
16
+ def self.supports_cache_versioning?
17
+ true
18
+ end
19
+
15
20
  def clear(options = nil)
16
21
  end
17
22
 
@@ -28,14 +33,14 @@ module ActiveSupport
28
33
  end
29
34
 
30
35
  private
31
- def read_entry(key, options)
36
+ def read_entry(key, **options)
32
37
  end
33
38
 
34
- def write_entry(key, entry, options)
39
+ def write_entry(key, entry, **options)
35
40
  true
36
41
  end
37
42
 
38
- def delete_entry(key, options)
43
+ def delete_entry(key, **options)
39
44
  false
40
45
  end
41
46
  end
@@ -17,7 +17,6 @@ end
17
17
 
18
18
  require "digest/sha2"
19
19
  require "active_support/core_ext/marshal"
20
- require "active_support/core_ext/hash/transform_values"
21
20
 
22
21
  module ActiveSupport
23
22
  module Cache
@@ -63,38 +62,36 @@ module ActiveSupport
63
62
  end
64
63
  end
65
64
 
66
- DELETE_GLOB_LUA = "for i, name in ipairs(redis.call('KEYS', ARGV[1])) do redis.call('DEL', name); end"
67
- private_constant :DELETE_GLOB_LUA
65
+ # The maximum number of entries to receive per SCAN call.
66
+ SCAN_BATCH_SIZE = 1000
67
+ private_constant :SCAN_BATCH_SIZE
68
+
69
+ # Advertise cache versioning support.
70
+ def self.supports_cache_versioning?
71
+ true
72
+ end
68
73
 
69
74
  # Support raw values in the local cache strategy.
70
75
  module LocalCacheWithRaw # :nodoc:
71
76
  private
72
- def read_entry(key, options)
73
- entry = super
74
- if options[:raw] && local_cache && entry
75
- entry = deserialize_entry(entry.value)
76
- end
77
- entry
78
- end
79
-
80
- def write_entry(key, entry, options)
77
+ def write_entry(key, entry, **options)
81
78
  if options[:raw] && local_cache
82
79
  raw_entry = Entry.new(serialize_entry(entry, raw: true))
83
80
  raw_entry.expires_at = entry.expires_at
84
- super(key, raw_entry, options)
81
+ super(key, raw_entry, **options)
85
82
  else
86
83
  super
87
84
  end
88
85
  end
89
86
 
90
- def write_multi_entries(entries, options)
87
+ def write_multi_entries(entries, **options)
91
88
  if options[:raw] && local_cache
92
89
  raw_entries = entries.map do |key, entry|
93
90
  raw_entry = Entry.new(serialize_entry(entry, raw: true))
94
91
  raw_entry.expires_at = entry.expires_at
95
92
  end.to_h
96
93
 
97
- super(raw_entries, options)
94
+ super(raw_entries, **options)
98
95
  else
99
96
  super
100
97
  end
@@ -147,15 +144,17 @@ module ActiveSupport
147
144
 
148
145
  # Creates a new Redis cache store.
149
146
  #
150
- # Handles three options: block provided to instantiate, single URL
151
- # provided, and multiple URLs provided.
147
+ # Handles four options: :redis block, :redis instance, single :url
148
+ # string, and multiple :url strings.
152
149
  #
153
- # :redis Proc -> options[:redis].call
154
- # :url String -> Redis.new(url: …)
155
- # :url Array -> Redis::Distributed.new([{ url: … }, { url: … }, …])
150
+ # Option Class Result
151
+ # :redis Proc -> options[:redis].call
152
+ # :redis Object -> options[:redis]
153
+ # :url String -> Redis.new(url: …)
154
+ # :url Array -> Redis::Distributed.new([{ url: … }, { url: … }, …])
156
155
  #
157
156
  # No namespace is set by default. Provide one if the Redis cache
158
- # server is shared with other apps: <tt>namespace: 'myapp-cache'<tt>.
157
+ # server is shared with other apps: <tt>namespace: 'myapp-cache'</tt>.
159
158
  #
160
159
  # Compression is enabled by default with a 1kB threshold, so cached
161
160
  # values larger than 1kB are automatically compressed. Disable by
@@ -232,12 +231,18 @@ module ActiveSupport
232
231
  # Failsafe: Raises errors.
233
232
  def delete_matched(matcher, options = nil)
234
233
  instrument :delete_matched, matcher do
235
- case matcher
236
- when String
237
- redis.with { |c| c.eval DELETE_GLOB_LUA, [], [namespace_key(matcher, options)] }
238
- else
234
+ unless String === matcher
239
235
  raise ArgumentError, "Only Redis glob strings are supported: #{matcher.inspect}"
240
236
  end
237
+ redis.with do |c|
238
+ pattern = namespace_key(matcher, options)
239
+ cursor = "0"
240
+ # Fetch keys in batches using SCAN to avoid blocking the Redis server.
241
+ begin
242
+ cursor, keys = c.scan(cursor, match: pattern, count: SCAN_BATCH_SIZE)
243
+ c.del(*keys) unless keys.empty?
244
+ end until cursor == "0"
245
+ end
241
246
  end
242
247
  end
243
248
 
@@ -252,7 +257,14 @@ module ActiveSupport
252
257
  def increment(name, amount = 1, options = nil)
253
258
  instrument :increment, name, amount: amount do
254
259
  failsafe :increment do
255
- redis.with { |c| c.incrby normalize_key(name, options), amount }
260
+ options = merged_options(options)
261
+ key = normalize_key(name, options)
262
+
263
+ redis.with do |c|
264
+ c.incrby(key, amount).tap do
265
+ write_key_expiry(c, key, options)
266
+ end
267
+ end
256
268
  end
257
269
  end
258
270
  end
@@ -268,7 +280,14 @@ module ActiveSupport
268
280
  def decrement(name, amount = 1, options = nil)
269
281
  instrument :decrement, name, amount: amount do
270
282
  failsafe :decrement do
271
- redis.with { |c| c.decrby normalize_key(name, options), amount }
283
+ options = merged_options(options)
284
+ key = normalize_key(name, options)
285
+
286
+ redis.with do |c|
287
+ c.decrby(key, amount).tap do
288
+ write_key_expiry(c, key, options)
289
+ end
290
+ end
272
291
  end
273
292
  end
274
293
  end
@@ -287,7 +306,7 @@ module ActiveSupport
287
306
  # Failsafe: Raises errors.
288
307
  def clear(options = nil)
289
308
  failsafe :clear do
290
- if namespace = merged_options(options)[namespace]
309
+ if namespace = merged_options(options)[:namespace]
291
310
  delete_matched "*", namespace: namespace
292
311
  else
293
312
  redis.with { |c| c.flushdb }
@@ -319,13 +338,14 @@ module ActiveSupport
319
338
 
320
339
  # Store provider interface:
321
340
  # Read an entry from the cache.
322
- def read_entry(key, options = nil)
341
+ def read_entry(key, **options)
323
342
  failsafe :read_entry do
324
- deserialize_entry redis.with { |c| c.get(key) }
343
+ raw = options&.fetch(:raw, false)
344
+ deserialize_entry(redis.with { |c| c.get(key) }, raw: raw)
325
345
  end
326
346
  end
327
347
 
328
- def read_multi_entries(names, _options)
348
+ def read_multi_entries(names, **options)
329
349
  if mget_capable?
330
350
  read_multi_mget(*names)
331
351
  else
@@ -336,6 +356,8 @@ module ActiveSupport
336
356
  def read_multi_mget(*names)
337
357
  options = names.extract_options!
338
358
  options = merged_options(options)
359
+ return {} if names == []
360
+ raw = options&.fetch(:raw, false)
339
361
 
340
362
  keys = names.map { |name| normalize_key(name, options) }
341
363
 
@@ -345,7 +367,7 @@ module ActiveSupport
345
367
 
346
368
  names.zip(values).each_with_object({}) do |(name, value), results|
347
369
  if value
348
- entry = deserialize_entry(value)
370
+ entry = deserialize_entry(value, raw: raw)
349
371
  unless entry.nil? || entry.expired? || entry.mismatched?(normalize_version(name, options))
350
372
  results[name] = entry.value
351
373
  end
@@ -379,6 +401,12 @@ module ActiveSupport
379
401
  end
380
402
  end
381
403
 
404
+ def write_key_expiry(client, key, options)
405
+ if options[:expires_in] && client.ttl(key).negative?
406
+ client.expire key, options[:expires_in].to_i
407
+ end
408
+ end
409
+
382
410
  # Delete an entry from the cache.
383
411
  def delete_entry(key, options)
384
412
  failsafe :delete_entry, returning: false do
@@ -414,9 +442,20 @@ module ActiveSupport
414
442
  end
415
443
  end
416
444
 
417
- def deserialize_entry(serialized_entry)
445
+ def deserialize_entry(serialized_entry, raw:)
418
446
  if serialized_entry
419
447
  entry = Marshal.load(serialized_entry) rescue serialized_entry
448
+
449
+ written_raw = serialized_entry.equal?(entry)
450
+ if raw != written_raw
451
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
452
+ Using a different value for the raw option when reading and writing
453
+ to a cache key is deprecated for :redis_cache_store and Rails 6.0
454
+ will stop automatically detecting the format when reading to avoid
455
+ marshal loading untrusted raw strings.
456
+ MSG
457
+ end
458
+
420
459
  entry.is_a?(Entry) ? entry : Entry.new(entry)
421
460
  end
422
461
  end
@@ -437,7 +476,7 @@ module ActiveSupport
437
476
 
438
477
  def failsafe(method, returning: nil)
439
478
  yield
440
- rescue ::Redis::BaseConnectionError => e
479
+ rescue ::Redis::BaseError => e
441
480
  handle_exception exception: e, method: method, returning: returning
442
481
  returning
443
482
  end