activesupport 6.0.6.1 → 7.1.3.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (245) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +865 -438
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +6 -6
  5. data/lib/active_support/actionable_error.rb +4 -2
  6. data/lib/active_support/array_inquirer.rb +4 -2
  7. data/lib/active_support/backtrace_cleaner.rb +30 -10
  8. data/lib/active_support/benchmarkable.rb +4 -3
  9. data/lib/active_support/broadcast_logger.rb +250 -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 +53 -20
  14. data/lib/active_support/cache/mem_cache_store.rb +208 -63
  15. data/lib/active_support/cache/memory_store.rb +120 -38
  16. data/lib/active_support/cache/null_store.rb +16 -2
  17. data/lib/active_support/cache/redis_cache_store.rb +201 -208
  18. data/lib/active_support/cache/serializer_with_fallback.rb +175 -0
  19. data/lib/active_support/cache/strategy/local_cache.rb +73 -66
  20. data/lib/active_support/cache.rb +539 -261
  21. data/lib/active_support/callbacks.rb +273 -142
  22. data/lib/active_support/code_generator.rb +65 -0
  23. data/lib/active_support/concern.rb +53 -7
  24. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +44 -7
  25. data/lib/active_support/concurrency/null_lock.rb +13 -0
  26. data/lib/active_support/concurrency/share_lock.rb +2 -2
  27. data/lib/active_support/configurable.rb +19 -6
  28. data/lib/active_support/configuration_file.rb +51 -0
  29. data/lib/active_support/core_ext/array/access.rb +1 -5
  30. data/lib/active_support/core_ext/array/conversions.rb +15 -13
  31. data/lib/active_support/core_ext/array/grouping.rb +6 -6
  32. data/lib/active_support/core_ext/array/inquiry.rb +2 -2
  33. data/lib/active_support/core_ext/benchmark.rb +2 -2
  34. data/lib/active_support/core_ext/big_decimal/conversions.rb +1 -1
  35. data/lib/active_support/core_ext/class/attribute.rb +34 -44
  36. data/lib/active_support/core_ext/class/subclasses.rb +19 -29
  37. data/lib/active_support/core_ext/date/blank.rb +1 -1
  38. data/lib/active_support/core_ext/date/calculations.rb +24 -9
  39. data/lib/active_support/core_ext/date/conversions.rb +18 -16
  40. data/lib/active_support/core_ext/date_and_time/calculations.rb +27 -4
  41. data/lib/active_support/core_ext/date_and_time/compatibility.rb +15 -0
  42. data/lib/active_support/core_ext/date_time/blank.rb +1 -1
  43. data/lib/active_support/core_ext/date_time/calculations.rb +4 -0
  44. data/lib/active_support/core_ext/date_time/conversions.rb +19 -15
  45. data/lib/active_support/core_ext/digest/uuid.rb +30 -13
  46. data/lib/active_support/core_ext/enumerable.rb +146 -72
  47. data/lib/active_support/core_ext/erb/util.rb +196 -0
  48. data/lib/active_support/core_ext/file/atomic.rb +3 -1
  49. data/lib/active_support/core_ext/hash/conversions.rb +3 -4
  50. data/lib/active_support/core_ext/hash/deep_merge.rb +22 -14
  51. data/lib/active_support/core_ext/hash/deep_transform_values.rb +4 -4
  52. data/lib/active_support/core_ext/hash/indifferent_access.rb +3 -3
  53. data/lib/active_support/core_ext/hash/keys.rb +5 -5
  54. data/lib/active_support/core_ext/hash/slice.rb +3 -2
  55. data/lib/active_support/core_ext/integer/inflections.rb +12 -12
  56. data/lib/active_support/core_ext/kernel/reporting.rb +4 -4
  57. data/lib/active_support/core_ext/kernel/singleton_class.rb +1 -1
  58. data/lib/active_support/core_ext/load_error.rb +1 -1
  59. data/lib/active_support/core_ext/module/attr_internal.rb +2 -2
  60. data/lib/active_support/core_ext/module/attribute_accessors.rb +31 -29
  61. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +51 -20
  62. data/lib/active_support/core_ext/module/concerning.rb +14 -8
  63. data/lib/active_support/core_ext/module/delegation.rb +75 -42
  64. data/lib/active_support/core_ext/module/deprecation.rb +15 -12
  65. data/lib/active_support/core_ext/module/introspection.rb +1 -26
  66. data/lib/active_support/core_ext/name_error.rb +23 -2
  67. data/lib/active_support/core_ext/numeric/bytes.rb +9 -0
  68. data/lib/active_support/core_ext/numeric/conversions.rb +82 -73
  69. data/lib/active_support/core_ext/object/acts_like.rb +29 -5
  70. data/lib/active_support/core_ext/object/blank.rb +2 -2
  71. data/lib/active_support/core_ext/object/deep_dup.rb +17 -1
  72. data/lib/active_support/core_ext/object/duplicable.rb +15 -4
  73. data/lib/active_support/core_ext/object/inclusion.rb +13 -5
  74. data/lib/active_support/core_ext/object/instance_variables.rb +22 -12
  75. data/lib/active_support/core_ext/object/json.rb +52 -28
  76. data/lib/active_support/core_ext/object/to_query.rb +2 -4
  77. data/lib/active_support/core_ext/object/try.rb +20 -20
  78. data/lib/active_support/core_ext/object/with.rb +44 -0
  79. data/lib/active_support/core_ext/object/with_options.rb +25 -6
  80. data/lib/active_support/core_ext/object.rb +1 -0
  81. data/lib/active_support/core_ext/pathname/blank.rb +16 -0
  82. data/lib/active_support/core_ext/pathname/existence.rb +23 -0
  83. data/lib/active_support/core_ext/pathname.rb +4 -0
  84. data/lib/active_support/core_ext/range/compare_range.rb +6 -25
  85. data/lib/active_support/core_ext/range/conversions.rb +34 -13
  86. data/lib/active_support/core_ext/range/each.rb +1 -1
  87. data/lib/active_support/core_ext/range/overlap.rb +40 -0
  88. data/lib/active_support/core_ext/range.rb +1 -2
  89. data/lib/active_support/core_ext/regexp.rb +8 -1
  90. data/lib/active_support/core_ext/securerandom.rb +25 -13
  91. data/lib/active_support/core_ext/string/access.rb +5 -24
  92. data/lib/active_support/core_ext/string/conversions.rb +3 -2
  93. data/lib/active_support/core_ext/string/filters.rb +21 -15
  94. data/lib/active_support/core_ext/string/indent.rb +1 -1
  95. data/lib/active_support/core_ext/string/inflections.rb +51 -10
  96. data/lib/active_support/core_ext/string/inquiry.rb +2 -1
  97. data/lib/active_support/core_ext/string/multibyte.rb +2 -2
  98. data/lib/active_support/core_ext/string/output_safety.rb +85 -194
  99. data/lib/active_support/core_ext/string/starts_ends_with.rb +2 -2
  100. data/lib/active_support/core_ext/symbol/starts_ends_with.rb +6 -0
  101. data/lib/active_support/core_ext/symbol.rb +3 -0
  102. data/lib/active_support/core_ext/thread/backtrace/location.rb +12 -0
  103. data/lib/active_support/core_ext/time/calculations.rb +46 -8
  104. data/lib/active_support/core_ext/time/conversions.rb +16 -13
  105. data/lib/active_support/core_ext/time/zones.rb +12 -28
  106. data/lib/active_support/core_ext.rb +2 -1
  107. data/lib/active_support/current_attributes/test_helper.rb +13 -0
  108. data/lib/active_support/current_attributes.rb +54 -22
  109. data/lib/active_support/deep_mergeable.rb +53 -0
  110. data/lib/active_support/dependencies/autoload.rb +17 -12
  111. data/lib/active_support/dependencies/interlock.rb +10 -18
  112. data/lib/active_support/dependencies/require_dependency.rb +28 -0
  113. data/lib/active_support/dependencies.rb +58 -769
  114. data/lib/active_support/deprecation/behaviors.rb +77 -38
  115. data/lib/active_support/deprecation/constant_accessor.rb +5 -4
  116. data/lib/active_support/deprecation/deprecators.rb +104 -0
  117. data/lib/active_support/deprecation/disallowed.rb +54 -0
  118. data/lib/active_support/deprecation/instance_delegator.rb +31 -5
  119. data/lib/active_support/deprecation/method_wrappers.rb +12 -28
  120. data/lib/active_support/deprecation/proxy_wrappers.rb +40 -25
  121. data/lib/active_support/deprecation/reporting.rb +76 -16
  122. data/lib/active_support/deprecation.rb +36 -4
  123. data/lib/active_support/deprecator.rb +7 -0
  124. data/lib/active_support/descendants_tracker.rb +150 -68
  125. data/lib/active_support/digest.rb +5 -3
  126. data/lib/active_support/duration/iso8601_parser.rb +3 -3
  127. data/lib/active_support/duration/iso8601_serializer.rb +24 -12
  128. data/lib/active_support/duration.rb +136 -56
  129. data/lib/active_support/encrypted_configuration.rb +72 -9
  130. data/lib/active_support/encrypted_file.rb +46 -13
  131. data/lib/active_support/environment_inquirer.rb +40 -0
  132. data/lib/active_support/error_reporter/test_helper.rb +15 -0
  133. data/lib/active_support/error_reporter.rb +203 -0
  134. data/lib/active_support/evented_file_update_checker.rb +86 -137
  135. data/lib/active_support/execution_context/test_helper.rb +13 -0
  136. data/lib/active_support/execution_context.rb +53 -0
  137. data/lib/active_support/execution_wrapper.rb +31 -12
  138. data/lib/active_support/executor/test_helper.rb +7 -0
  139. data/lib/active_support/file_update_checker.rb +4 -2
  140. data/lib/active_support/fork_tracker.rb +79 -0
  141. data/lib/active_support/gem_version.rb +5 -5
  142. data/lib/active_support/gzip.rb +2 -0
  143. data/lib/active_support/hash_with_indifferent_access.rb +86 -42
  144. data/lib/active_support/html_safe_translation.rb +53 -0
  145. data/lib/active_support/i18n.rb +2 -1
  146. data/lib/active_support/i18n_railtie.rb +29 -27
  147. data/lib/active_support/inflector/inflections.rb +26 -9
  148. data/lib/active_support/inflector/methods.rb +54 -64
  149. data/lib/active_support/inflector/transliterate.rb +7 -5
  150. data/lib/active_support/isolated_execution_state.rb +76 -0
  151. data/lib/active_support/json/decoding.rb +6 -5
  152. data/lib/active_support/json/encoding.rb +31 -45
  153. data/lib/active_support/key_generator.rb +32 -7
  154. data/lib/active_support/lazy_load_hooks.rb +33 -7
  155. data/lib/active_support/locale/en.yml +10 -4
  156. data/lib/active_support/log_subscriber/test_helper.rb +2 -2
  157. data/lib/active_support/log_subscriber.rb +101 -32
  158. data/lib/active_support/logger.rb +9 -60
  159. data/lib/active_support/logger_silence.rb +2 -26
  160. data/lib/active_support/logger_thread_safe_level.rb +24 -25
  161. data/lib/active_support/message_encryptor.rb +205 -58
  162. data/lib/active_support/message_encryptors.rb +141 -0
  163. data/lib/active_support/message_pack/cache_serializer.rb +23 -0
  164. data/lib/active_support/message_pack/extensions.rb +292 -0
  165. data/lib/active_support/message_pack/serializer.rb +63 -0
  166. data/lib/active_support/message_pack.rb +50 -0
  167. data/lib/active_support/message_verifier.rb +237 -86
  168. data/lib/active_support/message_verifiers.rb +135 -0
  169. data/lib/active_support/messages/codec.rb +65 -0
  170. data/lib/active_support/messages/metadata.rb +112 -46
  171. data/lib/active_support/messages/rotation_configuration.rb +2 -1
  172. data/lib/active_support/messages/rotation_coordinator.rb +93 -0
  173. data/lib/active_support/messages/rotator.rb +35 -32
  174. data/lib/active_support/messages/serializer_with_fallback.rb +158 -0
  175. data/lib/active_support/multibyte/chars.rb +15 -52
  176. data/lib/active_support/multibyte/unicode.rb +8 -122
  177. data/lib/active_support/multibyte.rb +1 -1
  178. data/lib/active_support/notifications/fanout.rb +310 -105
  179. data/lib/active_support/notifications/instrumenter.rb +113 -48
  180. data/lib/active_support/notifications.rb +56 -29
  181. data/lib/active_support/number_helper/number_converter.rb +15 -8
  182. data/lib/active_support/number_helper/number_to_currency_converter.rb +11 -6
  183. data/lib/active_support/number_helper/number_to_delimited_converter.rb +1 -1
  184. data/lib/active_support/number_helper/number_to_human_converter.rb +1 -1
  185. data/lib/active_support/number_helper/number_to_human_size_converter.rb +5 -5
  186. data/lib/active_support/number_helper/number_to_phone_converter.rb +2 -1
  187. data/lib/active_support/number_helper/number_to_rounded_converter.rb +9 -5
  188. data/lib/active_support/number_helper/rounding_helper.rb +12 -32
  189. data/lib/active_support/number_helper.rb +379 -304
  190. data/lib/active_support/option_merger.rb +11 -18
  191. data/lib/active_support/ordered_hash.rb +4 -4
  192. data/lib/active_support/ordered_options.rb +23 -3
  193. data/lib/active_support/parameter_filter.rb +104 -75
  194. data/lib/active_support/proxy_object.rb +2 -0
  195. data/lib/active_support/rails.rb +1 -4
  196. data/lib/active_support/railtie.rb +90 -6
  197. data/lib/active_support/reloader.rb +12 -4
  198. data/lib/active_support/rescuable.rb +18 -16
  199. data/lib/active_support/ruby_features.rb +7 -0
  200. data/lib/active_support/secure_compare_rotator.rb +58 -0
  201. data/lib/active_support/security_utils.rb +19 -12
  202. data/lib/active_support/string_inquirer.rb +5 -3
  203. data/lib/active_support/subscriber.rb +23 -47
  204. data/lib/active_support/syntax_error_proxy.rb +70 -0
  205. data/lib/active_support/tagged_logging.rb +84 -23
  206. data/lib/active_support/test_case.rb +166 -27
  207. data/lib/active_support/testing/assertions.rb +73 -20
  208. data/lib/active_support/testing/autorun.rb +0 -2
  209. data/lib/active_support/testing/constant_stubbing.rb +32 -0
  210. data/lib/active_support/testing/deprecation.rb +53 -2
  211. data/lib/active_support/testing/error_reporter_assertions.rb +107 -0
  212. data/lib/active_support/testing/isolation.rb +30 -29
  213. data/lib/active_support/testing/method_call_assertions.rb +24 -11
  214. data/lib/active_support/testing/parallelization/server.rb +82 -0
  215. data/lib/active_support/testing/parallelization/worker.rb +103 -0
  216. data/lib/active_support/testing/parallelization.rb +16 -95
  217. data/lib/active_support/testing/parallelize_executor.rb +81 -0
  218. data/lib/active_support/testing/stream.rb +4 -6
  219. data/lib/active_support/testing/strict_warnings.rb +39 -0
  220. data/lib/active_support/testing/tagged_logging.rb +1 -1
  221. data/lib/active_support/testing/time_helpers.rb +89 -19
  222. data/lib/active_support/time_with_zone.rb +105 -70
  223. data/lib/active_support/values/time_zone.rb +59 -26
  224. data/lib/active_support/version.rb +1 -1
  225. data/lib/active_support/xml_mini/jdom.rb +4 -11
  226. data/lib/active_support/xml_mini/libxml.rb +5 -5
  227. data/lib/active_support/xml_mini/libxmlsax.rb +1 -1
  228. data/lib/active_support/xml_mini/nokogiri.rb +5 -5
  229. data/lib/active_support/xml_mini/nokogirisax.rb +2 -2
  230. data/lib/active_support/xml_mini/rexml.rb +9 -2
  231. data/lib/active_support/xml_mini.rb +7 -6
  232. data/lib/active_support.rb +40 -1
  233. metadata +127 -40
  234. data/lib/active_support/core_ext/array/prepend_and_append.rb +0 -5
  235. data/lib/active_support/core_ext/hash/compact.rb +0 -5
  236. data/lib/active_support/core_ext/hash/transform_values.rb +0 -5
  237. data/lib/active_support/core_ext/marshal.rb +0 -24
  238. data/lib/active_support/core_ext/module/reachable.rb +0 -6
  239. data/lib/active_support/core_ext/numeric/inquiry.rb +0 -5
  240. data/lib/active_support/core_ext/range/include_range.rb +0 -9
  241. data/lib/active_support/core_ext/range/include_time_with_zone.rb +0 -23
  242. data/lib/active_support/core_ext/range/overlaps.rb +0 -10
  243. data/lib/active_support/core_ext/uri.rb +0 -25
  244. data/lib/active_support/dependencies/zeitwerk_integration.rb +0 -117
  245. data/lib/active_support/per_thread_registry.rb +0 -60
