activesupport 8.0.0 → 8.1.0

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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +391 -67
  3. data/README.rdoc +1 -1
  4. data/lib/active_support/backtrace_cleaner.rb +72 -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 +26 -28
  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 +37 -31
  11. data/lib/active_support/cache/strategy/local_cache.rb +72 -27
  12. data/lib/active_support/cache/strategy/local_cache_middleware.rb +7 -7
  13. data/lib/active_support/cache.rb +72 -9
  14. data/lib/active_support/callbacks.rb +27 -11
  15. data/lib/active_support/class_attribute.rb +15 -8
  16. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +8 -62
  17. data/lib/active_support/concurrency/thread_monitor.rb +55 -0
  18. data/lib/active_support/configurable.rb +34 -0
  19. data/lib/active_support/continuous_integration.rb +145 -0
  20. data/lib/active_support/core_ext/array/conversions.rb +3 -3
  21. data/lib/active_support/core_ext/array.rb +7 -7
  22. data/lib/active_support/core_ext/benchmark.rb +4 -11
  23. data/lib/active_support/core_ext/big_decimal.rb +1 -1
  24. data/lib/active_support/core_ext/class/attribute.rb +20 -5
  25. data/lib/active_support/core_ext/class.rb +2 -2
  26. data/lib/active_support/core_ext/date.rb +5 -5
  27. data/lib/active_support/core_ext/date_and_time/compatibility.rb +0 -35
  28. data/lib/active_support/core_ext/date_time/compatibility.rb +3 -5
  29. data/lib/active_support/core_ext/date_time/conversions.rb +4 -2
  30. data/lib/active_support/core_ext/date_time.rb +5 -5
  31. data/lib/active_support/core_ext/digest.rb +1 -1
  32. data/lib/active_support/core_ext/enumerable.rb +17 -5
  33. data/lib/active_support/core_ext/erb/util.rb +5 -5
  34. data/lib/active_support/core_ext/file.rb +1 -1
  35. data/lib/active_support/core_ext/hash/deep_merge.rb +1 -0
  36. data/lib/active_support/core_ext/hash.rb +8 -8
  37. data/lib/active_support/core_ext/integer.rb +3 -3
  38. data/lib/active_support/core_ext/kernel.rb +3 -3
  39. data/lib/active_support/core_ext/module/introspection.rb +3 -0
  40. data/lib/active_support/core_ext/module.rb +11 -11
  41. data/lib/active_support/core_ext/numeric.rb +3 -3
  42. data/lib/active_support/core_ext/object/json.rb +8 -1
  43. data/lib/active_support/core_ext/object/to_query.rb +7 -1
  44. data/lib/active_support/core_ext/object/try.rb +2 -2
  45. data/lib/active_support/core_ext/object.rb +13 -13
  46. data/lib/active_support/core_ext/pathname.rb +2 -2
  47. data/lib/active_support/core_ext/range/overlap.rb +3 -3
  48. data/lib/active_support/core_ext/range/sole.rb +17 -0
  49. data/lib/active_support/core_ext/range.rb +4 -4
  50. data/lib/active_support/core_ext/securerandom.rb +24 -8
  51. data/lib/active_support/core_ext/string/filters.rb +3 -3
  52. data/lib/active_support/core_ext/string/multibyte.rb +12 -3
  53. data/lib/active_support/core_ext/string/output_safety.rb +19 -12
  54. data/lib/active_support/core_ext/string.rb +13 -13
  55. data/lib/active_support/core_ext/symbol.rb +1 -1
  56. data/lib/active_support/core_ext/time/calculations.rb +0 -7
  57. data/lib/active_support/core_ext/time/compatibility.rb +2 -27
  58. data/lib/active_support/core_ext/time.rb +5 -5
  59. data/lib/active_support/core_ext.rb +1 -1
  60. data/lib/active_support/current_attributes/test_helper.rb +2 -2
  61. data/lib/active_support/current_attributes.rb +27 -17
  62. data/lib/active_support/dependencies/interlock.rb +11 -5
  63. data/lib/active_support/dependencies.rb +6 -1
  64. data/lib/active_support/deprecation/reporting.rb +4 -2
  65. data/lib/active_support/deprecation.rb +1 -1
  66. data/lib/active_support/editor.rb +70 -0
  67. data/lib/active_support/error_reporter.rb +61 -8
  68. data/lib/active_support/event_reporter/test_helper.rb +32 -0
  69. data/lib/active_support/event_reporter.rb +592 -0
  70. data/lib/active_support/evented_file_update_checker.rb +5 -1
  71. data/lib/active_support/execution_context.rb +64 -7
  72. data/lib/active_support/execution_wrapper.rb +1 -1
  73. data/lib/active_support/file_update_checker.rb +8 -6
  74. data/lib/active_support/gem_version.rb +1 -1
  75. data/lib/active_support/gzip.rb +1 -0
  76. data/lib/active_support/hash_with_indifferent_access.rb +47 -24
  77. data/lib/active_support/i18n_railtie.rb +1 -2
  78. data/lib/active_support/inflector/inflections.rb +31 -15
  79. data/lib/active_support/inflector/transliterate.rb +6 -8
  80. data/lib/active_support/isolated_execution_state.rb +12 -15
  81. data/lib/active_support/json/decoding.rb +6 -4
  82. data/lib/active_support/json/encoding.rb +155 -19
  83. data/lib/active_support/lazy_load_hooks.rb +1 -1
  84. data/lib/active_support/log_subscriber.rb +2 -6
  85. data/lib/active_support/logger_thread_safe_level.rb +6 -3
  86. data/lib/active_support/message_encryptors.rb +54 -2
  87. data/lib/active_support/message_pack/extensions.rb +5 -0
  88. data/lib/active_support/message_verifier.rb +9 -0
  89. data/lib/active_support/message_verifiers.rb +57 -3
  90. data/lib/active_support/messages/rotation_coordinator.rb +9 -0
  91. data/lib/active_support/messages/rotator.rb +10 -0
  92. data/lib/active_support/multibyte/chars.rb +12 -2
  93. data/lib/active_support/multibyte.rb +4 -0
  94. data/lib/active_support/notifications/fanout.rb +64 -42
  95. data/lib/active_support/notifications/instrumenter.rb +1 -1
  96. data/lib/active_support/railtie.rb +32 -13
  97. data/lib/active_support/structured_event_subscriber.rb +99 -0
  98. data/lib/active_support/subscriber.rb +0 -5
  99. data/lib/active_support/syntax_error_proxy.rb +3 -0
  100. data/lib/active_support/test_case.rb +61 -6
  101. data/lib/active_support/testing/assertions.rb +34 -6
  102. data/lib/active_support/testing/error_reporter_assertions.rb +18 -1
  103. data/lib/active_support/testing/event_reporter_assertions.rb +227 -0
  104. data/lib/active_support/testing/notification_assertions.rb +92 -0
  105. data/lib/active_support/testing/parallelization/server.rb +15 -2
  106. data/lib/active_support/testing/parallelization/worker.rb +4 -2
  107. data/lib/active_support/testing/parallelization.rb +25 -1
  108. data/lib/active_support/testing/tests_without_assertions.rb +1 -1
  109. data/lib/active_support/testing/time_helpers.rb +7 -3
  110. data/lib/active_support/time_with_zone.rb +23 -23
  111. data/lib/active_support/values/time_zone.rb +8 -1
  112. data/lib/active_support/xml_mini.rb +3 -2
  113. data/lib/active_support.rb +20 -15
  114. metadata +27 -23
  115. data/lib/active_support/core_ext/range/each.rb +0 -24
  116. data/lib/active_support/testing/strict_warnings.rb +0 -43
