activejob 8.1.0.beta1 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c9a034077d88450ca67c9f9921d2b9678d506724adeb387742be2be30b30b038
4
- data.tar.gz: 6efdff593dfb0f7d0773a6a8419c084919346d1dc15c44619e58f5009886ad3b
3
+ metadata.gz: bc270930c258d16e62b3f01ee4ed3591e91b680ddea4c43ddba1870206bda41f
4
+ data.tar.gz: 6bc1d66fb46babd36a3959754021ddbc6d115fbeef7e78d025b4880e48c63db1
5
5
  SHA512:
6
- metadata.gz: 7a6be4dfbb96daba9f49b695a5aa568420a446cb303bf3a9ba6083a1d3d4c3bb7663fb428107a2f0b3f954a2ba4b4c5781dad015f4d56b97201096d75ab4bf35
7
- data.tar.gz: 4b01822ccd36668f128a6754b8ad9ea06d7874b2f3584c7fe44e54948df92552c6e93609ab7f2a688a20c23116078b0a84711ac1c851308657e438f0892ceb32
6
+ metadata.gz: aed3444d15f38a03c9df4321d898958c8c4611c427dd4ad6ada5d14804db2dced96aa035d98e3d777c0280d1c7c0433a1888cb930ff55692882eb1dbecfb729e
7
+ data.tar.gz: 47396127fb20f53dc5ec782619913145fa911128b04ab3e8f80dc52d4b3071634653f49c6271f86a1fe1ccf58595d1dd6a7fc0453d37eb2a3a9a250fa601ded8
data/CHANGELOG.md CHANGED
@@ -1,4 +1,20 @@
1
- ## Rails 8.1.0.beta1 (September 04, 2025) ##
1
+ ## Rails 8.1.0 (October 22, 2025) ##
2
+
3
+ * Add structured events for Active Job:
4
+ - `active_job.enqueued`
5
+ - `active_job.bulk_enqueued`
6
+ - `active_job.started`
7
+ - `active_job.completed`
8
+ - `active_job.retry_scheduled`
9
+ - `active_job.retry_stopped`
10
+ - `active_job.discarded`
11
+ - `active_job.interrupt`
12
+ - `active_job.resume`
13
+ - `active_job.step_skipped`
14
+ - `active_job.step_started`
15
+ - `active_job.step`
16
+
17
+ *Adrianna Chang*
2
18
 
3
19
  * Deprecate built-in `sidekiq` adapter.
4
20
 
@@ -31,7 +31,11 @@ module ActiveJob
31
31
  # serialized without mutation are returned as-is. Arrays/Hashes are
32
32
  # serialized element by element. All other types are serialized using
33
33
  # GlobalID.
34
- def serialize(argument)
34
+ def serialize(arguments)
35
+ arguments.map { |argument| serialize_argument(argument) }
36
+ end
37
+
38
+ def serialize_argument(argument) # :nodoc:
35
39
  case argument
36
40
  when nil, true, false, Integer, Float # Types that can hardly be subclassed
37
41
  argument
@@ -50,7 +54,7 @@ module ActiveJob
50
54
  when GlobalID::Identification
51
55
  convert_to_global_id_hash(argument)
52
56
  when Array
53
- argument.map { |arg| serialize(arg) }
57
+ argument.map { |arg| serialize_argument(arg) }
54
58
  when ActiveSupport::HashWithIndifferentAccess
55
59
  serialize_indifferent_hash(argument)
56
60
  when Hash
@@ -137,7 +141,7 @@ module ActiveJob
137
141
 
138
142
  def serialize_hash(argument)
139
143
  argument.each_with_object({}) do |(key, value), hash|
140
- hash[serialize_hash_key(key)] = serialize(value)
144
+ hash[serialize_hash_key(key)] = serialize_argument(value)
141
145
  end
142
146
  end
143
147
 
@@ -9,6 +9,7 @@ require "active_job/execution"
9
9
  require "active_job/callbacks"
10
10
  require "active_job/exceptions"
11
11
  require "active_job/log_subscriber"
12
+ require "active_job/structured_event_subscriber"
12
13
  require "active_job/logging"
13
14
  require "active_job/instrumentation"
14
15
  require "active_job/execution_state"
@@ -167,8 +167,8 @@ module ActiveJob
167
167
  self.exception_executions = job_data["exception_executions"]
168
168
  self.locale = job_data["locale"] || I18n.locale.to_s
169
169
  self.timezone = job_data["timezone"] || Time.zone&.name