@@ -0,0 +1,203 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveSupport
4
+ # = Active Support \Error Reporter
5
+ #
6
+ # +ActiveSupport::ErrorReporter+ is a common interface for error reporting services.
7
+ #
8
+ # To rescue and report any unhandled error, you can use the #handle method:
9
+ #
10
+ # Rails.error.handle do
11
+ # do_something!
12
+ # end
13
+ #
14
+ # If an error is raised, it will be reported and swallowed.
15
+ #
16
+ # Alternatively, if you want to report the error but not swallow it, you can use #record:
17
+ #
18
+ # Rails.error.record do
19
+ # do_something!
20
+ # end
21
+ #
22
+ # Both methods can be restricted to handle only a specific error class:
23
+ #
24
+ # maybe_tags = Rails.error.handle(Redis::BaseError) { redis.get("tags") }
25
+ #
26
+ class ErrorReporter
27
+ SEVERITIES = %i(error warning info)
28
+ DEFAULT_SOURCE = "application"
29
+
30
+ attr_accessor :logger
31
+
32
+ def initialize(*subscribers, logger: nil)
33
+ @subscribers = subscribers.flatten
34
+ @logger = logger
35
+ end
36
+
37
+ # Evaluates the given block, reporting and swallowing any unhandled error.
38
+ # If no error is raised, returns the return value of the block. Otherwise,
39
+ # returns the result of +fallback.call+, or +nil+ if +fallback+ is not
40
+ # specified.
41
+ #
42
+ # # Will report a TypeError to all subscribers and return nil.
43
+ # Rails.error.handle do
44
+ # 1 + '1'
45
+ # end
46
+ #
47
+ # Can be restricted to handle only specific error classes:
48
+ #
49
+ # maybe_tags = Rails.error.handle(Redis::BaseError) { redis.get("tags") }
50
+ #
51
+ # ==== Options
52
+ #
53
+ # * +:severity+ - This value is passed along to subscribers to indicate how
54
+ # important the error report is. Can be +:error+, +:warning+, or +:info+.
55
+ # Defaults to +:warning+.
56
+ #
57
+ # * +:context+ - Extra information that is passed along to subscribers. For
58
+ # example:
59
+ #
60
+ # Rails.error.handle(context: { section: "admin" }) do
61
+ # # ...
62
+ # end
63
+ #
64
+ # * +:fallback+ - A callable that provides +handle+'s return value when an
65
+ # unhandled error is raised. For example:
66
+ #
67
+ # user = Rails.error.handle(fallback: -> { User.anonymous }) do
68
+ # User.find_by(params)
69
+ # end
70
+ #
71
+ # * +:source+ - This value is passed along to subscribers to indicate the
72
+ # source of the error. Subscribers can use this value to ignore certain
73
+ # errors. Defaults to <tt>"application"</tt>.
74
+ def handle(*error_classes, severity: :warning, context: {}, fallback: nil, source: DEFAULT_SOURCE)
75
+ error_classes = [StandardError] if error_classes.blank?
76
+ yield
77
+ rescue *error_classes => error
78
+ report(error, handled: true, severity: severity, context: context, source: source)
79
+ fallback.call if fallback
80
+ end
81
+
82
+ # Evaluates the given block, reporting and re-raising any unhandled error.
83
+ # If no error is raised, returns the return value of the block.
84
+ #
85
+ # # Will report a TypeError to all subscribers and re-raise it.
86
+ # Rails.error.record do
87
+ # 1 + '1'
88
+ # end
89
+ #
90
+ # Can be restricted to handle only specific error classes:
91
+ #
92
+ # tags = Rails.error.record(Redis::BaseError) { redis.get("tags") }
93
+ #
94
+ # ==== Options
95
+ #
96
+ # * +:severity+ - This value is passed along to subscribers to indicate how
97
+ # important the error report is. Can be +:error+, +:warning+, or +:info+.
98
+ # Defaults to +:error+.
99
+ #
100
+ # * +:context+ - Extra information that is passed along to subscribers. For
101
+ # example:
102
+ #
103
+ # Rails.error.record(context: { section: "admin" }) do
104
+ # # ...
105
+ # end
106
+ #
107
+ # * +:source+ - This value is passed along to subscribers to indicate the
108
+ # source of the error. Subscribers can use this value to ignore certain
109
+ # errors. Defaults to <tt>"application"</tt>.
110
+ def record(*error_classes, severity: :error, context: {}, source: DEFAULT_SOURCE)
111
+ error_classes = [StandardError] if error_classes.blank?
112
+ yield
113
+ rescue *error_classes => error
114
+ report(error, handled: false, severity: severity, context: context, source: source)
115
+ raise
116
+ end
117
+
118
+ # Register a new error subscriber. The subscriber must respond to
119
+ #
120
+ # report(Exception, handled: Boolean, severity: (:error OR :warning OR :info), context: Hash, source: String)
121
+ #
122
+ # The +report+ method <b>should never</b> raise an error.
123
+ def subscribe(subscriber)
124
+ unless subscriber.respond_to?(:report)
125
+ raise ArgumentError, "Error subscribers must respond to #report"
126
+ end
127
+ @subscribers << subscriber
128
+ end
129
+
130
+ # Unregister an error subscriber. Accepts either a subscriber or a class.
131
+ #
132
+ # subscriber = MyErrorSubscriber.new
133
+ # Rails.error.subscribe(subscriber)
134
+ #
135
+ # Rails.error.unsubscribe(subscriber)
136
+ # # or
137
+ # Rails.error.unsubscribe(MyErrorSubscriber)
138
+ def unsubscribe(subscriber)
139
+ @subscribers.delete_if { |s| subscriber === s }
140
+ end
141
+
142
+ # Prevent a subscriber from being notified of errors for the
143
+ # duration of the block. You may pass in the subscriber itself, or its class.
144
+ #
145
+ # This can be helpful for error reporting service integrations, when they wish
146
+ # to handle any errors higher in the stack.
147
+ def disable(subscriber)
148
+ disabled_subscribers = (ActiveSupport::IsolatedExecutionState[self] ||= [])
149
+ disabled_subscribers << subscriber
150
+ begin
151
+ yield
152
+ ensure
153
+ disabled_subscribers.delete(subscriber)
154
+ end
155
+ end
156
+
157
+ # Update the execution context that is accessible to error subscribers. Any
158
+ # context passed to #handle, #record, or #report will be merged with the
159
+ # context set here.
160
+ #
161
+ # Rails.error.set_context(section: "checkout", user_id: @user.id)
162
+ #
163
+ def set_context(...)
164
+ ActiveSupport::ExecutionContext.set(...)
165
+ end
166
+
167
+ # Report an error directly to subscribers. You can use this method when the
168
+ # block-based #handle and #record methods are not suitable.
169
+ #
170
+ # Rails.error.report(error)
171
+ #
172
+ def report(error, handled: true, severity: handled ? :warning : :error, context: {}, source: DEFAULT_SOURCE)
173
+ return if error.instance_variable_defined?(:@__rails_error_reported)
174
+
175
+ unless SEVERITIES.include?(severity)
176
+ raise ArgumentError, "severity must be one of #{SEVERITIES.map(&:inspect).join(", ")}, got: #{severity.inspect}"
177
+ end
178
+
179
+ full_context = ActiveSupport::ExecutionContext.to_h.merge(context)
180
+ disabled_subscribers = ActiveSupport::IsolatedExecutionState[self]
181
+ @subscribers.each do |subscriber|
182
+ unless disabled_subscribers&.any? { |s| s === subscriber }
183
+ subscriber.report(error, handled: handled, severity: severity, context: full_context, source: source)
184
+ end
185
+ rescue => subscriber_error
186
+ if logger
187
+ logger.fatal(
188
+ "Error subscriber raised an error: #{subscriber_error.message} (#{subscriber_error.class})\n" +
189
+ subscriber_error.backtrace.join("\n")
190
+ )
191
+ else
192
+ raise
193
+ end
194
+ end
195
+
196
+ unless error.frozen?
197
+ error.instance_variable_set(:@__rails_error_reported, true)
198
+ end
199
+
200
+ nil
201
+ end
202
+ end
203
+ end
@@ -1,13 +1,17 @@
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"
9
+ require "active_support/fork_tracker"
6
10
 