@@ -11,11 +11,12 @@ module ActiveSupport
11
11
  # This class wraps up local storage for middlewares. Only the middleware method should
12
12
  # construct them.
13
13
  class Middleware # :nodoc:
14
- attr_reader :name, :local_cache_key
14
+ attr_reader :name
15
+ attr_accessor :cache
15
16
 
16
- def initialize(name, local_cache_key)
17
+ def initialize(name, cache)
17
18
  @name = name
18
- @local_cache_key = local_cache_key
19
+ @cache = cache
19
20
  @app = nil
20
21
  end
21
22
 
@@ -25,18 +26,17 @@ module ActiveSupport
25
26
  end
26
27
 
27
28
  def call(env)
28
- LocalCacheRegistry.set_cache_for(local_cache_key, LocalStore.new)
29
+ cache.new_local_cache
29
30
  response = @app.call(env)
30
31
  response[2] = ::Rack::BodyProxy.new(response[2]) do
31
- LocalCacheRegistry.set_cache_for(local_cache_key, nil)
32
+ cache.unset_local_cache
32
33
  end
33
34
  cleanup_on_body_close = true
34
35
  response
35
36
  rescue Rack::Utils::InvalidParameterError
36
37
  [400, {}, []]
37
38
  ensure
38
- LocalCacheRegistry.set_cache_for(local_cache_key, nil) unless
39
- cleanup_on_body_close
39
+ cache.unset_local_cache unless cleanup_on_body_close
40
40
  end
