job-iteration 0.9.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.

Potentially problematic release.


This version of job-iteration might be problematic. Click here for more details.

@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'resque'
4
+
5
+ module JobIteration
6
+ module Integrations
7
+ # The trick is required in order to call shutdown? on a Resque::Worker instance
8
+ module ResqueIterationExtension
9
+ def initialize(*)
10
+ $resque_worker = self
11
+ super
12
+ end
13
+ end
14
+ Resque::Worker.prepend(ResqueIterationExtension)
15
+
16
+ module ResqueInterruptionAdapter
17
+ extend self
18
+
19
+ def shutdown?
20
+ $resque_worker.try!(:shutdown?)
21
+ end
22
+ end
23
+
24
+ JobIteration.interruption_adapter = ResqueInterruptionAdapter
25
+ end
26
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sidekiq'
4
+
5
+ module JobIteration
6
+ module Integrations
7
+ module SidekiqInterruptionAdapter
8
+ extend self
9
+
10
+ def shutdown?
11
+ if defined?(Sidekiq::CLI) && Sidekiq::CLI.instance
12
+ Sidekiq::CLI.instance.launcher.stopping?
13
+ else
14
+ false
15
+ end
16
+ end
17
+ end
18
+
19
+ JobIteration.interruption_adapter = SidekiqInterruptionAdapter
20
+ end
21
+ end
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/all'
4
+
5
+ module JobIteration
6
+ module Iteration
7
+ extend ActiveSupport::Concern
8
+
9
+ included do |_base|
10
+ attr_accessor(
11
+ :cursor_position,
12
+ :start_time,
13
+ :times_interrupted,
14
+ :total_time,
15
+ )
16
+
17
+ define_callbacks :start
18
+ define_callbacks :shutdown
19
+ define_callbacks :complete
20
+ end
21
+
22
+ module ClassMethods
23
+ def method_added(method_name)
24
+ ban_perform_definition if method_name.to_sym == :perform
25
+ end
26
+
27
+ def on_start(*filters, &blk)
28
+ set_callback(:start, :after, *filters, &blk)
29
+ end
30
+
31
+ def on_shutdown(*filters, &blk)
32
+ set_callback(:shutdown, :after, *filters, &blk)
33
+ end
34
+
35
+ def on_complete(*filters, &blk)
36
+ set_callback(:complete, :after, *filters, &blk)
37
+ end
38
+
39
+ def supports_interruption?
40
+ true
41
+ end
42
+
43
+ private
44
+
45
+ def ban_perform_definition
46
+ raise "Job that is using Iteration (#{self}) cannot redefine #perform"
47
+ end
48
+ end
49
+
50
+ def initialize(*arguments)
51
+ super
52
+ self.times_interrupted = 0
53
+ self.total_time = 0.0
54
+ end
55
+
56
+ def serialize
57
+ super.merge(
58
+ 'cursor_position' => cursor_position,
59
+ 'times_interrupted' => times_interrupted,
60
+ 'total_time' => total_time,
61
+ )
62
+ end
63
+
64
+ def deserialize(job_data)
65
+ super
66
+ self.cursor_position = job_data['cursor_position']
67
+ self.times_interrupted = job_data['times_interrupted'] || 0
68
+ self.total_time = job_data['total_time'] || 0
69
+ end
70
+
71
+ def perform(*params)
72
+ interruptible_perform(*params)
73
+ end
74
+
75
+ private
76
+
77
+ def enumerator_builder
78
+ JobIteration::EnumeratorBuilder.new(self)
79
+ end
80
+
81
+ def interruptible_perform(*arguments)
82
+ assert_implements_methods!
83
+
84
+ self.start_time = Time.now.utc
85
+
86
+ enumerator = nil
87
+ ActiveSupport::Notifications.instrument("build_enumerator.iteration", iteration_instrumentation_tags) do
88
+ enumerator = build_enumerator(*arguments, cursor: cursor_position)
89
+ end
90
+
91
+ unless enumerator
92
+ logger.info "[JobIteration::Iteration] `build_enumerator` returned nil. " \
93
+ "Skipping the job."
94
+ return
95
+ end
96
+
97
+ assert_enumerator!(enumerator)
98
+
99
+ if executions == 1 && times_interrupted == 0
100
+ run_callbacks :start
101
+ else
102
+ ActiveSupport::Notifications.instrument("resumed.iteration", iteration_instrumentation_tags)
103
+ end
104
+
105
+ catch(:abort) do
106
+ return unless iterate_with_enumerator(enumerator, arguments)
107
+ end
108
+
109
+ run_callbacks :shutdown
110
+ run_callbacks :complete
111
+
112
+ output_interrupt_summary
113
+ end
114
+
115
+ def iterate_with_enumerator(enumerator, arguments)
116
+ arguments = arguments.dup.freeze
117
+ enumerator.each do |iteration, index|
118
+ record_unit_of_work do
119
+ each_iteration(iteration, *arguments)
120
+ self.cursor_position = index
121
+ end
122
+
123
+ next unless job_should_exit?
124
+ self.executions -= 1 if executions > 1
125
+ shutdown_and_reenqueue
126
+ return false
127
+ end
128
+
129
+ true
130
+ end
131
+
132
+ def record_unit_of_work
133
+ ActiveSupport::Notifications.instrument("each_iteration.iteration", iteration_instrumentation_tags) do
134
+ yield
135
+ end
136
+ end
137
+
138
+ def shutdown_and_reenqueue
139
+ ActiveSupport::Notifications.instrument("interrupted.iteration", iteration_instrumentation_tags)
140
+ logger.info "[JobIteration::Iteration] Interrupting and re-enqueueing the job cursor_position=#{cursor_position}"
141
+
142
+ adjust_total_time
143
+ self.times_interrupted += 1
144
+
145
+ self.already_in_queue = true if respond_to?(:already_in_queue=)
146
+ run_callbacks :shutdown
147
+ enqueue
148
+
149
+ true
150
+ end
151
+
152
+ def adjust_total_time
153
+ self.total_time += (Time.now.utc.to_f - start_time.to_f).round(6)
154
+ end
155
+
156
+ def assert_enumerator!(enum)
157
+ return if enum.is_a?(Enumerator)
158
+
159
+ raise ArgumentError, <<~EOS
160
+ #build_enumerator is expected to return Enumerator object, but returned #{enum.class}.
161
+ Example:
162
+ def build_enumerator(params, cursor:)
163
+ enumerator_builder.active_record_on_records(
164
+ Shop.find(params[:shop_id]).products,
165
+ cursor: cursor
166
+ )
167
+ end
168
+ EOS
169
+ end
170
+
171
+ def assert_implements_methods!
172
+ unless respond_to?(:each_iteration, true)
173
+ raise(
174
+ ArgumentError,
175
+ "Iteration job (#{self.class}) must implement #each_iteration method"
176
+ )
177
+ end
178
+
179
+ unless respond_to?(:build_enumerator, true)
180
+ raise ArgumentError, "Iteration job (#{self.class}) must implement #build_enumerator " \
181
+ "to provide a collection to iterate"
182
+ end
183
+ end
184
+
185
+ def iteration_instrumentation_tags
186
+ { job_class: self.class.name }
187
+ end
188
+
189
+ def output_interrupt_summary
190
+ adjust_total_time
191
+
192
+ message = "[JobIteration::Iteration] Completed iterating. times_interrupted=%d total_time=%.3f"
193
+ logger.info Kernel.format(message, times_interrupted, total_time)
194
+ end
195
+
196
+ def job_should_exit?
197
+ if ::JobIteration.max_job_runtime && start_time && (Time.now.utc - start_time) > ::JobIteration.max_job_runtime
198
+ return true
199
+ end
200
+
201
+ JobIteration.interruption_adapter.shutdown?
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JobIteration
4
+ module TestHelper
5
+ class StoppingSupervisor
6
+ def initialize(stop_after_count)
7
+ @stop_after_count = stop_after_count
8
+ @calls = 0
9
+ end
10
+
11
+ def shutdown?
12
+ @calls += 1
13
+ (@calls % @stop_after_count) == 0
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def iterate_exact_times(n_times)
20
+ JobIteration.stubs(:interruption_adapter).returns(StoppingSupervisor.new(n_times.size))
21
+ end
22
+
23
+ def iterate_once
24
+ iterate_exact_times(1.times)
25
+ end
26
+
27
+ def continue_iterating
28
+ stub_shutdown_adapter_to_return(false)
29
+ end
30
+
31
+ def mark_job_worker_as_interrupted
32
+ stub_shutdown_adapter_to_return(true)
33
+ end
34
+
35
+ def stub_shutdown_adapter_to_return(_value)
36
+ adapter = mock
37
+ adapter.stubs(:shutdown?).returns(false)
38
+ JobIteration.stubs(:interruption_adapter).returns(adapter)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JobIteration
4
+ VERSION = "0.9.0"
5
+ end
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: job-iteration
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.0
5
+ platform: ruby
6
+ authors:
7
+ - Shopify
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-07-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activejob
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.16'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.16'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '5.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '5.0'
69
+ description: Makes your background jobs interruptible and resumable.
70
+ email:
71
+ - ops-accounts+shipit@shopify.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".gitignore"
77
+ - ".rubocop.yml"
78
+ - ".travis.yml"
79
+ - CODE_OF_CONDUCT.md
80
+ - Gemfile
81
+ - Gemfile.lock
82
+ - LICENSE.txt
83
+ - README.md
84
+ - Rakefile
85
+ - guides/best-practices.md
86
+ - guides/iteration-how-it-works.md
87
+ - job-iteration.gemspec
88
+ - lib/job-iteration.rb
89
+ - lib/job-iteration/active_record_cursor.rb
90
+ - lib/job-iteration/active_record_enumerator.rb
91
+ - lib/job-iteration/csv_enumerator.rb
92
+ - lib/job-iteration/enumerator_builder.rb
93
+ - lib/job-iteration/integrations/resque.rb
94
+ - lib/job-iteration/integrations/sidekiq.rb
95
+ - lib/job-iteration/iteration.rb
96
+ - lib/job-iteration/test_helper.rb
97
+ - lib/job-iteration/version.rb
98
+ homepage: https://github.com/shopify/job-iteration
99
+ licenses:
100
+ - MIT
101
+ metadata: {}
102
+ post_install_message:
103
+ rdoc_options: []
104
+ require_paths:
105
+ - lib
106
+ required_ruby_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ required_rubygems_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ requirements: []
117
+ rubyforge_project:
118
+ rubygems_version: 2.6.14
119
+ signing_key:
120
+ specification_version: 4
121
+ summary: Makes your background jobs interruptible and resumable.
122
+ test_files: []