7
11
  module ActiveSupport
8
12
  # Allows you to "listen" to changes in a file system.
9
- # The evented file updater does not hit disk when checking for updates
10
- # instead it uses platform specific file system events to trigger a change
13
+ # The evented file updater does not hit disk when checking for updates.
14
+ # Instead, it uses platform-specific file system events to trigger a change
11
15
  # in state.
12
16
  #
13
17
  # The file checker takes an array of files to watch or a hash specifying directories
@@ -15,8 +19,6 @@ module ActiveSupport
15
19
  # EventedFileUpdateChecker#execute is run or when EventedFileUpdateChecker#execute_if_updated
16
20
  # is run and there have been changes to the file system.
17
21
  #
18
- # Note: Forking will cause the first call to `updated?` to return `true`.
19
- #
20
22
  # Example:
21
23
  #
22
24
  # checker = ActiveSupport::EventedFileUpdateChecker.new(["/tmp/foo"]) { puts "changed" }
@@ -32,68 +34,32 @@ module ActiveSupport
32
34
  # checker.execute_if_updated
33
35
  # # => "changed"
34
36
  #
35
- class EventedFileUpdateChecker #:nodoc: all
37
+ class EventedFileUpdateChecker # :nodoc: all
36
38
  def initialize(files, dirs = {}, &block)
