activesupport 8.0.2.1 → 8.1.0.beta1

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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +247 -136
  3. data/README.rdoc +1 -1
  4. data/lib/active_support/backtrace_cleaner.rb +71 -0
  5. data/lib/active_support/broadcast_logger.rb +46 -59
  6. data/lib/active_support/cache/mem_cache_store.rb +25 -27
  7. data/lib/active_support/cache/redis_cache_store.rb +36 -30
  8. data/lib/active_support/cache/strategy/local_cache.rb +16 -7
  9. data/lib/active_support/cache/strategy/local_cache_middleware.rb +7 -7
  10. data/lib/active_support/cache.rb +70 -6
  11. data/lib/active_support/configurable.rb +28 -0
  12. data/lib/active_support/continuous_integration.rb +145 -0
  13. data/lib/active_support/core_ext/date_and_time/compatibility.rb +1 -1
  14. data/lib/active_support/core_ext/date_time/conversions.rb +4 -2
  15. data/lib/active_support/core_ext/enumerable.rb +16 -4
  16. data/lib/active_support/core_ext/erb/util.rb +3 -3
  17. data/lib/active_support/core_ext/object/json.rb +8 -1
  18. data/lib/active_support/core_ext/object/to_query.rb +7 -1
  19. data/lib/active_support/core_ext/object/try.rb +2 -2
  20. data/lib/active_support/core_ext/range/overlap.rb +3 -3
  21. data/lib/active_support/core_ext/range/sole.rb +17 -0
  22. data/lib/active_support/core_ext/range.rb +1 -1
  23. data/lib/active_support/core_ext/string/filters.rb +3 -3
  24. data/lib/active_support/core_ext/string/multibyte.rb +12 -3
  25. data/lib/active_support/core_ext/string/output_safety.rb +19 -12
  26. data/lib/active_support/current_attributes/test_helper.rb +2 -2
  27. data/lib/active_support/current_attributes.rb +26 -16
  28. data/lib/active_support/deprecation/reporting.rb +4 -2
  29. data/lib/active_support/deprecation.rb +1 -1
  30. data/lib/active_support/editor.rb +70 -0
  31. data/lib/active_support/error_reporter.rb +50 -6
  32. data/lib/active_support/event_reporter/test_helper.rb +32 -0
  33. data/lib/active_support/event_reporter.rb +570 -0
  34. data/lib/active_support/evented_file_update_checker.rb +5 -1
  35. data/lib/active_support/execution_context.rb +64 -7
  36. data/lib/active_support/file_update_checker.rb +7 -5
  37. data/lib/active_support/gem_version.rb +3 -3
  38. data/lib/active_support/gzip.rb +1 -0
  39. data/lib/active_support/hash_with_indifferent_access.rb +47 -24
  40. data/lib/active_support/i18n_railtie.rb +1 -2
  41. data/lib/active_support/inflector/inflections.rb +31 -15
  42. data/lib/active_support/inflector/transliterate.rb +6 -8
  43. data/lib/active_support/isolated_execution_state.rb +7 -13
  44. data/lib/active_support/json/decoding.rb +6 -4
  45. data/lib/active_support/json/encoding.rb +103 -14
  46. data/lib/active_support/lazy_load_hooks.rb +1 -1
  47. data/lib/active_support/log_subscriber.rb +2 -0
  48. data/lib/active_support/logger_thread_safe_level.rb +6 -3
  49. data/lib/active_support/message_encryptors.rb +52 -0
  50. data/lib/active_support/message_pack/extensions.rb +5 -0
  51. data/lib/active_support/message_verifiers.rb +52 -0
  52. data/lib/active_support/messages/rotation_coordinator.rb +9 -0
  53. data/lib/active_support/messages/rotator.rb +5 -0
  54. data/lib/active_support/multibyte/chars.rb +8 -1
  55. data/lib/active_support/multibyte.rb +4 -0
  56. data/lib/active_support/railtie.rb +26 -12
  57. data/lib/active_support/syntax_error_proxy.rb +3 -0
  58. data/lib/active_support/test_case.rb +61 -6
  59. data/lib/active_support/testing/assertions.rb +34 -6
  60. data/lib/active_support/testing/error_reporter_assertions.rb +18 -1
  61. data/lib/active_support/testing/event_reporter_assertions.rb +217 -0
  62. data/lib/active_support/testing/notification_assertions.rb +92 -0
  63. data/lib/active_support/testing/parallelization/worker.rb +2 -0
  64. data/lib/active_support/testing/parallelization.rb +13 -0
  65. data/lib/active_support/testing/tests_without_assertions.rb +1 -1
  66. data/lib/active_support/testing/time_helpers.rb +7 -3
  67. data/lib/active_support/time_with_zone.rb +19 -5
  68. data/lib/active_support/values/time_zone.rb +8 -1
  69. data/lib/active_support/xml_mini.rb +1 -2
  70. data/lib/active_support.rb +11 -0
  71. metadata +11 -5
  72. data/lib/active_support/core_ext/range/each.rb +0 -24
