sidekiq 6.5.10 → 8.1.7

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 (177) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +568 -18
  3. data/README.md +66 -38
  4. data/bin/kiq +17 -0
  5. data/bin/lint-herb +13 -0
  6. data/bin/multi_queue_bench +271 -0
  7. data/bin/sidekiq +3 -8
  8. data/bin/sidekiqload +215 -118
  9. data/bin/sidekiqmon +3 -0
  10. data/bin/webload +69 -0
  11. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +124 -0
  12. data/lib/generators/sidekiq/job_generator.rb +17 -3
  13. data/lib/generators/sidekiq/templates/job.rb.erb +1 -1
  14. data/lib/sidekiq/api.rb +526 -218
  15. data/lib/sidekiq/capsule.rb +135 -0
  16. data/lib/sidekiq/cli.rb +88 -96
  17. data/lib/sidekiq/client.rb +114 -47
  18. data/lib/sidekiq/component.rb +71 -4
  19. data/lib/sidekiq/config.rb +316 -0
  20. data/lib/sidekiq/deploy.rb +64 -0
  21. data/lib/sidekiq/embedded.rb +64 -0
  22. data/lib/sidekiq/fetch.rb +12 -14
  23. data/lib/sidekiq/iterable_job.rb +56 -0
  24. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  25. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  26. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  27. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  28. data/lib/sidekiq/job/iterable.rb +322 -0
  29. data/lib/sidekiq/job.rb +397 -10
  30. data/lib/sidekiq/job_logger.rb +14 -11
  31. data/lib/sidekiq/job_retry.rb +78 -28
  32. data/lib/sidekiq/job_util.rb +58 -16
  33. data/lib/sidekiq/launcher.rb +86 -70
  34. data/lib/sidekiq/loader.rb +57 -0
  35. data/lib/sidekiq/logger.rb +25 -94
  36. data/lib/sidekiq/manager.rb +11 -13
  37. data/lib/sidekiq/metrics/query.rb +84 -48
  38. data/lib/sidekiq/metrics/shared.rb +29 -14
  39. data/lib/sidekiq/metrics/tracking.rb +49 -30
  40. data/lib/sidekiq/middleware/chain.rb +19 -18
  41. data/lib/sidekiq/middleware/current_attributes.rb +78 -21
  42. data/lib/sidekiq/middleware/i18n.rb +2 -0
  43. data/lib/sidekiq/middleware/modules.rb +2 -0
  44. data/lib/sidekiq/monitor.rb +15 -5
  45. data/lib/sidekiq/paginator.rb +26 -4
  46. data/lib/sidekiq/processor.rb +78 -65
  47. data/lib/sidekiq/profiler.rb +73 -0
  48. data/lib/sidekiq/rails.rb +45 -60
  49. data/lib/sidekiq/redis_client_adapter.rb +35 -72
  50. data/lib/sidekiq/redis_connection.rb +54 -114
  51. data/lib/sidekiq/ring_buffer.rb +3 -0
  52. data/lib/sidekiq/scheduled.rb +28 -30
  53. data/lib/sidekiq/systemd.rb +2 -0
  54. data/lib/sidekiq/test_api.rb +331 -0
  55. data/lib/sidekiq/testing/inline.rb +2 -30
  56. data/lib/sidekiq/testing.rb +2 -342
  57. data/lib/sidekiq/transaction_aware_client.rb +23 -9
  58. data/lib/sidekiq/tui/controls.rb +53 -0
  59. data/lib/sidekiq/tui/filtering.rb +53 -0
  60. data/lib/sidekiq/tui/tabs/base_tab.rb +230 -0
  61. data/lib/sidekiq/tui/tabs/busy.rb +104 -0
  62. data/lib/sidekiq/tui/tabs/dead.rb +14 -0
  63. data/lib/sidekiq/tui/tabs/home.rb +115 -0
  64. data/lib/sidekiq/tui/tabs/metrics.rb +114 -0
  65. data/lib/sidekiq/tui/tabs/queues.rb +83 -0
  66. data/lib/sidekiq/tui/tabs/retries.rb +14 -0
  67. data/lib/sidekiq/tui/tabs/scheduled.rb +14 -0
  68. data/lib/sidekiq/tui/tabs/set_tab.rb +91 -0
  69. data/lib/sidekiq/tui/tabs.rb +15 -0
  70. data/lib/sidekiq/tui.rb +390 -0
  71. data/lib/sidekiq/version.rb +6 -1
  72. data/lib/sidekiq/web/action.rb +149 -64
  73. data/lib/sidekiq/web/application.rb +379 -281
  74. data/lib/sidekiq/web/config.rb +117 -0
  75. data/lib/sidekiq/web/helpers.rb +190 -71
  76. data/lib/sidekiq/web/router.rb +62 -75
  77. data/lib/sidekiq/web.rb +68 -101
  78. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  79. data/lib/sidekiq.rb +85 -269
  80. data/sidekiq.gemspec +15 -11
  81. data/web/assets/images/logo.png +0 -0
  82. data/web/assets/images/status.png +0 -0
  83. data/web/assets/javascripts/application.js +84 -24
  84. data/web/assets/javascripts/base-charts.js +120 -0
  85. data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  86. data/web/assets/javascripts/dashboard-charts.js +194 -0
  87. data/web/assets/javascripts/dashboard.js +18 -234
  88. data/web/assets/javascripts/metrics.js +138 -120
  89. data/web/assets/stylesheets/style.css +776 -0
  90. data/web/locales/ar.yml +72 -70
  91. data/web/locales/cs.yml +64 -62
  92. data/web/locales/da.yml +62 -53
  93. data/web/locales/de.yml +67 -65
  94. data/web/locales/el.yml +4 -7
  95. data/web/locales/en.yml +89 -71
  96. data/web/locales/es.yml +91 -68
  97. data/web/locales/fa.yml +67 -65
  98. data/web/locales/fr.yml +82 -67
  99. data/web/locales/gd.yml +110 -0
  100. data/web/locales/he.yml +67 -64
  101. data/web/locales/hi.yml +95 -59
  102. data/web/locales/it.yml +94 -54
  103. data/web/locales/ja.yml +69 -70
  104. data/web/locales/ko.yml +54 -52
  105. data/web/locales/lt.yml +68 -66
  106. data/web/locales/nb.yml +63 -61
  107. data/web/locales/nl.yml +54 -52
  108. data/web/locales/pl.yml +47 -45
  109. data/web/locales/{pt-br.yml → pt-BR.yml} +81 -70
  110. data/web/locales/pt.yml +53 -51
  111. data/web/locales/ru.yml +69 -66
  112. data/web/locales/sv.yml +55 -53
  113. data/web/locales/ta.yml +100 -64
  114. data/web/locales/tr.yml +102 -0
  115. data/web/locales/uk.yml +87 -61
  116. data/web/locales/ur.yml +66 -64
  117. data/web/locales/vi.yml +69 -67
  118. data/web/locales/zh-CN.yml +107 -0
  119. data/web/locales/{zh-tw.yml → zh-TW.yml} +13 -3
  120. data/web/views/_footer.html.erb +32 -0
  121. data/web/views/_job_info.html.erb +115 -0
  122. data/web/views/_metrics_period_select.html.erb +15 -0
  123. data/web/views/_nav.html.erb +45 -0
  124. data/web/views/_paging.html.erb +26 -0
  125. data/web/views/_poll_link.html.erb +4 -0
  126. data/web/views/_summary.html.erb +40 -0
  127. data/web/views/busy.html.erb +155 -0
  128. data/web/views/dashboard.html.erb +104 -0
  129. data/web/views/dead.html.erb +38 -0
  130. data/web/views/filtering.html.erb +6 -0
  131. data/web/views/layout.html.erb +26 -0
  132. data/web/views/metrics.html.erb +85 -0
  133. data/web/views/metrics_for_job.html.erb +58 -0
  134. data/web/views/morgue.html.erb +69 -0
  135. data/web/views/profiles.html.erb +43 -0
  136. data/web/views/queue.html.erb +57 -0
  137. data/web/views/queues.html.erb +46 -0
  138. data/web/views/retries.html.erb +77 -0
  139. data/web/views/retry.html.erb +39 -0
  140. data/web/views/scheduled.html.erb +64 -0
  141. data/web/views/{scheduled_job_info.erb → scheduled_job_info.html.erb} +3 -3
  142. metadata +113 -70
  143. data/lib/sidekiq/delay.rb +0 -43
  144. data/lib/sidekiq/extensions/action_mailer.rb +0 -48
  145. data/lib/sidekiq/extensions/active_record.rb +0 -43
  146. data/lib/sidekiq/extensions/class_methods.rb +0 -43
  147. data/lib/sidekiq/extensions/generic_proxy.rb +0 -33
  148. data/lib/sidekiq/metrics/deploy.rb +0 -47
  149. data/lib/sidekiq/web/csrf_protection.rb +0 -180
  150. data/lib/sidekiq/worker.rb +0 -370
  151. data/web/assets/javascripts/graph.js +0 -16
  152. data/web/assets/stylesheets/application-dark.css +0 -143
  153. data/web/assets/stylesheets/application-rtl.css +0 -242
  154. data/web/assets/stylesheets/application.css +0 -999
  155. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  156. data/web/assets/stylesheets/bootstrap.css +0 -5
  157. data/web/locales/zh-cn.yml +0 -93
  158. data/web/views/_footer.erb +0 -20
  159. data/web/views/_job_info.erb +0 -89
  160. data/web/views/_nav.erb +0 -52
  161. data/web/views/_paging.erb +0 -23
  162. data/web/views/_poll_link.erb +0 -4
  163. data/web/views/_status.erb +0 -4
  164. data/web/views/_summary.erb +0 -40
  165. data/web/views/busy.erb +0 -137
  166. data/web/views/dashboard.erb +0 -84
  167. data/web/views/dead.erb +0 -34
  168. data/web/views/layout.erb +0 -42
  169. data/web/views/metrics.erb +0 -69
  170. data/web/views/metrics_for_job.erb +0 -87
  171. data/web/views/morgue.erb +0 -78
  172. data/web/views/queue.erb +0 -59
  173. data/web/views/queues.erb +0 -38
  174. data/web/views/retries.erb +0 -83
  175. data/web/views/retry.erb +0 -34
  176. data/web/views/scheduled.erb +0 -57
  177. /data/{LICENSE → LICENSE.txt} +0 -0
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq/redis_connection"
4
+ require "time"
5
+
6
+ # This file is designed to be required within the user's
7
+ # deployment script; it should need a bare minimum of dependencies.
8
+ # Usage:
9
+ #
10
+ # require "sidekiq/deploy"
11
+ # Sidekiq::Deploy.mark!("Some change")
12
+ #
13
+ # If you do not pass a label, Sidekiq will try to use the latest
14
+ # git commit info.
15
+ #
16
+
17
+ module Sidekiq
18
+ class Deploy
19
+ MARK_TTL = 90 * 24 * 60 * 60 # 90 days
20
+
21
+ LABEL_MAKER = -> {
22
+ `git log -1 --format="%h %s"`.strip
23
+ }
24
+
25
+ def self.mark!(label = nil)
26
+ Sidekiq::Deploy.new.mark!(label: label)
27
+ end
28
+
29
+ def initialize(pool = Sidekiq::RedisConnection.create)
30
+ @pool = pool
31
+ end
32
+
33
+ def mark!(at: Time.now, label: nil)
34
+ label ||= LABEL_MAKER.call
35
+ # we need to round the timestamp so that we gracefully
36
+ # handle an very common error in marking deploys:
37
+ # having every process mark its deploy, leading
38
+ # to N marks for each deploy. Instead we round the time
39
+ # to the minute so that multiple marks within that minute
40
+ # will all naturally rollup into one mark per minute.
41
+ whence = at.utc
42
+ floor = Time.utc(whence.year, whence.month, whence.mday, whence.hour, whence.min, 0)
43
+ datecode = floor.strftime("%Y%m%d")
44
+ key = "#{datecode}-marks"
45
+ stamp = floor.iso8601
46
+
47
+ @pool.with do |c|
48
+ # only allow one deploy mark for a given label for the next minute
49
+ lock = c.set("deploylock-#{label}", stamp, "nx", "ex", "60")
50
+ if lock
51
+ c.multi do |pipe|
52
+ pipe.hsetnx(key, stamp, label)
53
+ pipe.expire(key, MARK_TTL)
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ def fetch(date = Time.now.utc.to_date)
60
+ datecode = date.strftime("%Y%m%d")
61
+ @pool.with { |c| c.hgetall("#{datecode}-marks") }
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq/component"
4
+ require "sidekiq/launcher"
5
+ require "sidekiq/metrics/tracking"
6
+
7
+ module Sidekiq
8
+ class Embedded
9
+ include Sidekiq::Component
10
+
11
+ def initialize(config)
12
+ @config = config
13
+ end
14
+
15
+ def run
16
+ housekeeping
17
+ fire_event(:startup, reverse: false, reraise: true)
18
+ @launcher = Sidekiq::Launcher.new(@config, embedded: true)
19
+ @launcher.run
20
+ sleep 0.2 # pause to give threads time to spin up
21
+
22
+ logger.info "Sidekiq running embedded, total process thread count: #{Thread.list.size}"
23
+ logger.debug { Thread.list.map(&:name) }
24
+ end
25
+
26
+ def quiet
27
+ @launcher&.quiet
28
+ end
29
+
30
+ def stop
31
+ @launcher&.stop
32
+ end
33
+
34
+ private
35
+
36
+ def housekeeping
37
+ @config[:tag] ||= default_tag
38
+ logger.info "Running in #{RUBY_DESCRIPTION}"
39
+ logger.info Sidekiq::LICENSE
40
+ logger.info "Upgrade to Sidekiq Pro for more features and support: https://sidekiq.org" unless defined?(::Sidekiq::Pro)
41
+
42
+ # touch the connection pool so it is created before we
43
+ # fire startup and start multithreading.
44
+ info = config.redis_info
45
+ ver = Gem::Version.new(info["redis_version"])
46
+ raise "You are connected to Redis #{ver}, Sidekiq requires Redis 7.0.0 or greater" if ver < Gem::Version.new("7.0.0")
47
+
48
+ maxmemory_policy = info["maxmemory_policy"]
49
+ if maxmemory_policy != "noeviction"
50
+ logger.warn <<~EOM
51
+
52
+
53
+ WARNING: Your Redis instance will evict Sidekiq data under heavy load.
54
+ The 'noeviction' maxmemory policy is recommended (current policy: '#{maxmemory_policy}').
55
+ See: https://github.com/sidekiq/sidekiq/wiki/Using-Redis#memory
56
+
57
+ EOM
58
+ end
59
+
60
+ logger.debug { "Client Middleware: #{@config.default_capsule.client_middleware.map(&:klass).join(", ")}" }
61
+ logger.debug { "Server Middleware: #{@config.default_capsule.server_middleware.map(&:klass).join(", ")}" }
62
+ end
63
+ end
64
+ end
data/lib/sidekiq/fetch.rb CHANGED
@@ -2,10 +2,12 @@
2
2
 
