activesupport 7.0.0 → 7.2.2.1

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 (211) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +156 -255
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +6 -6
  5. data/lib/active_support/actionable_error.rb +3 -1
  6. data/lib/active_support/array_inquirer.rb +3 -1
  7. data/lib/active_support/backtrace_cleaner.rb +41 -9
  8. data/lib/active_support/benchmarkable.rb +1 -0
  9. data/lib/active_support/broadcast_logger.rb +251 -0
  10. data/lib/active_support/builder.rb +1 -1
  11. data/lib/active_support/cache/coder.rb +153 -0
  12. data/lib/active_support/cache/entry.rb +134 -0
  13. data/lib/active_support/cache/file_store.rb +49 -17
  14. data/lib/active_support/cache/mem_cache_store.rb +111 -129
  15. data/lib/active_support/cache/memory_store.rb +81 -26
  16. data/lib/active_support/cache/null_store.rb +6 -0
  17. data/lib/active_support/cache/redis_cache_store.rb +175 -154
  18. data/lib/active_support/cache/serializer_with_fallback.rb +152 -0
  19. data/lib/active_support/cache/strategy/local_cache.rb +31 -13
  20. data/lib/active_support/cache.rb +457 -377
  21. data/lib/active_support/callbacks.rb +123 -139
  22. data/lib/active_support/code_generator.rb +15 -10
  23. data/lib/active_support/concern.rb +4 -2
  24. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +42 -3
  25. data/lib/active_support/concurrency/null_lock.rb +13 -0
  26. data/lib/active_support/configurable.rb +12 -2
  27. data/lib/active_support/core_ext/array/conversions.rb +7 -9
  28. data/lib/active_support/core_ext/array/inquiry.rb +2 -2
  29. data/lib/active_support/core_ext/array.rb +0 -1
  30. data/lib/active_support/core_ext/class/subclasses.rb +4 -15
  31. data/lib/active_support/core_ext/date/blank.rb +4 -0
  32. data/lib/active_support/core_ext/date/calculations.rb +20 -5
  33. data/lib/active_support/core_ext/date/conversions.rb +15 -16
  34. data/lib/active_support/core_ext/date.rb +0 -1
  35. data/lib/active_support/core_ext/date_and_time/calculations.rb +14 -4
  36. data/lib/active_support/core_ext/date_and_time/compatibility.rb +29 -2
  37. data/lib/active_support/core_ext/date_time/blank.rb +4 -0
  38. data/lib/active_support/core_ext/date_time/calculations.rb +4 -0
  39. data/lib/active_support/core_ext/date_time/conversions.rb +15 -15
  40. data/lib/active_support/core_ext/date_time.rb +0 -1
  41. data/lib/active_support/core_ext/digest/uuid.rb +7 -10
  42. data/lib/active_support/core_ext/enumerable.rb +51 -101
  43. data/lib/active_support/core_ext/erb/util.rb +201 -0
  44. data/lib/active_support/core_ext/file/atomic.rb +2 -0
  45. data/lib/active_support/core_ext/hash/conversions.rb +1 -2
  46. data/lib/active_support/core_ext/hash/deep_merge.rb +22 -14
  47. data/lib/active_support/core_ext/hash/deep_transform_values.rb +3 -3
  48. data/lib/active_support/core_ext/hash/indifferent_access.rb +3 -3
  49. data/lib/active_support/core_ext/hash/keys.rb +7 -7
  50. data/lib/active_support/core_ext/integer/inflections.rb +12 -12
  51. data/lib/active_support/core_ext/kernel/singleton_class.rb +1 -1
  52. data/lib/active_support/core_ext/module/attr_internal.rb +17 -6
  53. data/lib/active_support/core_ext/module/attribute_accessors.rb +6 -0
  54. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +38 -20
  55. data/lib/active_support/core_ext/module/concerning.rb +6 -6
  56. data/lib/active_support/core_ext/module/delegation.rb +20 -119
  57. data/lib/active_support/core_ext/module/deprecation.rb +12 -12
  58. data/lib/active_support/core_ext/module/introspection.rb +0 -1
  59. data/lib/active_support/core_ext/numeric/bytes.rb +9 -0
  60. data/lib/active_support/core_ext/numeric/conversions.rb +77 -75
  61. data/lib/active_support/core_ext/numeric.rb +0 -1
  62. data/lib/active_support/core_ext/object/acts_like.rb +29 -5
  63. data/lib/active_support/core_ext/object/blank.rb +45 -1
  64. data/lib/active_support/core_ext/object/deep_dup.rb +16 -0
  65. data/lib/active_support/core_ext/object/duplicable.rb +25 -16
  66. data/lib/active_support/core_ext/object/inclusion.rb +13 -5
  67. data/lib/active_support/core_ext/object/instance_variables.rb +4 -2
  68. data/lib/active_support/core_ext/object/json.rb +17 -7
  69. data/lib/active_support/core_ext/object/to_query.rb +0 -2
  70. data/lib/active_support/core_ext/object/with.rb +46 -0
  71. data/lib/active_support/core_ext/object/with_options.rb +9 -9
  72. data/lib/active_support/core_ext/object.rb +1 -0
  73. data/lib/active_support/core_ext/pathname/blank.rb +20 -0
  74. data/lib/active_support/core_ext/pathname/existence.rb +2 -0
  75. data/lib/active_support/core_ext/pathname.rb +1 -0
  76. data/lib/active_support/core_ext/range/conversions.rb +32 -11
  77. data/lib/active_support/core_ext/range/overlap.rb +40 -0
  78. data/lib/active_support/core_ext/range.rb +1 -2
  79. data/lib/active_support/core_ext/securerandom.rb +2 -6
  80. data/lib/active_support/core_ext/string/conversions.rb +3 -3
  81. data/lib/active_support/core_ext/string/filters.rb +21 -15
  82. data/lib/active_support/core_ext/string/indent.rb +1 -1
  83. data/lib/active_support/core_ext/string/inflections.rb +16 -9
  84. data/lib/active_support/core_ext/string/inquiry.rb +1 -1
  85. data/lib/active_support/core_ext/string/multibyte.rb +1 -1
  86. data/lib/active_support/core_ext/string/output_safety.rb +39 -150
  87. data/lib/active_support/core_ext/thread/backtrace/location.rb +12 -0
  88. data/lib/active_support/core_ext/time/calculations.rb +42 -32
  89. data/lib/active_support/core_ext/time/compatibility.rb +16 -0
  90. data/lib/active_support/core_ext/time/conversions.rb +13 -15
  91. data/lib/active_support/core_ext/time/zones.rb +8 -9
  92. data/lib/active_support/core_ext/time.rb +0 -1
  93. data/lib/active_support/core_ext.rb +0 -1
  94. data/lib/active_support/current_attributes.rb +53 -46
  95. data/lib/active_support/deep_mergeable.rb +53 -0
  96. data/lib/active_support/delegation.rb +202 -0
  97. data/lib/active_support/dependencies/autoload.rb +9 -16
  98. data/lib/active_support/deprecation/behaviors.rb +65 -42
  99. data/lib/active_support/deprecation/constant_accessor.rb +47 -25
  100. data/lib/active_support/deprecation/deprecators.rb +104 -0
  101. data/lib/active_support/deprecation/disallowed.rb +6 -8
  102. data/lib/active_support/deprecation/method_wrappers.rb +6 -23
  103. data/lib/active_support/deprecation/proxy_wrappers.rb +34 -22
  104. data/lib/active_support/deprecation/reporting.rb +49 -27
  105. data/lib/active_support/deprecation.rb +39 -9
  106. data/lib/active_support/deprecator.rb +7 -0
  107. data/lib/active_support/descendants_tracker.rb +66 -175
  108. data/lib/active_support/duration/iso8601_parser.rb +2 -2
  109. data/lib/active_support/duration/iso8601_serializer.rb +1 -4
  110. data/lib/active_support/duration.rb +13 -7
  111. data/lib/active_support/encrypted_configuration.rb +63 -10
  112. data/lib/active_support/encrypted_file.rb +29 -13
  113. data/lib/active_support/environment_inquirer.rb +22 -2
  114. data/lib/active_support/error_reporter/test_helper.rb +15 -0
  115. data/lib/active_support/error_reporter.rb +160 -36
  116. data/lib/active_support/evented_file_update_checker.rb +19 -7
  117. data/lib/active_support/execution_wrapper.rb +23 -28
  118. data/lib/active_support/file_update_checker.rb +5 -3
  119. data/lib/active_support/fork_tracker.rb +4 -32
  120. data/lib/active_support/gem_version.rb +4 -4
  121. data/lib/active_support/gzip.rb +2 -0
  122. data/lib/active_support/hash_with_indifferent_access.rb +41 -25
  123. data/lib/active_support/html_safe_translation.rb +19 -6
  124. data/lib/active_support/i18n.rb +1 -1
  125. data/lib/active_support/i18n_railtie.rb +20 -13
  126. data/lib/active_support/inflector/inflections.rb +2 -0
  127. data/lib/active_support/inflector/methods.rb +28 -18
  128. data/lib/active_support/inflector/transliterate.rb +4 -2
  129. data/lib/active_support/isolated_execution_state.rb +39 -19
  130. data/lib/active_support/json/decoding.rb +2 -1
  131. data/lib/active_support/json/encoding.rb +25 -43
  132. data/lib/active_support/key_generator.rb +13 -5
  133. data/lib/active_support/lazy_load_hooks.rb +33 -7
  134. data/lib/active_support/locale/en.yml +2 -0
  135. data/lib/active_support/log_subscriber/test_helper.rb +2 -2
  136. data/lib/active_support/log_subscriber.rb +76 -36
  137. data/lib/active_support/logger.rb +22 -60
  138. data/lib/active_support/logger_thread_safe_level.rb +10 -32
  139. data/lib/active_support/message_encryptor.rb +200 -55
  140. data/lib/active_support/message_encryptors.rb +141 -0
  141. data/lib/active_support/message_pack/cache_serializer.rb +23 -0
  142. data/lib/active_support/message_pack/extensions.rb +305 -0
  143. data/lib/active_support/message_pack/serializer.rb +63 -0
  144. data/lib/active_support/message_pack.rb +50 -0
  145. data/lib/active_support/message_verifier.rb +220 -89
  146. data/lib/active_support/message_verifiers.rb +135 -0
  147. data/lib/active_support/messages/codec.rb +65 -0
  148. data/lib/active_support/messages/metadata.rb +111 -45
  149. data/lib/active_support/messages/rotation_coordinator.rb +93 -0
  150. data/lib/active_support/messages/rotator.rb +34 -32
  151. data/lib/active_support/messages/serializer_with_fallback.rb +158 -0
  152. data/lib/active_support/multibyte/chars.rb +4 -2
  153. data/lib/active_support/multibyte/unicode.rb +9 -37
  154. data/lib/active_support/notifications/fanout.rb +248 -87
  155. data/lib/active_support/notifications/instrumenter.rb +93 -25
  156. data/lib/active_support/notifications.rb +38 -31
  157. data/lib/active_support/number_helper/number_converter.rb +16 -7
  158. data/lib/active_support/number_helper/number_to_currency_converter.rb +6 -6
  159. data/lib/active_support/number_helper/number_to_human_size_converter.rb +3 -3
  160. data/lib/active_support/number_helper/number_to_phone_converter.rb +1 -0
  161. data/lib/active_support/number_helper.rb +379 -317
  162. data/lib/active_support/option_merger.rb +4 -4
  163. data/lib/active_support/ordered_hash.rb +3 -3
  164. data/lib/active_support/ordered_options.rb +68 -16
  165. data/lib/active_support/parameter_filter.rb +103 -84
  166. data/lib/active_support/proxy_object.rb +8 -3
  167. data/lib/active_support/railtie.rb +30 -25
  168. data/lib/active_support/reloader.rb +13 -5
  169. data/lib/active_support/rescuable.rb +12 -10
  170. data/lib/active_support/secure_compare_rotator.rb +17 -10
  171. data/lib/active_support/string_inquirer.rb +4 -2
  172. data/lib/active_support/subscriber.rb +10 -27
  173. data/lib/active_support/syntax_error_proxy.rb +60 -0
  174. data/lib/active_support/tagged_logging.rb +64 -25
  175. data/lib/active_support/test_case.rb +160 -7
  176. data/lib/active_support/testing/assertions.rb +29 -13
  177. data/lib/active_support/testing/autorun.rb +0 -2
  178. data/lib/active_support/testing/constant_stubbing.rb +54 -0
  179. data/lib/active_support/testing/deprecation.rb +20 -27
  180. data/lib/active_support/testing/error_reporter_assertions.rb +107 -0
  181. data/lib/active_support/testing/isolation.rb +46 -33
  182. data/lib/active_support/testing/method_call_assertions.rb +7 -8
  183. data/lib/active_support/testing/parallelization/server.rb +3 -0
  184. data/lib/active_support/testing/parallelize_executor.rb +8 -3
  185. data/lib/active_support/testing/setup_and_teardown.rb +2 -0
  186. data/lib/active_support/testing/stream.rb +1 -1
  187. data/lib/active_support/testing/strict_warnings.rb +43 -0
  188. data/lib/active_support/testing/tests_without_assertions.rb +19 -0
  189. data/lib/active_support/testing/time_helpers.rb +38 -16
  190. data/lib/active_support/time_with_zone.rb +28 -54
  191. data/lib/active_support/values/time_zone.rb +26 -15
  192. data/lib/active_support/version.rb +1 -1
  193. data/lib/active_support/xml_mini/jdom.rb +3 -10
  194. data/lib/active_support/xml_mini/nokogiri.rb +1 -1
  195. data/lib/active_support/xml_mini/nokogirisax.rb +1 -1
  196. data/lib/active_support/xml_mini/rexml.rb +1 -1
  197. data/lib/active_support/xml_mini.rb +13 -4
  198. data/lib/active_support.rb +15 -3
  199. metadata +142 -21
  200. data/lib/active_support/core_ext/array/deprecated_conversions.rb +0 -25
  201. data/lib/active_support/core_ext/date/deprecated_conversions.rb +0 -26
  202. data/lib/active_support/core_ext/date_time/deprecated_conversions.rb +0 -22
  203. data/lib/active_support/core_ext/numeric/deprecated_conversions.rb +0 -60
  204. data/lib/active_support/core_ext/range/deprecated_conversions.rb +0 -26
  205. data/lib/active_support/core_ext/range/include_time_with_zone.rb +0 -7
  206. data/lib/active_support/core_ext/range/overlaps.rb +0 -10
  207. data/lib/active_support/core_ext/time/deprecated_conversions.rb +0 -22
  208. data/lib/active_support/core_ext/uri.rb +0 -5
  209. data/lib/active_support/deprecation/instance_delegator.rb +0 -38
  210. data/lib/active_support/per_thread_registry.rb +0 -65
  211. data/lib/active_support/ruby_features.rb +0 -7