170
- self.enqueued_at = Time.iso8601(job_data["enqueued_at"]) if job_data["enqueued_at"]
171
- self.scheduled_at = Time.iso8601(job_data["scheduled_at"]) if job_data["scheduled_at"]
170
+ self.enqueued_at = deserialize_time(job_data["enqueued_at"]) if job_data["enqueued_at"]
171
+ self.scheduled_at = deserialize_time(job_data["scheduled_at"]) if job_data["scheduled_at"]
172
172
  end
173
173
 
174
174
  # Configures the job with the given options.
@@ -208,5 +208,13 @@ module ActiveJob
208
208
  def arguments_serialized?
209
209
  @serialized_arguments
210
210
  end
211
+
212
+ def deserialize_time(time)
213
+ if time.is_a?(Time)
214
+ time
215
+ else
216
+ Time.iso8601(time)
217
+ end
218
+ end
211
219
  end
212
220
  end
@@ -10,7 +10,7 @@ module ActiveJob
10
10
  MAJOR = 8
11
11
  MINOR = 1
12
12
  TINY = 0
13
- PRE = "beta1"
13
+ PRE = nil
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -4,7 +4,7 @@ module ActiveJob
4
4
  module Serializers
5
5
  class ActionControllerParametersSerializer < ObjectSerializer
6
6
  def serialize(argument)
7
- Arguments.serialize(argument.to_h.with_indifferent_access)
7
+ Arguments.serialize_argument(argument.to_h.with_indifferent_access)
8
8
  end
9
9
 
10
10
  def deserialize(hash)
@@ -28,6 +28,11 @@ module ActiveJob
28
28
  delegate :serialize?, :serialize, :deserialize, to: :instance
29
29
  end
30
30
 
31
+ def initialize
32
+ super
33
+ @template = { Arguments::OBJECT_SERIALIZER_KEY => self.class.name }.freeze
34
+ end
35
+
31
36
  # Determines if an argument should be serialized by a serializer.
32
37
  def serialize?(argument)
33
38
  argument.is_a?(klass)
@@ -35,19 +40,13 @@ module ActiveJob
35
40
 
36
41
  # Serializes an argument to a JSON primitive type.
37
42
  def serialize(hash)
38
- hash[Arguments::OBJECT_SERIALIZER_KEY] = self.class.name
39
- hash
43
+ @template.merge(hash)
40
44
  end
41
45
 
42
46
  # Deserializes an argument from a JSON primitive type.
43
47
  def deserialize(hash)
44
48
  raise NotImplementedError, "#{self.class.name} should implement a public #deserialize(hash) method"
45
49
  end
46
-
47
- # The class of the object that will be serialized.
48
- def klass
49
- raise NotImplementedError, "#{self.class.name} should implement a public #klass method"
50
- end
51
50
  end
52
51
  end
53
52
  end
@@ -5,8 +5,8 @@ module ActiveJob
5
5
  class RangeSerializer < ObjectSerializer
6
6
  def serialize(range)
7
7
  super(
8
- "begin" => Arguments.serialize(range.begin),
9
- "end" => Arguments.serialize(range.end),
8
+ "begin" => Arguments.serialize_argument(range.begin),
9
+ "end" => Arguments.serialize_argument(range.end),
10
10
  "exclude_end" => range.exclude_end?, # Always boolean, no need to serialize
11
11
  )
12
12
  end
@@ -81,8 +81,16 @@ module ActiveJob
81
81
  klass = s.send(:klass)
82
82
  ActiveJob.deprecator.warn(<<~MSG.squish)
83
83
  #{s.class.name}#klass method should be public.
84
+ This will raise an error in Rails 8.2.
84
85
  MSG
85
86
  @serializers_index[klass] = s
87
+ else
88
+ ActiveJob.deprecator.warn(
89
+ <<~MSG.squish
90
+ #{s.class.name} should implement a public #klass method.
91
+ This will raise an error in Rails 8.2.
92
+ MSG
93
+ )
86
94
  end
87
95
  end
88
96
  end