41
41
  end
42
42
  end
@@ -35,6 +35,8 @@ module ActiveSupport
35
35
  :race_condition_ttl,
36
36
  :serializer,
37
37
  :skip_nil,
38
+ :raw,
39
+ :max_key_size,
38
40
  ]
39
41
 
40
42
  # Mapping of canonical option names to aliases that a store will recognize.
@@ -186,6 +188,12 @@ module ActiveSupport
186
188
  # @last_mod_time = Time.now # Invalidate the entire cache by changing namespace
187
189
  #
188
190
  class Store
191
+ # Default +ConnectionPool+ options
192
+ DEFAULT_POOL_OPTIONS = { size: 5, timeout: 5 }.freeze
193
+
194
+ # Keys are truncated with the Active Support digest if they exceed the limit.
195
+ MAX_KEY_SIZE = 250
196
+
189
197
  cattr_accessor :logger, instance_writer: true
190
198
  cattr_accessor :raise_on_invalid_cache_expiration_time, default: false
191
199
 
@@ -194,9 +202,6 @@ module ActiveSupport
194
202
 
195
203
  class << self
196
204
  private
197
- DEFAULT_POOL_OPTIONS = { size: 5, timeout: 5 }.freeze
198
- private_constant :DEFAULT_POOL_OPTIONS
199
-
200
205
  def retrieve_pool_options(options)
201
206
  if options.key?(:pool)
202
207
  pool_options = options.delete(:pool)
@@ -298,6 +303,9 @@ module ActiveSupport
298
303
  @options[:compress] = true unless @options.key?(:compress)
299
304
  @options[:compress_threshold] ||= DEFAULT_COMPRESS_LIMIT
300
305
 
306
+ @max_key_size = @options.delete(:max_key_size)
307
+ @max_key_size = MAX_KEY_SIZE if @max_key_size.nil? # allow 'false' as a value
308
+
301
309
  @coder = @options.delete(:coder) do
302
310
  legacy_serializer = Cache.format_version < 7.1 && !@options[:serializer]
303
311
  serializer = @options.delete(:serializer) || default_serializer
@@ -386,7 +394,7 @@ module ActiveSupport
386
394
  # process can try to generate a new value after the extended time window
387
395
  # has elapsed.
388
396
  #
389
- # # Set all values to expire after one minute.
397
+ # # Set all values to expire after one second.
390
398
  # cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 1)
391
399
  #
392
400
  # cache.write("foo", "original value")
@@ -556,7 +564,7 @@ module ActiveSupport
556
564
 
557
565
  instrument_multi :write_multi, normalized_hash, options do |payload|
558
566
  entries = hash.each_with_object({}) do |(name, value), memo|
559
- memo[normalize_key(name, options)] = Entry.new(value, **options.merge(version: normalize_version(name, options)))
567
+ memo[normalize_key(name, options)] = Entry.new(value, **options, version: normalize_version(name, options))
560
568
  end
561
569
 
562
570
  write_multi_entries entries, **options
@@ -658,13 +666,15 @@ module ActiveSupport
658
666
  # version, the read will be treated as a cache miss. This feature is
659
667
  # used to support recyclable cache keys.
660
668
  #
669
+ # * +:unless_exist+ - Prevents overwriting an existing cache entry.
670
+ #
661
671
  # Other options will be handled by the specific cache store implementation.
662
672
  def write(name, value, options = nil)
663
673
  options = merged_options(options)
664
674
  key = normalize_key(name, options)
665
675
 
666
676
  instrument(:write, key, options) do
667
- entry = Entry.new(value, **options.merge(version: normalize_version(name, options)))
677
+ entry = Entry.new(value, **options, version: normalize_version(name, options))
668
678
  write_entry(key, entry, **options)
669
679
  end
670
680
  end
@@ -741,6 +751,32 @@ module ActiveSupport
741
751
  raise NotImplementedError.new("#{self.class.name} does not support decrement")
742
752
  end
743
753
 