@@ -4,40 +4,87 @@ require "yaml"
4
4
  require "active_support/encrypted_file"
5
5
  require "active_support/ordered_options"
6
6
  require "active_support/core_ext/object/inclusion"
7
+ require "active_support/core_ext/hash/keys"
7
8
  require "active_support/core_ext/module/delegation"
8
9
 
9
10
  module ActiveSupport
11
+ # = Encrypted Configuration
12
+ #
13
+ # Provides convenience methods on top of EncryptedFile to access values stored
14
+ # as encrypted YAML.
15
+ #
16
+ # Values can be accessed via +Hash+ methods, such as +fetch+ and +dig+, or via
17
+ # dynamic accessor methods, similar to OrderedOptions.
18
+ #
19
+ # my_config = ActiveSupport::EncryptedConfiguration.new(...)
20
+ # my_config.read # => "some_secret: 123\nsome_namespace:\n another_secret: 456"
21
+ #
22
+ # my_config[:some_secret]
23
+ # # => 123
24
+ # my_config.some_secret
25
+ # # => 123
26
+ # my_config.dig(:some_namespace, :another_secret)
27
+ # # => 456
28
+ # my_config.some_namespace.another_secret
29
+ # # => 456
30
+ # my_config.fetch(:foo)
31
+ # # => KeyError
32
+ # my_config.foo!
33
+ # # => KeyError
34
+ #
10
35
  class EncryptedConfiguration < EncryptedFile
