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.
- checksums.yaml +5 -5
- data/.github/workflows/tests.yml +51 -0
- data/.gitignore +2 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +502 -97
- data/Dockerfile +20 -0
- data/LICENSE.txt +1 -1
- data/README.md +205 -59
- data/auto/dev +21 -0
- data/auto/pre-push-hook +30 -0
- data/auto/psql +9 -0
- data/auto/test +5 -0
- data/auto/test-postgres-14 +17 -0
- data/bin/que +8 -81
- data/docker-compose.yml +47 -0
- data/docs/README.md +881 -0
- data/lib/que/active_job/extensions.rb +114 -0
- data/lib/que/active_record/connection.rb +51 -0
- data/lib/que/active_record/model.rb +48 -0
- data/lib/que/command_line_interface.rb +259 -0
- data/lib/que/connection.rb +198 -0
- data/lib/que/connection_pool.rb +78 -0
- data/lib/que/job.rb +210 -103
- data/lib/que/job_buffer.rb +255 -0
- data/lib/que/job_methods.rb +176 -0
- data/lib/que/listener.rb +176 -0
- data/lib/que/locker.rb +507 -0
- data/lib/que/metajob.rb +47 -0
- data/lib/que/migrations/4/down.sql +48 -0
- data/lib/que/migrations/4/up.sql +267 -0
- data/lib/que/migrations/5/down.sql +73 -0
- data/lib/que/migrations/5/up.sql +76 -0
- data/lib/que/migrations/6/down.sql +8 -0
- data/lib/que/migrations/6/up.sql +8 -0
- data/lib/que/migrations/7/down.sql +5 -0
- data/lib/que/migrations/7/up.sql +13 -0
- data/lib/que/migrations.rb +37 -18
- data/lib/que/poller.rb +274 -0
- data/lib/que/rails/railtie.rb +12 -0
- data/lib/que/result_queue.rb +35 -0
- data/lib/que/sequel/model.rb +52 -0
- data/lib/que/utils/assertions.rb +62 -0
- data/lib/que/utils/constantization.rb +19 -0
- data/lib/que/utils/error_notification.rb +68 -0
- data/lib/que/utils/freeze.rb +20 -0
- data/lib/que/utils/introspection.rb +50 -0
- data/lib/que/utils/json_serialization.rb +21 -0
- data/lib/que/utils/logging.rb +79 -0
- data/lib/que/utils/middleware.rb +46 -0
- data/lib/que/utils/queue_management.rb +18 -0
- data/lib/que/utils/ruby2_keywords.rb +19 -0
- data/lib/que/utils/transactions.rb +34 -0
- data/lib/que/version.rb +5 -1
- data/lib/que/worker.rb +145 -149
- data/lib/que.rb +103 -159
- data/que.gemspec +17 -4
- data/scripts/docker-entrypoint +14 -0
- data/scripts/test +6 -0
- metadata +59 -95
- data/.rspec +0 -2
- data/.travis.yml +0 -17
- data/Gemfile +0 -24
- data/docs/advanced_setup.md +0 -106
- data/docs/customizing_que.md +0 -200
- data/docs/error_handling.md +0 -47
- data/docs/inspecting_the_queue.md +0 -114
- data/docs/logging.md +0 -50
- data/docs/managing_workers.md +0 -80
- data/docs/migrating.md +0 -30
- data/docs/multiple_queues.md +0 -27
- data/docs/shutting_down_safely.md +0 -7
- data/docs/using_plain_connections.md +0 -41
- data/docs/using_sequel.md +0 -31
- data/docs/writing_reliable_jobs.md +0 -117
- data/lib/generators/que/install_generator.rb +0 -24
- data/lib/generators/que/templates/add_que.rb +0 -13
- data/lib/que/adapters/active_record.rb +0 -54
- data/lib/que/adapters/base.rb +0 -127
- data/lib/que/adapters/connection_pool.rb +0 -16
- data/lib/que/adapters/pg.rb +0 -21
- data/lib/que/adapters/pond.rb +0 -16
- data/lib/que/adapters/sequel.rb +0 -20
- data/lib/que/railtie.rb +0 -16
- data/lib/que/rake_tasks.rb +0 -59
- data/lib/que/sql.rb +0 -152
- data/spec/adapters/active_record_spec.rb +0 -152
- data/spec/adapters/connection_pool_spec.rb +0 -22
- data/spec/adapters/pg_spec.rb +0 -41
- data/spec/adapters/pond_spec.rb +0 -22
- data/spec/adapters/sequel_spec.rb +0 -57
- data/spec/gemfiles/Gemfile1 +0 -18
- data/spec/gemfiles/Gemfile2 +0 -18
- data/spec/spec_helper.rb +0 -118
- data/spec/support/helpers.rb +0 -19
- data/spec/support/jobs.rb +0 -35
- data/spec/support/shared_examples/adapter.rb +0 -37
- data/spec/support/shared_examples/multi_threaded_adapter.rb +0 -46
- data/spec/travis.rb +0 -23
- data/spec/unit/connection_spec.rb +0 -14
- data/spec/unit/customization_spec.rb +0 -251
- data/spec/unit/enqueue_spec.rb +0 -245
- data/spec/unit/helper_spec.rb +0 -12
- data/spec/unit/logging_spec.rb +0 -101
- data/spec/unit/migrations_spec.rb +0 -84
- data/spec/unit/pool_spec.rb +0 -365
- data/spec/unit/run_spec.rb +0 -14
- data/spec/unit/states_spec.rb +0 -50
- data/spec/unit/stats_spec.rb +0 -46
- data/spec/unit/transaction_spec.rb +0 -36
- data/spec/unit/work_spec.rb +0 -407
- data/spec/unit/worker_spec.rb +0 -167
- data/tasks/benchmark.rb +0 -3
- data/tasks/rspec.rb +0 -14
- data/tasks/safe_shutdown.rb +0 -67
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Que
|
|
4
|
+
module ActiveJob
|
|
5
|
+
# A module that devs can include into their ApplicationJob classes to get
|
|
6
|
+
# access to Que-like job behavior.
|
|
7
|
+
module JobExtensions
|
|
8
|
+
include JobMethods
|
|
9
|
+
|
|
10
|
+
def run(*args)
|
|
11
|
+
raise Error, "Job class #{self.class} didn't define a run() method!"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def perform(*args)
|
|
15
|
+
args, kwargs = Que.split_out_ruby2_keywords(args)
|
|
16
|
+
|
|
17
|
+
Que.internal_log(:active_job_perform, self) do
|
|
18
|
+
{args: args, kwargs: kwargs}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
_run(
|
|
22
|
+
args: Que.recursively_freeze(
|
|
23
|
+
que_filter_args(
|
|
24
|
+
args.map { |a| a.is_a?(Hash) ? a.deep_symbolize_keys : a }
|
|
25
|
+
)
|
|
26
|
+
),
|
|
27
|
+
kwargs: Que.recursively_freeze(
|
|
28
|
+
que_filter_args(
|
|
29
|
+
kwargs.deep_symbolize_keys,
|
|
30
|
+
)
|
|
31
|
+
),
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# Have helper methods like `destroy` and `retry_in` delegate to the actual
|
|
38
|
+
# job object. If the current job is being run through an ActiveJob adapter
|
|
39
|
+
# other than Que's, this will return nil, which is fine.
|
|
40
|
+
def que_target
|
|
41
|
+
Thread.current[:que_current_job]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Filter out :_aj_symbol_keys constructs so that keywords work as
|
|
45
|
+
# expected.
|
|
46
|
+
def que_filter_args(thing)
|
|
47
|
+
case thing
|
|
48
|
+
when Array
|
|
49
|
+
thing.map { |t| que_filter_args(t) }
|
|
50
|
+
when Hash
|
|
51
|
+
thing.each_with_object({}) do |(k, v), hash|
|
|
52
|
+
hash[k] = que_filter_args(v) unless k == :_aj_symbol_keys
|
|
53
|
+
end
|
|
54
|
+
else
|
|
55
|
+
thing
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# A module that we mix into ActiveJob's wrapper for Que::Job, to maintain
|
|
61
|
+
# backwards-compatibility with internal changes we make.
|
|
62
|
+
module WrapperExtensions
|
|
63
|
+
module ClassMethods
|
|
64
|
+
# We've dropped support for job options supplied as top-level keywords, but ActiveJob's QueAdapter still uses them. So we have to move them into the job_options hash ourselves.
|
|
65
|
+
def enqueue(args, priority:, queue:, run_at: nil)
|
|
66
|
+
super(args, job_options: { priority: priority, queue: queue, run_at: run_at })
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
module InstanceMethods
|
|
71
|
+
# The Rails adapter (built against a pre-1.0 version of this gem)
|
|
72
|
+
# assumes that it can access a job's id via job.attrs["job_id"]. So,
|
|
73
|
+
# oblige it.
|
|
74
|
+
def attrs
|
|
75
|
+
{"job_id" => que_attrs[:id]}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def run(args)
|
|
79
|
+
# Our ActiveJob extensions expect to be able to operate on the actual
|
|
80
|
+
# job object, but there's no way to access it through ActiveJob. So,
|
|
81
|
+
# scope it to the current thread. It's a bit messy, but it's the best
|
|
82
|
+
# option under the circumstances (doesn't require hacking ActiveJob in
|
|
83
|
+
# any more extensive way).
|
|
84
|
+
|
|
85
|
+
# There's no reason this logic should ever nest, because it wouldn't
|
|
86
|
+
# make sense to run a worker inside of a job, but even so, assert that
|
|
87
|
+
# nothing absurd is going on.
|
|
88
|
+
Que.assert NilClass, Thread.current[:que_current_job]
|
|
89
|
+
|
|
90
|
+
begin
|
|
91
|
+
Thread.current[:que_current_job] = self
|
|
92
|
+
|
|
93
|
+
# We symbolize the args hash but ActiveJob doesn't like that :/
|
|
94
|
+
super(args.deep_stringify_keys)
|
|
95
|
+
ensure
|
|
96
|
+
# Also assert that the current job state was only removed now, but
|
|
97
|
+
# unset the job first so that an assertion failure doesn't mess up
|
|
98
|
+
# the state any more than it already has.
|
|
99
|
+
current = Thread.current[:que_current_job]
|
|
100
|
+
Thread.current[:que_current_job] = nil
|
|
101
|
+
Que.assert(self, current)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
class ActiveJob::QueueAdapters::QueAdapter
|
|
110
|
+
class JobWrapper < Que::Job
|
|
111
|
+
extend Que::ActiveJob::WrapperExtensions::ClassMethods
|
|
112
|
+
prepend Que::ActiveJob::WrapperExtensions::InstanceMethods
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Que
|
|
4
|
+
module ActiveRecord
|
|
5
|
+
module Connection
|
|
6
|
+
class << self
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
# Check out a PG::Connection object from ActiveRecord's pool.
|
|
10
|
+
def checkout
|
|
11
|
+
wrap_in_rails_executor do
|
|
12
|
+
::ActiveRecord::Base.connection_pool.with_connection do |conn|
|
|
13
|
+
yield conn.raw_connection
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Use Rails' executor (if present) to make sure that the connection
|
|
19
|
+
# we're using isn't taken from us while the block runs. See
|
|
20
|
+
# https://github.com/que-rb/que/issues/166#issuecomment-274218910
|
|
21
|
+
def wrap_in_rails_executor(&block)
|
|
22
|
+
if defined?(::Rails.application.executor)
|
|
23
|
+
::Rails.application.executor.wrap(&block)
|
|
24
|
+
else
|
|
25
|
+
yield
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
module JobMiddleware
|
|
31
|
+
class << self
|
|
32
|
+
def call(job)
|
|
33
|
+
yield
|
|
34
|
+
|
|
35
|
+
# ActiveRecord will check out connections to the current thread when
|
|
36
|
+
# queries are executed and not return them to the pool until
|
|
37
|
+
# explicitly requested to. I'm not wild about this API design, and
|
|
38
|
+
# it doesn't pose a problem for the typical case of workers using a
|
|
39
|
+
# single PG connection (since we ensure that connection is checked
|
|
40
|
+
# in and checked out responsibly), but since ActiveRecord supports
|
|
41
|
+
# connections to multiple databases, it's easy for people using that
|
|
42
|
+
# feature to unknowingly leak connections to other databases. So,
|
|
43
|
+
# take the additional step of telling ActiveRecord to check in all
|
|
44
|
+
# of the current thread's connections after each job is run.
|
|
45
|
+
::ActiveRecord::Base.clear_active_connections!
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Que
|
|
4
|
+
module ActiveRecord
|
|
5
|
+
class Model < ::ActiveRecord::Base
|
|
6
|
+
self.table_name = 'public.que_jobs'
|
|
7
|
+
|
|
8
|
+
t = arel_table
|
|
9
|
+
|
|
10
|
+
scope :errored, -> { where(t[:error_count].gt(0)) }
|
|
11
|
+
scope :not_errored, -> { where(t[:error_count].eq(0)) }
|
|
12
|
+
|
|
13
|
+
scope :expired, -> { where(t[:expired_at].not_eq(nil)) }
|
|
14
|
+
scope :not_expired, -> { where(t[:expired_at].eq(nil)) }
|
|
15
|
+
|
|
16
|
+
scope :finished, -> { where(t[:finished_at].not_eq(nil)) }
|
|
17
|
+
scope :not_finished, -> { where(t[:finished_at].eq(nil)) }
|
|
18
|
+
|
|
19
|
+
scope :scheduled, -> { where(t[:run_at].gt (Arel.sql("now()"))) }
|
|
20
|
+
scope :not_scheduled, -> { where(t[:run_at].lteq(Arel.sql("now()"))) }
|
|
21
|
+
|
|
22
|
+
scope :ready, -> { not_errored.not_expired.not_finished.not_scheduled }
|
|
23
|
+
scope :not_ready, -> { where(t[:error_count].gt(0).or(t[:expired_at].not_eq(nil)).or(t[:finished_at].not_eq(nil)).or(t[:run_at].gt(Arel.sql("now()")))) }
|
|
24
|
+
|
|
25
|
+
class << self
|
|
26
|
+
def by_job_class(job_class)
|
|
27
|
+
job_class = job_class.name if job_class.is_a?(Class)
|
|
28
|
+
where(
|
|
29
|
+
"que_jobs.job_class = ? OR (que_jobs.job_class = 'ActiveJob::QueueAdapters::QueAdapter::JobWrapper' AND que_jobs.args->0->>'job_class' = ?)",
|
|
30
|
+
job_class, job_class,
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def by_queue(queue)
|
|
35
|
+
where(arel_table[:queue].eq(queue))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def by_tag(tag)
|
|
39
|
+
where("que_jobs.data @> ?", JSON.dump(tags: [tag]))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def by_args(*args, **kwargs)
|
|
43
|
+
where("que_jobs.args @> ? AND que_jobs.kwargs @> ?", JSON.dump(args), JSON.dump(kwargs))
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'optparse'
|
|
4
|
+
require 'logger'
|
|
5
|
+
require 'uri'
|
|
6
|
+
|
|
7
|
+
module Que
|
|
8
|
+
module CommandLineInterface
|
|
9
|
+
# Have a sensible default require file for Rails.
|
|
10
|
+
RAILS_ENVIRONMENT_FILE = './config/environment.rb'
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
# Need to rely on dependency injection a bit to make this method cleanly
|
|
14
|
+
# testable :/
|
|
15
|
+
def parse(
|
|
16
|
+
args:,
|
|
17
|
+
output:,
|
|
18
|
+
default_require_file: RAILS_ENVIRONMENT_FILE
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
options = {}
|
|
22
|
+
queues = []
|
|
23
|
+
log_level = 'info'
|
|
24
|
+
log_internals = false
|
|
25
|
+
poll_interval = 5
|
|
26
|
+
connection_url = nil
|
|
27
|
+
worker_count = nil
|
|
28
|
+
worker_priorities = nil
|
|
29
|
+
|
|
30
|
+
parser =
|
|
31
|
+
OptionParser.new do |opts|
|
|
32
|
+
opts.banner = 'usage: que [options] [file/to/require] ...'
|
|
33
|
+
|
|
34
|
+
opts.on(
|
|
35
|
+
'-h',
|
|
36
|
+
'--help',
|
|
37
|
+
"Show this help text.",
|
|
38
|
+
) do
|
|
39
|
+
output.puts opts.help
|
|
40
|
+
return 0
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
opts.on(
|
|
44
|
+
'-i',
|
|
45
|
+
'--poll-interval [INTERVAL]',
|
|
46
|
+
Float,
|
|
47
|
+
"Set maximum interval between polls for available jobs, " \
|
|
48
|
+
"in seconds (default: 5)",
|
|
49
|
+
) do |i|
|
|
50
|
+
poll_interval = i
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
opts.on(
|
|
54
|
+
'-l',
|
|
55
|
+
'--log-level [LEVEL]',
|
|
56
|
+
String,
|
|
57
|
+
"Set level at which to log to STDOUT " \
|
|
58
|
+
"(debug, info, warn, error, fatal) (default: info)",
|
|
59
|
+
) do |l|
|
|
60
|
+
log_level = l
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
opts.on(
|
|
64
|
+
'-p',
|
|
65
|
+
'--worker-priorities [LIST]',
|
|
66
|
+
Array,
|
|
67
|
+
"List of priorities to assign to workers (default: 10,30,50,any,any,any)",
|
|
68
|
+
) do |priority_array|
|
|
69
|
+
worker_priorities =
|
|
70
|
+
priority_array.map do |p|
|
|
71
|
+
case p
|
|
72
|
+
when /\Aany\z/i
|
|
73
|
+
nil
|
|
74
|
+
when /\A\d+\z/
|
|
75
|
+
Integer(p)
|
|
76
|
+
else
|
|
77
|
+
output.puts "Invalid priority option: '#{p}'. Please use an integer or the word 'any'."
|
|
78
|
+
return 1
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
opts.on(
|
|
84
|
+
'-q',
|
|
85
|
+
'--queue-name [NAME]',
|
|
86
|
+
String,
|
|
87
|
+
"Set a queue name to work jobs from. " \
|
|
88
|
+
"Can be passed multiple times. " \
|
|
89
|
+
"(default: the default queue only)",
|
|
90
|
+
) do |queue_name|
|
|
91
|
+
queues << queue_name
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
opts.on(
|
|
95
|
+
'-w',
|
|
96
|
+
'--worker-count [COUNT]',
|
|
97
|
+
Integer,
|
|
98
|
+
"Set number of workers in process (default: 6)",
|
|
99
|
+
) do |w|
|
|
100
|
+
worker_count = w
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
opts.on(
|
|
104
|
+
'-v',
|
|
105
|
+
'--version',
|
|
106
|
+
"Print Que version and exit.",
|
|
107
|
+
) do
|
|
108
|
+
require 'que'
|
|
109
|
+
output.puts "Que version #{Que::VERSION}"
|
|
110
|
+
return 0
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
opts.on(
|
|
114
|
+
'--connection-url [URL]',
|
|
115
|
+
String,
|
|
116
|
+
"Set a custom database url to connect to for locking purposes.",
|
|
117
|
+
) do |url|
|
|
118
|
+
options[:connection_url] = url
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
opts.on(
|
|
122
|
+
'--log-internals',
|
|
123
|
+
"Log verbosely about Que's internal state. " \
|
|
124
|
+
"Only recommended for debugging issues",
|
|
125
|
+
) do |l|
|
|
126
|
+
log_internals = true
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
opts.on(
|
|
130
|
+
'--maximum-buffer-size [SIZE]',
|
|
131
|
+
Integer,
|
|
132
|
+
"Set maximum number of jobs to be locked and held in this " \
|
|
133
|
+
"process awaiting a worker (default: 8)",
|
|
134
|
+
) do |s|
|
|
135
|
+
options[:maximum_buffer_size] = s
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
opts.on(
|
|
139
|
+
'--minimum-buffer-size [SIZE]',
|
|
140
|
+
Integer,
|
|
141
|
+
"Unused (deprecated)",
|
|
142
|
+
) do |s|
|
|
143
|
+
warn "The --minimum-buffer-size SIZE option has been deprecated and will be removed in v2.0 (it's actually been unused since v1.0.0.beta4)"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
opts.on(
|
|
147
|
+
'--wait-period [PERIOD]',
|
|
148
|
+
Float,
|
|
149
|
+
"Set maximum interval between checks of the in-memory job queue, " \
|
|
150
|
+
"in milliseconds (default: 50)",
|
|
151
|
+
) do |p|
|
|
152
|
+
options[:wait_period] = p
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
parser.parse!(args)
|
|
157
|
+
|
|
158
|
+
options[:worker_priorities] =
|
|
159
|
+
if worker_count && worker_priorities
|
|
160
|
+
worker_priorities.values_at(0...worker_count)
|
|
161
|
+
elsif worker_priorities
|
|
162
|
+
worker_priorities
|
|
163
|
+
elsif worker_count
|
|
164
|
+
Array.new(worker_count) { nil }
|
|
165
|
+
else
|
|
166
|
+
[10, 30, 50, nil, nil, nil]
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
if args.length.zero?
|
|
170
|
+
if File.exist?(default_require_file)
|
|
171
|
+
args << default_require_file
|
|
172
|
+
else
|
|
173
|
+
output.puts <<-OUTPUT
|
|
174
|
+
You didn't include any Ruby files to require!
|
|
175
|
+
Que needs to be able to load your application before it can process jobs.
|
|
176
|
+
(Or use `que -h` for a list of options)
|
|
177
|
+
OUTPUT
|
|
178
|
+
return 1
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
args.each do |file|
|
|
183
|
+
begin
|
|
184
|
+
require file
|
|
185
|
+
rescue LoadError => e
|
|
186
|
+
output.puts "Could not load file '#{file}': #{e}"
|
|
187
|
+
return 1
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
Que.logger ||= Logger.new(STDOUT)
|
|
192
|
+
|
|
193
|
+
if log_internals
|
|
194
|
+
Que.internal_logger = Que.logger
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
begin
|
|
198
|
+
Que.get_logger.level = Logger.const_get(log_level.upcase)
|
|
199
|
+
rescue NameError
|
|
200
|
+
output.puts "Unsupported logging level: #{log_level} (try debug, info, warn, error, or fatal)"
|
|
201
|
+
return 1
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
if queues.any?
|
|
205
|
+
queues_hash = {}
|
|
206
|
+
|
|
207
|
+
queues.each do |queue|
|
|
208
|
+
name, interval = queue.split('=')
|
|
209
|
+
p = interval ? Float(interval) : poll_interval
|
|
210
|
+
|
|
211
|
+
Que.assert(p > 0.01) { "Poll intervals can't be less than 0.01 seconds!" }
|
|
212
|
+
|
|
213
|
+
queues_hash[name] = p
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
options[:queues] = queues_hash
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
options[:poll_interval] = poll_interval
|
|
220
|
+
|
|
221
|
+
locker =
|
|
222
|
+
begin
|
|
223
|
+
Que::Locker.new(**options)
|
|
224
|
+
rescue => e
|
|
225
|
+
output.puts(e.message)
|
|
226
|
+
return 1
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# It's a bit sloppy to use a global for this when a local variable would
|
|
230
|
+
# do, but we want to stop the locker from the CLI specs, so...
|
|
231
|
+
$stop_que_executable = false
|
|
232
|
+
%w[INT TERM].each { |signal| trap(signal) { $stop_que_executable = true } }
|
|
233
|
+
|
|
234
|
+
output.puts(
|
|
235
|
+
<<~STARTUP
|
|
236
|
+
Que #{Que::VERSION} started worker process with:
|
|
237
|
+
Worker threads: #{locker.workers.length} (priorities: #{locker.workers.map { |w| w.priority || 'any' }.join(', ')})
|
|
238
|
+
Buffer size: #{locker.job_buffer.maximum_size}
|
|
239
|
+
Queues:
|
|
240
|
+
#{locker.queues.map { |queue, interval| " - #{queue} (poll interval: #{interval}s)" }.join("\n")}
|
|
241
|
+
Que waiting for jobs...
|
|
242
|
+
STARTUP
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
loop do
|
|
246
|
+
sleep 0.01
|
|
247
|
+
break if $stop_que_executable || locker.stopping?
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
output.puts "\nFinishing Que's current jobs before exiting..."
|
|
251
|
+
|
|
252
|
+
locker.stop!
|
|
253
|
+
|
|
254
|
+
output.puts "Que's jobs finished, exiting..."
|
|
255
|
+
return 0
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# A simple wrapper class around connections that basically just improves the
|
|
4
|
+
# query API a bit. Currently, our connection pool wrapper discards these
|
|
5
|
+
# connection wrappers once the connection is returned to the source connection
|
|
6
|
+
# pool, so this class isn't currently suitable for storing data about the
|
|
7
|
+
# connection long-term (like what statements it has prepared, for example).
|
|
8
|
+
|
|
9
|
+
# If we wanted to do that, we'd probably need to sneak a reference to the
|
|
10
|
+
# wrapper into the PG::Connection object itself, by just setting a instance
|
|
11
|
+
# variable that's something namespaced and hopefully safe, like
|
|
12
|
+
# `@que_connection_wrapper`. It's a bit ugly, but it should ensure that we don't
|
|
13
|
+
# cause any memory leaks in esoteric setups where one-off connections are being
|
|
14
|
+
# established and then garbage-collected.
|
|
15
|
+
|
|
16
|
+
require 'time' # For Time.parse
|
|
17
|
+
require 'set'
|
|
18
|
+
|
|
19
|
+
module Que
|
|
20
|
+
class Connection
|
|
21
|
+
extend Forwardable
|
|
22
|
+
|
|
23
|
+
attr_reader :wrapped_connection
|
|
24
|
+
|
|
25
|
+
def_delegators :wrapped_connection, :backend_pid, :wait_for_notify
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
def wrap(conn)
|
|
29
|
+
case conn
|
|
30
|
+
when self
|
|
31
|
+
conn
|
|
32
|
+
when PG::Connection
|
|
33
|
+
if conn.instance_variable_defined?(:@que_wrapper)
|
|
34
|
+
conn.instance_variable_get(:@que_wrapper)
|
|
35
|
+
else
|
|
36
|
+
conn.instance_variable_set(:@que_wrapper, new(conn))
|
|
37
|
+
end
|
|
38
|
+
else
|
|
39
|
+
raise Error, "Unsupported input for Connection.wrap: #{conn.class}"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def initialize(connection)
|
|
45
|
+
@wrapped_connection = connection
|
|
46
|
+
@prepared_statements = Set.new
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def execute(command, params = [])
|
|
50
|
+
sql =
|
|
51
|
+
case command
|
|
52
|
+
when Symbol then SQL[command]
|
|
53
|
+
when String then command
|
|
54
|
+
else raise Error, "Bad command! #{command.inspect}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
params = convert_params(params)
|
|
58
|
+
|
|
59
|
+
result =
|
|
60
|
+
Que.run_sql_middleware(sql, params) do
|
|
61
|
+
# Some versions of the PG gem dislike an empty/nil params argument.
|
|
62
|
+
if params.empty?
|
|
63
|
+
wrapped_connection.async_exec(sql)
|
|
64
|
+
else
|
|
65
|
+
wrapped_connection.async_exec_params(sql, params)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
Que.internal_log :connection_execute, self do
|
|
70
|
+
{
|
|
71
|
+
backend_pid: backend_pid,
|
|
72
|
+
command: command,
|
|
73
|
+
params: params,
|
|
74
|
+
ntuples: result.ntuples,
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
convert_result(result)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def execute_prepared(command, params = nil)
|
|
82
|
+
Que.assert(Symbol, command)
|
|
83
|
+
|
|
84
|
+
if !Que.use_prepared_statements || in_transaction?
|
|
85
|
+
return execute(command, params)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
name = "que_#{command}"
|
|
89
|
+
|
|
90
|
+
begin
|
|
91
|
+
unless @prepared_statements.include?(command)
|
|
92
|
+
wrapped_connection.prepare(name, SQL[command])
|
|
93
|
+
@prepared_statements.add(command)
|
|
94
|
+
prepared_just_now = true
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
convert_result(
|
|
98
|
+
wrapped_connection.exec_prepared(name, params)
|
|
99
|
+
)
|
|
100
|
+
rescue ::PG::InvalidSqlStatementName => error
|
|
101
|
+
# Reconnections on ActiveRecord can cause the same connection
|
|
102
|
+
# objects to refer to new backends, so recover as well as we can.
|
|
103
|
+
|
|
104
|
+
unless prepared_just_now
|
|
105
|
+
Que.log level: :warn, event: :reprepare_statement, command: command
|
|
106
|
+
@prepared_statements.delete(command)
|
|
107
|
+
retry
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
raise error
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def next_notification
|
|
115
|
+
wrapped_connection.notifies
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def drain_notifications
|
|
119
|
+
loop { break if next_notification.nil? }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def server_version
|
|
123
|
+
wrapped_connection.server_version
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def in_transaction?
|
|
127
|
+
wrapped_connection.transaction_status != ::PG::PQTRANS_IDLE
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
def convert_params(params)
|
|
133
|
+
params.map do |param|
|
|
134
|
+
case param
|
|
135
|
+
when Time
|
|
136
|
+
# The pg gem unfortunately doesn't convert fractions of time
|
|
137
|
+
# instances, so cast them to a string.
|
|
138
|
+
param.strftime('%Y-%m-%d %H:%M:%S.%6N %z')
|
|
139
|
+
when Array, Hash
|
|
140
|
+
# Handle JSON.
|
|
141
|
+
Que.serialize_json(param)
|
|
142
|
+
else
|
|
143
|
+
param
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Procs used to convert strings from Postgres into Ruby types.
|
|
149
|
+
CAST_PROCS = {
|
|
150
|
+
# Boolean
|
|
151
|
+
16 => -> (value) {
|
|
152
|
+
case value
|
|
153
|
+
when String then value == 't'.freeze
|
|
154
|
+
else !!value
|
|
155
|
+
end
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
# Timestamp with time zone
|
|
159
|
+
1184 => -> (value) {
|
|
160
|
+
case value
|
|
161
|
+
when Time then value
|
|
162
|
+
when String then Time.parse(value)
|
|
163
|
+
else raise "Unexpected time class: #{value.class} (#{value.inspect})"
|
|
164
|
+
end
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# JSON, JSONB
|
|
169
|
+
CAST_PROCS[114] = CAST_PROCS[3802] = -> (j) { Que.deserialize_json(j) }
|
|
170
|
+
|
|
171
|
+
# Integer, bigint, smallint
|
|
172
|
+
CAST_PROCS[23] = CAST_PROCS[20] = CAST_PROCS[21] = proc(&:to_i)
|
|
173
|
+
|
|
174
|
+
CAST_PROCS.freeze
|
|
175
|
+
|
|
176
|
+
def convert_result(result)
|
|
177
|
+
output = result.to_a
|
|
178
|
+
|
|
179
|
+
result.fields.each_with_index do |field, index|
|
|
180
|
+
symbol = field.to_sym
|
|
181
|
+
|
|
182
|
+
if converter = CAST_PROCS[result.ftype(index)]
|
|
183
|
+
output.each do |hash|
|
|
184
|
+
value = hash.delete(field)
|
|
185
|
+
value = converter.call(value) if value
|
|
186
|
+
hash[symbol] = value
|
|
187
|
+
end
|
|
188
|
+
else
|
|
189
|
+
output.each do |hash|
|
|
190
|
+
hash[symbol] = hash.delete(field)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
output
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|