3
3
  require "sidekiq"
4
4
  require "sidekiq/component"
5
+ require "sidekiq/capsule"
5
6
 
6
7
  module Sidekiq # :nodoc:
7
8
  class BasicFetch
8
9
  include Sidekiq::Component
10
+
9
11
  # We want the fetch operation to timeout every few seconds so the thread
10
12
  # can check if the process is shutting down.
11
13
  TIMEOUT = 2
@@ -26,31 +28,28 @@ module Sidekiq # :nodoc:
26
28
  end
27
29
  }
28
30
 
29
- def initialize(config)
30
- raise ArgumentError, "missing queue list" unless config[:queues]
31
- @config = config
32
- @strictly_ordered_queues = !!@config[:strict]
33
- @queues = @config[:queues].map { |q| "queue:#{q}" }
34
- if @strictly_ordered_queues
35
- @queues.uniq!
36
- @queues << {timeout: TIMEOUT}
37
- end
31
+ def initialize(cap)
32
+ raise ArgumentError, "missing queue list" unless cap.queues
33
+ @config = cap
34
+ @strictly_ordered_queues = cap.mode == :strict
35
+ @queues = config.queues.map { |q| "queue:#{q}" }
36
+ @queues.uniq! if @strictly_ordered_queues
38
37
  end