11
- delegate :[], :fetch, to: :config
36
+ class InvalidContentError < RuntimeError
37
+ def initialize(content_path)
38
+ super "Invalid YAML in '#{content_path}'."
39
+ end
40
+
41
+ def message
42
+ cause.is_a?(Psych::SyntaxError) ? "#{super}\n\n #{cause.message}" : super
43
+ end
44
+ end
45
+
12
46
  delegate_missing_to :options
13
47
 
14
48
  def initialize(config_path:, key_path:, env_key:, raise_if_missing_key:)
15
49
  super content_path: config_path, key_path: key_path,
16
50
  env_key: env_key, raise_if_missing_key: raise_if_missing_key
51
+ @config = nil
52
+ @options = nil
17
53
  end
18
54
 
19
- # Allow a config to be started without a file present
55
+ # Reads the file and returns the decrypted content. See EncryptedFile#read.
20
56
  def read
21
57
  super
22
58
  rescue ActiveSupport::EncryptedFile::MissingContentError
59
+ # Allow a config to be started without a file present
23
60
  ""
24
61
  end
25
62
 
26
- def write(contents)
27
- deserialize(contents)
28
-
29
- super
63
+ def validate! # :nodoc:
64
+ deserialize(read)
30
65
  end
31
66
 
67
+ # Returns the decrypted content as a Hash with symbolized keys.
68
+ #
69
+ # my_config = ActiveSupport::EncryptedConfiguration.new(...)
70
+ # my_config.read # => "some_secret: 123\nsome_namespace:\n another_secret: 456"
71
+ #
72
+ # my_config.config
73
+ # # => { some_secret: 123, some_namespace: { another_secret: 789 } }
74
+ #
32
75
  def config