754
+ # Reads a counter that was set by #increment / #decrement.
755
+ #
756
+ # cache.write_counter("foo", 1)
757
+ # cache.read_counter("foo") # => 1
758
+ # cache.increment("foo")
759
+ # cache.read_counter("foo") # => 2
760
+ #
761
+ # Options are passed to the underlying cache implementation.
762
+ def read_counter(name, **options)
763
+ options = merged_options(options).merge(raw: true)
764
+ read(name, **options)&.to_i
765
+ end
766
+
767
+ # Writes a counter that can then be modified by #increment / #decrement.
768
+ #
769
+ # cache.write_counter("foo", 1)
770
+ # cache.read_counter("foo") # => 1
771
+ # cache.increment("foo")
772
+ # cache.read_counter("foo") # => 2
773
+ #
774
+ # Options are passed to the underlying cache implementation.
775
+ def write_counter(name, value, **options)
776
+ options = merged_options(options).merge(raw: true)
777
+ write(name, value.to_i, **options)
778
+ end
779
+
744
780
  # Cleans up the cache by removing expired entries.
745
781
  #
746
782
  # Options are passed to the underlying cache implementation.
@@ -760,6 +796,17 @@ module ActiveSupport
760
796
  raise NotImplementedError.new("#{self.class.name} does not support clear")
761
797
  end
762
798
 
799
+ # Get the current namespace
800
+ def namespace
801
+ @options[:namespace]
802
+ end
803
+
804
+ # Set the current namespace. Note, this will be ignored if custom
805
+ # options are passed to cache wills with a namespace key.
806
+ def namespace=(namespace)
807
+ @options[:namespace] = namespace
808
+ end
809
+
763
810
  private
764
811
  def default_serializer
765
812
  case Cache.format_version
@@ -926,16 +973,33 @@ module ActiveSupport
926
973
  options
927
974
  end
928
975
 
929
- # Expands and namespaces the cache key.
976
+ # Expands, namespaces and truncates the cache key.
930
977
  # Raises an exception when the key is +nil+ or an empty string.
931
978
  # May be overridden by cache stores to do additional normalization.
932
979
  def normalize_key(key, options = nil)
980
+ key = expand_and_namespace_key(key, options)
981
+ truncate_key(key)
982
+ end
983
+
984
+ def expand_and_namespace_key(key, options = nil)
933
985
  str_key = expanded_key(key)
934
986
  raise(ArgumentError, "key cannot be blank") if !str_key || str_key.empty?
935
987
 
936
988
  namespace_key str_key, options
937
989
  end
938
990
 
991
+ def truncate_key(key)
992
+ if key && @max_key_size && key.bytesize > @max_key_size
993
+ suffix = ":hash:#{ActiveSupport::Digest.hexdigest(key)}"
994
+ truncate_at = @max_key_size - suffix.bytesize
995
+ key = key.byteslice(0, truncate_at)
996
+ key.scrub!("")
997
+ "#{key}#{suffix}"
998
+ else
999
+ key
1000
+ end
1001
+ end
1002
+
939
1003
  # Prefix the key with a namespace string:
940
1004
  #
941
1005
  # namespace_key 'foo', namespace: 'cache'
@@ -1035,8 +1099,7 @@ module ActiveSupport
1035
1099
  # When an entry has a positive :race_condition_ttl defined, put the stale entry back into the cache
1036
1100
  # for a brief period while the entry is being recalculated.
1037
1101
  entry.expires_at = Time.now.to_f + race_ttl
1038
- options[:expires_in] = race_ttl * 2
1039
- write_entry(key, entry, **options)
1102
+ write_entry(key, entry, **options, expires_in: race_ttl * 2)
1040
1103
  else
1041
1104
  delete_entry(key, **options)
1042
1105
  end
@@ -4,6 +4,7 @@ require "active_support/concern"
4
4
  require "active_support/descendants_tracker"
5
5
  require "active_support/core_ext/array/extract_options"
6
6
  require "active_support/core_ext/class/attribute"
7
+ require "active_support/core_ext/module/redefine_method"
7
8
  require "active_support/core_ext/string/filters"
8
9
  require "active_support/core_ext/object/blank"
9
10
 
@@ -498,9 +499,10 @@ module ActiveSupport
498
499
  when Conditionals::Value
499
500
  ProcCall.new(filter)
500
501
  when ::Proc
501
- if filter.arity > 1
502
+ case filter.arity
503
+ when 2
502
504
  InstanceExec2.new(filter)
503
- elsif filter.arity > 0
505
+ when 1, -2
504
506
  InstanceExec1.new(filter)
505
507
  else