@@ -0,0 +1,570 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveSupport
4
+ class TagStack # :nodoc:
5
+ EMPTY_TAGS = {}.freeze
6
+ FIBER_KEY = :event_reporter_tags
7
+
8
+ class << self
9
+ def tags
10
+ Fiber[FIBER_KEY] || EMPTY_TAGS
11
+ end
12
+
13
+ def with_tags(*args, **kwargs)
14
+ existing_tags = tags
15
+ tags = existing_tags.dup
16
+ tags.merge!(resolve_tags(args, kwargs))
17
+ new_tags = tags.freeze
18
+
19
+ begin
20
+ Fiber[FIBER_KEY] = new_tags
21
+ yield
22
+ ensure
23
+ Fiber[FIBER_KEY] = existing_tags
24
+ end
25
+ end
26
+
27
+ private
28
+ def resolve_tags(args, kwargs)
29
+ tags = args.each_with_object({}) do |arg, tags|
30
+ case arg
31
+ when String
32
+ tags[arg.to_sym] = true
33
+ when Symbol
34
+ tags[arg] = true
35
+ when Hash
36
+ arg.each { |key, value| tags[key.to_sym] = value }
37
+ else
38
+ tags[arg.class.name.to_sym] = arg
39
+ end
40
+ end
41
+ kwargs.each { |key, value| tags[key.to_sym] = value }
42
+ tags
43
+ end
44
+ end
45
+ end
46
+
47
+ class EventContext # :nodoc:
48
+ EMPTY_CONTEXT = {}.freeze
49
+ FIBER_KEY = :event_reporter_context
50
+
51
+ class << self
52
+ def context
53
+ Fiber[FIBER_KEY] || EMPTY_CONTEXT
54
+ end
55
+
56
+ def set_context(context_hash)
57
+ new_context = self.context.dup
58
+ context_hash.each { |key, value| new_context[key.to_sym] = value }
59
+
60
+ Fiber[FIBER_KEY] = new_context.freeze
61
+ end
62
+
63
+ def clear
64
+ Fiber[FIBER_KEY] = EMPTY_CONTEXT
65
+ end
66
+ end
67
+ end
68
+
69
+ # = Active Support \Event Reporter
70
+ #
71
+ # +ActiveSupport::EventReporter+ provides an interface for reporting structured events to subscribers.
72
+ #
73
+ # To report an event, you can use the +notify+ method:
74
+ #
75
+ # Rails.event.notify("user_created", { id: 123 })
76
+ # # Emits event:
77
+ # # {
78
+ # # name: "user_created",
79
+ # # payload: { id: 123 },
80
+ # # timestamp: 1738964843208679035,
81
+ # # source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
82
+ # # }
83
+ #
84
+ # The +notify+ API can receive either an event name and a payload hash, or an event object. Names are coerced to strings.
85
+ #
86
+ # === Event Objects
87
+ #
88
+ # If an event object is passed to the +notify+ API, it will be passed through to subscribers as-is, and the name of the
89
+ # object's class will be used as the event name.
90
+ #
91
+ # class UserCreatedEvent
92
+ # def initialize(id:, name:)
93
+ # @id = id
94
+ # @name = name
95
+ # end
96
+ #
97
+ # def serialize
98
+ # {
99
+ # id: @id,
100
+ # name: @name
101
+ # }
102
+ # end
103
+ # end
104
+ #
105
+ # Rails.event.notify(UserCreatedEvent.new(id: 123, name: "John Doe"))
106
+ # # Emits event:
107
+ # # {
108
+ # # name: "UserCreatedEvent",
109
+ # # payload: #<UserCreatedEvent:0x111>,
110
+ # # timestamp: 1738964843208679035,
111
+ # # source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
112
+ # # }
113
+ #
114
+ # An event is any Ruby object representing a schematized event. While payload hashes allow arbitrary,
115
+ # implicitly-structured data, event objects are intended to enforce a particular schema.
116
+ #
117
+ # Subscribers are responsible for serializing event objects.
118
+ #
119
+ # === Subscribers
120
+ #
121
+ # Subscribers must implement the +emit+ method, which will be called with the event hash.
122
+ #
123
+ # The event hash has the following keys:
124
+ #
125
+ # name: String (The name of the event)
126
+ # payload: Hash, Object (The payload of the event, or the event object itself)
127
+ # tags: Hash (The tags of the event)
128
+ # context: Hash (The context of the event)
129
+ # timestamp: Float (The timestamp of the event, in nanoseconds)
130
+ # source_location: Hash (The source location of the event, containing the filepath, lineno, and label)
131
+ #
132
+ # Subscribers are responsible for encoding events to their desired format before emitting them to their
133
+ # target destination, such as a streaming platform, a log device, or an alerting service.
134
+ #
135
+ # class JSONEventSubscriber
136
+ # def emit(event)
137
+ # json_data = JSON.generate(event)
138
+ # LogExporter.export(json_data)
139
+ # end
140
+ # end
141
+ #
142
+ # class LogSubscriber
143
+ # def emit(event)
144
+ # payload = event[:payload].map { |key, value| "#{key}=#{value}" }.join(" ")
145
+ # source_location = event[:source_location]
146
+ # log = "[#{event[:name]}] #{payload} at #{source_location[:filepath]}:#{source_location[:lineno]}"
147
+ # Rails.logger.info(log)
148
+ # end
149
+ # end
150
+ #
151
+ # Note that event objects are passed through to subscribers as-is, and may need to be serialized before being encoded:
152
+ #
153
+ # class UserCreatedEvent
154
+ # def initialize(id:, name:)
155
+ # @id = id
156
+ # @name = name
157
+ # end
158
+ #
159
+ # def serialize
160
+ # {
161
+ # id: @id,
162
+ # name: @name
163
+ # }
164
+ # end
165
+ # end
166
+ #
167
+ # class LogSubscriber
168
+ # def emit(event)
169
+ # payload = event[:payload]
170
+ # json_data = JSON.generate(payload.serialize)
171
+ # LogExporter.export(json_data)
172
+ # end
173
+ # end
174
+ #
175
+ # ==== Filtered Subscriptions
176
+ #
177
+ # Subscribers can be configured with an optional filter proc to only receive a subset of events:
178
+ #
179
+ # # Only receive events with names starting with "user."
180
+ # Rails.event.subscribe(user_subscriber) { |event| event[:name].start_with?("user.") }
181
+ #
182
+ # # Only receive events with specific payload types
183
+ # Rails.event.subscribe(audit_subscriber) { |event| event[:payload].is_a?(AuditEvent) }
184
+ #
185
+ # === Debug Events
186
+ #
187
+ # You can use the +debug+ method to report an event that will only be reported if the
188
+ # event reporter is in debug mode:
189
+ #
190
+ # Rails.event.debug("my_debug_event", { foo: "bar" })
191
+ #
192
+ # === Tags
193
+ #
194
+ # To add additional context to an event, separate from the event payload, you can add
195
+ # tags via the +tagged+ method:
196
+ #
197
+ # Rails.event.tagged("graphql") do
198
+ # Rails.event.notify("user_created", { id: 123 })
199
+ # end
200
+ #
201
+ # # Emits event:
202
+ # # {
203
+ # # name: "user_created",
204
+ # # payload: { id: 123 },
205
+ # # tags: { graphql: true },
206
+ # # context: {},
207
+ # # timestamp: 1738964843208679035,
208
+ # # source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
209
+ # # }
210
+ #
211
+ # === Context Store
212
+ #
213
+ # You may want to attach metadata to every event emitted by the reporter. While tags
214
+ # provide domain-specific context for a series of events, context is scoped to the job / request
215
+ # and should be used for metadata associated with the execution context.
216
+ # Context can be set via the +set_context+ method:
217
+ #
218
+ # Rails.event.set_context(request_id: "abcd123", user_agent: "TestAgent")
219
+ # Rails.event.notify("user_created", { id: 123 })
220
+ #
221
+ # # Emits event:
222
+ # # {
223
+ # # name: "user_created",
224
+ # # payload: { id: 123 },
225
+ # # tags: {},
226
+ # # context: { request_id: "abcd123", user_agent: TestAgent" },
227
+ # # timestamp: 1738964843208679035,
228
+ # # source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
229
+ # # }
230
+ #
231
+ # Context is reset automatically before and after each request.
232
+ #
233
+ # A custom context store can be configured via +config.active_support.event_reporter_context_store+.
234
+ #
235
+ # # config/application.rb
236
+ # config.active_support.event_reporter_context_store = CustomContextStore
237
+ #
238
+ # class CustomContextStore
239
+ # class << self
240
+ # def context
241
+ # # Return the context.
242
+ # end
243
+ #
244
+ # def set_context(context_hash)
245
+ # # Append context_hash to the existing context store.
246
+ # end
247
+ #
248
+ # def clear
249
+ # # Delete the stored context.
250
+ # end
251
+ # end
252
+ # end
253
+ #
254
+ # The Event Reporter standardizes on symbol keys for all payload data, tags, and context store entries.
255
+ # String keys are automatically converted to symbols for consistency.
256
+ #
257
+ # Rails.event.notify("user.created", { "id" => 123 })
258
+ # # Emits event:
259
+ # # {
260
+ # # name: "user.created",
261
+ # # payload: { id: 123 },
262
+ # # }
263
+ class EventReporter
264
+ # Sets whether to raise an error if a subscriber raises an error during
265
+ # event emission, or when unexpected arguments are passed to +notify+.
266
+ attr_writer :raise_on_error
267
+
268
+ attr_writer :debug_mode # :nodoc:
269
+
270
+ class << self
271
+ attr_accessor :context_store # :nodoc:
272
+ end
273
+
274
+ self.context_store = EventContext
275
+
276
+ def initialize(*subscribers, raise_on_error: false)
277
+ @subscribers = []
278
+ subscribers.each { |subscriber| subscribe(subscriber) }
279
+ @debug_mode = false
280
+ @raise_on_error = raise_on_error
281
+ end
282
+
283
+ # Registers a new event subscriber. The subscriber must respond to
284
+ #
285
+ # emit(event: Hash)
286
+ #
287
+ # The event hash will have the following keys:
288
+ #
289
+ # name: String (The name of the event)
290
+ # payload: Hash, Object (The payload of the event, or the event object itself)
291
+ # tags: Hash (The tags of the event)
292
+ # context: Hash (The context of the event)
293
+ # timestamp: Float (The timestamp of the event, in nanoseconds)
294
+ # source_location: Hash (The source location of the event, containing the filepath, lineno, and label)
295
+ #
296
+ # An optional filter proc can be provided to only receive a subset of events:
297
+ #
298
+ # Rails.event.subscribe(subscriber) { |event| event[:name].start_with?("user.") }
299
+ # Rails.event.subscribe(subscriber) { |event| event[:payload].is_a?(UserEvent) }
300
+ #
301
+ def subscribe(subscriber, &filter)
302
+ unless subscriber.respond_to?(:emit)
303
+ raise ArgumentError, "Event subscriber #{subscriber.class.name} must respond to #emit"
304
+ end
305
+ @subscribers << { subscriber: subscriber, filter: filter }
306
+ end
307
+
308
+ # Unregister an event subscriber. Accepts either a subscriber or a class.
309
+ #
310
+ # subscriber = MyEventSubscriber.new
311
+ # Rails.event.subscribe(subscriber)
312
+ #
313
+ # Rails.event.unsubscribe(subscriber)
314
+ # # or
315
+ # Rails.event.unsubscribe(MyEventSubscriber)
316
+ def unsubscribe(subscriber)
317
+ @subscribers.delete_if { |s| subscriber === s[:subscriber] }
318
+ end
319
+
320
+ # Reports an event to all registered subscribers. An event name and payload can be provided:
321
+ #
322
+ # Rails.event.notify("user.created", { id: 123 })
323
+ # # Emits event:
324
+ # # {
325
+ # # name: "user.created",
326
+ # # payload: { id: 123 },
327
+ # # tags: {},
328
+ # # context: {},
329
+ # # timestamp: 1738964843208679035,
330
+ # # source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
331
+ # # }
332
+ #
333
+ # Alternatively, an event object can be provided:
334
+ #
335
+ # Rails.event.notify(UserCreatedEvent.new(id: 123))
336
+ # # Emits event:
337
+ # # {
338
+ # # name: "UserCreatedEvent",
339
+ # # payload: #<UserCreatedEvent:0x111>,
340
+ # # tags: {},
341
+ # # context: {},
342
+ # # timestamp: 1738964843208679035,
343
+ # # source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
344
+ # # }
345
+ #
346
+ # ==== Arguments
347
+ #
348
+ # * +:payload+ - The event payload when using string/symbol event names.
349
+ #
350
+ # * +:caller_depth+ - The stack depth to use for source location (default: 1).
351
+ #
352
+ # * +:kwargs+ - Additional payload data when using string/symbol event names.
353
+ def notify(name_or_object, payload = nil, caller_depth: 1, **kwargs)
354
+ name = resolve_name(name_or_object)
355
+ payload = resolve_payload(name_or_object, payload, **kwargs)
356
+
357
+ event = {
358
+ name: name,
359
+ payload: payload,
360
+ tags: TagStack.tags,
361
+ context: context_store.context,
362
+ timestamp: Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond),
363
+ }
364
+
365
+ caller_location = caller_locations(caller_depth, 1)&.first
366
+
367
+ if caller_location
368
+ source_location = {
369
+ filepath: caller_location.path,
370
+ lineno: caller_location.lineno,
371
+ label: caller_location.label,
372
+ }
373
+ event[:source_location] = source_location
374
+ end
375
+
376
+ @subscribers.each do |subscriber_entry|
377
+ subscriber = subscriber_entry[:subscriber]
378
+ filter = subscriber_entry[:filter]
379
+
380
+ next if filter && !filter.call(event)
381
+
382
+ subscriber.emit(event)
383
+ rescue => subscriber_error
384
+ if raise_on_error?
385
+ raise
386
+ else
387
+ ActiveSupport.error_reporter.report(subscriber_error, handled: true)
388
+ end
389
+ end
390
+
391
+ nil
392
+ end
393
+
394
+ # Temporarily enables debug mode for the duration of the block.
395
+ # Calls to +debug+ will only be reported if debug mode is enabled.
396
+ #
397
+ # Rails.event.with_debug do
398
+ # Rails.event.debug("sql.query", { sql: "SELECT * FROM users" })
399
+ # end
400
+ def with_debug
401
+ prior = Fiber[:event_reporter_debug_mode]
402
+ Fiber[:event_reporter_debug_mode] = true
403
+ yield
404
+ ensure
405
+ Fiber[:event_reporter_debug_mode] = prior
406
+ end
407
+
408
+ # Check if debug mode is currently enabled. Debug mode is enabled on the reporter
409
+ # via +with_debug+, and in local environments.
410
+ def debug_mode?
411
+ @debug_mode || Fiber[:event_reporter_debug_mode]
412
+ end
413
+
414
+ # Report an event only when in debug mode. For example:
415
+ #
416
+ # Rails.event.debug("sql.query", { sql: "SELECT * FROM users" })
417
+ #
418
+ # ==== Arguments
419
+ #
420
+ # * +:payload+ - The event payload when using string/symbol event names.
421
+ #
422
+ # * +:caller_depth+ - The stack depth to use for source location (default: 1).
423
+ #
424
+ # * +:kwargs+ - Additional payload data when using string/symbol event names.
425
+ def debug(name_or_object, payload = nil, caller_depth: 1, **kwargs)
426
+ if debug_mode?
427
+ if block_given?
428
+ notify(name_or_object, payload, caller_depth: caller_depth + 1, **kwargs.merge(yield))
429
+ else
430
+ notify(name_or_object, payload, caller_depth: caller_depth + 1, **kwargs)
431
+ end
432
+ end
433
+ end
434
+
435
+ # Add tags to events to supply additional context. Tags operate in a stack-oriented manner,
436
+ # so all events emitted within the block inherit the same set of tags. For example:
437
+ #
438
+ # Rails.event.tagged("graphql") do
439
+ # Rails.event.notify("user.created", { id: 123 })
440
+ # end
441
+ #
442
+ # # Emits event:
443
+ # # {
444
+ # # name: "user.created",
445
+ # # payload: { id: 123 },
446
+ # # tags: { graphql: true },
447
+ # # context: {},
448
+ # # timestamp: 1738964843208679035,
449
+ # # source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
450
+ # # }
451
+ #
452
+ # Tags can be provided as arguments or as keyword arguments, and can be nested:
453
+ #
454
+ # Rails.event.tagged("graphql") do
455
+ # # Other code here...
456
+ # Rails.event.tagged(section: "admin") do
457
+ # Rails.event.notify("user.created", { id: 123 })
458
+ # end
459
+ # end
460
+ #
461
+ # # Emits event:
462
+ # # {
463
+ # # name: "user.created",
464
+ # # payload: { id: 123 },
465
+ # # tags: { section: "admin", graphql: true },
466
+ # # context: {},
467
+ # # timestamp: 1738964843208679035,
468
+ # # source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
469
+ # # }
470
+ #
471
+ # The +tagged+ API can also receive a tag object:
472
+ #
473
+ # graphql_tag = GraphqlTag.new(operation_name: "user_created", operation_type: "mutation")
474
+ # Rails.event.tagged(graphql_tag) do
475
+ # Rails.event.notify("user.created", { id: 123 })
476
+ # end
477
+ #
478
+ # # Emits event:
479
+ # # {
480
+ # # name: "user.created",
481
+ # # payload: { id: 123 },
482
+ # # tags: { "GraphqlTag": #<GraphqlTag:0x111> },
483
+ # # context: {},
484
+ # # timestamp: 1738964843208679035,
485
+ # # source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
486
+ # # }
487
+ def tagged(*args, **kwargs, &block)
488
+ TagStack.with_tags(*args, **kwargs, &block)
489
+ end
490
+
491
+ # Sets context data that will be included with all events emitted by the reporter.
492
+ # Context data should be scoped to the job or request, and is reset automatically
493
+ # before and after each request and job.
494
+ #
495
+ # Rails.event.set_context(user_agent: "TestAgent")
496
+ # Rails.event.set_context(job_id: "abc123")
497
+ # Rails.event.tagged("graphql") do
498
+ # Rails.event.notify("user_created", { id: 123 })
499
+ # end
500
+ #
501
+ # # Emits event:
502
+ # # {
503
+ # # name: "user_created",
504
+ # # payload: { id: 123 },
505
+ # # tags: { graphql: true },
506
+ # # context: { user_agent: "TestAgent", job_id: "abc123" },
507
+ # # timestamp: 1738964843208679035
508
+ # # source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
509
+ # # }
510
+ def set_context(context)
511
+ context_store.set_context(context)
512
+ end
513
+
514
+ # Clears all context data.
515
+ def clear_context
516
+ context_store.clear
517
+ end
518
+
519
+ # Returns the current context data.
520
+ def context
521
+ context_store.context
522
+ end
523
+
524
+ private
525
+ def raise_on_error?
526
+ @raise_on_error
527
+ end
528
+
529
+ def context_store
530
+ self.class.context_store
531
+ end
532
+
533
+ def resolve_name(name_or_object)
534
+ case name_or_object
535
+ when String, Symbol
536
+ name_or_object.to_s
537
+ else
538
+ name_or_object.class.name
539
+ end
540
+ end
541
+
542
+ def resolve_payload(name_or_object, payload, **kwargs)
543
+ case name_or_object
544
+ when String, Symbol
545
+ handle_unexpected_args(name_or_object, payload, kwargs) if payload && kwargs.any?
546
+ if kwargs.any?
547
+ kwargs.transform_keys(&:to_sym)
548
+ elsif payload
549
+ payload.transform_keys(&:to_sym)
550
+ end
551
+ else
552
+ handle_unexpected_args(name_or_object, payload, kwargs) if payload || kwargs.any?
553
+ name_or_object
554
+ end
555
+ end
556
+
557
+ def handle_unexpected_args(name_or_object, payload, kwargs)
558
+ message = <<~MESSAGE
559
+ Rails.event.notify accepts either an event object, a payload hash, or keyword arguments.
560
+ Received: #{name_or_object.inspect}, #{payload.inspect}, #{kwargs.inspect}
561
+ MESSAGE
562
+
563
+ if raise_on_error?
564
+ raise ArgumentError, message
565
+ else
566
+ ActiveSupport.error_reporter.report(ArgumentError.new(message), handled: true)
567
+ end
568
+ end
569
+ end
570
+ end
@@ -73,9 +73,13 @@ module ActiveSupport
73
73
  attr_reader :updated, :files