33
76
  @config ||= deserialize(read).deep_symbolize_keys
34
77
  end
35
78
 
79
+ def inspect # :nodoc:
80
+ "#<#{self.class.name}:#{'%#016x' % (object_id << 1)}>"
81
+ end
82
+
36
83
  private
37
84
  def deep_transform(hash)
38
85
  return hash unless hash.is_a?(Hash)
39
86
 
40
- h = ActiveSupport::InheritableOptions.new
87
+ h = ActiveSupport::OrderedOptions.new
41
88
  hash.each do |k, v|
42
89
  h[k] = deep_transform(v)
43
90
  end
@@ -45,11 +92,17 @@ module ActiveSupport
45
92
  end
46
93
 
47
94
  def options
48
- @options ||= ActiveSupport::InheritableOptions.new(deep_transform(config))
95
+ @options ||= deep_transform(config)
49
96
  end
50
97
 
51
- def deserialize(config)
52
- YAML.load(config).presence || {}
98
+ def deserialize(content)
99
+ config = YAML.respond_to?(:unsafe_load) ?
100
+ YAML.unsafe_load(content, filename: content_path) :
101
+ YAML.load(content, filename: content_path)
102
+
103
+ config.presence || {}
104
+ rescue Psych::SyntaxError
105
+ raise InvalidContentError.new(content_path)
53
106
  end
54
107
  end
55
108
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "pathname"
4
- require "tmpdir"
4
+ require "tempfile"
5
5
  require "active_support/message_encryptor"
6
6
 
7
7
  module ActiveSupport
@@ -45,13 +45,31 @@ module ActiveSupport
45
45
  @env_key, @raise_if_missing_key = env_key, raise_if_missing_key
46
46
  end
47
47
 
