que 0.11.3 → 2.2.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.
Files changed (114) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/tests.yml +51 -0
  3. data/.gitignore +2 -0
  4. data/.ruby-version +1 -0
  5. data/CHANGELOG.md +502 -97
  6. data/Dockerfile +20 -0
  7. data/LICENSE.txt +1 -1
  8. data/README.md +205 -59
  9. data/auto/dev +21 -0
  10. data/auto/pre-push-hook +30 -0
  11. data/auto/psql +9 -0
  12. data/auto/test +5 -0
  13. data/auto/test-postgres-14 +17 -0
  14. data/bin/que +8 -81
  15. data/docker-compose.yml +47 -0
  16. data/docs/README.md +881 -0
  17. data/lib/que/active_job/extensions.rb +114 -0
  18. data/lib/que/active_record/connection.rb +51 -0
  19. data/lib/que/active_record/model.rb +48 -0
  20. data/lib/que/command_line_interface.rb +259 -0
  21. data/lib/que/connection.rb +198 -0
  22. data/lib/que/connection_pool.rb +78 -0
  23. data/lib/que/job.rb +210 -103
  24. data/lib/que/job_buffer.rb +255 -0
  25. data/lib/que/job_methods.rb +176 -0
  26. data/lib/que/listener.rb +176 -0
  27. data/lib/que/locker.rb +507 -0
  28. data/lib/que/metajob.rb +47 -0
  29. data/lib/que/migrations/4/down.sql +48 -0
  30. data/lib/que/migrations/4/up.sql +267 -0
  31. data/lib/que/migrations/5/down.sql +73 -0
  32. data/lib/que/migrations/5/up.sql +76 -0
  33. data/lib/que/migrations/6/down.sql +8 -0
  34. data/lib/que/migrations/6/up.sql +8 -0
  35. data/lib/que/migrations/7/down.sql +5 -0
  36. data/lib/que/migrations/7/up.sql +13 -0
  37. data/lib/que/migrations.rb +37 -18
  38. data/lib/que/poller.rb +274 -0
  39. data/lib/que/rails/railtie.rb +12 -0
  40. data/lib/que/result_queue.rb +35 -0
  41. data/lib/que/sequel/model.rb +52 -0
  42. data/lib/que/utils/assertions.rb +62 -0
  43. data/lib/que/utils/constantization.rb +19 -0
  44. data/lib/que/utils/error_notification.rb +68 -0
  45. data/lib/que/utils/freeze.rb +20 -0
  46. data/lib/que/utils/introspection.rb +50 -0
  47. data/lib/que/utils/json_serialization.rb +21 -0
  48. data/lib/que/utils/logging.rb +79 -0
  49. data/lib/que/utils/middleware.rb +46 -0
  50. data/lib/que/utils/queue_management.rb +18 -0
  51. data/lib/que/utils/ruby2_keywords.rb +19 -0
  52. data/lib/que/utils/transactions.rb +34 -0
  53. data/lib/que/version.rb +5 -1
  54. data/lib/que/worker.rb +145 -149
  55. data/lib/que.rb +103 -159
  56. data/que.gemspec +17 -4
  57. data/scripts/docker-entrypoint +14 -0
  58. data/scripts/test +6 -0
  59. metadata +59 -95
  60. data/.rspec +0 -2
  61. data/.travis.yml +0 -17
  62. data/Gemfile +0 -24
  63. data/docs/advanced_setup.md +0 -106
  64. data/docs/customizing_que.md +0 -200
  65. data/docs/error_handling.md +0 -47
  66. data/docs/inspecting_the_queue.md +0 -114
  67. data/docs/logging.md +0 -50
  68. data/docs/managing_workers.md +0 -80
  69. data/docs/migrating.md +0 -30
  70. data/docs/multiple_queues.md +0 -27
  71. data/docs/shutting_down_safely.md +0 -7
  72. data/docs/using_plain_connections.md +0 -41
  73. data/docs/using_sequel.md +0 -31
  74. data/docs/writing_reliable_jobs.md +0 -117
  75. data/lib/generators/que/install_generator.rb +0 -24
  76. data/lib/generators/que/templates/add_que.rb +0 -13
  77. data/lib/que/adapters/active_record.rb +0 -54
  78. data/lib/que/adapters/base.rb +0 -127
  79. data/lib/que/adapters/connection_pool.rb +0 -16
  80. data/lib/que/adapters/pg.rb +0 -21
  81. data/lib/que/adapters/pond.rb +0 -16
  82. data/lib/que/adapters/sequel.rb +0 -20
  83. data/lib/que/railtie.rb +0 -16
  84. data/lib/que/rake_tasks.rb +0 -59
  85. data/lib/que/sql.rb +0 -152
  86. data/spec/adapters/active_record_spec.rb +0 -152
  87. data/spec/adapters/connection_pool_spec.rb +0 -22
  88. data/spec/adapters/pg_spec.rb +0 -41
  89. data/spec/adapters/pond_spec.rb +0 -22
  90. data/spec/adapters/sequel_spec.rb +0 -57
  91. data/spec/gemfiles/Gemfile1 +0 -18
  92. data/spec/gemfiles/Gemfile2 +0 -18
  93. data/spec/spec_helper.rb +0 -118
  94. data/spec/support/helpers.rb +0 -19
  95. data/spec/support/jobs.rb +0 -35
  96. data/spec/support/shared_examples/adapter.rb +0 -37
  97. data/spec/support/shared_examples/multi_threaded_adapter.rb +0 -46
  98. data/spec/travis.rb +0 -23
  99. data/spec/unit/connection_spec.rb +0 -14
  100. data/spec/unit/customization_spec.rb +0 -251
  101. data/spec/unit/enqueue_spec.rb +0 -245
  102. data/spec/unit/helper_spec.rb +0 -12
  103. data/spec/unit/logging_spec.rb +0 -101
  104. data/spec/unit/migrations_spec.rb +0 -84
  105. data/spec/unit/pool_spec.rb +0 -365
  106. data/spec/unit/run_spec.rb +0 -14
  107. data/spec/unit/states_spec.rb +0 -50
  108. data/spec/unit/stats_spec.rb +0 -46
  109. data/spec/unit/transaction_spec.rb +0 -36
  110. data/spec/unit/work_spec.rb +0 -407
  111. data/spec/unit/worker_spec.rb +0 -167
  112. data/tasks/benchmark.rb +0 -3
  113. data/tasks/rspec.rb +0 -14
  114. data/tasks/safe_shutdown.rb +0 -67
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Logic for middleware to wrap jobs.
4
+
5
+ module Que
6
+ module Utils
7
+ module Middleware
8
+ TYPES = [
9
+ :job,
10
+ :sql,
11
+ ].freeze
12
+
13
+ TYPES.each do |type|
14
+ module_eval <<-CODE
15
+ def #{type}_middleware
16
+ @#{type}_middleware ||= []
17
+ end
18
+
19
+ def run_#{type}_middleware(*args)
20
+ m = #{type}_middleware
21
+
22
+ if m.empty?
23
+ yield
24
+ else
25
+ invoke_middleware(middleware: m.dup, args: args) { yield }
26
+ end
27
+ end
28
+ CODE
29
+ end
30
+
31
+ private
32
+
33
+ def invoke_middleware(middleware:, args:, &block)
34
+ if m = middleware.shift
35
+ r = nil
36
+ m.call(*args) do
37
+ r = invoke_middleware(middleware: middleware, args: args, &block)
38
+ end
39
+ r
40
+ else
41
+ yield
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Tools for managing the contents/state of the queue.
4
+
5
+ module Que
6
+ module Utils
7
+ module QueueManagement
8
+ def clear!
9
+ execute "DELETE FROM que_jobs"
10
+ end
11
+
12
+ # Very old migrations may use Que.create! and Que.drop!, which just
13
+ # created and dropped the initial version of the jobs table.
14
+ def create!; migrate!(version: 1); end
15
+ def drop!; migrate!(version: 0); end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Temporary module allowing ruby2 keyword args to be extracted from an *args splat
4
+ # Allows us to ensure consistent behaviour when running on ruby 2 vs ruby 3
5
+ # We can remove this if/when we drop support for ruby 2
6
+
7
+ require 'json'
8
+
9
+ module Que
10
+ module Utils
11
+ module Ruby2Keywords
12
+ def split_out_ruby2_keywords(args)
13
+ return [args, {}] unless args.last&.is_a?(Hash) && Hash.ruby2_keywords_hash?(args.last)
14
+
15
+ [args[0..-2], args.last]
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A helper method to manage transactions, used mainly by the migration system.
4
+ # It's available for general use, but if you're using an ORM that provides its
5
+ # own transaction helper, be sure to use that instead, or the two may interfere
6
+ # with one another.
7
+
8
+ module Que
9
+ module Utils
10
+ module Transactions
11
+ def transaction
12
+ pool.checkout do
13
+ if pool.in_transaction?
14
+ yield
15
+ else
16
+ begin
17
+ execute "BEGIN"
18
+ yield
19
+ rescue => error
20
+ raise
21
+ ensure
22
+ # Handle a raised error or a killed thread.
23
+ if error || Thread.current.status == 'aborting'
24
+ execute "ROLLBACK"
25
+ else
26
+ execute "COMMIT"
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
data/lib/que/version.rb CHANGED
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Que
4
- Version = '0.11.3'
4
+ VERSION = '2.2.0'
5
+
6
+ def self.job_schema_version
7
+ 2
8
+ end
5
9
  end