506
508
  InstanceExec0.new(filter)
@@ -572,7 +574,7 @@ module ActiveSupport
572
574
  @name = name
573
575
  @config = {
574
576
  scope: [:kind],
575
- terminator: default_terminator
577
+ terminator: DEFAULT_TERMINATOR
576
578
  }.merge!(config)
577
579
  @chain = []
578
580
  @all_callbacks = nil
@@ -660,8 +662,8 @@ module ActiveSupport
660
662
  @chain.delete_if { |c| callback.duplicates?(c) }
661
663
  end
662
664
 
663
- def default_terminator
664
- Proc.new do |target, result_lambda|
665
+ class DefaultTerminator # :nodoc:
666
+ def call(target, result_lambda)
665
667
  terminate = true
666
668
  catch(:abort) do
667
669
  result_lambda.call
@@ -670,6 +672,7 @@ module ActiveSupport
670
672
  terminate
671
673
  end
672
674
  end
675
+ DEFAULT_TERMINATOR = DefaultTerminator.new.freeze
673
676
  end
674
677
 
675
678
  module ClassMethods
@@ -903,12 +906,13 @@ module ActiveSupport
903
906
  names.each do |name|
904
907
  name = name.to_sym
905
908
 
906
- ([self] + self.descendants).each do |target|
907
- target.set_callbacks name, CallbackChain.new(name, options)
908
- end
909
-
910
909
  module_eval <<-RUBY, __FILE__, __LINE__ + 1