48
+ # Returns the encryption key, first trying the environment variable
49
+ # specified by +env_key+, then trying the key file specified by +key_path+.
50
+ # If +raise_if_missing_key+ is true, raises MissingKeyError if the
51
+ # environment variable is not set and the key file does not exist.
48
52
  def key
49
53
  read_env_key || read_key_file || handle_missing_key
50
54
  end
51
55
 
56
+ # Returns truthy if #key is truthy. Returns falsy otherwise. Unlike #key,
57
+ # does not raise MissingKeyError when +raise_if_missing_key+ is true.
58
+ def key?
59
+ read_env_key || read_key_file
60
+ end
61
+
62
+ # Reads the file and returns the decrypted content.
63
+ #
64
+ # Raises:
65
+ # - MissingKeyError if the key is missing and +raise_if_missing_key+ is true.
66
+ # - MissingContentError if the encrypted file does not exist or otherwise
67
+ # if the key is missing.
68
+ # - ActiveSupport::MessageEncryptor::InvalidMessage if the content cannot be
69
+ # decrypted or verified.
52
70
  def read
53
71
  if !key.nil? && content_path.exist?
54
- decrypt content_path.binread
72
+ decrypt content_path.binread.strip
55
73
  else
56
74
  raise MissingContentError, content_path
57
75
  end
@@ -69,17 +87,16 @@ module ActiveSupport
69
87
 
70
88
  private
71
89
  def writing(contents)
72
- tmp_file = "#{Process.pid}.#{content_path.basename.to_s.chomp('.enc')}"
73
- tmp_path = Pathname.new File.join(Dir.tmpdir, tmp_file)
74
- tmp_path.binwrite contents
90
+ Tempfile.create(["", "-" + content_path.basename.to_s.chomp(".enc")]) do |tmp_file|
91
+ tmp_path = Pathname.new(tmp_file)
92
+ tmp_path.binwrite contents
75
93
 
76
- yield tmp_path
94
+ yield tmp_path
77
95
 
78
- updated_contents = tmp_path.binread
96
+ updated_contents = tmp_path.binread
79
97
 
80
- write(updated_contents) if updated_contents != contents
81
- ensure
82
- FileUtils.rm(tmp_path) if tmp_path&.exist?
98
+ write(updated_contents) if updated_contents != contents
99
+ end
83
100
  end
84
101
 
85
102
 
@@ -93,7 +110,7 @@ module ActiveSupport
93
110
  end
94
111
 
95
112
  def encryptor
96
- @encryptor ||= ActiveSupport::MessageEncryptor.new([ key ].pack("H*"), cipher: CIPHER)
113
+ @encryptor ||= ActiveSupport::MessageEncryptor.new([ key ].pack("H*"), cipher: CIPHER, serializer: Marshal)
97
114
  end
98
115
 
99
116
 
@@ -102,8 +119,7 @@ module ActiveSupport
102
119
  end
103
120
 
104
121
  def read_key_file
105
- return @key_file_contents if defined?(@key_file_contents)
106
- @key_file_contents = (key_path.binread.strip if key_path.exist?)
122
+ @key_file_contents ||= (key_path.binread.strip if key_path.exist?)
107
123
  end
108
124
 
109
125
  def handle_missing_key
@@ -1,20 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/string_inquirer"
4
+ require "active_support/core_ext/object/inclusion"
4
5
 
5
6
  module ActiveSupport
6
7
  class EnvironmentInquirer < StringInquirer # :nodoc:
7
- DEFAULT_ENVIRONMENTS = ["development", "test", "production"]
8
+ # Optimization for the three default environments, so this inquirer doesn't need to rely on
9
+ # the slower delegation through method_missing that StringInquirer would normally entail.
10
+ DEFAULT_ENVIRONMENTS = %w[ development test production ]
11
+
12
+ # Environments that'll respond true for #local?
13
+ LOCAL_ENVIRONMENTS = %w[ development test ]
14
+
8
15
  def initialize(env)
16
+ raise(ArgumentError, "'local' is a reserved environment name") if env == "local"
17
+
9
18
  super(env)
10
19
 
11
20
  DEFAULT_ENVIRONMENTS.each do |default|
12
21
  instance_variable_set :"@#{default}", env == default
13
22
  end
23
+
24
+ @local = in? LOCAL_ENVIRONMENTS
14
25
  end
15
26
 
16
27
  DEFAULT_ENVIRONMENTS.each do |env|
17
- class_eval "def #{env}?; @#{env}; end"
28
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
29
+ def #{env}?
30
+ @#{env}
31
+ end
32
+ RUBY
33
+ end
34
+
35
+ # Returns true if we're in the development or test environment.
36
+ def local?
37
+ @local
18
38
  end
19
39
  end
20
40
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveSupport::ErrorReporter::TestHelper # :nodoc:
4
+ class ErrorSubscriber
5
+ attr_reader :events
6
+
7
+ def initialize
8
+ @events = []
9
+ end
10
+
11
+ def report(error, handled:, severity:, source:, context:)
12
+ @events << [error, handled, severity, source, context]
13
+ end
14
+ end
15
+ end
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveSupport
4
+ # = Active Support \Error Reporter
5
+ #
4
6
  # +ActiveSupport::ErrorReporter+ is a common interface for error reporting services.
