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