@@ -0,0 +1,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/structured_event_subscriber"
4
+
5
+ module ActiveJob
6
+ class StructuredEventSubscriber < ActiveSupport::StructuredEventSubscriber # :nodoc:
7
+ def enqueue(event)
8
+ job = event.payload[:job]
9
+ adapter = event.payload[:adapter]
10
+ exception = event.payload[:exception_object] || job.enqueue_error
11
+ payload = {
12
+ job_class: job.class.name,
13
+ job_id: job.job_id,
14
+ queue: job.queue_name,
15
+ adapter: ActiveJob.adapter_name(adapter),
16
+ aborted: event.payload[:aborted],
17
+ }
18
+
19
+ if exception
20
+ payload[:exception_class] = exception.class.name
21
+ payload[:exception_message] = exception.message
22
+ end
23
+
24
+ if job.class.log_arguments?
25
+ payload[:arguments] = job.arguments
26
+ end
27
+
28
+ emit_event("active_job.enqueued", payload)
29
+ end
30
+
31
+ def enqueue_at(event)
32
+ job = event.payload[:job]
33
+ adapter = event.payload[:adapter]
34
+ exception = event.payload[:exception_object] || job.enqueue_error
35
+ payload = {
36
+ job_class: job.class.name,
37
+ job_id: job.job_id,
38
+ queue: job.queue_name,
39
+ scheduled_at: job.scheduled_at,
40
+ adapter: ActiveJob.adapter_name(adapter),
41
+ aborted: event.payload[:aborted],
42
+ }
43
+
44
+ if exception
45
+ payload[:exception_class] = exception.class.name
46
+ payload[:exception_message] = exception.message
47
+ end
48
+
49
+ if job.class.log_arguments?
50
+ payload[:arguments] = job.arguments
51
+ end
52
+
53
+ emit_event("active_job.enqueued_at", payload)
54
+ end
55
+
56
+ def enqueue_all(event)
57
+ jobs = event.payload[:jobs]
58
+ adapter = event.payload[:adapter]
59
+ enqueued_count = event.payload[:enqueued_count].to_i
60
+ failed_count = jobs.size - enqueued_count
61
+
62
+ emit_event("active_job.bulk_enqueued",
63
+ adapter: ActiveJob.adapter_name(adapter),
64
+ job_count: jobs.size,
65
+ enqueued_count: enqueued_count,
66
+ failed_enqueue_count: failed_count,
67
+ enqueued_classes: jobs.filter_map do |job|
68
+ job.class.name if jobs.count == enqueued_count || job.successfully_enqueued?
69
+ end.tally
70
+ )
71
+ end
72
+
73
+ def perform_start(event)
74
+ job = event.payload[:job]
75
+ payload = {
76
+ job_class: job.class.name,
77
+ job_id: job.job_id,
78
+ queue: job.queue_name,
79
+ enqueued_at: job.enqueued_at&.utc&.iso8601(9),
80
+ }
81
+ if job.class.log_arguments?
82
+ payload[:arguments] = job.arguments
83
+ end
84
+ emit_event("active_job.started", payload)
85
+ end
86
+
87
+ def perform(event)
88
+ job = event.payload[:job]
89
+ exception = event.payload[:exception_object]
90
+ adapter = event.payload[:adapter]
91
+ payload = {
92
+ job_class: job.class.name,
93
+ job_id: job.job_id,
94
+ queue: job.queue_name,
95
+ adapter: ActiveJob.adapter_name(adapter),
96
+ aborted: event.payload[:aborted],
97
+ duration: event.duration.round(2),
98
+ }
99
+
100
+ if exception
101
+ payload[:exception_class] = exception.class.name
102
+ payload[:exception_message] = exception.message
103
+ payload[:exception_backtrace] = exception.backtrace
104
+ end
105
+
106
+ emit_event("active_job.completed", payload)
107
+ end
108
+
109
+ def enqueue_retry(event)
110
+ job = event.payload[:job]
111
+ exception = event.payload[:error]
112
+ wait = event.payload[:wait]
113
+
114
+ emit_event("active_job.retry_scheduled",
115
+ job_class: job.class.name,
116
+ job_id: job.job_id,
117
+ executions: job.executions,
118
+ wait_seconds: wait.to_i,
119
+ exception_class: exception&.class&.name,
120
+ exception_message: exception&.message
121
+ )
122
+ end
123
+
124
+ def retry_stopped(event)
125
+ job = event.payload[:job]
126
+ exception = event.payload[:error]
127
+
128
+ emit_event("active_job.retry_stopped",
129
+ job_class: job.class.name,
130
+ job_id: job.job_id,
131
+ executions: job.executions,
132
+ exception_class: exception.class.name,
133
+ exception_message: exception.message
134
+ )
135
+ end
136
+
137
+ def discard(event)
138
+ job = event.payload[:job]
139
+ exception = event.payload[:error]
140
+
141
+ emit_event("active_job.discarded",
142
+ job_class: job.class.name,
143
+ job_id: job.job_id,
144
+ exception_class: exception.class.name,
145
+ exception_message: exception.message
146
+ )
147
+ end
148
+
149
+ def interrupt(event)
150
+ job = event.payload[:job]
151
+ description = event.payload[:description]
152
+ reason = event.payload[:reason]
153
+
154
+ emit_event("active_job.interrupt",
155
+ job_class: job.class.name,
156
+ job_id: job.job_id,
157
+ description: description,
158
+ reason: reason,
159
+ )
160
+ end
161
+
162
+ def resume(event)
163
+ job = event.payload[:job]
164
+ description = event.payload[:description]
165
+
166
+ emit_event("active_job.resume",
167
+ job_class: job.class.name,
168
+ job_id: job.job_id,
169
+ description: description,
170
+ )
171
+ end
172
+
173
+ def step_skipped(event)
174
+ job = event.payload[:job]
175
+ step = event.payload[:step]
176
+
177
+ emit_event("active_job.step_skipped",
178
+ job_class: job.class.name,
179
+ job_id: job.job_id,
180
+ step: step.name,
181
+ )
182
+ end
183
+
184
+ def step_started(event)
185
+ job = event.payload[:job]
186
+ step = event.payload[:step]
187
+
188
+ emit_event("active_job.step_started",
189
+ job_class: job.class.name,
190
+ job_id: job.job_id,
191
+ step: step.name,
192
+ cursor: step.cursor,
193
+ resumed: step.resumed?,
194
+ )
195
+ end
196
+
197
+ def step(event)
198
+ job = event.payload[:job]
199
+ step = event.payload[:step]
200
+ exception = event.payload[:exception_object]
201
+ payload = {
202
+ job_class: job.class.name,
203
+ job_id: job.job_id,
204
+ step: step.name,
205
+ cursor: step.cursor,
206
+ interrupted: event.payload[:interrupted],
207
+ duration: event.duration.round(2),
208
+ }
209
+
210
+ if exception
211
+ payload[:exception_class] = exception.class.name
212
+ payload[:exception_message] = exception.message
213
+ end
214
+
215
+ emit_event("active_job.step", payload)
216
+ end
217
+ end
218
+ end
219
+
220
+ ActiveJob::StructuredEventSubscriber.attach_to :active_job
data/lib/active_job.rb CHANGED
@@ -43,6 +43,7 @@ module ActiveJob
43
43
  autoload :EnqueueAfterTransactionCommit