5
7
  #
6
- # To rescue and report any unhandled error, you can use the +handle+ method:
8
+ # To rescue and report any unhandled error, you can use the #handle method:
7
9
  #
8
10
  # Rails.error.handle do
9
11
  # do_something!
@@ -11,68 +13,151 @@ module ActiveSupport
11
13
  #
12
14
  # If an error is raised, it will be reported and swallowed.
13
15
  #
14
- # Alternatively if you want to report the error but not swallow it, you can use +record+
16
+ # Alternatively, if you want to report the error but not swallow it, you can use #record:
15
17
  #
16
18
  # Rails.error.record do
17
19
  # do_something!
18
20
  # end
19
21
  #
20
- # Both methods can be restricted to only handle a specific exception class
22
+ # Both methods can be restricted to handle only a specific error class:
21
23
  #
22
24
  # maybe_tags = Rails.error.handle(Redis::BaseError) { redis.get("tags") }
23
25
  #
24
- # You can also pass some extra context information that may be used by the error subscribers:
25
- #
26
- # Rails.error.handle(context: { section: "admin" }) do
27
- # # ...
28
- # end
29
- #
30
- # Additionally a +severity+ can be passed along to communicate how important the error report is.
31
- # +severity+ can be one of +:error+, +:warning+ or +:info+. Handled errors default to the +:warning+
32
- # severity, and unhandled ones to +error+.
33
- #
34
- # Both +handle+ and +record+ pass through the return value from the block. In the case of +handle+
35
- # rescuing an error, a fallback can be provided. The fallback must be a callable whose result will
36
- # be returned when the block raises and is handled:
37
- #
38
- # user = Rails.error.handle(fallback: -> { User.anonymous }) do
39
- # User.find_by(params)
40
- # end
41
26
  class ErrorReporter
42
27
  SEVERITIES = %i(error warning info)
28
+ DEFAULT_SOURCE = "application"
29
+ DEFAULT_RESCUE = [StandardError].freeze
43
30
 
44
- attr_accessor :logger
31
+ attr_accessor :logger, :debug_mode
32
+
33
+ UnexpectedError = Class.new(Exception)
45
34
 
46
35
  def initialize(*subscribers, logger: nil)
47
36
  @subscribers = subscribers.flatten
48
37
  @logger = logger
38
+ @debug_mode = false
49
39
  end
50
40
 
51
- # Report any unhandled exception, and swallow it.
41
+ # Evaluates the given block, reporting and swallowing any unhandled error.
42
+ # If no error is raised, returns the return value of the block. Otherwise,
43
+ # returns the result of +fallback.call+, or +nil+ if +fallback+ is not
44
+ # specified.
52
45
  #
46
+ # # Will report a TypeError to all subscribers and return nil.
53
47
  # Rails.error.handle do
54
48
  # 1 + '1'
55
49
  # end
56
50
  #
57
- def handle(error_class = StandardError, severity: :warning, context: {}, fallback: nil)
51
+ # Can be restricted to handle only specific error classes:
52
+ #
53
+ # maybe_tags = Rails.error.handle(Redis::BaseError) { redis.get("tags") }
54
+ #
55
+ # ==== Options
56
+ #
57
+ # * +:severity+ - This value is passed along to subscribers to indicate how
58
+ # important the error report is. Can be +:error+, +:warning+, or +:info+.
59
+ # Defaults to +:warning+.
60
+ #
61
+ # * +:context+ - Extra information that is passed along to subscribers. For
62
+ # example:
63
+ #
64
+ # Rails.error.handle(context: { section: "admin" }) do
65
+ # # ...
66
+ # end
67
+ #
68
+ # * +:fallback+ - A callable that provides +handle+'s return value when an
69
+ # unhandled error is raised. For example:
70
+ #
71
+ # user = Rails.error.handle(fallback: -> { User.anonymous }) do
72
+ # User.find_by(params)
73
+ # end
74
+ #
75
+ # * +:source+ - This value is passed along to subscribers to indicate the
76
+ # source of the error. Subscribers can use this value to ignore certain
77
+ # errors. Defaults to <tt>"application"</tt>.
78
+ def handle(*error_classes, severity: :warning, context: {}, fallback: nil, source: DEFAULT_SOURCE)
79
+ error_classes = DEFAULT_RESCUE if error_classes.empty?
58
80
  yield
59
- rescue error_class => error
60
- report(error, handled: true, severity: severity, context: context)
81
+ rescue *error_classes => error
82
+ report(error, handled: true, severity: severity, context: context, source: source)
61
83
  fallback.call if fallback
62
84
  end
63
85
 
