que 0.11.3 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
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