74
74
 
75
75
  def initialize(files, dirs)
76
- @files = files.map { |file| Pathname(file).expand_path }.to_set
76
+ gem_paths = Gem.path
77
+ files = files.map { |f| Pathname(f).expand_path }
78
+ files.reject! { |f| f.to_s.start_with?(*gem_paths) }
79
+ @files = files.to_set
77
80
 
78
81
  @dirs = dirs.each_with_object({}) do |(dir, exts), hash|
82
+ next if dir.start_with?(*gem_paths)
79
83
  hash[Pathname(dir).expand_path] = Array(exts).map { |ext| ext.to_s.sub(/\A\.?/, ".") }.to_set
80
84
  end
81
85
 
@@ -2,8 +2,41 @@
2
2
 
3
3
  module ActiveSupport
4
4
  module ExecutionContext # :nodoc:
5
+ class Record
6
+ attr_reader :store, :current_attributes_instances
7
+
8
+ def initialize
9
+ @store = {}
10
+ @current_attributes_instances = {}
11
+ @stack = []
12
+ end
13
+
14
+ def push
15
+ @stack << @store << @current_attributes_instances
16
+ @store = {}
17
+ @current_attributes_instances = {}
18
+ self
19
+ end
20
+
21
+ def pop
22
+ @current_attributes_instances = @stack.pop
23
+ @store = @stack.pop
24
+ self
25
+ end
26
+ end
27
+
5
28
  @after_change_callbacks = []
29
+
30
+ # Execution context nesting should only legitimately happen during test
31
+ # because the test case itself is wrapped in an executor, and it might call
32
+ # into a controller or job which should be executed with their own fresh context.
33
+ # However in production this should never happen, and for extra safety we make sure to
34
+ # fully clear the state at the end of the request or job cycle.
35
+ @nestable = false
36
+
6
37
  class << self
38
+ attr_accessor :nestable
39
+
7
40
  def after_change(&block)
8
41
  @after_change_callbacks << block
9
42
  end
@@ -14,9 +47,11 @@ module ActiveSupport
14
47
  options.symbolize_keys!
15
48
  keys = options.keys
16
49
 
17
- store = self.store
50
+ store = record.store
18
51
 
19
- previous_context = keys.zip(store.values_at(*keys)).to_h
52
+ previous_context = if block_given?
53
+ keys.zip(store.values_at(*keys)).to_h
54
+ end
20
55
 
21
56
  store.merge!(options)
22
57
  @after_change_callbacks.each(&:call)
@@ -32,21 +67,43 @@ module ActiveSupport
32
67
  end
33
68
 
34
69
  def []=(key, value)
35
- store[key.to_sym] = value
70
+ record.store[key.to_sym] = value
36
71
  @after_change_callbacks.each(&:call)
37
72
  end
38
73
 
39
74
  def to_h
40
- store.dup
75
+ record.store.dup
76
+ end
77
+
78
+ def push
79
+ if @nestable
80
+ record.push
81
+ else
82
+ clear
83
+ end
84
+ self
85
+ end
86
+
87
+ def pop
88
+ if @nestable
89
+ record.pop
90
+ else
91
+ clear
92
+ end
93
+ self
41
94
  end
42
95
 
43
96
  def clear
44
- store.clear
97
+ IsolatedExecutionState[:active_support_execution_context] = nil
98
+ end
99
+
100
+ def current_attributes_instances
101
+ record.current_attributes_instances
45
102
  end
46
103
 
47
104
  private
48
- def store
49
- IsolatedExecutionState[:active_support_execution_context] ||= {}
105
+ def record
106
+ IsolatedExecutionState[:active_support_execution_context] ||= Record.new
50
107
  end
51
108
  end
52
109
  end