64
- def record(error_class = StandardError, severity: :error, context: {})
86
+ # Evaluates the given block, reporting and re-raising any unhandled error.
87
+ # If no error is raised, returns the return value of the block.
88
+ #
89
+ # # Will report a TypeError to all subscribers and re-raise it.
90
+ # Rails.error.record do
91
+ # 1 + '1'
92
+ # end
93
+ #
94
+ # Can be restricted to handle only specific error classes:
95
+ #
96
+ # tags = Rails.error.record(Redis::BaseError) { redis.get("tags") }
97
+ #
98
+ # ==== Options
99
+ #
100
+ # * +:severity+ - This value is passed along to subscribers to indicate how
101
+ # important the error report is. Can be +:error+, +:warning+, or +:info+.
102
+ # Defaults to +:error+.
103
+ #
104
+ # * +:context+ - Extra information that is passed along to subscribers. For
105
+ # example:
106
+ #
107
+ # Rails.error.record(context: { section: "admin" }) do
108
+ # # ...
109
+ # end
110
+ #
111
+ # * +:source+ - This value is passed along to subscribers to indicate the
112
+ # source of the error. Subscribers can use this value to ignore certain
113
+ # errors. Defaults to <tt>"application"</tt>.
114
+ def record(*error_classes, severity: :error, context: {}, source: DEFAULT_SOURCE)
115
+ error_classes = DEFAULT_RESCUE if error_classes.empty?
65
116
  yield
66
- rescue error_class => error
67
- report(error, handled: false, severity: severity, context: context)
117
+ rescue *error_classes => error
118
+ report(error, handled: false, severity: severity, context: context, source: source)
68
119
  raise
69
120
  end
70
121
 
122
+ # Either report the given error when in production, or raise it when in development or test.
123
+ #
124
+ # When called in production, after the error is reported, this method will return
125
+ # nil and execution will continue.
126
+ #
127
+ # When called in development, the original error is wrapped in a different error class to ensure
128
+ # it's not being rescued higher in the stack and will be surfaced to the developer.
129
+ #
130
+ # This method is intended for reporting violated assertions about preconditions, or similar
131
+ # cases that can and should be gracefully handled in production, but that aren't supposed to happen.
132
+ #
133
+ # The error can be either an exception instance or a String.
134
+ #
135
+ # example:
136
+ #
137
+ # def edit
138
+ # if published?
139
+ # Rails.error.unexpected("[BUG] Attempting to edit a published article, that shouldn't be possible")
140
+ # return false
141
+ # end
142
+ # # ...
143
+ # end
144
+ #
145
+ def unexpected(error, severity: :warning, context: {}, source: DEFAULT_SOURCE)
146
+ error = RuntimeError.new(error) if error.is_a?(String)
147
+ error.set_backtrace(caller(1)) if error.backtrace.nil?
148
+
149
+ if @debug_mode
150
+ raise UnexpectedError, "#{error.class.name}: #{error.message}", error.backtrace, cause: error
151
+ else
152
+ report(error, handled: true, severity: severity, context: context, source: source)
153
+ end
154
+ end
155
+
71
156
  # Register a new error subscriber. The subscriber must respond to
72
157
  #
73
- # report(Exception, handled: Boolean, context: Hash)
158
+ # report(Exception, handled: Boolean, severity: (:error OR :warning OR :info), context: Hash, source: String)
74
159
  #
75
- # The +report+ method +should+ never raise an error.
160
+ # The +report+ method <b>should never</b> raise an error.
76
161
  def subscribe(subscriber)
77
162
  unless subscriber.respond_to?(:report)
78
163
  raise ArgumentError, "Error subscribers must respond to #report"
@@ -80,26 +165,61 @@ module ActiveSupport
80
165
  @subscribers << subscriber
81
166
  end
82
167
 
83
- # Update the execution context that is accessible to error subscribers
168
+ # Unregister an error subscriber. Accepts either a subscriber or a class.
169
+ #
170
+ # subscriber = MyErrorSubscriber.new
171
+ # Rails.error.subscribe(subscriber)
172
+ #
173
+ # Rails.error.unsubscribe(subscriber)
174
+ # # or
175
+ # Rails.error.unsubscribe(MyErrorSubscriber)
176
+ def unsubscribe(subscriber)
177
+ @subscribers.delete_if { |s| subscriber === s }
178
+ end
179
+
180
+ # Prevent a subscriber from being notified of errors for the
181
+ # duration of the block. You may pass in the subscriber itself, or its class.
182
+ #
183
+ # This can be helpful for error reporting service integrations, when they wish
184
+ # to handle any errors higher in the stack.
185
+ def disable(subscriber)
186
+ disabled_subscribers = (ActiveSupport::IsolatedExecutionState[self] ||= [])
187
+ disabled_subscribers << subscriber
188
+ begin
189
+ yield
190
+ ensure
191
+ disabled_subscribers.delete(subscriber)
192
+ end
193
+ end
194
+
195
+ # Update the execution context that is accessible to error subscribers. Any
196
+ # context passed to #handle, #record, or #report will be merged with the
197
+ # context set here.
84
198
  #
85
199
  # Rails.error.set_context(section: "checkout", user_id: @user.id)
86
200
  #