37
39
  unless block
38
40
  raise ArgumentError, "A block is required to initialize an EventedFileUpdateChecker"
39
41
  end
40
42
 
41
- @ph = PathHelper.new
42
- @files = files.map { |f| @ph.xpath(f) }.to_set
43
-
44
- @dirs = {}
45
- dirs.each do |dir, exts|
46
- @dirs[@ph.xpath(dir)] = Array(exts).map { |ext| @ph.normalize_extension(ext) }
47
- end
43
+ @block = block
44
+ @core = Core.new(files, dirs)
45
+ ObjectSpace.define_finalizer(self, @core.finalizer)
46
+ end
48
47
 
49
- @block = block
50
- @updated = Concurrent::AtomicBoolean.new(false)
51
- @lcsp = @ph.longest_common_subpath(@dirs.keys)
52
- @pid = Process.pid
53
- @boot_mutex = Mutex.new
54
-
55
- dtw = directories_to_watch
56
- @dtw, @missing = dtw.partition(&:exist?)
57
-
58
- if @dtw.any?
59
- # Loading listen triggers warnings. These are originated by a legit
60
- # usage of attr_* macros for private attributes, but adds a lot of noise
61
- # to our test suite. Thus, we lazy load it and disable warnings locally.
62
- silence_warnings do
63
- require "listen"
64
- rescue LoadError => e
65
- raise LoadError, "Could not load the 'listen' gem. Add `gem 'listen'` to the development group of your Gemfile", e.backtrace
66
- end
67
- end
68
- boot!
48
+ def inspect
49
+ "#<ActiveSupport::EventedFileUpdateChecker:#{object_id} @files=#{@core.files.to_a.inspect}"
69
50
  end