data/lib/que/worker.rb CHANGED
@@ -1,184 +1,180 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'monitor'
3
+ # Workers wrap threads which continuously pull job pks from JobBuffer objects,
4
+ # fetch and work those jobs, and export relevant data to ResultQueues.
5
+
6
+ require 'set'
4
7
 
5
8
  module Que
6
9
  class Worker
7
- # Each worker has a thread that does the actual work of running jobs.
8
- # Since both the worker's thread and whatever thread is managing the
9
- # worker are capable of affecting the worker's state, we need to
10
- # synchronize access to it.
11
- include MonitorMixin
12
-
13
- attr_reader :thread, :state, :queue
14
-
15
- def initialize(queue = '')
16
- super() # For MonitorMixin.
17
- @queue = queue
18
- @state = :working
19
- @thread = Thread.new { work_loop }
20
- @thread.abort_on_exception = true
21
- end
22
-
23
- def alive?
24
- !!@thread.status
25
- end
26
-
27
- def sleeping?
28
- synchronize { _sleeping? }
29
- end
30
-
31
- def working?
32
- synchronize { @state == :working }
33
- end
34
-
35
- def wake!
36
- synchronize do
37
- if sleeping?
38
- # Have to set the state here so that another thread checking
39
- # immediately after this won't see the worker as asleep.
40
- @state = :working
41
- @thread.wakeup
42
- true
43
- end
10
+ attr_reader :thread, :priority
11
+
12
+ VALID_LOG_LEVELS = [:debug, :info, :warn, :error, :fatal, :unknown].to_set.freeze
13
+
14
+ SQL[:check_job] =
15
+ %{
16
+ SELECT 1 AS one
17
+ FROM public.que_jobs
18
+ WHERE id = $1::bigint
19
+ }
20
+
21
+ def initialize(
22
+ job_buffer:,
23
+ result_queue:,
24
+ priority: nil,
25
+ start_callback: nil
26
+ )
27
+
28
+ @priority = Que.assert([NilClass, Integer], priority)
29
+ @job_buffer = Que.assert(JobBuffer, job_buffer)
30
+ @result_queue = Que.assert(ResultQueue, result_queue)
31
+
32
+ Que.internal_log(:worker_instantiate, self) do
33
+ {
34
+ priority: priority,
35
+ job_buffer: job_buffer.object_id,
36
+ result_queue: result_queue.object_id,
37
+ }
44
38
  end
