activesupport 7.2.2.2 → 7.2.3

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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +143 -0
  3. data/README.rdoc +1 -1
  4. data/lib/active_support/backtrace_cleaner.rb +1 -1
  5. data/lib/active_support/broadcast_logger.rb +61 -74
  6. data/lib/active_support/cache/file_store.rb +2 -2
  7. data/lib/active_support/cache/mem_cache_store.rb +13 -15
  8. data/lib/active_support/cache/memory_store.rb +5 -5
  9. data/lib/active_support/cache/null_store.rb +2 -2
  10. data/lib/active_support/cache/redis_cache_store.rb +1 -1
  11. data/lib/active_support/cache/strategy/local_cache.rb +56 -20
  12. data/lib/active_support/cache.rb +3 -3
  13. data/lib/active_support/callbacks.rb +3 -2
  14. data/lib/active_support/core_ext/benchmark.rb +1 -0
  15. data/lib/active_support/core_ext/class/attribute.rb +2 -2
  16. data/lib/active_support/core_ext/date_time/conversions.rb +4 -2
  17. data/lib/active_support/core_ext/enumerable.rb +17 -5
  18. data/lib/active_support/core_ext/erb/util.rb +2 -2
  19. data/lib/active_support/core_ext/module/introspection.rb +3 -0
  20. data/lib/active_support/core_ext/object/try.rb +2 -2
  21. data/lib/active_support/core_ext/range/sole.rb +17 -0
  22. data/lib/active_support/core_ext/range.rb +1 -0
  23. data/lib/active_support/core_ext/securerandom.rb +24 -8
  24. data/lib/active_support/core_ext/string/filters.rb +3 -3
  25. data/lib/active_support/core_ext/string/multibyte.rb +2 -2
  26. data/lib/active_support/core_ext/time/compatibility.rb +9 -1
  27. data/lib/active_support/current_attributes.rb +14 -7
  28. data/lib/active_support/error_reporter.rb +5 -2
  29. data/lib/active_support/execution_wrapper.rb +1 -1
  30. data/lib/active_support/file_update_checker.rb +1 -1
  31. data/lib/active_support/gem_version.rb +2 -2
  32. data/lib/active_support/hash_with_indifferent_access.rb +20 -16
  33. data/lib/active_support/json/decoding.rb +1 -1
  34. data/lib/active_support/json/encoding.rb +23 -5
  35. data/lib/active_support/lazy_load_hooks.rb +1 -1
  36. data/lib/active_support/message_encryptors.rb +2 -2
  37. data/lib/active_support/message_verifier.rb +9 -0
  38. data/lib/active_support/message_verifiers.rb +5 -3
  39. data/lib/active_support/messages/rotator.rb +5 -0
  40. data/lib/active_support/multibyte/chars.rb +4 -1
  41. data/lib/active_support/testing/parallelization/server.rb +15 -2
  42. data/lib/active_support/testing/parallelization/worker.rb +2 -2
  43. data/lib/active_support/testing/parallelization.rb +12 -1
  44. data/lib/active_support/xml_mini.rb +2 -0
  45. metadata +5 -5
  46. data/lib/active_support/testing/strict_warnings.rb +0 -43
@@ -11,7 +11,8 @@ class DateTime
11
11
  #
12
12
  # This method is aliased to <tt>to_formatted_s</tt>.
13
13
  #
14
- # === Examples
14
+ # ==== Examples
15
+ #
15
16
  # datetime = DateTime.civil(2007, 12, 4, 0, 0, 0, 0) # => Tue, 04 Dec 2007 00:00:00 +0000
16
17
  #
17
18
  # datetime.to_fs(:db) # => "2007-12-04 00:00:00"
@@ -23,7 +24,8 @@ class DateTime
23
24
  # datetime.to_fs(:rfc822) # => "Tue, 04 Dec 2007 00:00:00 +0000"
24
25
  # datetime.to_fs(:iso8601) # => "2007-12-04T00:00:00+00:00"
25
26
  #
26
- # == Adding your own datetime formats to to_fs
27
+ # ==== Adding your own datetime formats to +to_fs+
28
+ #
27
29
  # DateTime formats are shared with Time. You can add your own to the
