sidekiq 7.0.9 → 7.3.10

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.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +263 -0
  3. data/README.md +5 -5
  4. data/bin/multi_queue_bench +271 -0
  5. data/bin/sidekiqload +41 -14
  6. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +75 -0
  7. data/lib/generators/sidekiq/job_generator.rb +2 -0
  8. data/lib/sidekiq/api.rb +170 -52
  9. data/lib/sidekiq/capsule.rb +8 -3
  10. data/lib/sidekiq/cli.rb +6 -2
  11. data/lib/sidekiq/client.rb +58 -22
  12. data/lib/sidekiq/component.rb +23 -1
  13. data/lib/sidekiq/config.rb +52 -11
  14. data/lib/sidekiq/deploy.rb +4 -2
  15. data/lib/sidekiq/embedded.rb +2 -0
  16. data/lib/sidekiq/fetch.rb +2 -1
  17. data/lib/sidekiq/iterable_job.rb +55 -0
  18. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  19. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  20. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  21. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  22. data/lib/sidekiq/job/iterable.rb +294 -0
  23. data/lib/sidekiq/job.rb +15 -8
  24. data/lib/sidekiq/job_logger.rb +7 -6
  25. data/lib/sidekiq/job_retry.rb +30 -8
  26. data/lib/sidekiq/job_util.rb +6 -2
  27. data/lib/sidekiq/launcher.rb +8 -6
  28. data/lib/sidekiq/logger.rb +1 -1
  29. data/lib/sidekiq/metrics/query.rb +6 -1
  30. data/lib/sidekiq/metrics/shared.rb +16 -5
  31. data/lib/sidekiq/metrics/tracking.rb +20 -8
  32. data/lib/sidekiq/middleware/current_attributes.rb +88 -16
  33. data/lib/sidekiq/middleware/i18n.rb +2 -0
  34. data/lib/sidekiq/middleware/modules.rb +2 -0
  35. data/lib/sidekiq/monitor.rb +2 -1
  36. data/lib/sidekiq/paginator.rb +8 -2
  37. data/lib/sidekiq/processor.rb +44 -33
  38. data/lib/sidekiq/rails.rb +28 -7
  39. data/lib/sidekiq/redis_client_adapter.rb +30 -31
  40. data/lib/sidekiq/redis_connection.rb +49 -9
  41. data/lib/sidekiq/ring_buffer.rb +3 -0
  42. data/lib/sidekiq/scheduled.rb +3 -3
  43. data/lib/sidekiq/systemd.rb +2 -0
  44. data/lib/sidekiq/testing.rb +32 -13
  45. data/lib/sidekiq/transaction_aware_client.rb +20 -5
  46. data/lib/sidekiq/version.rb +5 -1
  47. data/lib/sidekiq/web/action.rb +29 -7
  48. data/lib/sidekiq/web/application.rb +64 -25
  49. data/lib/sidekiq/web/csrf_protection.rb +9 -6
  50. data/lib/sidekiq/web/helpers.rb +95 -35
  51. data/lib/sidekiq/web/router.rb +5 -2
  52. data/lib/sidekiq/web.rb +67 -3
  53. data/lib/sidekiq.rb +5 -3
  54. data/sidekiq.gemspec +5 -13
  55. data/web/assets/javascripts/application.js +27 -0
  56. data/web/assets/javascripts/dashboard-charts.js +40 -12
  57. data/web/assets/javascripts/dashboard.js +14 -10
  58. data/web/assets/javascripts/metrics.js +34 -0
  59. data/web/assets/stylesheets/application-rtl.css +10 -0
  60. data/web/assets/stylesheets/application.css +38 -3
  61. data/web/locales/en.yml +5 -1
  62. data/web/locales/fr.yml +13 -0
  63. data/web/locales/gd.yml +0 -1
  64. data/web/locales/it.yml +32 -1
  65. data/web/locales/ja.yml +0 -1
  66. data/web/locales/pt-br.yml +20 -1
  67. data/web/locales/tr.yml +100 -0
  68. data/web/locales/uk.yml +24 -1
  69. data/web/locales/zh-cn.yml +0 -1
  70. data/web/locales/zh-tw.yml +0 -1
  71. data/web/views/_footer.erb +12 -1
  72. data/web/views/_job_info.erb +1 -1
  73. data/web/views/_metrics_period_select.erb +1 -1
  74. data/web/views/_summary.erb +7 -7
  75. data/web/views/busy.erb +7 -7
  76. data/web/views/dashboard.erb +29 -36
  77. data/web/views/filtering.erb +6 -0
  78. data/web/views/layout.erb +6 -6
  79. data/web/views/metrics.erb +38 -30
  80. data/web/views/metrics_for_job.erb +30 -39
  81. data/web/views/morgue.erb +2 -2
  82. data/web/views/queue.erb +1 -1
  83. data/web/views/queues.erb +6 -2
  84. metadata +52 -21