45
- end
46
39
 
47
- # This needs to be called when trapping a signal, so it can't lock the monitor.
48
- def stop
49
- @stop = true
50
- @thread.wakeup if _sleeping?
40
+ @thread =
41
+ Thread.new do
42
+ # An error causing this thread to exit is a bug in Que, which we want
43
+ # to know about ASAP, so propagate the error if it happens.
44
+ Thread.current.abort_on_exception = true
45
+ start_callback.call(self) if start_callback.respond_to?(:call)
46
+ work_loop
47
+ end
51
48
  end
52
49
 
53
50
  def wait_until_stopped
54
- wait while alive?
51
+ @thread.join
55
52
  end
56
53
 
57
54
  private
58
55
 
59
- # Sleep very briefly while waiting for a thread to get somewhere.
60
- def wait
61
- sleep 0.0001
62
- end
63
-
64
- def _sleeping?
65
- if @state == :sleeping
66
- # There's a very small period of time between when the Worker marks
67
- # itself as sleeping and when it actually goes to sleep. Only report
68
- # true when we're certain the thread is sleeping.
69
- wait until @thread.status == 'sleep'
70
- true
71
- end
72
- end
73
-
74
56
  def work_loop
75
- loop do
76
- cycle = nil
77
-
78
- if Que.mode == :async
79
- time = Time.now
80
- result = Job.work(queue)
81
-
82
- case result[:event]
83
- when :job_unavailable
84
- cycle = false
85
- result[:level] = :debug
86
- when :job_race_condition
87
- cycle = true
88
- result[:level] = :debug
89
- when :job_worked
90
- cycle = true
91
- result[:elapsed] = (Time.now - time).round(5)
92
- when :job_errored
93
- # For PG::Errors, assume we had a problem reaching the database, and
94
- # don't hit it again right away.
95
- cycle = !result[:error].is_a?(PG::Error)
96
- result[:error] = {:class => result[:error].class.to_s, :message => result[:error].message}
97
- else
98
- raise "Unknown Event: #{result[:event].inspect}"
99
- end
100
-
101
- Que.log(result)
57
+ # Blocks until a job of the appropriate priority is available.
58
+ # `fetch_next_metajob` normally returns a job to be processed.
59
+ # If the queue is shutting down it will return false, which breaks the loop and
60
+ # lets the thread finish.
61
+ while (metajob = fetch_next_metajob) != false
62
+ # If metajob is nil instead of false, we've hit a rare race condition where
63
+ # there was a job in the buffer when the worker code checked, but the job was
64
+ # picked up by the time we got around to shifting it off the buffer.
65
+ # Letting this case go unhandled leads to worker threads exiting pre-maturely, so
66
+ # we check explicitly and continue the loop.
67
+ next if metajob.nil?
68
+ id = metajob.id
69
+
70
+ Que.internal_log(:worker_received_job, self) { {id: id} }
71
+
72
+ if Que.execute(:check_job, [id]).first
73
+ Que.recursively_freeze(metajob.job)
74
+ Que.internal_log(:worker_fetched_job, self) { {id: id} }
75
+
76
+ work_job(metajob)
77
+ else
78
+ # The job was locked but doesn't exist anymore, due to a race
79
+ # condition that exists because advisory locks don't obey MVCC. Not
80
+ # necessarily a problem, but if it happens a lot it may be meaningful.
81
+ Que.internal_log(:worker_job_lock_race_condition, self) { {id: id} }
102
82
  end