87
- # See +ActiveSupport::ExecutionContext.set+
88
201
  def set_context(...)
89
202
  ActiveSupport::ExecutionContext.set(...)
90
203
  end
91
204
 
92
- # When the block based +handle+ and +record+ methods are not suitable, you can directly use +report+
205
+ # Report an error directly to subscribers. You can use this method when the
206
+ # block-based #handle and #record methods are not suitable.
207
+ #
208
+ # Rails.error.report(error)
93
209
  #
94
- # Rails.error.report(error, handled: true)
95
- def report(error, handled:, severity: handled ? :warning : :error, context: {})
210
+ def report(error, handled: true, severity: handled ? :warning : :error, context: {}, source: DEFAULT_SOURCE)
211
+ return if error.instance_variable_defined?(:@__rails_error_reported)
212
+
96
213
  unless SEVERITIES.include?(severity)
97
214
  raise ArgumentError, "severity must be one of #{SEVERITIES.map(&:inspect).join(", ")}, got: #{severity.inspect}"
98
215
  end
99
216
 
100
217
  full_context = ActiveSupport::ExecutionContext.to_h.merge(context)
218
+ disabled_subscribers = ActiveSupport::IsolatedExecutionState[self]
101
219
  @subscribers.each do |subscriber|
102
- subscriber.report(error, handled: handled, severity: severity, context: full_context)
220
+ unless disabled_subscribers&.any? { |s| s === subscriber }
221
+ subscriber.report(error, handled: handled, severity: severity, context: full_context, source: source)
222
+ end
103
223
  rescue => subscriber_error
104
224
  if logger
105
225
  logger.fatal(
@@ -111,6 +231,10 @@ module ActiveSupport
111
231
  end
112
232
  end
113
233
 
234
+ unless error.frozen?
235
+ error.instance_variable_set(:@__rails_error_reported, true)
236
+ end
237
+
114
238
  nil
115
239
  end
116
240
  end
@@ -1,15 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ gem "listen", "~> 3.5"
4
+ require "listen"
5
+
3
6
  require "set"
4
7
  require "pathname"
5
8
  require "concurrent/atomic/atomic_boolean"
6
- require "listen"
7
- require "active_support/fork_tracker"
8
9
 
9
10
  module ActiveSupport
10
11
  # Allows you to "listen" to changes in a file system.
11
- # The evented file updater does not hit disk when checking for updates
12
- # instead it uses platform specific file system events to trigger a change
12
+ # The evented file updater does not hit disk when checking for updates.
13
+ # Instead, it uses platform-specific file system events to trigger a change
13
14
  # in state.
14
15
  #
15
16
  # The file checker takes an array of files to watch or a hash specifying directories
@@ -17,8 +18,6 @@ module ActiveSupport
17
18
  # EventedFileUpdateChecker#execute is run or when EventedFileUpdateChecker#execute_if_updated
18
19
  # is run and there have been changes to the file system.
19
20
  #
20
- # Note: Forking will cause the first call to `updated?` to return `true`.
21
- #
22
21
  # Example:
23
22
  #
24
23
  # checker = ActiveSupport::EventedFileUpdateChecker.new(["/tmp/foo"]) { puts "changed" }
@@ -45,6 +44,10 @@ module ActiveSupport
45
44
  ObjectSpace.define_finalizer(self, @core.finalizer)
46
45
  end
47
46
 
47
+ def inspect
48
+ "#<ActiveSupport::EventedFileUpdateChecker:#{object_id} @files=#{@core.files.to_a.inspect}"
49
+ end
50
+
48
51
  def updated?
49
52
  if @core.restart?
50
53
  @core.thread_safely(&:restart)
@@ -68,7 +71,7 @@ module ActiveSupport
68
71
  end
69
72
 
70
73
  class Core
71
- attr_reader :updated
74
+ attr_reader :updated, :files
72
75
 
73
76
  def initialize(files, dirs)
74
77
  @files = files.map { |file| Pathname(file).expand_path }.to_set
@@ -86,6 +89,10 @@ module ActiveSupport
86
89
  @mutex = Mutex.new
87
90
 
88
91
  start
92
+ # inotify / FSEvents file descriptors are inherited on fork, so
93
+ # we need to reopen them otherwise only the parent or the child
94
+ # will be notified.
95
+ # FIXME: this callback is keeping a reference on the instance
89
96
  @after_fork = ActiveSupport::ForkTracker.after_fork { start }
90
97
  end
91
98
 
@@ -107,6 +114,11 @@ module ActiveSupport
107
114
  @dtw, @missing = [*@dtw, *@missing].partition(&:exist?)
108
115
  @listener = @dtw.any? ? Listen.to(*@dtw, &method(:changed)) : nil
109
116
  @listener&.start
117
+
118
+ # Wait for the listener to be ready to avoid race conditions
119
+ # Unfortunately this isn't quite enough on macOS because the Darwin backend
120
+ # has an extra private thread we can't wait on.
121
+ @listener&.wait_for_state(:processing_events)
110
122
  end
111
123
 
112
124
  def stop