@@ -15,7 +15,7 @@ module Sidekiq
15
15
 
16
16
  def safe_thread(name, &block)
17
17
  Thread.new do
18
- Thread.current.name = name
18
+ Thread.current.name = "sidekiq.#{name}"
19
19
  watchdog(name, &block)
20
20
  end
21
21
  end
@@ -64,5 +64,27 @@ module Sidekiq
64
64
  end
65
65
  arr.clear if oneshot # once we've fired an event, we never fire it again
66
66
  end
67
+
68
+ # When you have a large tree of components, the `inspect` output
69
+ # can get out of hand, especially with lots of Sidekiq::Config
70
+ # references everywhere. We avoid calling `inspect` on more complex
71
+ # state and use `to_s` instead to keep output manageable, #6553
72
+ def inspect
73
+ "#<#{self.class.name} #{
74
+ instance_variables.map do |name|
75
+ value = instance_variable_get(name)
76
+ case value
77
+ when Proc
78
+ "#{name}=#{value}"
79
+ when Sidekiq::Config
80
+ "#{name}=#{value}"
81
+ when Sidekiq::Component
82
+ "#{name}=#{value}"
83
+ else
84
+ "#{name}=#{value.inspect}"
85
+ end
86
+ end.join(", ")
87
+ }>"
88
+ end
67
89
  end
68
90
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "forwardable"
2
4
 
3
5
  require "set"
@@ -17,6 +19,10 @@ module Sidekiq
17
19
  poll_interval_average: nil,
18
20
  average_scheduled_poll_interval: 5,
19
21
  on_complex_arguments: :raise,
22
+ iteration: {
23
+ max_job_runtime: nil,
24
+ retry_backoff: 0
25
+ },
20
26
  error_handlers: [],
21
27
  death_handlers: [],
22
28
  lifecycle_events: {
@@ -30,15 +36,18 @@ module Sidekiq
30
36
  },
31
37
  dead_max_jobs: 10_000,
32
38
  dead_timeout_in_seconds: 180 * 24 * 60 * 60, # 6 months
33
- reloader: proc { |&block| block.call }
39
+ reloader: proc { |&block| block.call },
40
+ backtrace_cleaner: ->(backtrace) { backtrace }
34
41
  }
35
42
 
36
- ERROR_HANDLER = ->(ex, ctx) {
37
- cfg = ctx[:_config] || Sidekiq.default_configuration
43
+ ERROR_HANDLER = ->(ex, ctx, cfg = Sidekiq.default_configuration) {
38
44
  l = cfg.logger
39
45
  l.warn(Sidekiq.dump_json(ctx)) unless ctx.empty?
40
46
  l.warn("#{ex.class.name}: #{ex.message}")
41
- l.warn(ex.backtrace.join("\n")) unless ex.backtrace.nil?
47
+ unless ex.backtrace.nil?
48
+ backtrace = cfg[:backtrace_cleaner].call(ex.backtrace)
49
+ l.warn(backtrace.join("\n"))
50
+ end
42
51
  }
43
52
 
44
53
  def initialize(options = {})
@@ -49,9 +58,19 @@ module Sidekiq
49
58
  @capsules = {}
50
59
  end
51
60
 
52
- def_delegators :@options, :[], :[]=, :fetch, :key?, :has_key?, :merge!
61
+ def_delegators :@options, :[], :[]=, :fetch, :key?, :has_key?, :merge!, :dig
53
62
  attr_reader :capsules
54
63
 
64
+ def inspect
65
+ "#<#{self.class.name} @options=#{
66
+ @options.except(:lifecycle_events, :reloader, :death_handlers, :error_handlers).inspect
67
+ }>"
68
+ end
69
+
70
+ def to_json(*)
71
+ Sidekiq.dump_json(@options)
72
+ end
73
+
55
74
  # LEGACY: edits the default capsule