44
44
 
45
45
  eager_autoload do
46
+ autoload :Continuable
46
47
  autoload :Continuation
47
48
  autoload :Serializers
48
49
  autoload :ConfiguredJob
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activejob
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.1.0.beta1
4
+ version: 8.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Heinemeier Hansson
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 8.1.0.beta1
18
+ version: 8.1.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 8.1.0.beta1
25
+ version: 8.1.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: globalid
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -95,6 +95,7 @@ files:
95
95
  - lib/active_job/serializers/time_object_serializer.rb
96
96
  - lib/active_job/serializers/time_serializer.rb
97
97
  - lib/active_job/serializers/time_with_zone_serializer.rb
98
+ - lib/active_job/structured_event_subscriber.rb
98
99
  - lib/active_job/test_case.rb
99
100
  - lib/active_job/test_helper.rb
100
101
  - lib/active_job/version.rb
@@ -107,10 +108,10 @@ licenses:
107
108
  - MIT
108
109
  metadata:
109
110
  bug_tracker_uri: https://github.com/rails/rails/issues
110
- changelog_uri: https://github.com/rails/rails/blob/v8.1.0.beta1/activejob/CHANGELOG.md
111
- documentation_uri: https://api.rubyonrails.org/v8.1.0.beta1/
111
+ changelog_uri: https://github.com/rails/rails/blob/v8.1.0/activejob/CHANGELOG.md
112
+ documentation_uri: https://api.rubyonrails.org/v8.1.0/
112
113
  mailing_list_uri: https://discuss.rubyonrails.org/c/rubyonrails-talk
113
- source_code_uri: https://github.com/rails/rails/tree/v8.1.0.beta1/activejob
114
+ source_code_uri: https://github.com/rails/rails/tree/v8.1.0/activejob
114
115
  rubygems_mfa_required: 'true'
115
116
  rdoc_options: []
116
117
  require_paths: