sr-sidekiq 4.1.6

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 (186) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/3.0-Upgrade.md +70 -0
  4. data/4.0-Upgrade.md +50 -0
  5. data/COMM-LICENSE (sidekiq) +95 -0
  6. data/Changes.md +1241 -0
  7. data/Ent-Changes.md +112 -0
  8. data/Gemfile +29 -0
  9. data/LICENSE (sidekiq) +9 -0
  10. data/LICENSE (sr-sidekiq) +5 -0
  11. data/Pro-2.0-Upgrade.md +138 -0
  12. data/Pro-3.0-Upgrade.md +44 -0
  13. data/Pro-Changes.md +539 -0
  14. data/README.md +8 -0
  15. data/Rakefile +9 -0
  16. data/bin/sidekiq +18 -0
  17. data/bin/sidekiqctl +99 -0
  18. data/bin/sidekiqload +167 -0
  19. data/code_of_conduct.md +50 -0
  20. data/lib/generators/sidekiq/templates/worker.rb.erb +9 -0
  21. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +6 -0
  22. data/lib/generators/sidekiq/templates/worker_test.rb.erb +8 -0
  23. data/lib/generators/sidekiq/worker_generator.rb +49 -0
  24. data/lib/sidekiq.rb +237 -0
  25. data/lib/sidekiq/api.rb +844 -0
  26. data/lib/sidekiq/cli.rb +389 -0
  27. data/lib/sidekiq/client.rb +260 -0
  28. data/lib/sidekiq/core_ext.rb +106 -0
  29. data/lib/sidekiq/exception_handler.rb +31 -0
  30. data/lib/sidekiq/extensions/action_mailer.rb +57 -0
  31. data/lib/sidekiq/extensions/active_record.rb +40 -0
  32. data/lib/sidekiq/extensions/class_methods.rb +40 -0
  33. data/lib/sidekiq/extensions/generic_proxy.rb +25 -0
  34. data/lib/sidekiq/fetch.rb +81 -0
  35. data/lib/sidekiq/launcher.rb +160 -0
  36. data/lib/sidekiq/logging.rb +106 -0
  37. data/lib/sidekiq/manager.rb +137 -0
  38. data/lib/sidekiq/middleware/chain.rb +150 -0
  39. data/lib/sidekiq/middleware/i18n.rb +42 -0
  40. data/lib/sidekiq/middleware/server/active_record.rb +13 -0
  41. data/lib/sidekiq/middleware/server/logging.rb +40 -0
  42. data/lib/sidekiq/middleware/server/retry_jobs.rb +205 -0
  43. data/lib/sidekiq/paginator.rb +43 -0
  44. data/lib/sidekiq/processor.rb +186 -0
  45. data/lib/sidekiq/rails.rb +39 -0
  46. data/lib/sidekiq/redis_connection.rb +97 -0
  47. data/lib/sidekiq/scheduled.rb +146 -0
  48. data/lib/sidekiq/testing.rb +316 -0
  49. data/lib/sidekiq/testing/inline.rb +29 -0
  50. data/lib/sidekiq/util.rb +62 -0
  51. data/lib/sidekiq/version.rb +4 -0
  52. data/lib/sidekiq/web.rb +278 -0
  53. data/lib/sidekiq/web_helpers.rb +255 -0
  54. data/lib/sidekiq/worker.rb +121 -0
  55. data/sidekiq.gemspec +26 -0
  56. data/sr-sidekiq-4.1.3.gem +0 -0
  57. data/sr-sidekiq-4.1.4.gem +0 -0
  58. data/sr-sidekiq-4.1.5.gem +0 -0
  59. data/test/config.yml +9 -0
  60. data/test/env_based_config.yml +11 -0
  61. data/test/fake_env.rb +1 -0
  62. data/test/fixtures/en.yml +2 -0
  63. data/test/helper.rb +75 -0
  64. data/test/test_actors.rb +138 -0
  65. data/test/test_api.rb +528 -0
  66. data/test/test_cli.rb +406 -0
  67. data/test/test_client.rb +262 -0
  68. data/test/test_exception_handler.rb +56 -0
  69. data/test/test_extensions.rb +127 -0
  70. data/test/test_fetch.rb +50 -0
  71. data/test/test_launcher.rb +85 -0
  72. data/test/test_logging.rb +35 -0
  73. data/test/test_manager.rb +50 -0
  74. data/test/test_middleware.rb +158 -0
  75. data/test/test_processor.rb +201 -0
  76. data/test/test_rails.rb +22 -0
  77. data/test/test_redis_connection.rb +127 -0
  78. data/test/test_retry.rb +326 -0
  79. data/test/test_retry_exhausted.rb +149 -0
  80. data/test/test_scheduled.rb +115 -0
  81. data/test/test_scheduling.rb +50 -0
  82. data/test/test_sidekiq.rb +107 -0
  83. data/test/test_testing.rb +143 -0
  84. data/test/test_testing_fake.rb +357 -0
  85. data/test/test_testing_inline.rb +94 -0
  86. data/test/test_util.rb +13 -0
  87. data/test/test_web.rb +614 -0
  88. data/test/test_web_helpers.rb +54 -0
  89. data/web/assets/images/bootstrap/glyphicons-halflings-white.png +0 -0
  90. data/web/assets/images/bootstrap/glyphicons-halflings.png +0 -0
  91. data/web/assets/images/favicon.ico +0 -0
  92. data/web/assets/images/logo.png +0 -0
  93. data/web/assets/images/status-sd8051fd480.png +0 -0
  94. data/web/assets/images/status/active.png +0 -0
  95. data/web/assets/images/status/idle.png +0 -0
  96. data/web/assets/javascripts/application.js +88 -0
  97. data/web/assets/javascripts/dashboard.js +300 -0
  98. data/web/assets/javascripts/locales/README.md +27 -0
  99. data/web/assets/javascripts/locales/jquery.timeago.ar.js +96 -0
  100. data/web/assets/javascripts/locales/jquery.timeago.bg.js +18 -0
  101. data/web/assets/javascripts/locales/jquery.timeago.bs.js +49 -0
  102. data/web/assets/javascripts/locales/jquery.timeago.ca.js +18 -0
  103. data/web/assets/javascripts/locales/jquery.timeago.cs.js +18 -0
  104. data/web/assets/javascripts/locales/jquery.timeago.cy.js +20 -0
  105. data/web/assets/javascripts/locales/jquery.timeago.da.js +18 -0
  106. data/web/assets/javascripts/locales/jquery.timeago.de.js +18 -0
  107. data/web/assets/javascripts/locales/jquery.timeago.el.js +18 -0
  108. data/web/assets/javascripts/locales/jquery.timeago.en-short.js +20 -0
  109. data/web/assets/javascripts/locales/jquery.timeago.en.js +20 -0
  110. data/web/assets/javascripts/locales/jquery.timeago.es.js +18 -0
  111. data/web/assets/javascripts/locales/jquery.timeago.et.js +18 -0
  112. data/web/assets/javascripts/locales/jquery.timeago.fa.js +22 -0
  113. data/web/assets/javascripts/locales/jquery.timeago.fi.js +28 -0
  114. data/web/assets/javascripts/locales/jquery.timeago.fr-short.js +16 -0
  115. data/web/assets/javascripts/locales/jquery.timeago.fr.js +17 -0
  116. data/web/assets/javascripts/locales/jquery.timeago.he.js +18 -0
  117. data/web/assets/javascripts/locales/jquery.timeago.hr.js +49 -0
  118. data/web/assets/javascripts/locales/jquery.timeago.hu.js +18 -0
  119. data/web/assets/javascripts/locales/jquery.timeago.hy.js +18 -0
  120. data/web/assets/javascripts/locales/jquery.timeago.id.js +18 -0
  121. data/web/assets/javascripts/locales/jquery.timeago.it.js +16 -0
  122. data/web/assets/javascripts/locales/jquery.timeago.ja.js +19 -0
  123. data/web/assets/javascripts/locales/jquery.timeago.ko.js +17 -0
  124. data/web/assets/javascripts/locales/jquery.timeago.lt.js +20 -0
  125. data/web/assets/javascripts/locales/jquery.timeago.mk.js +20 -0
  126. data/web/assets/javascripts/locales/jquery.timeago.nl.js +20 -0
  127. data/web/assets/javascripts/locales/jquery.timeago.no.js +18 -0
  128. data/web/assets/javascripts/locales/jquery.timeago.pl.js +31 -0
  129. data/web/assets/javascripts/locales/jquery.timeago.pt-br.js +16 -0
  130. data/web/assets/javascripts/locales/jquery.timeago.pt.js +16 -0
  131. data/web/assets/javascripts/locales/jquery.timeago.ro.js +18 -0
  132. data/web/assets/javascripts/locales/jquery.timeago.rs.js +49 -0
  133. data/web/assets/javascripts/locales/jquery.timeago.ru.js +34 -0
  134. data/web/assets/javascripts/locales/jquery.timeago.sk.js +18 -0
  135. data/web/assets/javascripts/locales/jquery.timeago.sl.js +44 -0
  136. data/web/assets/javascripts/locales/jquery.timeago.sv.js +18 -0
  137. data/web/assets/javascripts/locales/jquery.timeago.th.js +20 -0
  138. data/web/assets/javascripts/locales/jquery.timeago.tr.js +16 -0
  139. data/web/assets/javascripts/locales/jquery.timeago.uk.js +34 -0
  140. data/web/assets/javascripts/locales/jquery.timeago.uz.js +19 -0
  141. data/web/assets/javascripts/locales/jquery.timeago.zh-cn.js +20 -0
  142. data/web/assets/javascripts/locales/jquery.timeago.zh-tw.js +20 -0
  143. data/web/assets/stylesheets/application.css +754 -0
  144. data/web/assets/stylesheets/bootstrap.css +9 -0
  145. data/web/locales/cs.yml +78 -0
  146. data/web/locales/da.yml +68 -0
  147. data/web/locales/de.yml +69 -0
  148. data/web/locales/el.yml +68 -0
  149. data/web/locales/en.yml +79 -0
  150. data/web/locales/es.yml +69 -0
  151. data/web/locales/fr.yml +78 -0
  152. data/web/locales/hi.yml +75 -0
  153. data/web/locales/it.yml +69 -0
  154. data/web/locales/ja.yml +78 -0
  155. data/web/locales/ko.yml +68 -0
  156. data/web/locales/nb.yml +77 -0
  157. data/web/locales/nl.yml +68 -0
  158. data/web/locales/pl.yml +59 -0
  159. data/web/locales/pt-br.yml +68 -0
  160. data/web/locales/pt.yml +67 -0
  161. data/web/locales/ru.yml +78 -0
  162. data/web/locales/sv.yml +68 -0
  163. data/web/locales/ta.yml +75 -0
  164. data/web/locales/uk.yml +76 -0
  165. data/web/locales/zh-cn.yml +68 -0
  166. data/web/locales/zh-tw.yml +68 -0
  167. data/web/views/_footer.erb +17 -0
  168. data/web/views/_job_info.erb +88 -0
  169. data/web/views/_nav.erb +66 -0
  170. data/web/views/_paging.erb +23 -0
  171. data/web/views/_poll_js.erb +5 -0
  172. data/web/views/_poll_link.erb +7 -0
  173. data/web/views/_status.erb +4 -0
  174. data/web/views/_summary.erb +40 -0
  175. data/web/views/busy.erb +94 -0
  176. data/web/views/dashboard.erb +75 -0
  177. data/web/views/dead.erb +34 -0
  178. data/web/views/layout.erb +32 -0
  179. data/web/views/morgue.erb +71 -0
  180. data/web/views/queue.erb +45 -0
  181. data/web/views/queues.erb +28 -0
  182. data/web/views/retries.erb +74 -0
  183. data/web/views/retry.erb +34 -0
  184. data/web/views/scheduled.erb +54 -0
  185. data/web/views/scheduled_job_info.erb +8 -0
  186. metadata +408 -0
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+ module Sidekiq
3
+ def self.hook_rails!
4
+ return if defined?(@delay_removed)
5
+
6
+ ActiveSupport.on_load(:active_record) do
7
+ include Sidekiq::Extensions::ActiveRecord
8
+ end
9
+
10
+ ActiveSupport.on_load(:action_mailer) do
11
+ extend Sidekiq::Extensions::ActionMailer
12
+ end
13
+
14
+ Module.__send__(:include, Sidekiq::Extensions::Klass)
15
+ end
16
+
17
+ # Removes the generic aliases which MAY clash with names of already
18
+ # created methods by other applications. The methods `sidekiq_delay`,
19
+ # `sidekiq_delay_for` and `sidekiq_delay_until` can be used instead.
20
+ def self.remove_delay!
21
+ @delay_removed = true
22
+
23
+ [Extensions::ActiveRecord,
24
+ Extensions::ActionMailer,
25
+ Extensions::Klass].each do |mod|
26
+ mod.module_eval do
27
+ remove_method :delay if respond_to?(:delay)
28
+ remove_method :delay_for if respond_to?(:delay_for)
29
+ remove_method :delay_until if respond_to?(:delay_until)
30
+ end
31
+ end
32
+ end
33
+
34
+ class Rails < ::Rails::Engine
35
+ initializer 'sidekiq' do
36
+ Sidekiq.hook_rails!
37
+ end
38
+ end if defined?(::Rails)
39
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+ require 'connection_pool'
3
+ require 'redis'
4
+ require 'uri'
5
+
6
+ module Sidekiq
7
+ class RedisConnection
8
+ class << self
9
+
10
+ def create(options={})
11
+ options[:url] ||= determine_redis_provider
12
+
13
+ size = options[:size] || (Sidekiq.server? ? (Sidekiq.options[:concurrency] + 5) : 5)
14
+
15
+ verify_sizing(size, Sidekiq.options[:concurrency]) if Sidekiq.server?
16
+
17
+ pool_timeout = options[:pool_timeout] || 1
18
+ log_info(options)
19
+
20
+ ConnectionPool.new(:timeout => pool_timeout, :size => size) do
21
+ build_client(options)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ # Sidekiq needs a lot of concurrent Redis connections.
28
+ #
29
+ # We need a connection for each Processor.
30
+ # We need a connection for Pro's real-time change listener
31
+ # We need a connection to various features to call Redis every few seconds:
32
+ # - the process heartbeat.
33
+ # - enterprise's leader election
34
+ # - enterprise's cron support
35
+ def verify_sizing(size, concurrency)
36
+ raise ArgumentError, "Your Redis connection pool is too small for Sidekiq to work, your pool has #{size} connections but really needs to have at least #{concurrency + 2}" if size <= concurrency
37
+ end
38
+
39
+ def build_client(options)
40
+ namespace = options[:namespace]
41
+
42
+ client = Redis.new client_opts(options)
43
+ if namespace
44
+ begin
45
+ require 'redis/namespace'
46
+ Redis::Namespace.new(namespace, :redis => client)
47
+ rescue LoadError
48
+ Sidekiq.logger.error("Your Redis configuration use the namespace '#{namespace}' but the redis-namespace gem not included in Gemfile." \
49
+ "Add the gem to your Gemfile in case you would like to keep using a namespace, otherwise remove the namespace parameter.")
50
+ exit(-127)
51
+ end
52
+ else
53
+ client
54
+ end
55
+ end
56
+
57
+ def client_opts(options)
58
+ opts = options.dup
59
+ if opts[:namespace]
60
+ opts.delete(:namespace)
61
+ end
62
+
63
+ if opts[:network_timeout]
64
+ opts[:timeout] = opts[:network_timeout]
65
+ opts.delete(:network_timeout)
66
+ end
67
+
68
+ opts[:driver] = opts[:driver] || 'ruby'
69
+
70
+ opts
71
+ end
72
+
73
+ def log_info(options)
74
+ # Don't log Redis AUTH password
75
+ redacted = "REDACTED"
76
+ scrubbed_options = options.dup
77
+ if scrubbed_options[:url] && (uri = URI.parse(scrubbed_options[:url])) && uri.password
78
+ uri.password = redacted
79
+ scrubbed_options[:url] = uri.to_s
80
+ end
81
+ if scrubbed_options[:password]
82
+ scrubbed_options[:password] = redacted
83
+ end
84
+ if Sidekiq.server?
85
+ Sidekiq.logger.info("Booting Sidekiq #{Sidekiq::VERSION} with redis options #{scrubbed_options}")
86
+ else
87
+ Sidekiq.logger.debug("#{Sidekiq::NAME} client with redis options #{scrubbed_options}")
88
+ end
89
+ end
90
+
91
+ def determine_redis_provider
92
+ ENV[ENV['REDIS_PROVIDER'] || 'REDIS_URL']
93
+ end
94
+
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+ require 'sidekiq'
3
+ require 'sidekiq/util'
4
+ require 'sidekiq/api'
5
+
6
+ module Sidekiq
7
+ module Scheduled
8
+ SETS = %w(retry schedule)
9
+
10
+ class Enq
11
+ def enqueue_jobs(now=Time.now.to_f.to_s, sorted_sets=SETS)
12
+ # A job's "score" in Redis is the time at which it should be processed.
13
+ # Just check Redis for the set of jobs with a timestamp before now.
14
+ Sidekiq.redis do |conn|
15
+ sorted_sets.each do |sorted_set|
16
+ # Get the next item in the queue if it's score (time to execute) is <= now.
17
+ # We need to go through the list one at a time to reduce the risk of something
18
+ # going wrong between the time jobs are popped from the scheduled queue and when
19
+ # they are pushed onto a work queue and losing the jobs.
20
+ while job = conn.zrangebyscore(sorted_set, '-inf'.freeze, now, :limit => [0, 1]).first do
21
+
22
+ # Pop item off the queue and add it to the work queue. If the job can't be popped from
23
+ # the queue, it's because another process already popped it so we can move on to the
24
+ # next one.
25
+ if conn.zrem(sorted_set, job)
26
+ Sidekiq::Client.push(Sidekiq.load_json(job))
27
+ Sidekiq::Logging.logger.debug { "enqueued #{sorted_set}: #{job}" }
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ ##
36
+ # The Poller checks Redis every N seconds for jobs in the retry or scheduled
37
+ # set have passed their timestamp and should be enqueued. If so, it
38
+ # just pops the job back onto its original queue so the
39
+ # workers can pick it up like any other job.
40
+ class Poller
41
+ include Util
42
+
43
+ INITIAL_WAIT = 10
44
+
45
+ def initialize
46
+ @enq = (Sidekiq.options[:scheduled_enq] || Sidekiq::Scheduled::Enq).new
47
+ @sleeper = ConnectionPool::TimedStack.new
48
+ @done = false
49
+ end
50
+
51
+ # Shut down this instance, will pause until the thread is dead.
52
+ def terminate
53
+ @done = true
54
+ if @thread
55
+ t = @thread
56
+ @thread = nil
57
+ @sleeper << 0
58
+ t.value
59
+ end
60
+ end
61
+
62
+ def start
63
+ @thread ||= safe_thread("scheduler") do
64
+ initial_wait
65
+
66
+ while !@done
67
+ enqueue
68
+ wait
69
+ end
70
+ Sidekiq.logger.info("Scheduler exiting...")
71
+ end
72
+ end
73
+
74
+ def enqueue
75
+ begin
76
+ @enq.enqueue_jobs
77
+ rescue => ex
78
+ # Most likely a problem with redis networking.
79
+ # Punt and try again at the next interval
80
+ logger.error ex.message
81
+ ex.backtrace.each do |bt|
82
+ logger.error(bt)
83
+ end
84
+ end
85
+ end
86
+
87
+ private
88
+
89
+ def wait
90
+ @sleeper.pop(random_poll_interval)
91
+ rescue Timeout::Error
92
+ # expected
93
+ rescue => ex
94
+ # if poll_interval_average hasn't been calculated yet, we can
95
+ # raise an error trying to reach Redis.
96
+ logger.error ex.message
97
+ logger.error ex.backtrace.first
98
+ sleep 5
99
+ end
100
+
101
+ # Calculates a random interval that is ±50% the desired average.
102
+ def random_poll_interval
103
+ poll_interval_average * rand + poll_interval_average.to_f / 2
104
+ end
105
+
106
+ # We do our best to tune the poll interval to the size of the active Sidekiq
107
+ # cluster. If you have 30 processes and poll every 15 seconds, that means one
108
+ # Sidekiq is checking Redis every 0.5 seconds - way too often for most people
109
+ # and really bad if the retry or scheduled sets are large.
110
+ #
111
+ # Instead try to avoid polling more than once every 15 seconds. If you have
112
+ # 30 Sidekiq processes, we'll poll every 30 * 15 or 450 seconds.
113
+ # To keep things statistically random, we'll sleep a random amount between
114
+ # 225 and 675 seconds for each poll or 450 seconds on average. Otherwise restarting
115
+ # all your Sidekiq processes at the same time will lead to them all polling at
116
+ # the same time: the thundering herd problem.
117
+ #
118
+ # We only do this if poll_interval_average is unset (the default).
119
+ def poll_interval_average
120
+ Sidekiq.options[:poll_interval_average] ||= scaled_poll_interval
121
+ end
122
+
123
+ # Calculates an average poll interval based on the number of known Sidekiq processes.
124
+ # This minimizes a single point of failure by dispersing check-ins but without taxing
125
+ # Redis if you run many Sidekiq processes.
126
+ def scaled_poll_interval
127
+ pcount = Sidekiq::ProcessSet.new.size
128
+ pcount = 1 if pcount == 0
129
+ pcount * Sidekiq.options[:average_scheduled_poll_interval]
130
+ end
131
+
132
+ def initial_wait
133
+ # Have all processes sleep between 5-15 seconds. 10 seconds
134
+ # to give time for the heartbeat to register (if the poll interval is going to be calculated by the number
135
+ # of workers), and 5 random seconds to ensure they don't all hit Redis at the same time.
136
+ total = 0
137
+ total += INITIAL_WAIT unless Sidekiq.options[:poll_interval_average]
138
+ total += (5 * rand)
139
+
140
+ @sleeper.pop(total)
141
+ rescue Timeout::Error
142
+ end
143
+
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,316 @@
1
+ # frozen_string_literal: true
2
+ require 'securerandom'
3
+ require 'sidekiq'
4
+
5
+ module Sidekiq
6
+
7
+ class Testing
8
+ class << self
9
+ attr_accessor :__test_mode
10
+
11
+ def __set_test_mode(mode)
12
+ if block_given?
13
+ current_mode = self.__test_mode
14
+ begin
15
+ self.__test_mode = mode
16
+ yield
17
+ ensure
18
+ self.__test_mode = current_mode
19
+ end
20
+ else
21
+ self.__test_mode = mode
22
+ end
23
+ end
24
+
25
+ def disable!(&block)
26
+ __set_test_mode(:disable, &block)
27
+ end
28
+
29
+ def fake!(&block)
30
+ __set_test_mode(:fake, &block)
31
+ end
32
+
33
+ def inline!(&block)
34
+ __set_test_mode(:inline, &block)
35
+ end
36
+
37
+ def enabled?
38
+ self.__test_mode != :disable
39
+ end
40
+
41
+ def disabled?
42
+ self.__test_mode == :disable
43
+ end
44
+
45
+ def fake?
46
+ self.__test_mode == :fake
47
+ end
48
+
49
+ def inline?
50
+ self.__test_mode == :inline
51
+ end
52
+
53
+ def server_middleware
54
+ @server_chain ||= Middleware::Chain.new
55
+ yield @server_chain if block_given?
56
+ @server_chain
57
+ end
58
+ end
59
+ end
60
+
61
+ # Default to fake testing to keep old behavior
62
+ Sidekiq::Testing.fake!
63
+
64
+ class EmptyQueueError < RuntimeError; end
65
+
66
+ class Client
67
+ alias_method :raw_push_real, :raw_push
68
+
69
+ def raw_push(payloads)
70
+ if Sidekiq::Testing.fake?
71
+ payloads.each do |job|
72
+ Queues.push(job['queue'], job['class'], Sidekiq.load_json(Sidekiq.dump_json(job)))
73
+ end
74
+ true
75
+ elsif Sidekiq::Testing.inline?
76
+ payloads.each do |job|
77
+ klass = job['class'].constantize
78
+ job['id'] ||= SecureRandom.hex(12)
79
+ job_hash = Sidekiq.load_json(Sidekiq.dump_json(job))
80
+ klass.process_job(job_hash)
81
+ end
82
+ true
83
+ else
84
+ raw_push_real(payloads)
85
+ end
86
+ end
87
+ end
88
+
89
+ module Queues
90
+ ##
91
+ # The Queues class is only for testing the fake queue implementation.
92
+ # There are 2 data structures involved in tandem. This is due to the
93
+ # Rspec syntax of change(QueueWorker.jobs, :size). It keeps a reference
94
+ # to the array. Because the array was dervied from a filter of the total
95
+ # jobs enqueued, it appeared as though the array didn't change.
96
+ #
97
+ # To solve this, we'll keep 2 hashes containing the jobs. One with keys based
98
+ # on the queue, and another with keys of the worker names, so the array for
99
+ # QueueWorker.jobs is a straight reference to a real array.
100
+ #
101
+ # Queue-based hash:
102
+ #
103
+ # {
104
+ # "default"=>[
105
+ # {
106
+ # "class"=>"TestTesting::QueueWorker",
107
+ # "args"=>[1, 2],
108
+ # "retry"=>true,
109
+ # "queue"=>"default",
110
+ # "jid"=>"abc5b065c5c4b27fc1102833",
111
+ # "created_at"=>1447445554.419934
112
+ # }
113
+ # ]
114
+ # }
115
+ #
116
+ # Worker-based hash:
117
+ #
118
+ # {
119
+ # "TestTesting::QueueWorker"=>[
120
+ # {
121
+ # "class"=>"TestTesting::QueueWorker",
122
+ # "args"=>[1, 2],
123
+ # "retry"=>true,
124
+ # "queue"=>"default",
125
+ # "jid"=>"abc5b065c5c4b27fc1102833",
126
+ # "created_at"=>1447445554.419934
127
+ # }
128
+ # ]
129
+ # }
130
+ #
131
+ # Example:
132
+ #
133
+ # require 'sidekiq/testing'
134
+ #
135
+ # assert_equal 0, Sidekiq::Queues["default"].size
136
+ # HardWorker.perform_async(:something)
137
+ # assert_equal 1, Sidekiq::Queues["default"].size
138
+ # assert_equal :something, Sidekiq::Queues["default"].first['args'][0]
139
+ #
140
+ # You can also clear all workers' jobs:
141
+ #
142
+ # assert_equal 0, Sidekiq::Queues["default"].size
143
+ # HardWorker.perform_async(:something)
144
+ # Sidekiq::Queues.clear_all
145
+ # assert_equal 0, Sidekiq::Queues["default"].size
146
+ #
147
+ # This can be useful to make sure jobs don't linger between tests:
148
+ #
149
+ # RSpec.configure do |config|
150
+ # config.before(:each) do
151
+ # Sidekiq::Queues.clear_all
152
+ # end
153
+ # end
154
+ #
155
+ class << self
156
+ def [](queue)
157
+ jobs_by_queue[queue]
158
+ end
159
+
160
+ def push(queue, klass, job)
161
+ jobs_by_queue[queue] << job
162
+ jobs_by_worker[klass] << job
163
+ end
164
+
165
+ def jobs_by_queue
166
+ @jobs_by_queue ||= Hash.new { |hash, key| hash[key] = [] }
167
+ end
168
+
169
+ def jobs_by_worker
170
+ @jobs_by_worker ||= Hash.new { |hash, key| hash[key] = [] }
171
+ end
172
+
173
+ def delete_for(jid, queue, klass)
174
+ jobs_by_queue[queue.to_s].delete_if { |job| job["jid"] == jid }
175
+ jobs_by_worker[klass].delete_if { |job| job["jid"] == jid }
176
+ end
177
+
178
+ def clear_for(queue, klass)
179
+ jobs_by_queue[queue].clear
180
+ jobs_by_worker[klass].clear
181
+ end
182
+
183
+ def clear_all
184
+ jobs_by_queue.clear
185
+ jobs_by_worker.clear
186
+ end
187
+ end
188
+ end
189
+
190
+ module Worker
191
+ ##
192
+ # The Sidekiq testing infrastructure overrides perform_async
193
+ # so that it does not actually touch the network. Instead it
194
+ # stores the asynchronous jobs in a per-class array so that
195
+ # their presence/absence can be asserted by your tests.
196
+ #
197
+ # This is similar to ActionMailer's :test delivery_method and its
198
+ # ActionMailer::Base.deliveries array.
199
+ #
200
+ # Example:
201
+ #
202
+ # require 'sidekiq/testing'
203
+ #
204
+ # assert_equal 0, HardWorker.jobs.size
205
+ # HardWorker.perform_async(:something)
206
+ # assert_equal 1, HardWorker.jobs.size
207
+ # assert_equal :something, HardWorker.jobs[0]['args'][0]
208
+ #
209
+ # assert_equal 0, Sidekiq::Extensions::DelayedMailer.jobs.size
210
+ # MyMailer.delay.send_welcome_email('foo@example.com')
211
+ # assert_equal 1, Sidekiq::Extensions::DelayedMailer.jobs.size
212
+ #
213
+ # You can also clear and drain all workers' jobs:
214
+ #
215
+ # assert_equal 0, Sidekiq::Extensions::DelayedMailer.jobs.size
216
+ # assert_equal 0, Sidekiq::Extensions::DelayedModel.jobs.size
217
+ #
218
+ # MyMailer.delay.send_welcome_email('foo@example.com')
219
+ # MyModel.delay.do_something_hard
220
+ #
221
+ # assert_equal 1, Sidekiq::Extensions::DelayedMailer.jobs.size
222
+ # assert_equal 1, Sidekiq::Extensions::DelayedModel.jobs.size
223
+ #
224
+ # Sidekiq::Worker.clear_all # or .drain_all
225
+ #
226
+ # assert_equal 0, Sidekiq::Extensions::DelayedMailer.jobs.size
227
+ # assert_equal 0, Sidekiq::Extensions::DelayedModel.jobs.size
228
+ #
229
+ # This can be useful to make sure jobs don't linger between tests:
230
+ #
231
+ # RSpec.configure do |config|
232
+ # config.before(:each) do
233
+ # Sidekiq::Worker.clear_all
234
+ # end
235
+ # end
236
+ #
237
+ # or for acceptance testing, i.e. with cucumber:
238
+ #
239
+ # AfterStep do
240
+ # Sidekiq::Worker.drain_all
241
+ # end
242
+ #
243
+ # When I sign up as "foo@example.com"
244
+ # Then I should receive a welcome email to "foo@example.com"
245
+ #
246
+ module ClassMethods
247
+
248
+ # Queue for this worker
249
+ def queue
250
+ self.sidekiq_options["queue"]
251
+ end
252
+
253
+ # Jobs queued for this worker
254
+ def jobs
255
+ Queues.jobs_by_worker[self.to_s]
256
+ end
257
+
258
+ # Clear all jobs for this worker
259
+ def clear
260
+ Queues.clear_for(queue, self.to_s)
261
+ end
262
+
263
+ # Drain and run all jobs for this worker
264
+ def drain
265
+ while jobs.any?
266
+ next_job = jobs.first
267
+ Queues.delete_for(next_job["jid"], queue, self.to_s)
268
+ process_job(next_job)
269
+ end
270
+ end
271
+
272
+ # Pop out a single job and perform it
273
+ def perform_one
274
+ raise(EmptyQueueError, "perform_one called with empty job queue") if jobs.empty?
275
+ next_job = jobs.first
276
+ Queues.delete_for(next_job["jid"], queue, self.to_s)
277
+ process_job(next_job)
278
+ end
279
+
280
+ def process_job(job)
281
+ worker = new
282
+ worker.jid = job['jid']
283
+ worker.bid = job['bid'] if worker.respond_to?(:bid=)
284
+ Sidekiq::Testing.server_middleware.invoke(worker, job, job['queue']) do
285
+ execute_job(worker, job['args'])
286
+ end
287
+ end
288
+
289
+ def execute_job(worker, args)
290
+ worker.perform(*args)
291
+ end
292
+ end
293
+
294
+ class << self
295
+ def jobs # :nodoc:
296
+ Queues.jobs_by_queue.values.flatten
297
+ end
298
+
299
+ # Clear all queued jobs across all workers
300
+ def clear_all
301
+ Queues.clear_all
302
+ end
303
+
304
+ # Drain all queued jobs across all workers
305
+ def drain_all
306
+ while jobs.any?
307
+ worker_classes = jobs.map { |job| job["class"] }.uniq
308
+
309
+ worker_classes.each do |worker_class|
310
+ worker_class.constantize.drain
311
+ end
312
+ end
313
+ end
314
+ end
315
+ end
316
+ end