39
38
 
40
39
  def retrieve_work
41
40
  qs = queues_cmd
42
41
  # 4825 Sidekiq Pro with all queues paused will return an
43
- # empty set of queues with a trailing TIMEOUT value.
44
- if qs.size <= 1
42
+ # empty set of queues
43
+ if qs.size <= 0
45
44
  sleep(TIMEOUT)
46
45
  return nil
47
46
  end
48
47
 
49
- queue, job = redis { |conn| conn.brpop(*qs) }
48
+ queue, job = redis { |conn| conn.blocking_call(TIMEOUT, "brpop", *qs, TIMEOUT) }
50
49
  UnitOfWork.new(queue, job, config) if queue
51
50
  end
52
51
 
53
- def bulk_requeue(inprogress, options)
52
+ def bulk_requeue(inprogress)
54
53
  return if inprogress.empty?
55
54
 
56
55
  logger.debug { "Re-queueing terminated jobs" }
@@ -83,7 +82,6 @@ module Sidekiq # :nodoc:
83
82
  else
84
83
  permute = @queues.shuffle
85
84
  permute.uniq!
86
- permute << {timeout: TIMEOUT}
87
85
  permute
88
86
  end
89
87
  end
@@ -0,0 +1,56 @@
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_cancel
50
+ # def on_complete
51
+ # def around_iteration
52
+ #
53
+ # To keep things simple and compatible, this is the same
54
+ # API as the `sidekiq-iteration` gem.
55
+ end
56
+ 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