56
75
  # config.concurrency = 5
57
76
  def concurrency=(val)
@@ -123,7 +142,7 @@ module Sidekiq
123
142
  private def local_redis_pool
124
143
  # this is our internal client/housekeeping pool. each capsule has its
125
144
  # own pool for executing threads.
126
- @redis ||= new_redis_pool(5, "internal")
145
+ @redis ||= new_redis_pool(10, "internal")
127
146
  end
128
147
 
129
148
  def new_redis_pool(size, name = "unset")
@@ -172,7 +191,13 @@ module Sidekiq
172
191
 
173
192
  # register global singletons which can be accessed elsewhere
174
193
  def register(name, instance)
175
- @directory[name] = instance
194
+ # logger.debug("register[#{name}] = #{instance}")
195
+ # Sidekiq Enterprise lazy registers a few services so we
196
+ # can't lock down this hash completely.
197
+ hash = @directory.dup
198
+ hash[name] = instance
199
+ @directory = hash.freeze
200
+ instance
176
201
  end
177
202
 
178
203
  # find a singleton
@@ -180,10 +205,16 @@ module Sidekiq
180
205
  # JNDI is just a fancy name for a hash lookup
181
206
  @directory.fetch(name) do |key|
182
207
  return nil unless default_class
183
- @directory[key] = default_class.new(self)
208
+ register(key, default_class.new(self))
184
209
  end
185
210
  end
186
211
 
212
+ def freeze!
213
+ @directory.freeze
214
+ @options.freeze
215
+ true
216
+ end
217
+
187
218
  ##
188
219
  # Death handlers are called when all retries for a job have been exhausted and
189
220
  # the job dies. It's the notification to your application
@@ -251,15 +282,25 @@ module Sidekiq
251
282
  @logger = logger
252
283
  end
253
284
 
285
+ private def parameter_size(handler)
286
+ target = handler.is_a?(Proc) ? handler : handler.method(:call)
287
+ target.parameters.size
288
+ end
289
+
254
290
  # INTERNAL USE ONLY
255
291
  def handle_exception(ex, ctx = {})
256
292
  if @options[:error_handlers].size == 0
257
293
  p ["!!!!!", ex]
258
294
  end
259
- ctx[:_config] = self
260
295
  @options[:error_handlers].each do |handler|
261
- handler.call(ex, ctx)
262
- rescue => e
296
+ if parameter_size(handler) == 2
297
+ # TODO Remove in 8.0
298
+ logger.info { "DEPRECATION: Sidekiq exception handlers now take three arguments, see #{handler}" }
299
+ handler.call(ex, {_config: self}.merge(ctx))
300
+ else
301
+ handler.call(ex, ctx, self)
302
+ end
303
+ rescue Exception => e
263
304
  l = logger
264
305
  l.error "!!! ERROR HANDLER THREW AN ERROR !!!"
265
306
  l.error e
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "sidekiq/redis_connection"
2
4
  require "time"
3
5
 
@@ -34,7 +36,7 @@ module Sidekiq
34
36
  # handle an very common error in marking deploys:
35
37
  # having every process mark its deploy, leading
36
38
  # to N marks for each deploy. Instead we round the time
37
- # to the minute so that multple marks within that minute
39
+ # to the minute so that multiple marks within that minute
38
40
  # will all naturally rollup into one mark per minute.
39
41
  whence = at.utc
40
42
  floor = Time.utc(whence.year, whence.month, whence.mday, whence.hour, whence.min, 0)
@@ -44,7 +46,7 @@ module Sidekiq
44
46
 
45
47
  @pool.with do |c|
46
48
  # only allow one deploy mark for a given label for the next minute
47
- lock = c.set("deploylock-#{label}", stamp, nx: true, ex: 60)
49
+ lock = c.set("deploylock-#{label}", stamp, "nx", "ex", "60")
48
50
  if lock
49
51
  c.multi do |pipe|
50
52
  pipe.hsetnx(key, stamp, label)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "sidekiq/component"
2
4
  require "sidekiq/launcher"
3
5
  require "sidekiq/metrics/tracking"
data/lib/sidekiq/fetch.rb CHANGED
@@ -7,6 +7,7 @@ require "sidekiq/capsule"
7
7
  module Sidekiq # :nodoc:
8
8
  class BasicFetch
9
9
  include Sidekiq::Component
10
+
10
11
  # We want the fetch operation to timeout every few seconds so the thread
11
12
  # can check if the process is shutting down.
12
13
  TIMEOUT = 2
@@ -44,7 +45,7 @@ module Sidekiq # :nodoc:
44
45
  return nil
45
46
  end
46
47
 
47
- queue, job = redis { |conn| conn.blocking_call(conn.read_timeout + TIMEOUT, "brpop", *qs, TIMEOUT) }
48
+ queue, job = redis { |conn| conn.blocking_call(TIMEOUT, "brpop", *qs, TIMEOUT) }
48
49
  UnitOfWork.new(queue, job, config) if queue
49
50
  end
