Skip to content

Commit 7f9a996

Browse files
bdewaterplasticine
andcommitted
Infer interruption handler from a job's queue adapter
...and allow Iteration to be used with multiple job backends simultaneously Removed test_mark_job_worker_as_interrupted since it was testing stubs Co-authored-by: Justin Morris <[email protected]>
1 parent a8422fa commit 7f9a996

File tree

12 files changed

+99
-109
lines changed

12 files changed

+99
-109
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- [310](https://github.com/Shopify/job-iteration/pull/310) - Support nested iteration
88
- [338](https://github.com/Shopify/job-iteration/pull/338) - All logs are now `ActiveSupport::Notifications` events and logged using `ActiveSupport::LogSubscriber` to allow customization. Events now always include the `cursor_position` tag.
99
- [341](https://github.com/Shopify/job-iteration/pull/341) - Add `JobIteration.default_retry_backoff`, which sets a default delay when jobs are re-enqueued after being interrupted. Defaults to `nil`, meaning no delay, which matches the current behaviour.
10+
- [367](https://github.com/Shopify/job-iteration/pull/367) - Interruption adapter is now inferred from the job's queue adapter instead of `JobIteration.interruption_adapter` (which has been removed). This allows using Iteration with multiple job backends simultaneously.
1011

1112
## v1.3.6 (Mar 9, 2022)
1213

lib/job-iteration.rb

Lines changed: 1 addition & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,12 @@
22

33
require "active_job"
44
require_relative "./job-iteration/version"
5+
require_relative "./job-iteration/integrations"
56
require_relative "./job-iteration/enumerator_builder"
67
require_relative "./job-iteration/iteration"
78
require_relative "./job-iteration/log_subscriber"
89

910
module JobIteration
10-
IntegrationLoadError = Class.new(StandardError)
11-
12-
INTEGRATIONS = [:resque, :sidekiq]
13-
1411
extend self
1512

1613
attr_accessor :logger
@@ -45,11 +42,6 @@ module JobIteration
4542
# where the throttle backoff value will take precedence over this setting.
4643
attr_accessor :default_retry_backoff
4744

48-
# Used internally for hooking into job processing frameworks like Sidekiq and Resque.
49-
attr_accessor :interruption_adapter
50-
51-
self.interruption_adapter = -> { false }
52-
5345
# Set if you want to use your own enumerator builder instead of default EnumeratorBuilder.
5446
# @example
5547
#
@@ -61,29 +53,4 @@ module JobIteration
6153
attr_accessor :enumerator_builder
6254

6355
self.enumerator_builder = JobIteration::EnumeratorBuilder
64-
65-
def load_integrations
66-
loaded = nil
67-
INTEGRATIONS.each do |integration|
68-
load_integration(integration)
69-
if loaded
70-
raise IntegrationLoadError,
71-
"#{loaded} integration has already been loaded, but #{integration} is also available. " \
72-
"Iteration will only work with one integration."
73-
end
74-
loaded = integration
75-
rescue LoadError
76-
end
77-
end
78-
79-
def load_integration(integration)
80-
unless INTEGRATIONS.include?(integration)
81-
raise IntegrationLoadError,
82-
"#{integration} integration is not supported. Available integrations: #{INTEGRATIONS.join(", ")}"
83-
end
84-
85-
require_relative "./job-iteration/integrations/#{integration}"
86-
end
8756
end
88-
89-
JobIteration.load_integrations unless ENV["ITERATION_DISABLE_AUTOCONFIGURE"]

lib/job-iteration/integrations.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# frozen_string_literal: true
2+
3+
module JobIteration
4+
module Integrations # @private
5+
IntegrationLoadError = Class.new(StandardError)
6+
7+
autoload :Fallback, "job-iteration/integrations/fallback"
8+
autoload :Sidekiq, "job-iteration/integrations/sidekiq"
9+
autoload :Resque, "job-iteration/integrations/resque"
10+
11+
extend self
12+
13+
def lookup(queue_adapter)
14+
const_get(queue_adapter.to_s.camelize)
15+
rescue NameError
16+
Fallback
17+
end
18+
end
19+
end
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# frozen_string_literal: true
2+
3+
module JobIteration
4+
module Integrations
5+
module Fallback
6+
extend self
7+
8+
def call
9+
false
10+
end
11+
end
12+
end
13+
end

lib/job-iteration/integrations/resque.rb

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,22 @@
44

55
module JobIteration
66
module Integrations
7-
module ResqueIterationExtension # @private
8-
def initialize(*) # @private
9-
$resque_worker = self
10-
super
7+
module Resque
8+
module IterationExtension
9+
def initialize(*)
10+
$resque_worker = self
11+
super
12+
end
1113
end
12-
end
1314

14-
# @private
15-
module ::Resque
16-
class Worker
17-
# The patch is required in order to call shutdown? on a Resque::Worker instance
18-
prepend(ResqueIterationExtension)
15+
# The patch is required in order to call shutdown? on a Resque::Worker instance
16+
::Resque::Worker.prepend(IterationExtension)
17+
18+
extend self
19+
20+
def call
21+
$resque_worker.try!(:shutdown?)
1922
end
2023
end
21-
22-
JobIteration.interruption_adapter = -> { $resque_worker.try!(:shutdown?) }
2324
end
2425
end

lib/job-iteration/integrations/sidekiq.rb

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@
33
require "sidekiq"
44

55
module JobIteration
6-
module Integrations # @private
7-
JobIteration.interruption_adapter = -> do
8-
if defined?(Sidekiq::CLI) && Sidekiq::CLI.instance
9-
Sidekiq::CLI.instance.launcher.stopping?
10-
else
11-
false
6+
module Integrations
7+
module Sidekiq
8+
extend self
9+
10+
def call
11+
if defined?(::Sidekiq::CLI) && (instance = ::Sidekiq::CLI.instance)
12+
instance.launcher.stopping?
13+
else
14+
false
15+
end
1216
end
1317
end
1418
end

lib/job-iteration/iteration.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ def retry_job(*, **)
134134

135135
private
136136

137+
def interruption_adapter
138+
@interruption_adapter ||= JobIteration::Integrations.lookup(self.class.queue_adapter_name)
139+
end
140+
137141
def enumerator_builder
138142
JobIteration.enumerator_builder.new(self)
139143
end
@@ -293,7 +297,7 @@ def job_should_exit?
293297
return true
294298
end
295299

296-
JobIteration.interruption_adapter.call || (defined?(super) && super)
300+
interruption_adapter.call || (defined?(super) && super)
297301
end
298302

299303
def handle_completed(completed)

lib/job-iteration/test_helper.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def call
2323
# MyJob.perform_now
2424
# end
2525
def iterate_exact_times(n_times)
26-
JobIteration.stubs(:interruption_adapter).returns(StoppingSupervisor.new(n_times.size))
26+
JobIteration::Integrations.stubs(:lookup).returns(StoppingSupervisor.new(n_times.size))
2727
end
2828

2929
# Stubs interruption adapter to interrupt the job after every sing iteration.
@@ -47,7 +47,7 @@ def mark_job_worker_as_interrupted
4747
def stub_shutdown_adapter_to_return(value)
4848
adapter = mock
4949
adapter.stubs(:call).returns(value)
50-
JobIteration.stubs(:interruption_adapter).returns(adapter)
50+
JobIteration::Integrations.stubs(:lookup).returns(adapter)
5151
end
5252
end
5353
end
Lines changed: 23 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,37 @@
11
# frozen_string_literal: true
22

33
require "test_helper"
4-
require "open3"
5-
6-
class IntegrationsTest < ActiveSupport::TestCase
7-
test "will prevent loading two integrations" do
8-
with_env("ITERATION_DISABLE_AUTOCONFIGURE", nil) do
9-
rubby = <<~RUBBY
10-
require 'bundler/setup'
11-
require 'job-iteration'
12-
RUBBY
13-
_stdout, stderr, status = run_ruby(rubby)
14-
15-
assert_equal false, status.success?
16-
assert_match(/resque integration has already been loaded, but sidekiq is also available/, stderr)
4+
5+
class IntegrationsTest < IterationUnitTest
6+
class IterationJob < ActiveJob::Base
7+
include JobIteration::Iteration
8+
9+
def build_enumerator(cursor:)
10+
enumerator_builder.build_once_enumerator(cursor: cursor)
1711
end
18-
end
1912

20-
test "successfully loads one (resque) integration" do
21-
with_env("ITERATION_DISABLE_AUTOCONFIGURE", nil) do
22-
rubby = <<~RUBBY
23-
require 'bundler/setup'
24-
# Remove sidekiq, only resque will be left
25-
$LOAD_PATH.delete_if { |p| p =~ /sidekiq/ }
26-
require 'job-iteration'
27-
RUBBY
28-
_stdout, _stderr, status = run_ruby(rubby)
29-
30-
assert_equal true, status.success?
13+
def each_iteration(*)
3114
end
3215
end
3316

34-
private
17+
class ResqueJob < IterationJob
18+
self.queue_adapter = :resque
19+
end
3520

36-
def run_ruby(body)
37-
stdout, stderr, status = nil
38-
Tempfile.open do |f|
39-
f.write(body)
40-
f.close
21+
class SidekiqJob < IterationJob
22+
self.queue_adapter = :sidekiq
23+
end
4124

42-
command = "ruby #{f.path}"
43-
stdout, stderr, status = Open3.capture3(command)
44-
end
45-
[stdout, stderr, status]
25+
test "will load two integrations" do
26+
resque_job = ResqueJob.new.serialize
27+
ActiveJob::Base.execute(resque_job)
28+
29+
sidekiq_job = SidekiqJob.new.serialize
30+
ActiveJob::Base.execute(sidekiq_job)
4631
end
4732

48-
def with_env(variable, value)
49-
original = ENV[variable]
50-
ENV[variable] = value
51-
yield
52-
ensure
53-
ENV[variable] = original
33+
test "handles unknown Active Job queue adapater names" do
34+
interruption_adapter = JobIteration::Integrations.lookup(:unknown)
35+
assert_equal(false, interruption_adapter.call)
5436
end
5537
end

test/support/sidekiq/init.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# frozen_string_literal: true
22

33
require "job-iteration"
4-
require "job-iteration/integrations/sidekiq"
54

65
require "active_job"
76
require "i18n"

0 commit comments

Comments
 (0)