911
- def _run_#{name}_callbacks(&block)
910
+ def _run_#{name}_callbacks
911
+ yield if block_given?
912
+ end
913
+ silence_redefinition_of_method(:_run_#{name}_callbacks)
914
+
915
+ def _run_#{name}_callbacks!(&block)
912
916
  run_callbacks #{name.inspect}, &block
913
917
  end
914
918
 
@@ -924,6 +928,10 @@ module ActiveSupport
924
928
  __callbacks[#{name.inspect}]
925
929
  end
926
930
  RUBY
931
+
932
+ ([self] + self.descendants).each do |target|
933
+ target.set_callbacks name, CallbackChain.new(name, options)
934
+ end
927
935
  end
928
936
  end
929
937
 
@@ -933,9 +941,17 @@ module ActiveSupport
933
941
  end
934
942
 
935
943
  def set_callbacks(name, callbacks) # :nodoc:
936
- unless singleton_class.method_defined?(:__callbacks, false)
944
+ # HACK: We're making assumption on how `class_attribute` is implemented
945
+ # to save constantly duping the callback hash. If this desync with class_attribute
946
+ # we'll lose the optimization, but won't cause an actual behavior bug.
947
+ unless singleton_class.private_method_defined?(:__class_attr__callbacks, false)
937
948
  self.__callbacks = __callbacks.dup
938
949
  end
950
+ name = name.to_sym
951
+ callbacks_was = self.__callbacks[name.to_sym]
952
+ if (callbacks_was.nil? || callbacks_was.empty?) && !callbacks.empty?
953
+ alias_method("_run_#{name}_callbacks", "_run_#{name}_callbacks!")
954
+ end
939
955
  self.__callbacks[name.to_sym] = callbacks
940
956
  self.__callbacks
941
957
  end
@@ -3,23 +3,30 @@
3
3
  module ActiveSupport
4
4
  module ClassAttribute # :nodoc:
5
5
  class << self
6
- def redefine(owner, name, value)
6
+ def redefine(owner, name, namespaced_name, value)
7
7
  if owner.singleton_class?
8
- owner.redefine_method(name) { value }
9
- owner.send(:public, name)
8
+ if owner.attached_object.is_a?(Module)
9
+ redefine_method(owner, namespaced_name, private: true) { value }
10
+ else
11
+ redefine_method(owner, name) { value }
12
+ end
10
13
  end
11
14
 
12
- owner.redefine_singleton_method(name) { value }
13
- owner.singleton_class.send(:public, name)
15
+ redefine_method(owner.singleton_class, namespaced_name, private: true) { value }
14
16
 
15
- owner.redefine_singleton_method("#{name}=") do |new_value|
17
+ redefine_method(owner.singleton_class, "#{namespaced_name}=", private: true) do |new_value|
16
18
  if owner.equal?(self)
17
19
  value = new_value
18
20
  else
19
- ::ActiveSupport::ClassAttribute.redefine(self, name, new_value)
21
+ ::ActiveSupport::ClassAttribute.redefine(self, name, namespaced_name, new_value)
20
22
  end
21
23
  end
22
- owner.singleton_class.send(:public, "#{name}=")
24
+ end
25
+
26
+ def redefine_method(owner, name, private: false, &block)
27
+ owner.silence_redefinition_of_method(name)
28
+ owner.define_method(name, &block)
29
+ owner.send(:private, name) if private
23
30
  end
24
31
  end
25
32
  end
@@ -4,69 +4,15 @@ require "monitor"
4
4
 
5
5
  module ActiveSupport
6
6
  module Concurrency
7
- module LoadInterlockAwareMonitorMixin # :nodoc:
8
- EXCEPTION_NEVER = { Exception => :never }.freeze
9
- EXCEPTION_IMMEDIATE = { Exception => :immediate }.freeze
10
- private_constant :EXCEPTION_NEVER, :EXCEPTION_IMMEDIATE
11
-
12
- # Enters an exclusive section, but allows dependency loading while blocked
13
- def mon_enter
14
- mon_try_enter ||
15
- ActiveSupport::Dependencies.interlock.permit_concurrent_loads { super }
16
- end
17
-
18
- def synchronize(&block)
19
- Thread.handle_interrupt(EXCEPTION_NEVER) do
20
- mon_enter
21
-
22
- begin
23
- Thread.handle_interrupt(EXCEPTION_IMMEDIATE, &block)
24
- ensure
25
- mon_exit
26
- end
27
- end
28
- end
29
- end
30
7
  # A monitor that will permit dependency loading while blocked waiting for
31
8
  # the lock.
32
- class LoadInterlockAwareMonitor < Monitor
33
- include LoadInterlockAwareMonitorMixin
34
- end
35
-
36
- class ThreadLoadInterlockAwareMonitor # :nodoc:
37
- prepend LoadInterlockAwareMonitorMixin
38
-
39
- def initialize
40
- @owner = nil
41
- @count = 0
42
- @mutex = Mutex.new
43
- end
44
-
45
- private
46
- def mon_try_enter
47
- if @owner != Thread.current
48
- return false unless @mutex.try_lock
49
- @owner = Thread.current
50
- end
51
- @count += 1
52
- end
53
-
54
- def mon_enter
55
- @mutex.lock if @owner != Thread.current
56
- @owner = Thread.current
57
- @count += 1
58
- end
59
-
60
- def mon_exit
61
- unless @owner == Thread.current
62
- raise ThreadError, "current thread not owner"
63
- end
64
-
65
- @count -= 1
66
- return unless @count == 0
67
- @owner = nil
68
- @mutex.unlock
69
- end
70
- end
9
+ LoadInterlockAwareMonitor = ActiveSupport::Deprecation::DeprecatedConstantProxy.new(
10
+ "ActiveSupport::Concurrency::LoadInterlockAwareMonitor",
11
+ "::Monitor",
12
+ ActiveSupport.deprecator,
13
+ message: "ActiveSupport::Concurrency::LoadInterlockAwareMonitor is deprecated and will be " \
14
+ "removed in Rails 9.0. Use Monitor directly instead, as the loading interlock is " \
15
+ "no longer used."
16
+ )
71
17
  end
72
18
  end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveSupport
4
+ module Concurrency
5
+ class ThreadMonitor # :nodoc:
6
+ EXCEPTION_NEVER = { Exception => :never }.freeze
7
+ EXCEPTION_IMMEDIATE = { Exception => :immediate }.freeze
8
+ private_constant :EXCEPTION_NEVER, :EXCEPTION_IMMEDIATE
9
+
10
+ def initialize
11
+ @owner = nil
12
+ @count = 0
13
+ @mutex = Mutex.new
14
+ end
15
+
16
+ def synchronize(&block)
17
+ Thread.handle_interrupt(EXCEPTION_NEVER) do
18
+ mon_enter
19
+
20
+ begin
21
+ Thread.handle_interrupt(EXCEPTION_IMMEDIATE, &block)
22
+ ensure
23
+ mon_exit
24
+ end
25
+ end
26
+ end
27
+
28
+ private
29
+ def mon_try_enter
30
+ if @owner != Thread.current
31
+ return false unless @mutex.try_lock
32
+ @owner = Thread.current
33
+ end
34
+ @count += 1
35
+ end
36
+
37
+ def mon_enter
38
+ @mutex.lock if @owner != Thread.current
39
+ @owner = Thread.current
40
+ @count += 1
41
+ end
42
+
43
+ def mon_exit
44
+ unless @owner == Thread.current
45
+ raise ThreadError, "current thread not owner"
46
+ end
47
+
48
+ @count -= 1
49
+ return unless @count == 0
50
+ @owner = nil
51
+ @mutex.unlock
52
+ end
53
+ end
54
+ end
55
+ end
@@ -1,5 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ ActiveSupport.deprecator.warn <<~MSG
4
+ ActiveSupport::Configurable is deprecated without replacement, and will be removed in Rails 8.2.
5
+
6
+ You can emulate the previous behavior with `class_attribute`.
7
+ MSG
8
+
3
9
  require "active_support/concern"
4
10
  require "active_support/ordered_options"
5
11
 
@@ -27,6 +33,19 @@ module ActiveSupport
27
33
  end
28
34
 
29
35
  module ClassMethods
36
+ # Reads and writes attributes from a configuration OrderedOptions.
37
+ #
38
+ # require "active_support/configurable"
39
+ #
40
+ # class User
41
+ # include ActiveSupport::Configurable
42
+ # end
43
+ #
44
+ # User.config.allowed_access = true
45
+ # User.config.level = 1
46
+ #
47
+ # User.config.allowed_access # => true
48
+ # User.config.level # => 1
30
49
  def config
31
50
  @_config ||= if respond_to?(:superclass) && superclass.respond_to?(:config)
32
51
  superclass.config.inheritable_copy
@@ -36,6 +55,21 @@ module ActiveSupport
36
55
  end
37
56
  end
38
57
 
58
+ # Configure values from within the passed block.
59
+ #
60
+ # require "active_support/configurable"
61
+ #
62
+ # class User
63
+ # include ActiveSupport::Configurable
64
+ # end
65
+ #
66
+ # User.allowed_access # => nil
67
+ #
68
+ # User.configure do |config|
69
+ # config.allowed_access = true
70
+ # end
71
+ #
72
+ # User.allowed_access # => true
39
73
  def configure
40
74
  yield config
41
75
  end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveSupport
4
+ # Provides a DSL for declaring a continuous integration workflow that can be run either locally or in the cloud.
5
+ # Each step is timed, reports success/error, and is aggregated into a collective report that reports total runtime,
6
+ # as well as whether the entire run was successful or not.
7
+ #
8
+ # Example:
9
+ #
10
+ # ActiveSupport::ContinuousIntegration.run do
11
+ # step "Setup", "bin/setup --skip-server"
12
+ # step "Style: Ruby", "bin/rubocop"
13
+ # step "Security: Gem audit", "bin/bundler-audit"
14
+ # step "Tests: Rails", "bin/rails test test:system"
15
+ #
16
+ # if success?
17
+ # step "Signoff: Ready for merge and deploy", "gh signoff"
18
+ # else
19
+ # failure "Skipping signoff; CI failed.", "Fix the issues and try again."
20
+ # end
21
+ # end
22
+ #
23
+ # Starting with Rails 8.1, a default `bin/ci` and `config/ci.rb` file are created to provide out-of-the-box CI.
24
+ class ContinuousIntegration
25
+ COLORS = {
26
+ banner: "\033[1;32m", # Green
27
+ title: "\033[1;35m", # Purple
28
+ subtitle: "\033[1;90m", # Medium Gray
29
+ error: "\033[1;31m", # Red
30
+ success: "\033[1;32m" # Green
31
+ }
32
+
33
+ attr_reader :results
34
+
35
+ # Perform a CI run. Execute each step, show their results and runtime, and exit with a non-zero status if there are any failures.
36
+ #
37
+ # Pass an optional title, subtitle, and a block that declares the steps to be executed.
38
+ #
39
+ # Sets the CI environment variable to "true" to allow for conditional behavior in the app, like enabling eager loading and disabling logging.
40
+ #
41
+ # Example:
42
+ #
43
+ # ActiveSupport::ContinuousIntegration.run do
44
+ # step "Setup", "bin/setup --skip-server"
45
+ # step "Style: Ruby", "bin/rubocop"
46
+ # step "Security: Gem audit", "bin/bundler-audit"
47
+ # step "Tests: Rails", "bin/rails test test:system"
48
+ #
49
+ # if success?
50
+ # step "Signoff: Ready for merge and deploy", "gh signoff"
51
+ # else
52
+ # failure "Skipping signoff; CI failed.", "Fix the issues and try again."
53
+ # end
54
+ # end
55
+ def self.run(title = "Continuous Integration", subtitle = "Running tests, style checks, and security audits", &block)
56
+ new.tap do |ci|
57
+ ENV["CI"] = "true"
58
+ ci.heading title, subtitle, padding: false
59
+ ci.report(title, &block)
60
+ abort unless ci.success?
61
+ end
62
+ end
63
+
64
+ def initialize
65
+ @results = []
66
+ end
67
+
68
+ # Declare a step with a title and a command. The command can either be given as a single string or as multiple
69
+ # strings that will be passed to `system` as individual arguments (and therefore correctly escaped for paths etc).
70
+ #
71
+ # Examples:
72
+ #
73
+ # step "Setup", "bin/setup"
74
+ # step "Single test", "bin/rails", "test", "--name", "test_that_is_one"
75
+ def step(title, *command)
76
+ heading title, command.join(" "), type: :title
77
+ report(title) { results << system(*command) }
78
+ end
79
+
80
+ # Returns true if all steps were successful.
81
+ def success?
82
+ results.all?
83
+ end
84
+
85
+ # Display an error heading with the title and optional subtitle to reflect that the run failed.
86
+ def failure(title, subtitle = nil)
87
+ heading title, subtitle, type: :error
88
+ end
89
+
90
+ # Display a colorized heading followed by an optional subtitle.
91
+ #
92
+ # Examples:
93
+ #
94
+ # heading "Smoke Testing", "End-to-end tests verifying key functionality", padding: false
95
+ # heading "Skipping video encoding tests", "Install FFmpeg to run these tests", type: :error
96
+ #
97
+ # See ActiveSupport::ContinuousIntegration::COLORS for a complete list of options.
98
+ def heading(heading, subtitle = nil, type: :banner, padding: true)
99
+ echo "#{padding ? "\n\n" : ""}#{heading}", type: type
100
+ echo "#{subtitle}#{padding ? "\n" : ""}", type: :subtitle if subtitle
101
+ end
102
+
103
+ # Echo text to the terminal in the color corresponding to the type of the text.
104
+ #
105
+ # Examples:
106
+ #
107
+ # echo "This is going to be green!", type: :success
108
+ # echo "This is going to be red!", type: :error
109
+ #
110
+ # See ActiveSupport::ContinuousIntegration::COLORS for a complete list of options.
111
+ def echo(text, type:)
112
+ puts colorize(text, type)
113
+ end
114
+
115
+ # :nodoc:
116
+ def report(title, &block)
117
+ Signal.trap("INT") { abort colorize(:error, "\n❌ #{title} interrupted") }
118
+
119
+ ci = self.class.new
120
+ elapsed = timing { ci.instance_eval(&block) }
121
+
122
+ if ci.success?
123
+ echo "\n✅ #{title} passed in #{elapsed}", type: :success
124
+ else
125
+ echo "\n❌ #{title} failed in #{elapsed}", type: :error
126
+ end
127
+
128
+ results.concat ci.results
129
+ ensure
130
+ Signal.trap("INT", "-")
131
+ end
132
+
133
+ private
134
+ def timing
135
+ started_at = Time.now.to_f
136
+ yield
137
+ min, sec = (Time.now.to_f - started_at).divmod(60)
138
+ "#{"#{min}m" if min > 0}%.2fs" % sec
139
+ end
140
+
141
+ def colorize(text, type)
142
+ "#{COLORS.fetch(type)}#{text}\033[0m"
143
+ end
144
+ end
145
+ end
@@ -16,11 +16,11 @@ class Array
16
16
  # ==== Options
17
17
  #
18
18
  # * <tt>:words_connector</tt> - The sign or word used to join all but the last
19
- # element in arrays with three or more elements (default: ", ").
19
+ # element in arrays with three or more elements (default: <tt>", "</tt>).
20
20
  # * <tt>:last_word_connector</tt> - The sign or word used to join the last element
21
- # in arrays with three or more elements (default: ", and ").
21
+ # in arrays with three or more elements (default: <tt>", and "</tt>).
22
22
  # * <tt>:two_words_connector</tt> - The sign or word used to join the elements
23
- # in arrays with two elements (default: " and ").
23
+ # in arrays with two elements (default: <tt>" and "</tt>).
24
24
  # * <tt>:locale</tt> - If +i18n+ is available, you can set a locale and use
25
25
  # the connector options defined on the 'support.array' namespace in the
26
26
  # corresponding dictionary file.