50
51
 
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq/job/iterable"
4
+
5
+ # Iterable jobs are ones which provide a sequence to process using
6
+ # `build_enumerator(*args, cursor: cursor)` and then process each
7
+ # element of that sequence in `each_iteration(item, *args)`.
8
+ #
9
+ # The job is kicked off as normal:
10
+ #
11
+ # ProcessUserSet.perform_async(123)
12
+ #
13
+ # but instead of calling `perform`, Sidekiq will call:
14
+ #
15
+ # enum = ProcessUserSet#build_enumerator(123, cursor:nil)
16
+ #
17
+ # Your Enumerator must yield `(object, updated_cursor)` and
18
+ # Sidekiq will call your `each_iteration` method:
19
+ #
20
+ # ProcessUserSet#each_iteration(object, 123)
21
+ #
22
+ # After every iteration, Sidekiq will check for shutdown. If we are
23
+ # stopping, the cursor will be saved to Redis and the job re-queued
24
+ # to pick up the rest of the work upon restart. Your job will get
25
+ # the updated_cursor so it can pick up right where it stopped.
26
+ #
27
+ # enum = ProcessUserSet#build_enumerator(123, cursor: updated_cursor)
28
+ #
29
+ # The cursor object must be serializable to JSON.
30
+ #
31
+ # Note there are several APIs to help you build enumerators for
32
+ # ActiveRecord Relations, CSV files, etc. See sidekiq/job/iterable/*.rb.
33
+ module Sidekiq
34
+ module IterableJob
35
+ def self.included(base)
36
+ base.include Sidekiq::Job
37
+ base.include Sidekiq::Job::Iterable
38
+ end
39
+
40
+ # def build_enumerator(*args, cursor:)
41
+ # def each_iteration(item, *args)
42
+
43
+ # Your job can also define several callbacks during points
44
+ # in each job's lifecycle.
45
+ #
46
+ # def on_start
47
+ # def on_resume
48
+ # def on_stop
49
+ # def on_complete
50
+ # def around_iteration
51
+ #
52
+ # To keep things simple and compatible, this is the same
53
+ # API as the `sidekiq-iteration` gem.
54
+ end
55
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Job
5
+ class InterruptHandler
6
+ include Sidekiq::ServerMiddleware
7
+
8
+ def call(instance, hash, queue)
9
+ yield
10
+ rescue Interrupted
11
+ logger.debug "Interrupted, re-queueing..."
12
+ c = Sidekiq::Client.new
13
+ c.push(hash)
14
+ raise Sidekiq::JobRetry::Skip
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ Sidekiq.configure_server do |config|
21
+ config.server_middleware do |chain|
22
+ chain.add Sidekiq::Job::InterruptHandler
23
+ end
24
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Job
5
+ module Iterable
6
+ # @api private
7
+ class ActiveRecordEnumerator
8
+ def initialize(relation, cursor: nil, **options)
9
+ @relation = relation
10
+ @cursor = cursor
11
+ @options = options
12
+ end
13
+
14
+ def records
15
+ Enumerator.new(-> { @relation.count }) do |yielder|
16
+ @relation.find_each(**@options, start: @cursor) do |record|
17
+ yielder.yield(record, record.id)
18
+ end
19
+ end
20
+ end
21
+
22
+ def batches
23
+ Enumerator.new(-> { @relation.count }) do |yielder|
24
+ @relation.find_in_batches(**@options, start: @cursor) do |batch|
25
+ yielder.yield(batch, batch.first.id)
26
+ end
27
+ end
28
+ end
29
+
30
+ def relations
31
+ Enumerator.new(-> { relations_size }) do |yielder|
32
+ # Convenience to use :batch_size for all the
33
+ # ActiveRecord batching methods.
34
+ options = @options.dup
35
+ options[:of] ||= options.delete(:batch_size)
36
+
37
+ @relation.in_batches(**options, start: @cursor) do |relation|
38
+ first_record = relation.first
39
+ yielder.yield(relation, first_record.id)
40
+ end
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def relations_size
47
+ batch_size = @options[:batch_size] || 1000
48
+ (@relation.count + batch_size - 1) / batch_size # ceiling division
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Job
5
+ module Iterable
6
+ # @api private
7
+ class CsvEnumerator
8
+ def initialize(csv)
9
+ unless defined?(CSV) && csv.instance_of?(CSV)
10
+ raise ArgumentError, "CsvEnumerator.new takes CSV object"
11
+ end
12
+
13
+ @csv = csv
14
+ end
15
+
16
+ def rows(cursor:)
17
+ @csv.lazy
18
+ .each_with_index
19
+ .drop(cursor || 0)
20
+ .to_enum { count_of_rows_in_file }
21
+ end
22
+
23
+ def batches(cursor:, batch_size: 100)
24
+ @csv.lazy
25
+ .each_slice(batch_size)
26
+ .with_index
27
+ .drop(cursor || 0)
28
+ .to_enum { (count_of_rows_in_file.to_f / batch_size).ceil }
29
+ end
30
+
31
+ private
32
+
33
+ def count_of_rows_in_file
34
+ filepath = @csv.path
35
+ return unless filepath
36
+
37
+ count = IO.popen(["wc", "-l", filepath]) do |out|
38
+ out.read.strip.to_i
39
+ end
40
+
41
+ count -= 1 if @csv.headers
42
+ count
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "active_record_enumerator"
4
+ require_relative "csv_enumerator"
5
+
6
+ module Sidekiq
7
+ module Job
8
+ module Iterable
9
+ module Enumerators
10
+ # Builds Enumerator object from a given array, using +cursor+ as an offset.
11
+ #
12
+ # @param array [Array]
13
+ # @param cursor [Integer] offset to start iteration from
14
+ #
15
+ # @return [Enumerator]
16
+ #
17
+ # @example
18
+ # array_enumerator(['build', 'enumerator', 'from', 'any', 'array'], cursor: cursor)
19
+ #
20
+ def array_enumerator(array, cursor:)
21
+ raise ArgumentError, "array must be an Array" unless array.is_a?(Array)
22
+
23
+ x = array.each_with_index.drop(cursor || 0)
24
+ x.to_enum { x.size }
25
+ end
26
+
27
+ # Builds Enumerator from `ActiveRecord::Relation`.
28
+ # Each Enumerator tick moves the cursor one row forward.
29
+ #
30
+ # @param relation [ActiveRecord::Relation] relation to iterate
31
+ # @param cursor [Object] offset id to start iteration from
32
+ # @param options [Hash] additional options that will be passed to relevant
33
+ # ActiveRecord batching methods
34
+ #
35
+ # @return [ActiveRecordEnumerator]
36
+ #
37
+ # @example
38
+ # def build_enumerator(cursor:)
39
+ # active_record_records_enumerator(User.all, cursor: cursor)
40
+ # end
41
+ #
42
+ # def each_iteration(user)
43
+ # user.notify_about_something
44
+ # end
45
+ #
46
+ def active_record_records_enumerator(relation, cursor:, **options)
47
+ ActiveRecordEnumerator.new(relation, cursor: cursor, **options).records
48
+ end
49
+
50
+ # Builds Enumerator from `ActiveRecord::Relation` and enumerates on batches of records.
51
+ # Each Enumerator tick moves the cursor `:batch_size` rows forward.
52
+ # @see #active_record_records_enumerator
53
+ #
54
+ # @example
55
+ # def build_enumerator(product_id, cursor:)
56
+ # active_record_batches_enumerator(
57
+ # Comment.where(product_id: product_id).select(:id),
58
+ # cursor: cursor,
59
+ # batch_size: 100
60
+ # )
61
+ # end
62
+ #
63
+ # def each_iteration(batch_of_comments, product_id)
64
+ # comment_ids = batch_of_comments.map(&:id)
65
+ # CommentService.call(comment_ids: comment_ids)
66
+ # end
67
+ #
68
+ def active_record_batches_enumerator(relation, cursor:, **options)
69
+ ActiveRecordEnumerator.new(relation, cursor: cursor, **options).batches
70
+ end
71
+
72
+ # Builds Enumerator from `ActiveRecord::Relation` and enumerates on batches,
73
+ # yielding `ActiveRecord::Relation`s.
74
+ # @see #active_record_records_enumerator
75
+ #
76
+ # @example
77
+ # def build_enumerator(product_id, cursor:)
78
+ # active_record_relations_enumerator(
79
+ # Product.find(product_id).comments,
80
+ # cursor: cursor,
81
+ # batch_size: 100,
82
+ # )
83
+ # end
84
+ #
85
+ # def each_iteration(batch_of_comments, product_id)
86
+ # # batch_of_comments will be a Comment::ActiveRecord_Relation
87
+ # batch_of_comments.update_all(deleted: true)
88
+ # end
89
+ #
90
+ def active_record_relations_enumerator(relation, cursor:, **options)
91
+ ActiveRecordEnumerator.new(relation, cursor: cursor, **options).relations
92
+ end
93
+
94
+ # Builds Enumerator from a CSV file.
95
+ #
96
+ # @param csv [CSV] an instance of CSV object
97
+ # @param cursor [Integer] offset to start iteration from
98
+ #
99
+ # @example
100
+ # def build_enumerator(import_id, cursor:)
101
+ # import = Import.find(import_id)
102
+ # csv_enumerator(import.csv, cursor: cursor)
103
+ # end
104
+ #
105
+ # def each_iteration(csv_row)
106
+ # # insert csv_row into database
107
+ # end
108
+ #
109
+ def csv_enumerator(csv, cursor:)
110
+ CsvEnumerator.new(csv).rows(cursor: cursor)
111
+ end
112
+
113
+ # Builds Enumerator from a CSV file and enumerates on batches of records.
114
+ #
115
+ # @param csv [CSV] an instance of CSV object
116
+ # @param cursor [Integer] offset to start iteration from
117
+ # @option options :batch_size [Integer] (100) size of the batch
118
+ #
119
+ # @example
120
+ # def build_enumerator(import_id, cursor:)
121
+ # import = Import.find(import_id)
122
+ # csv_batches_enumerator(import.csv, cursor: cursor)
123
+ # end
124
+ #
125
+ # def each_iteration(batch_of_csv_rows)
126
+ # # ...
127
+ # end
128
+ #
129
+ def csv_batches_enumerator(csv, cursor:, **options)
130
+ CsvEnumerator.new(csv).batches(cursor: cursor, **options)
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end