activejob 8.1.0.beta1 → 8.1.0.rc1

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: e7bf90a71d8700a26979dae457c18b43b8e226311c121bd3b7273e3cecc7d018
4
+ data.tar.gz: 9e615f4c6ade58c5fa0a76f2fdb72f6f4130875d23534905edc72c1879c75172
5
5
  SHA512:
6
- metadata.gz: 7a6be4dfbb96daba9f49b695a5aa568420a446cb303bf3a9ba6083a1d3d4c3bb7663fb428107a2f0b3f954a2ba4b4c5781dad015f4d56b97201096d75ab4bf35
7
- data.tar.gz: 4b01822ccd36668f128a6754b8ad9ea06d7874b2f3584c7fe44e54948df92552c6e93609ab7f2a688a20c23116078b0a84711ac1c851308657e438f0892ceb32
6
+ metadata.gz: a638175d1777f10b818bfa2ff7129228ec0980bad070a5d3ef3a5d1a138cb3b6d4039e8ec88b19772904a1603463e8e4492f490074cbdd7a9f94cd83805c49aa
7
+ data.tar.gz: 3ae7b32f544997dd1070a16ab1922c8bdaa536088430ce3b2e8206a6a6945d44825a6aeab76eb45c2d4a11b26317205cba543abbe19062dd38ff79e2da46ef9a
data/CHANGELOG.md CHANGED
@@ -1,3 +1,21 @@
1
+ ## Rails 8.1.0.rc1 (October 15, 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*
18
+
1
19
  ## Rails 8.1.0.beta1 (September 04, 2025) ##
2
20
 
3
21
  * Deprecate built-in `sidekiq` adapter.
@@ -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 = "rc1"
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,216 @@
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 { |job| job.class.name }.tally
68
+ )
69
+ end
70
+
71
+ def perform_start(event)
72
+ job = event.payload[:job]
73
+ payload = {
74
+ job_class: job.class.name,
75
+ job_id: job.job_id,
76
+ queue: job.queue_name,
77
+ enqueued_at: job.enqueued_at&.utc&.iso8601(9),
78
+ }
79
+ if job.class.log_arguments?
80
+ payload[:arguments] = job.arguments
81
+ end
82
+ emit_event("active_job.started", payload)
83
+ end
84
+
85
+ def perform(event)
86
+ job = event.payload[:job]
87
+ exception = event.payload[:exception_object]
88
+ payload = {
89
+ job_class: job.class.name,
90
+ job_id: job.job_id,
91
+ queue: job.queue_name,
92
+ aborted: event.payload[:aborted],
93
+ duration: event.duration.round(2),
94
+ }
95
+
96
+ if exception
97
+ payload[:exception_class] = exception.class.name
98
+ payload[:exception_message] = exception.message
99
+ payload[:exception_backtrace] = exception.backtrace
100
+ end
101
+
102
+ emit_event("active_job.completed", payload)
103
+ end
104
+
105
+ def enqueue_retry(event)
106
+ job = event.payload[:job]
107
+ exception = event.payload[:error]
108
+ wait = event.payload[:wait]
109
+
110
+ emit_event("active_job.retry_scheduled",
111
+ job_class: job.class.name,
112
+ job_id: job.job_id,
113
+ executions: job.executions,
114
+ wait_seconds: wait.to_i,
115
+ exception_class: exception&.class&.name,
116
+ exception_message: exception&.message
117
+ )
118
+ end
119
+
120
+ def retry_stopped(event)
121
+ job = event.payload[:job]
122
+ exception = event.payload[:error]
123
+
124
+ emit_event("active_job.retry_stopped",
125
+ job_class: job.class.name,
126
+ job_id: job.job_id,
127
+ executions: job.executions,
128
+ exception_class: exception.class.name,
129
+ exception_message: exception.message
130
+ )
131
+ end
132
+
133
+ def discard(event)
134
+ job = event.payload[:job]
135
+ exception = event.payload[:error]
136
+
137
+ emit_event("active_job.discarded",
138
+ job_class: job.class.name,
139
+ job_id: job.job_id,
140
+ exception_class: exception.class.name,
141
+ exception_message: exception.message
142
+ )
143
+ end
144
+
145
+ def interrupt(event)
146
+ job = event.payload[:job]
147
+ description = event.payload[:description]
148
+ reason = event.payload[:reason]
149
+
150
+ emit_event("active_job.interrupt",
151
+ job_class: job.class.name,
152
+ job_id: job.job_id,
153
+ description: description,
154
+ reason: reason,
155
+ )
156
+ end
157
+
158
+ def resume(event)
159
+ job = event.payload[:job]
160
+ description = event.payload[:description]
161
+
162
+ emit_event("active_job.resume",
163
+ job_class: job.class.name,
164
+ job_id: job.job_id,
165
+ description: description,
166
+ )
167
+ end
168
+
169
+ def step_skipped(event)
170
+ job = event.payload[:job]
171
+ step = event.payload[:step]
172
+
173
+ emit_event("active_job.step_skipped",
174
+ job_class: job.class.name,
175
+ job_id: job.job_id,
176
+ step: step.name,
177
+ )
178
+ end
179
+
180
+ def step_started(event)
181
+ job = event.payload[:job]
182
+ step = event.payload[:step]
183
+
184
+ emit_event("active_job.step_started",
185
+ job_class: job.class.name,
186
+ job_id: job.job_id,
187
+ step: step.name,
188
+ cursor: step.cursor,
189
+ resumed: step.resumed?,
190
+ )
191
+ end
192
+
193
+ def step(event)
194
+ job = event.payload[:job]
195
+ step = event.payload[:step]
196
+ exception = event.payload[:exception_object]
197
+ payload = {
198
+ job_class: job.class.name,
199
+ job_id: job.job_id,
200
+ step: step.name,
201
+ cursor: step.cursor,
202
+ interrupted: event.payload[:interrupted],
203
+ duration: event.duration.round(2),
204
+ }
205
+
206
+ if exception
207
+ payload[:exception_class] = exception.class.name
208
+ payload[:exception_message] = exception.message
209
+ end
210
+
211
+ emit_event("active_job.step", payload)
212
+ end
213
+ end
214
+ end
215
+
216
+ 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.rc1
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.rc1
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.rc1
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.rc1/activejob/CHANGELOG.md
112
+ documentation_uri: https://api.rubyonrails.org/v8.1.0.rc1/
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.rc1/activejob
114
115
  rubygems_mfa_required: 'true'
115
116
  rdoc_options: []
116
117
  require_paths: