Skip to content

Commit c148ad4

Browse files
committed
Deprecate usage of cursors not safe to serialize
It is unsafe to use cursors which cannot be serialized using JSON.dump and deserialized again using JSON.parse. Such cursors may result in a different cursor being deserialized, or in serialization failing entirely, which may result in incorrect behaviour in iterative jobs. Starting in 2.0, usage of such cursors will raise an ArgumentError. Until then, a deprecation warning will be logged.
1 parent 350910b commit c148ad4

File tree

4 files changed

+60
-60
lines changed

4 files changed

+60
-60
lines changed

lib/job-iteration.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ module JobIteration
99

1010
INTEGRATIONS = [:resque, :sidekiq]
1111

12+
Deprecation = ActiveSupport::Deprecation.new("2.0", "JobIteration")
13+
1214
extend self
1315

1416
# Use this to _always_ interrupt the job after it's been running for more than N seconds.

lib/job-iteration/iteration.rb

Lines changed: 6 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,6 @@ module JobIteration
66
module Iteration
77
extend ActiveSupport::Concern
88

9-
class CursorError < ArgumentError
10-
attr_reader :cursor
11-
12-
def initialize(message, cursor:)
13-
super(message)
14-
@cursor = cursor
15-
end
16-
17-
def message
18-
"#{super} (#{inspected_cursor})"
19-
end
20-
21-
private
22-
23-
def inspected_cursor
24-
cursor.inspect
25-
rescue NoMethodError
26-
# For those brave enough to try to use BasicObject as cursor. Nice try.
27-
Object.instance_method(:inspect).bind(cursor).call
28-
end
29-
end
30-
319
included do |_base|
3210
attr_accessor(
3311
:cursor_position,
@@ -142,8 +120,7 @@ def iterate_with_enumerator(enumerator, arguments)
142120
arguments = arguments.dup.freeze
143121
found_record = false
144122
enumerator.each do |object_from_enumerator, index|
145-
# Deferred until 2.0.0
146-
# assert_valid_cursor!(index)
123+
assert_valid_cursor!(index)
147124

148125
record_unit_of_work do
149126
found_record = true
@@ -208,11 +185,11 @@ def build_enumerator(params, cursor:)
208185
def assert_valid_cursor!(cursor)
209186
return if serializable?(cursor)
210187

211-
raise CursorError.new(
212-
"Cursor must be composed of objects capable of built-in (de)serialization: " \
213-
"Strings, Integers, Floats, Arrays, Hashes, true, false, or nil.",
214-
cursor: cursor,
215-
)
188+
Deprecation.warn(<<~DEPRECATION_MESSAGE)
189+
The Enumerator returned by #{self.class.name}#build_enumerator yielded a cursor which is unsafe to serialize.
190+
Cursors must be composed of objects capable of built-in (de)serialization: Strings, Integers, Floats, Arrays, Hashes, true, false, or nil.
191+
This will raise starting in version #{Deprecation.deprecation_horizon} of #{Deprecation.gem_name}!"
192+
DEPRECATION_MESSAGE
216193
end
217194

218195
def assert_implements_methods!

test/integration/integration_behaviour.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ module IntegrationBehaviour
3535
end
3636

3737
test "unserializable corruption is prevented" do
38-
skip "Deferred until 2.0.0"
38+
skip "Breaking change deferred until 2.0" if Gem::Version.new(JobIteration::VERSION) < Gem::Version.new("2.0")
3939
# Cursors are serialized as JSON, but not all objects are serializable.
4040
# time = Time.at(0).utc # => 1970-01-01 00:00:00 UTC
4141
# json = JSON.dump(time) # => "\"1970-01-01 00:00:00 UTC\""

test/unit/iteration_test.rb

Lines changed: 51 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ def each_iteration(*)
6666
class InvalidCursorJob < ActiveJob::Base
6767
include JobIteration::Iteration
6868
def each_iteration(*)
69+
return if Gem::Version.new(JobIteration::VERSION) < Gem::Version.new("2.0")
6970
raise "Cursor invalid. This should never run!"
7071
end
7172
end
@@ -199,41 +200,37 @@ def foo
199200
assert_includes(methods_added, :foo)
200201
end
201202

202-
def test_jobs_using_time_cursor_will_raise
203-
skip("Deferred until 2.0.0")
203+
def test_jobs_using_time_cursor_is_deprecated
204204
push(JobWithTimeCursor)
205-
assert_raises_cursor_error { work_one_job }
205+
assert_cursor_deprecation_warning { work_one_job }
206206
end
207207

208-
def test_jobs_using_active_record_cursor_will_raise
209-
skip("Deferred until 2.0.0")
208+
def test_jobs_using_active_record_cursor_is_deprecated
210209
refute_nil(Product.first)
211210
push(JobWithActiveRecordCursor)
212-
assert_raises_cursor_error { work_one_job }
211+
assert_cursor_deprecation_warning { work_one_job }
213212
end
214213

215-
def test_jobs_using_symbol_cursor_will_raise
216-
skip("Deferred until 2.0.0")
214+
def test_jobs_using_symbol_cursor_is_deprecated
217215
push(JobWithSymbolCursor)
218-
assert_raises_cursor_error { work_one_job }
216+
assert_cursor_deprecation_warning { work_one_job }
219217
end
220218

221-
def test_jobs_using_string_subclass_cursor_will_raise
222-
skip("Deferred until 2.0.0")
219+
def test_jobs_using_string_subclass_cursor_is_deprecated
223220
push(JobWithStringSubclassCursor)
224-
assert_raises_cursor_error { work_one_job }
221+
assert_cursor_deprecation_warning { work_one_job }
225222
end
226223

227-
def test_jobs_using_basic_object_cursor_will_raise
228-
skip("Deferred until 2.0.0")
224+
def test_jobs_using_basic_object_cursor_is_deprecated
229225
push(JobWithBasicObjectCursor)
230-
assert_raises_cursor_error { work_one_job }
226+
assert_cursor_deprecation_warning { work_one_job }
231227
end
232228

233-
def test_jobs_using_complex_but_serializable_cursor_will_not_raise
234-
skip("Deferred until 2.0.0")
229+
def test_jobs_using_complex_but_serializable_cursor_is_not_deprecated
235230
push(JobWithComplexCursor)
236-
work_one_job
231+
assert_no_cursor_deprecation_warning do
232+
work_one_job
233+
end
237234
end
238235

239236
def test_jobs_using_on_complete_have_accurate_total_time
@@ -244,21 +241,45 @@ def test_jobs_using_on_complete_have_accurate_total_time
244241

245242
private
246243

247-
def assert_raises_cursor_error(&block)
248-
error = assert_raises(JobIteration::Iteration::CursorError, &block)
249-
inspected_cursor = begin
250-
error.cursor.inspect
251-
rescue NoMethodError
252-
Object.instance_method(:inspect).bind(error.cursor).call
253-
end
254-
assert_equal(
255-
"Cursor must be composed of objects capable of built-in (de)serialization: " \
256-
"Strings, Integers, Floats, Arrays, Hashes, true, false, or nil. " \
257-
"(#{inspected_cursor})",
258-
error.message,
244+
def assert_cursor_deprecation_warning(&block)
245+
job_class = ActiveJob::Base.queue_adapter.enqueued_jobs.first.fetch("job_class")
246+
expected_message = <<~MESSAGE.chomp
247+
DEPRECATION WARNING: The Enumerator returned by #{job_class}#build_enumerator yielded a cursor which is unsafe to serialize.
248+
Cursors must be composed of objects capable of built-in (de)serialization: Strings, Integers, Floats, Arrays, Hashes, true, false, or nil.
249+
This will raise starting in version #{JobIteration::Deprecation.deprecation_horizon} of #{JobIteration::Deprecation.gem_name}!
250+
MESSAGE
251+
252+
warned = false
253+
with_deprecation_behavior(
254+
lambda do |message, *|
255+
flunk("expected only one deprecation warning") if warned
256+
warned = true
257+
assert(
258+
message.start_with?(expected_message),
259+
"expected deprecation warning \n#{message.inspect}\n to start_with? \n#{expected_message.inspect}",
260+
)
261+
end,
262+
&block
263+
)
264+
265+
assert(warned, "expected deprecation warning")
266+
end
267+
268+
def assert_no_cursor_deprecation_warning(&block)
269+
with_deprecation_behavior(
270+
-> (message, *) { flunk("Expected no deprecation warning: #{message}") },
271+
&block
259272
)
260273
end
261274

275+
def with_deprecation_behavior(behavior)
276+
original_behaviour = JobIteration::Deprecation.behavior
277+
JobIteration::Deprecation.behavior = behavior
278+
yield
279+
ensure
280+
JobIteration::Deprecation.behavior = original_behaviour
281+
end
282+
262283
def push(job, *args)
263284
job.perform_later(*args)
264285
end

0 commit comments

Comments
 (0)