70
51
 
71
52
  def updated?
72
- @boot_mutex.synchronize do
73
- if @pid != Process.pid
74
- boot!
75
- @pid = Process.pid
76
- @updated.make_true
77
- end
53
+ if @core.restart?
54
+ @core.thread_safely(&:restart)
55
+ @core.updated.make_true
78
56
  end
79
57
 
80
- if @missing.any?(&:exist?)
81
- @boot_mutex.synchronize do
82
- appeared, @missing = @missing.partition(&:exist?)
83
- shutdown!
84
-
85
- @dtw += appeared
86
- boot!
87
-
88
- @updated.make_true
89
- end
90
- end
91
-
92
- @updated.true?
58
+ @core.updated.true?
93
59
  end
94
60
 
95
61
  def execute
96
- @updated.make_false
62
+ @core.updated.make_false
97
63
  @block.call
98
64
  end
99
65
 
@@ -105,17 +71,68 @@ module ActiveSupport
105
71
  end
106
72
  end
107
73
 
108
- private
109
- def boot!
110
- normalize_dirs!
74
+ class Core
75
+ attr_reader :updated, :files
76
+
77
+ def initialize(files, dirs)
78
+ @files = files.map { |file| Pathname(file).expand_path }.to_set
111
79
 
112
- unless @dtw.empty?
113
- Listen.to(*@dtw, &method(:changed)).start
80
+ @dirs = dirs.each_with_object({}) do |(dir, exts), hash|
81
+ hash[Pathname(dir).expand_path] = Array(exts).map { |ext| ext.to_s.sub(/\A\.?/, ".") }.to_set
114
82
  end
83
+
84
+ @common_path = common_path(@dirs.keys)
85
+
86
+ @dtw = directories_to_watch
87
+ @missing = []
88
+
89
+ @updated = Concurrent::AtomicBoolean.new(false)
90
+ @mutex = Mutex.new
91
+
92
+ start
93
+ # inotify / FSEvents file descriptors are inherited on fork, so
94
+ # we need to reopen them otherwise only the parent or the child
95
+ # will be notified.
96
+ # FIXME: this callback is keeping a reference on the instance
97
+ @after_fork = ActiveSupport::ForkTracker.after_fork { start }
98
+ end
99
+
100
+ def finalizer
101
+ proc do
102
+ stop
103
+ ActiveSupport::ForkTracker.unregister(@after_fork)
104
+ end
105
+ end
106
+
107
+ def thread_safely
108
+ @mutex.synchronize do
109
+ yield self
110
+ end
111
+ end
112
+
113
+ def start
114
+ normalize_dirs!
115
+ @dtw, @missing = [*@dtw, *@missing].partition(&:exist?)
116
+ @listener = @dtw.any? ? Listen.to(*@dtw, &method(:changed)) : nil
117
+ @listener&.start
118
+
119
+ # Wait for the listener to be ready to avoid race conditions
120
+ # Unfortunately this isn't quite enough on macOS because the Darwin backend
121
+ # has an extra private thread we can't wait on.
122
+ @listener&.wait_for_state(:processing_events)
123
+ end
124
+
125
+ def stop
126
+ @listener&.stop
127
+ end
128
+
129
+ def restart
130
+ stop
131
+ start
115
132
  end
116
133
 
117
- def shutdown!
118
- Listen.stop
134
+ def restart?
135
+ @missing.any?(&:exist?)
119
136
  end
120
137
 
121
138
  def normalize_dirs!
@@ -125,27 +142,27 @@ module ActiveSupport
125
142
  end
126
143
 
127
144
  def changed(modified, added, removed)
128
- unless updated?
145
+ unless @updated.true?
129
146
  @updated.make_true if (modified + added + removed).any? { |f| watching?(f) }
130
147
  end
131
148
  end
132
149
 
133
150
  def watching?(file)
134
- file = @ph.xpath(file)
151
+ file = Pathname(file)
135
152
 
136
153
  if @files.member?(file)
137
154
  true
138
155
  elsif file.directory?
139
156
  false
140
157
  else
141
- ext = @ph.normalize_extension(file.extname)
158
+ ext = file.extname
142
159
 
143
160
  file.dirname.ascend do |dir|
144
161
  matching = @dirs[dir]
145
162
 
146
163
  if matching && (matching.empty? || matching.include?(ext))
147
164
  break true
148
- elsif dir == @lcsp || dir.root?
165
+ elsif dir == @common_path || dir.root?
149
166
  break false
150
167
  end
151
168
  end
@@ -153,82 +170,14 @@ module ActiveSupport
153
170
  end
154
171
 
155
172
  def directories_to_watch
156
- dtw = @files.map(&:dirname) + @dirs.keys
157
- dtw.compact!
158
- dtw.uniq!
159
-
160
- normalized_gem_paths = Gem.path.map { |path| File.join path, "" }
161
- dtw = dtw.reject do |path|
162
- normalized_gem_paths.any? { |gem_path| path.to_s.start_with?(gem_path) }
163
- end
164
-
165
- @ph.filter_out_descendants(dtw)
173
+ dtw = @dirs.keys | @files.map(&:dirname)
174
+ accounted_for = dtw.to_set + Gem.path.map { |path| Pathname(path) }
175
+ dtw.reject { |dir| dir.ascend.drop(1).any? { |parent| accounted_for.include?(parent) } }
166
176
  end
167
177
 
168
- class PathHelper
169
- def xpath(path)
170
- Pathname.new(path).expand_path
171
- end
172
-
173
- def normalize_extension(ext)
174
- ext.to_s.sub(/\A\./, "")
175
- end
176
-
177
- # Given a collection of Pathname objects returns the longest subpath
178
- # common to all of them, or +nil+ if there is none.
179
- def longest_common_subpath(paths)
180
- return if paths.empty?
181
-
182
- lcsp = Pathname.new(paths[0])
183
-
184
- paths[1..-1].each do |path|
185
- until ascendant_of?(lcsp, path)
186
- if lcsp.root?
187
- # If we get here a root directory is not an ascendant of path.
188
- # This may happen if there are paths in different drives on
189
- # Windows.
190
- return
191
- else
192
- lcsp = lcsp.parent
193
- end
194
- end
195
- end
196
-
197
- lcsp
198
- end
199
-
200
- # Returns the deepest existing ascendant, which could be the argument itself.
201
- def existing_parent(dir)
202
- dir.ascend do |ascendant|
203
- break ascendant if ascendant.directory?
204
- end
205
- end
206
-
207
- # Filters out directories which are descendants of others in the collection (stable).
208
- def filter_out_descendants(dirs)
209
- return dirs if dirs.length < 2
210
-
211
- dirs_sorted_by_nparts = dirs.sort_by { |dir| dir.each_filename.to_a.length }
212
- descendants = []
213
-
214
- until dirs_sorted_by_nparts.empty?
215
- dir = dirs_sorted_by_nparts.shift
216
-
217
- dirs_sorted_by_nparts.reject! do |possible_descendant|
218
- ascendant_of?(dir, possible_descendant) && descendants << possible_descendant
219
- end
220
- end
221
-
222
- # Array#- preserves order.
223
- dirs - descendants
224
- end
225
-
226
- private
227
- def ascendant_of?(base, other)
228
- base != other && other.ascend do |ascendant|
229
- break true if base == ascendant
230
- end
231
- end
178
+ def common_path(paths)
179
+ paths.map { |path| path.ascend.to_a }.reduce(&:&)&.first
232
180
  end
181
+ end
233
182
  end