28
30
  # Time::DATE_FORMATS hash. Use the format name as the hash key and
29
31
  # either a strftime string or Proc instance that takes a time or
@@ -198,16 +198,28 @@ module Enumerable
198
198
  end
199
199
 
200
200
  # Returns the sole item in the enumerable. If there are no items, or more
201
- # than one item, raises +Enumerable::SoleItemExpectedError+.
201
+ # than one item, raises Enumerable::SoleItemExpectedError.
202
202
  #
203
203
  # ["x"].sole # => "x"
204
204
  # Set.new.sole # => Enumerable::SoleItemExpectedError: no item found
205
205
  # { a: 1, b: 2 }.sole # => Enumerable::SoleItemExpectedError: multiple items found
206
206
  def sole
207
- case count
208
- when 1 then return first # rubocop:disable Style/RedundantReturn
209
- when 0 then raise ActiveSupport::EnumerableCoreExt::SoleItemExpectedError, "no item found"
210
- when 2.. then raise ActiveSupport::EnumerableCoreExt::SoleItemExpectedError, "multiple items found"
207
+ result = nil
208
+ found = false
209
+
210
+ each do |*element|
211
+ if found
212
+ raise SoleItemExpectedError, "multiple items found"
213
+ end
214
+
215
+ result = element.size == 1 ? element[0] : element
216
+ found = true
217
+ end
218
+
219
+ if found
220
+ result
221
+ else
222
+ raise SoleItemExpectedError, "no item found"
211
223
  end
212
224
  end
213
225
  end
@@ -174,7 +174,7 @@ class ERB
174
174
 
175
175
  case source.matched
176
176
  when start_re
177
- tokens << [:TEXT, source.string[pos, len]] if len > 0
177
+ tokens << [:TEXT, source.string.byteslice(pos, len)] if len > 0
178
178
  tokens << [:OPEN, source.matched]
179
179
  if source.scan(/(.*?)(?=#{finish_re}|\z)/m)
180
180
  tokens << [:CODE, source.matched] unless source.matched.empty?
@@ -183,7 +183,7 @@ class ERB
183
183
  raise NotImplementedError
184
184
  end
185
185
  when finish_re
186
- tokens << [:CODE, source.string[pos, len]] if len > 0
186
+ tokens << [:CODE, source.string.byteslice(pos, len)] if len > 0
187
187
  tokens << [:CLOSE, source.matched]
188
188
  else
189
189
  raise NotImplementedError, source.matched
@@ -10,6 +10,9 @@ class Module
10
10
  if defined?(@parent_name)
11
11
  @parent_name
12
12
  else
13
+ name = self.name
14
+ return if name.nil?
15
+
13
16
  parent_name = name =~ /::[^:]+\z/ ? -$` : nil
14
17
  @parent_name = parent_name unless frozen?
15
18
  parent_name
@@ -145,14 +145,14 @@ class NilClass
145
145
  #
146
146
  # With +try+
147
147
  # @person.try(:children).try(:first).try(:name)
148
- def try(*)
148
+ def try(*, &)
149
149
  nil
150
150
  end
151
151
 
152
152
  # Calling +try!+ on +nil+ always returns +nil+.
153
153
  #
154
154
  # nil.try!(:name) # => nil
155
- def try!(*)
155
+ def try!(*, &)
156
156
  nil
157
157
  end
158
158
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Range
4
+ # Returns the sole item in the range. If there are no items, or more
5
+ # than one item, raises Enumerable::SoleItemExpectedError.
6
+ #
7
+ # (1..1).sole # => 1
8
+ # (2..1).sole # => Enumerable::SoleItemExpectedError: no item found
9
+ # (..1).sole # => Enumerable::SoleItemExpectedError: infinite range cannot represent a sole item
10
+ def sole
11
+ if self.begin.nil? || self.end.nil?
12
+ raise ActiveSupport::EnumerableCoreExt::SoleItemExpectedError, "infinite range '#{inspect}' cannot represent a sole item"
13
+ end
14
+
15
+ super
16
+ end
17
+ end
@@ -4,3 +4,4 @@ require "active_support/core_ext/range/conversions"
4
4
  require "active_support/core_ext/range/compare_range"
5
5
  require "active_support/core_ext/range/overlap"
6
6
  require "active_support/core_ext/range/each"
7
+ require "active_support/core_ext/range/sole"
@@ -16,8 +16,18 @@ module SecureRandom
16
16
  #
17
17
  # p SecureRandom.base58 # => "4kUgL2pdQMSCQtjE"
18
18
  # p SecureRandom.base58(24) # => "77TMHrHJFvFDwodq8w7Ev2m7"
19
- def self.base58(n = 16)
20
- SecureRandom.alphanumeric(n, chars: BASE58_ALPHABET)
19
+ if SecureRandom.method(:alphanumeric).parameters.size == 2 # Remove check when Ruby 3.3 is the minimum supported version
20
+ def self.base58(n = 16)
21
+ alphanumeric(n, chars: BASE58_ALPHABET)
22
+ end
23
+ else
24
+ def self.base58(n = 16)
25
+ SecureRandom.random_bytes(n).unpack("C*").map do |byte|
26
+ idx = byte % 64
27
+ idx = SecureRandom.random_number(58) if idx >= 58
28
+ BASE58_ALPHABET[idx]
29
+ end.join
30
+ end
21
31
  end
22
32
 
23
33
  # SecureRandom.base36 generates a random base36 string in lowercase.
@@ -31,11 +41,17 @@ module SecureRandom
31
41
  #
32
42
  # p SecureRandom.base36 # => "4kugl2pdqmscqtje"
33
43
  # p SecureRandom.base36(24) # => "77tmhrhjfvfdwodq8w7ev2m7"
34
- def self.base36(n = 16)
35
- SecureRandom.random_bytes(n).unpack("C*").map do |byte|
36
- idx = byte % 64
37
- idx = SecureRandom.random_number(36) if idx >= 36
38
- BASE36_ALPHABET[idx]
39
- end.join
44
+ if SecureRandom.method(:alphanumeric).parameters.size == 2 # Remove check when Ruby 3.3 is the minimum supported version
45
+ def self.base36(n = 16)
46
+ alphanumeric(n, chars: BASE36_ALPHABET)
47
+ end
48
+ else
49
+ def self.base36(n = 16)
50
+ SecureRandom.random_bytes(n).unpack("C*").map do |byte|
51
+ idx = byte % 64
52
+ idx = SecureRandom.random_number(36) if idx >= 36
53
+ BASE36_ALPHABET[idx]
54
+ end.join
55
+ end
40
56
  end
41
57
  end
@@ -88,11 +88,11 @@ class String
88
88
  # characters.
89
89
  #
90
90
  # >> "🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪".size
91
- # => 20
91
+ # # => 20
92
92
  # >> "🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪".bytesize
93
- # => 80
93
+ # # => 80
94
94
  # >> "🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪".truncate_bytes(20)
95
- # => "🔪🔪🔪🔪…"
95
+ # # => "🔪🔪🔪🔪…"
96
96
  #
97
97
  # The truncated text ends with the <tt>:omission</tt> string, defaulting
98
98
  # to "…", for a total length not exceeding <tt>truncate_to</tt>.
@@ -12,12 +12,12 @@ class String
12
12
  # class. If the proxy class doesn't respond to a certain method, it's forwarded to the encapsulated string.
13
13
  #
14
14
  # >> "lj".mb_chars.upcase.to_s
15
- # => "LJ"
15
+ # # => "LJ"
16
16
  #
17
17
  # NOTE: Ruby 2.4 and later support native Unicode case mappings:
18
18
  #
19
19
  # >> "lj".upcase
20
- # => "LJ"
20
+ # # => "LJ"
21
21
  #
22
22
  # == \Method chaining
23
23
  #
@@ -15,10 +15,18 @@ class Time
15
15
  end
16
16
 
17
17
  def preserve_timezone # :nodoc:
18
- active_support_local_zone == zone || super
18
+ system_local_time? || super
19
19
  end
20
20
 
21
21
  private
22
+ def system_local_time?
23
+ if ::Time.equal?(self.class)
24
+ zone = self.zone
25
+ String === zone &&
26
+ (zone != "UTC" || active_support_local_zone == "UTC")
27
+ end
28
+ end
29
+
22
30
  @@active_support_local_tz = nil
23
31
 
24
32
  def active_support_local_zone
@@ -108,15 +108,18 @@ module ActiveSupport
108
108
  # ==== Options
109
109
  #
110
110
  # * <tt>:default</tt> - The default value for the attributes. If the value
111
- # is a proc or lambda, it will be called whenever an instance is
112
- # constructed. Otherwise, the value will be duplicated with +#dup+.
113
- # Default values are re-assigned when the attributes are reset.
111
+ # is a proc or lambda, it will be called whenever an instance is
112
+ # constructed. Otherwise, the value will be duplicated with +#dup+.
113
+ # Default values are re-assigned when the attributes are reset.
114
114
  def attribute(*names, default: NOT_SET)
115
115
  invalid_attribute_names = names.map(&:to_sym) & INVALID_ATTRIBUTE_NAMES
116
116
  if invalid_attribute_names.any?
117
117
  raise ArgumentError, "Restricted attribute names: #{invalid_attribute_names.join(", ")}"
118
118
  end
119
119
 
120
+ Delegation.generate(singleton_class, names, to: :instance, nilable: false, signature: "")
121
+ Delegation.generate(singleton_class, names.map { |n| "#{n}=" }, to: :instance, nilable: false, signature: "value")
122
+
120
123
  ActiveSupport::CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |owner|
121
124
  names.each do |name|
122
125
  owner.define_cached_method(name, namespace: :current_attributes) do |batch|
@@ -134,9 +137,6 @@ module ActiveSupport
134
137
  end
135
138
  end
136
139
 
137
- Delegation.generate(singleton_class, names, to: :instance, nilable: false, signature: "")
138
- Delegation.generate(singleton_class, names.map { |n| "#{n}=" }, to: :instance, nilable: false, signature: "value")
139
-
140
140
  self.defaults = defaults.merge(names.index_with { default })
141
141
  end
142
142
 
@@ -185,9 +185,16 @@ module ActiveSupport
185
185
 
186
186
  def method_added(name)
187
187
  super
188
+
189
+ # We try to generate instance delegators early to not rely on method_missing.
188
190
  return if name == :initialize
191
+
192
+ # If the added method isn't public, we don't delegate it.
189
193
  return unless public_method_defined?(name)
190
- return if respond_to?(name, true)
194
+
195
+ # If we already have a class method by that name, we don't override it.
196
+ return if singleton_class.method_defined?(name) || singleton_class.private_method_defined?(name)
197
+
191
198
  Delegation.generate(singleton_class, [name], to: :instance, as: self, nilable: false)
192
199
  end
193
200
  end
@@ -231,8 +231,11 @@ module ActiveSupport
231
231
  end
232
232
  end
233
233
 
234
- unless error.frozen?
235
- error.instance_variable_set(:@__rails_error_reported, true)
234
+ while error
235
+ unless error.frozen?
236
+ error.instance_variable_set(:@__rails_error_reported, true)
237
+ end
238
+ error = error.cause
236
239
  end
237
240
 
238
241
  nil
@@ -89,7 +89,7 @@ module ActiveSupport
89
89
  instance = run!
90
90
  begin
91
91
  yield
92
- rescue => error
92
+ rescue Exception => error
93
93
  error_reporter&.report(error, handled: false, source: source)
94
94
  raise
95
95
  ensure
@@ -120,7 +120,7 @@ module ActiveSupport
120
120
  # healthy to consider this edge case because with mtimes in the future
121
121
  # reloading is not triggered.
122
122
  def max_mtime(paths)
123
- time_now = Time.now
123
+ time_now = Time.at(0, Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond), :nanosecond)
124
124
  max_mtime = nil
125
125
 
126
126
  # Time comparisons are performed with #compare_without_coercion because
@@ -9,8 +9,8 @@ module ActiveSupport
9
9
  module VERSION
10
10
  MAJOR = 7
11
11
  MINOR = 2
12
- TINY = 2
13
- PRE = "2"
12
+ TINY = 3
13
+ PRE = nil
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -262,9 +262,7 @@ module ActiveSupport
262
262
  # hash[:a][:c] # => "c"
263
263
  # dup[:a][:c] # => "c"
264
264
  def dup
265
- self.class.new(self).tap do |new_hash|
266
- set_defaults(new_hash)
267
- end
265
+ copy_defaults(self.class.new(self))
268
266
  end
269
267
 
270
268
  # This method has the same semantics of +update+, except it does not
@@ -342,21 +340,26 @@ module ActiveSupport
342
340
  NOT_GIVEN = Object.new # :nodoc:
343
341
 
344
342
  def transform_keys(hash = NOT_GIVEN, &block)
345
- return to_enum(:transform_keys) if NOT_GIVEN.equal?(hash) && !block_given?
346
- dup.tap { |h| h.transform_keys!(hash, &block) }
343
+ if NOT_GIVEN.equal?(hash)
344
+ if block_given?
345
+ self.class.new(super(&block))
346
+ else
347
+ to_enum(:transform_keys)
348
+ end
349
+ else
350
+ self.class.new(super)
351
+ end
347
352
  end
348
353
 
349
354
  def transform_keys!(hash = NOT_GIVEN, &block)
350
- return to_enum(:transform_keys!) if NOT_GIVEN.equal?(hash) && !block_given?
351
-
352
- if hash.nil?
353
- super
354
- elsif NOT_GIVEN.equal?(hash)
355
- keys.each { |key| self[yield(key)] = delete(key) }
356
- elsif block_given?
357
- keys.each { |key| self[hash[key] || yield(key)] = delete(key) }
355
+ if NOT_GIVEN.equal?(hash)
356
+ if block_given?
357
+ replace(copy_defaults(transform_keys(&block)))
358
+ else
359
+ return to_enum(:transform_keys!)
360
+ end
358
361
  else
359
- keys.each { |key| self[hash[key] || key] = delete(key) }
362
+ replace(copy_defaults(transform_keys(hash, &block)))
360
363
  end
361
364
 
362
365
  self
@@ -379,7 +382,7 @@ module ActiveSupport
379
382
  # Convert to a regular hash with string keys.
380
383
  def to_hash
381
384
  _new_hash = Hash.new
382
- set_defaults(_new_hash)
385
+ copy_defaults(_new_hash)
383
386
 
384
387
  each do |key, value|
385
388
  _new_hash[key] = convert_value(value, conversion: :to_hash)
@@ -413,12 +416,13 @@ module ActiveSupport
413
416
  end
414
417
  end
415
418
 
416
- def set_defaults(target)
419
+ def copy_defaults(target)
417
420
  if default_proc
418
421
  target.default_proc = default_proc.dup
419
422
  else
420
423
  target.default = default
421
424
  end
425
+ target
422
426
  end
423
427
 
424
428
  def update_with_single_argument(other_hash, block)
@@ -18,7 +18,7 @@ module ActiveSupport
18
18
  # See http://www.json.org for more info.
19
19
  #
20
20
  # ActiveSupport::JSON.decode("{\"team\":\"rails\",\"players\":\"36\"}")
21
- # => {"team" => "rails", "players" => "36"}
21
+ # # => {"team" => "rails", "players" => "36"}
22
22
  def decode(json)
23
23
  data = ::JSON.parse(json, quirks_mode: true)
24
24
 
@@ -13,12 +13,30 @@ module ActiveSupport
13
13
  end
14
14
 
15
15
  module JSON
16
- # Dumps objects in JSON (JavaScript Object Notation).
17
- # See http://www.json.org for more info.
18
- #
19
- # ActiveSupport::JSON.encode({ team: 'rails', players: '36' })
20
- # # => "{\"team\":\"rails\",\"players\":\"36\"}"
21
16
  class << self
17
+ # Dumps objects in JSON (JavaScript Object Notation).
18
+ # See http://www.json.org for more info.
19
+ #
20
+ # ActiveSupport::JSON.encode({ team: 'rails', players: '36' })
21
+ # # => "{\"team\":\"rails\",\"players\":\"36\"}"
22
+ #
23
+ # Generates JSON that is safe to include in JavaScript as it escapes
24
+ # U+2028 (Line Separator) and U+2029 (Paragraph Separator):
25
+ #
26
+ # ActiveSupport::JSON.encode({ key: "\u2028" })
27
+ # # => "{\"key\":\"\\u2028\"}"
28
+ #
29
+ # By default, it also generates JSON that is safe to include in HTML, as
30
+ # it escapes <tt><</tt>, <tt>></tt>, and <tt>&</tt>:
31
+ #
32
+ # ActiveSupport::JSON.encode({ key: "<>&" })
33
+ # # => "{\"key\":\"\\u003c\\u003e\\u0026\"}"
34
+ #
35
+ # This can be changed with the +escape_html_entities+ option, or the
36
+ # global escape_html_entities_in_json configuration option.
37
+ #
38
+ # ActiveSupport::JSON.encode({ key: "<>&" }, escape_html_entities: false)
39
+ # # => "{\"key\":\"<>&\"}"
22
40
  def encode(value, options = nil)
23
41
  Encoding.json_encoder.new(options).encode(value)
24
42
  end
@@ -53,7 +53,7 @@ module ActiveSupport
53
53
  # loaded. If the component has already loaded, the block is executed
54
54
  # immediately.
55
55
  #
56
- # Options:
56
+ # ==== Options
57
57
  #
58
58
  # * <tt>:yield</tt> - Yields the object that run_load_hooks to +block+.
59
59
  # * <tt>:run_once</tt> - Given +block+ will run only once.
@@ -28,8 +28,8 @@ module ActiveSupport
28
28
  # <tt>transitional = false</tt>.
29
29
 
30
30
  ##
31
- # :method: initialize
32
- # :call-seq: initialize(&secret_generator)
31
+ # :singleton-method: new
32
+ # :call-seq: new(&secret_generator)
33
33
  #
34
34
  # Initializes a new instance. +secret_generator+ must accept a salt and a
35
35
  # +secret_length+ kwarg, and return a suitable secret (string) or secrets
@@ -154,6 +154,8 @@ module ActiveSupport
154
154
  # not URL-safe. In other words, they can contain "+" and "/". If you want to
155
155
  # generate URL-safe strings (in compliance with "Base 64 Encoding with URL
156
156
  # and Filename Safe Alphabet" in RFC 4648), you can pass +true+.
157
+ # Note that MessageVerifier will always accept both URL-safe and URL-unsafe
158
+ # encoded messages, to allow a smooth transition between the two settings.
157
159
  #
158
160
  # [+:force_legacy_metadata_serializer+]
159
161
  # Whether to use the legacy metadata serializer, which serializes the
@@ -318,6 +320,13 @@ module ActiveSupport
318
320
  end
319
321
 
320
322
  private
323
+ def decode(encoded, url_safe: @url_safe)
324
+ catch :invalid_message_format do
325
+ return super
326
+ end
327
+ super(encoded, url_safe: !url_safe)
328
+ end
329
+
321
330
  def sign_encoded(encoded)
322
331
  digest = generate_digest(encoded)
323
332
  encoded << SEPARATOR << digest
@@ -28,8 +28,8 @@ module ActiveSupport
28
28
  # <tt>transitional = false</tt>.
29
29
 
30
30
  ##
31
- # :method: initialize
32
- # :call-seq: initialize(&secret_generator)
31
+ # :singleton-method: new
32
+ # :call-seq: new(&secret_generator)
33
33
  #
34
34
  # Initializes a new instance. +secret_generator+ must accept a salt, and
35
35
  # return a suitable secret (string). +secret_generator+ may also accept
@@ -59,7 +59,9 @@ module ActiveSupport
59
59
 
60
60
  ##
61
61
  # :method: rotate
62
- # :call-seq: rotate(**options)
62
+ # :call-seq:
63
+ # rotate(**options)
64
+ # rotate(&block)
63
65
  #
64
66
  # Adds +options+ to the list of option sets. Messages will be signed using
65
67
  # the first set in the list. When verifying, however, each set will be
@@ -15,6 +15,11 @@ module ActiveSupport
15
15
  fall_back_to build_rotation(*args, **options)
16
16
  end
17
17
 
18
+ def on_rotation(&on_rotation)
19
+ @on_rotation = on_rotation
20
+ self
21
+ end
22
+
18
23
  def fall_back_to(fallback)
19
24
  @rotations << fallback
20
25
  self
@@ -55,7 +55,10 @@ module ActiveSupport # :nodoc:
55
55
  # Creates a new Chars instance by wrapping _string_.
56
56
  def initialize(string)
57
57
  @wrapped_string = string
58
- @wrapped_string.force_encoding(Encoding::UTF_8) unless @wrapped_string.frozen?
58
+ if string.encoding != Encoding::UTF_8
59
+ @wrapped_string = @wrapped_string.dup
60
+ @wrapped_string.force_encoding(Encoding::UTF_8)
61
+ end
59
62
  end
60
63
 
61
64
  # Forward all undefined methods to the wrapped string.
@@ -14,6 +14,7 @@ module ActiveSupport
14
14
  def initialize
15
15
  @queue = Queue.new
16
16
  @active_workers = Concurrent::Map.new
17
+ @worker_pids = Concurrent::Map.new
17
18
  @in_flight = Concurrent::Map.new
18
19
  end
19
20
 
@@ -40,12 +41,24 @@ module ActiveSupport
40
41
  end
41
42
  end
42
43
 
43
- def start_worker(worker_id)
44
+ def start_worker(worker_id, worker_pid)
44
45
  @active_workers[worker_id] = true
46
+ @worker_pids[worker_id] = worker_pid
45
47
  end
46
48
 
47
- def stop_worker(worker_id)
49
+ def stop_worker(worker_id, worker_pid)
48
50
  @active_workers.delete(worker_id)
51
+ @worker_pids.delete(worker_id)
52
+ end
53
+
54
+ def remove_dead_workers(dead_pids)
55
+ dead_pids.each do |dead_pid|
56
+ worker_id = @worker_pids.key(dead_pid)
57
+ if worker_id
58
+ @active_workers.delete(worker_id)
59
+ @worker_pids.delete(worker_id)
60
+ end
61
+ end
49
62
  end
50
63
 
51
64
  def active_workers?
@@ -18,7 +18,7 @@ module ActiveSupport
18
18
  DRb.stop_service
19
19
 
20
20
  @queue = DRbObject.new_with_uri(@url)
21
- @queue.start_worker(@id)
21
+ @queue.start_worker(@id, Process.pid)
22
22
 
23
23
  begin
24
24
  after_fork
@@ -29,7 +29,7 @@ module ActiveSupport
29
29
  set_process_title("(stopping)")
30
30
 
31
31
  run_cleanup
32
- @queue.stop_worker(@id)
32
+ @queue.stop_worker(@id, Process.pid)
33
33
  end
34
34
  end
35
35
 
@@ -47,8 +47,19 @@ module ActiveSupport
47
47
  end
48
48
 
49
49
  def shutdown
50
+ dead_worker_pids = @worker_pool.filter_map do |pid|
51
+ Process.waitpid(pid, Process::WNOHANG)
52
+ rescue Errno::ECHILD
53
+ pid
54
+ end
55
+ @queue_server.remove_dead_workers(dead_worker_pids)
56
+
50
57
  @queue_server.shutdown
51
- @worker_pool.each { |pid| Process.waitpid pid }
58
+ @worker_pool.each do |pid|
59
+ Process.waitpid(pid)
60
+ rescue Errno::ECHILD
61
+ nil
62
+ end
52
63
  end
53
64
  end
54
65
  end
@@ -74,6 +74,8 @@ module ActiveSupport
74
74
  "decimal" => Proc.new do |number|
75
75
  if String === number
76
76
  number.to_d
77
+ elsif Float === number
78
+ BigDecimal(number, 0)
77
79
  else
78
80
  BigDecimal(number)
79
81
  end