103
83
 
104
- synchronize { @state = :sleeping unless cycle || @stop }
105
- sleep if @state == :sleeping
106
- break if @stop
84
+ Que.internal_log(:worker_pushing_finished_job, self) { {id: id} }
85
+
86
+ @result_queue.push(
87
+ metajob: metajob,
88
+ message_type: :job_finished,
89
+ )
107
90
  end
108
- ensure
109
- @state = :stopped
110
91
  end
111
92
 
112
- # Setting Que.wake_interval = nil should ensure that the wrangler thread
113
- # doesn't wake up a worker again, even if it's currently sleeping for a
114
- # set period. So, we double-check that @wake_interval is set before waking
115
- # a worker, and make sure to wake up the wrangler when @wake_interval is
116
- # changed in Que.wake_interval= below.
117
- @wake_interval = 5
118
-
119
- # Four workers is a sensible default for most use cases.
120
- @worker_count = 4
93
+ def fetch_next_metajob
94
+ @job_buffer.shift(*priority)
95
+ end
121
96
 
122
- class << self
123
- attr_reader :mode, :wake_interval, :worker_count
124
- attr_accessor :queue_name
97
+ def work_job(metajob)
98
+ job = metajob.job
99
+ start = Time.now
100
+ klass = Que.constantize(job.fetch(:job_class))
101
+ instance = klass.new(job)
125
102
 
126
- # In order to work in a forking webserver, we need to be able to accept
127
- # worker_count and wake_interval settings without actually instantiating
128
- # the relevant threads until the mode is actually set to :async in a
129
- # post-fork hook (since forking will kill any running background threads).
103
+ Que.run_job_middleware(instance) { instance.tap(&:_run) }
130
104
 
131
- def mode=(mode)
132
- Que.log :event => 'mode_change', :value => mode.to_s
133
- @mode = mode
105
+ elapsed = Time.now - start
134
106
 
135
- if mode == :async
136
- set_up_workers
137
- wrangler
107
+ log_level =
108
+ if instance.que_error
109
+ :error
110
+ else
111
+ instance.log_level(elapsed)
138
112
  end
139
- end
140
-
141
- def worker_count=(count)
142
- Que.log :event => 'worker_count_change', :value => count.to_s
143
- @worker_count = count
144
- set_up_workers if mode == :async
145
- end
146
-
147
- def workers
148
- @workers ||= []
149
- end
150
-
151
- def wake_interval=(interval)
152
- @wake_interval = interval
153
- wrangler.wakeup if mode == :async
154
- end
155
113
 
156
- def wake!
157
- workers.find(&:wake!)
158
- end
114
+ if VALID_LOG_LEVELS.include?(log_level)
115
+ log_message = {
116
+ level: log_level,
117
+ job_id: metajob.id,
118
+ elapsed: elapsed,
119
+ }
120
+
121
+ if error = instance.que_error
122
+ log_message[:event] = :job_errored
123
+ log_message[:error] = "#{error.class}: #{error.message}".slice(0, 500)
124
+ else
125
+ log_message[:event] = :job_worked
126
+ end
159
127
 
160
- def wake_all!
161
- workers.each(&:wake!)
128
+ Que.log(**log_message)
162
129
  end
163
130
 
164
- private
165
-
166
- def set_up_workers
167
- if worker_count > workers.count
168
- workers.push(*(worker_count - workers.count).times.map{new(queue_name || '')})
169
- elsif worker_count < workers.count
170
- workers.pop(workers.count - worker_count).each(&:stop).each(&:wait_until_stopped)
131
+ instance
132
+ rescue => error
133
+ Que.log(
134
+ level: :debug,
135
+ event: :job_errored,
136
+ job_id: metajob.id,
137
+ error: {
138
+ class: error.class.to_s,
139
+ message: error.message,
140
+ backtrace: (error.backtrace || []).join("\n").slice(0, 10000),
141
+ },
142
+ )
143
+
144
+ Que.notify_error(error)
145
+
146
+ begin
147
+ # If the Job class couldn't be resolved, use the default retry
148
+ # backoff logic in Que::Job.
149
+ job_class = (klass && klass <= Job) ? klass : Job
150
+
151
+ error_count = job.fetch(:error_count) + 1
152
+
153
+ max_retry_count = job_class.resolve_que_setting(:maximum_retry_count)
154
+
155
+ if max_retry_count && error_count > max_retry_count
156
+ Que.execute :expire_job, [job.fetch(:id)]
157
+ else
158
+ delay =
159
+ job_class.
160
+ resolve_que_setting(
161
+ :retry_interval,
162
+ error_count,
163
+ )
164
+
165
+ Que.execute :set_error, [
166
+ delay,
167
+ "#{error.class}: #{error.message}".slice(0, 500),
168
+ (error.backtrace || []).join("\n").slice(0, 10000),
169
+ job.fetch(:id),
170
+ ]
171
171
  end
172
+ rescue
173
+ # If we can't reach the database for some reason, too bad, but
174
+ # don't let it crash the work loop.
172
175
  end
173
176
 
174
- def wrangler
175
- @wrangler ||= Thread.new do
176
- loop do
177
- sleep(*@wake_interval)
178
- wake! if @wake_interval && mode == :async
179
- end
180
- end
181
- end
177
+ error
182
178
  end
183
179
  end
184
180
  end