234
183
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveSupport::ExecutionContext::TestHelper # :nodoc:
4
+ def before_setup
5
+ ActiveSupport::ExecutionContext.clear
6
+ super
7
+ end
8
+
9
+ def after_teardown
10
+ super
11
+ ActiveSupport::ExecutionContext.clear
12
+ end
13
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveSupport
4
+ module ExecutionContext # :nodoc:
5
+ @after_change_callbacks = []
6
+ class << self
7
+ def after_change(&block)
8
+ @after_change_callbacks << block
9
+ end
10
+
11
+ # Updates the execution context. If a block is given, it resets the provided keys to their
12
+ # previous value once the block exits.
13
+ def set(**options)
14
+ options.symbolize_keys!
15
+ keys = options.keys
16
+
17
+ store = self.store
18
+
19
+ previous_context = keys.zip(store.values_at(*keys)).to_h
20
+
21
+ store.merge!(options)
22
+ @after_change_callbacks.each(&:call)
23
+
24
+ if block_given?
25
+ begin
26
+ yield
27
+ ensure
28
+ store.merge!(previous_context)
29
+ @after_change_callbacks.each(&:call)
30
+ end
31
+ end
32
+ end
33
+
34
+ def []=(key, value)
35
+ store[key.to_sym] = value
36
+ @after_change_callbacks.each(&:call)
37
+ end
38
+
39
+ def to_h
40
+ store.dup
41
+ end
42
+
43
+ def clear
44
+ store.clear
45
+ end
46
+
47
+ private
48
+ def store
49
+ IsolatedExecutionState[:active_support_execution_context] ||= {}
50
+ end
51
+ end
52
+ end
53
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/error_reporter"
3
4
  require "active_support/callbacks"
4
5
  require "concurrent/hash"
5
6
 
@@ -65,7 +66,7 @@ module ActiveSupport
65
66
  # Where possible, prefer +wrap+.
66
67
  def self.run!(reset: false)
67
68
  if reset
68
- lost_instance = active.delete(Thread.current)
69
+ lost_instance = IsolatedExecutionState.delete(active_key)
69
70
  lost_instance&.complete!
70
71
  else
71
72
  return Null if active?
@@ -83,34 +84,48 @@ module ActiveSupport
83
84
  end
84
85
 
85
86
  # Perform the work in the supplied block as an execution.
86
- def self.wrap
87
+ def self.wrap(source: "application.active_support")
87
88
  return yield if active?
88
89
 
89
90
  instance = run!
90
91
  begin
91
92
  yield
93
+ rescue => error
94
+ error_reporter&.report(error, handled: false, source: source)
95
+ raise
92
96
  ensure
93
97
  instance.complete!
94
98
  end
95
99
  end
96
100
 
97
- class << self # :nodoc:
98
- attr_accessor :active
101
+ def self.perform # :nodoc:
102
+ instance = new
103
+ instance.run
104
+ begin
105
+ yield
106
+ ensure
107
+ instance.complete
108
+ end
99
109
  end
100
110
 
101
- def self.inherited(other) # :nodoc:
102
- super
103
- other.active = Concurrent::Hash.new
111
+ def self.error_reporter # :nodoc:
112
+ ActiveSupport.error_reporter
104
113
  end
105
114
 
106
- self.active = Concurrent::Hash.new
115
+ def self.active_key # :nodoc:
116
+ @active_key ||= :"active_execution_wrapper_#{object_id}"
117
+ end
107
118
 
108
119
  def self.active? # :nodoc:
109
- @active.key?(Thread.current)
120
+ IsolatedExecutionState.key?(active_key)
110
121
  end
111
122
 
112
123
  def run! # :nodoc:
113
- self.class.active[Thread.current] = self
124
+ IsolatedExecutionState[self.class.active_key] = self
125
+ run
126
+ end
127
+
128
+ def run # :nodoc:
114
129
  run_callbacks(:run)
115
130
  end
116
131
 
@@ -119,9 +134,13 @@ module ActiveSupport
119
134
  #
120
135
  # Where possible, prefer +wrap+.
121
136
  def complete!
122
- run_callbacks(:complete)
137
+ complete
123
138
  ensure
124
- self.class.active.delete Thread.current
139
+ IsolatedExecutionState.delete(self.class.active_key)
140
+ end
141
+
142
+ def complete # :nodoc:
143
+ run_callbacks(:complete)
125
144
  end
126
145
 
127
146
  private
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveSupport::Executor::TestHelper # :nodoc:
4
+ def run(...)
5
+ Rails.application.executor.perform { super }
6
+ end
7
+ end
@@ -3,7 +3,9 @@
3
3
  require "active_support/core_ext/time/calculations"
4
4
 
5
5
  module ActiveSupport
6
- # FileUpdateChecker specifies the API used by Rails to watch files
6
+ # = \File Update Checker
7
+ #
8
+ # FileUpdateChecker specifies the API used by \Rails to watch files
7
9
  # and control reloading. The API depends on four methods:
8
10
  #
9
11
  # * +initialize+ which expects two parameters and one block as
@@ -20,7 +22,7 @@ module ActiveSupport
20
22
  # After initialization, a call to +execute_if_updated+ must execute
21
23
  # the block only if there was really a change in the filesystem.
22
24
  #
23
- # This class is used by Rails to reload the I18n framework whenever
25
+ # This class is used by \Rails to reload the I18n framework whenever
24
26
  # they are changed upon a new request.
25
27
  #
26
28
  # i18n_reloader = ActiveSupport::FileUpdateChecker.new(paths) do