que 0.14.3 → 1.0.0.beta
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +2 -0
- data/CHANGELOG.md +108 -14
- data/LICENSE.txt +1 -1
- data/README.md +49 -45
- data/bin/command_line_interface.rb +239 -0
- data/bin/que +8 -82
- data/docs/README.md +2 -0
- data/docs/active_job.md +6 -0
- data/docs/advanced_setup.md +7 -64
- data/docs/command_line_interface.md +45 -0
- data/docs/error_handling.md +65 -18
- data/docs/inspecting_the_queue.md +30 -80
- data/docs/job_helper_methods.md +27 -0
- data/docs/logging.md +3 -22
- data/docs/managing_workers.md +6 -61
- data/docs/middleware.md +15 -0
- data/docs/migrating.md +4 -7
- data/docs/multiple_queues.md +8 -4
- data/docs/shutting_down_safely.md +1 -1
- data/docs/using_plain_connections.md +39 -15
- data/docs/using_sequel.md +5 -3
- data/docs/writing_reliable_jobs.md +15 -24
- data/lib/que.rb +98 -182
- data/lib/que/active_job/extensions.rb +97 -0
- data/lib/que/active_record/connection.rb +51 -0
- data/lib/que/active_record/model.rb +48 -0
- data/lib/que/connection.rb +179 -0
- data/lib/que/connection_pool.rb +78 -0
- data/lib/que/job.rb +107 -156
- data/lib/que/job_cache.rb +240 -0
- data/lib/que/job_methods.rb +168 -0
- data/lib/que/listener.rb +176 -0
- data/lib/que/locker.rb +466 -0
- data/lib/que/metajob.rb +47 -0
- data/lib/que/migrations.rb +24 -17
- data/lib/que/migrations/4/down.sql +48 -0
- data/lib/que/migrations/4/up.sql +265 -0
- data/lib/que/poller.rb +267 -0
- data/lib/que/rails/railtie.rb +14 -0
- data/lib/que/result_queue.rb +35 -0
- data/lib/que/sequel/model.rb +51 -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 +78 -0
- data/lib/que/utils/middleware.rb +33 -0
- data/lib/que/utils/queue_management.rb +18 -0
- data/lib/que/utils/transactions.rb +34 -0
- data/lib/que/version.rb +1 -1
- data/lib/que/worker.rb +128 -167
- data/que.gemspec +13 -2
- metadata +37 -80
- data/.rspec +0 -2
- data/.travis.yml +0 -64
- data/Gemfile +0 -24
- data/docs/customizing_que.md +0 -200
- 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 -40
- data/lib/que/adapters/base.rb +0 -133
- 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 -170
- data/spec/adapters/active_record_spec.rb +0 -175
- 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/Gemfile.current +0 -19
- data/spec/gemfiles/Gemfile.old +0 -19
- data/spec/gemfiles/Gemfile.older +0 -19
- data/spec/gemfiles/Gemfile.oldest +0 -19
- data/spec/spec_helper.rb +0 -129
- data/spec/support/helpers.rb +0 -25
- data/spec/support/jobs.rb +0 -35
- data/spec/support/shared_examples/adapter.rb +0 -42
- data/spec/support/shared_examples/multi_threaded_adapter.rb +0 -46
- data/spec/unit/configuration_spec.rb +0 -31
- 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 -596
- 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,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Que
|
4
|
+
module Rails
|
5
|
+
class Railtie < Rails::Railtie
|
6
|
+
config.que = Que
|
7
|
+
|
8
|
+
Que.run_asynchronously = true if Rails.env.test?
|
9
|
+
|
10
|
+
Que.logger = proc { Rails.logger }
|
11
|
+
Que.connection = ::ActiveRecord if defined? ::ActiveRecord
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# A thread-safe queue that holds ids for jobs that have been worked. Allows
|
4
|
+
# appending single/retrieving all ids in a thread-safe fashion.
|
5
|
+
|
6
|
+
module Que
|
7
|
+
class ResultQueue
|
8
|
+
def initialize
|
9
|
+
@array = []
|
10
|
+
@mutex = Mutex.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def push(item)
|
14
|
+
sync { @array.push(item) }
|
15
|
+
end
|
16
|
+
|
17
|
+
def clear
|
18
|
+
sync { @array.pop(@array.size) }
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_a
|
22
|
+
sync { @array.dup }
|
23
|
+
end
|
24
|
+
|
25
|
+
def length
|
26
|
+
sync { @array.length }
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def sync
|
32
|
+
@mutex.synchronize { yield }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Que
|
4
|
+
module Sequel
|
5
|
+
QUALIFIED_TABLE = ::Sequel.qualify(:public, :que_jobs)
|
6
|
+
|
7
|
+
class Model < ::Sequel::Model(QUALIFIED_TABLE)
|
8
|
+
dataset_module do
|
9
|
+
conditions = {
|
10
|
+
errored: ::Sequel.qualify(QUALIFIED_TABLE, :error_count) > 0,
|
11
|
+
expired: ::Sequel.~(::Sequel.qualify(QUALIFIED_TABLE, :expired_at) => nil),
|
12
|
+
finished: ::Sequel.~(::Sequel.qualify(QUALIFIED_TABLE, :finished_at) => nil),
|
13
|
+
scheduled: ::Sequel.qualify(QUALIFIED_TABLE, :run_at) > ::Sequel::CURRENT_TIMESTAMP,
|
14
|
+
}
|
15
|
+
|
16
|
+
conditions.each do |name, condition|
|
17
|
+
subset name, condition
|
18
|
+
subset :"not_#{name}", ~condition
|
19
|
+
end
|
20
|
+
|
21
|
+
subset :ready, conditions.values.map(&:~).inject{|a, b| a & b}
|
22
|
+
subset :not_ready, conditions.values. inject{|a, b| a | b}
|
23
|
+
|
24
|
+
def by_job_class(job_class)
|
25
|
+
job_class = job_class.name if job_class.is_a?(Class)
|
26
|
+
where(
|
27
|
+
::Sequel.|(
|
28
|
+
{::Sequel.qualify(QUALIFIED_TABLE, :job_class) => job_class},
|
29
|
+
{
|
30
|
+
::Sequel.qualify(QUALIFIED_TABLE, :job_class) => "ActiveJob::QueueAdapters::QueAdapter::JobWrapper",
|
31
|
+
::Sequel.lit("public.que_jobs.args->0->>'job_class'") => job_class,
|
32
|
+
}
|
33
|
+
)
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
def by_queue(queue)
|
38
|
+
where(::Sequel.qualify(QUALIFIED_TABLE, :queue) => queue)
|
39
|
+
end
|
40
|
+
|
41
|
+
def by_tag(tag)
|
42
|
+
where(::Sequel.lit("public.que_jobs.data @> ?", JSON.dump(tags: [tag])))
|
43
|
+
end
|
44
|
+
|
45
|
+
def by_args(*args)
|
46
|
+
where(::Sequel.lit("public.que_jobs.args @> ?", JSON.dump(args)))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Assertion helpers. Que has a fair amount of internal state, and there's no
|
4
|
+
# telling what users will try to throw at it, so for ease of debugging issues it
|
5
|
+
# makes sense to sanity-check frequently.
|
6
|
+
|
7
|
+
module Que
|
8
|
+
module Utils
|
9
|
+
module Assertions
|
10
|
+
class AssertionFailed < Error; end
|
11
|
+
|
12
|
+
def assert(*args)
|
13
|
+
comparison, object, pass = _check_assertion_args(*args)
|
14
|
+
return object if pass
|
15
|
+
|
16
|
+
message =
|
17
|
+
if block_given?
|
18
|
+
yield.to_s
|
19
|
+
elsif comparison
|
20
|
+
"Expected #{comparison.inspect}, got #{object.inspect}!"
|
21
|
+
else
|
22
|
+
"Assertion failed!"
|
23
|
+
end
|
24
|
+
|
25
|
+
# Remove this method from the backtrace, to make errors clearer.
|
26
|
+
raise AssertionFailed, message, caller
|
27
|
+
end
|
28
|
+
|
29
|
+
def assert?(*args)
|
30
|
+
_, _, pass = _check_assertion_args(*args)
|
31
|
+
!!pass
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# Want to support:
|
37
|
+
# assert(x) # Truthiness.
|
38
|
+
# assert(thing, other) # Trip-equals.
|
39
|
+
# assert([thing1, thing2], other) # Multiple Trip-equals.
|
40
|
+
def _check_assertion_args(first, second = (second_omitted = true; nil))
|
41
|
+
if second_omitted
|
42
|
+
comparison = nil
|
43
|
+
object = first
|
44
|
+
else
|
45
|
+
comparison = first
|
46
|
+
object = second
|
47
|
+
end
|
48
|
+
|
49
|
+
pass =
|
50
|
+
if second_omitted
|
51
|
+
object
|
52
|
+
elsif comparison.is_a?(Array)
|
53
|
+
comparison.any? { |k| k === object }
|
54
|
+
else
|
55
|
+
comparison === object
|
56
|
+
end
|
57
|
+
|
58
|
+
[comparison, object, pass]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Que
|
4
|
+
module Utils
|
5
|
+
module Constantization
|
6
|
+
def constantize(string)
|
7
|
+
assert String, string
|
8
|
+
|
9
|
+
if string.respond_to?(:constantize)
|
10
|
+
string.constantize
|
11
|
+
else
|
12
|
+
names = string.split('::')
|
13
|
+
names.reject!(&:empty?)
|
14
|
+
names.inject(Object, &:const_get)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Que
|
4
|
+
module Utils
|
5
|
+
module ErrorNotification
|
6
|
+
attr_accessor :error_notifier
|
7
|
+
|
8
|
+
def notify_error(*args)
|
9
|
+
Que.internal_log(:error_notification_attempted) do
|
10
|
+
{args: args.inspect}
|
11
|
+
end
|
12
|
+
|
13
|
+
if notifier = error_notifier
|
14
|
+
arity = notifier.arity
|
15
|
+
args = args.first(arity) if arity >= 0
|
16
|
+
|
17
|
+
notifier.call(*args)
|
18
|
+
end
|
19
|
+
rescue => error
|
20
|
+
Que.log(
|
21
|
+
event: :error_notifier_failed,
|
22
|
+
level: :error,
|
23
|
+
message: "error_notifier callable raised an error",
|
24
|
+
|
25
|
+
error_class: error.class.name,
|
26
|
+
error_message: error.message,
|
27
|
+
error_backtrace: error.backtrace,
|
28
|
+
)
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
|
32
|
+
ASYNC_QUEUE = Queue.new
|
33
|
+
MAX_QUEUE_SIZE = 5
|
34
|
+
|
35
|
+
# Helper method to notify errors asynchronously. For use in high-priority
|
36
|
+
# code, where we don't want to be held up by whatever I/O the error
|
37
|
+
# notification proc contains.
|
38
|
+
def notify_error_async(*args)
|
39
|
+
# We don't synchronize around the size check and the push, so there's a
|
40
|
+
# race condition where the queue could grow to more than the maximum
|
41
|
+
# number of errors, but no big deal if it does. The size check is mainly
|
42
|
+
# here to ensure that the error queue doesn't grow unboundedly large in
|
43
|
+
# pathological cases.
|
44
|
+
|
45
|
+
if ASYNC_QUEUE.size < MAX_QUEUE_SIZE
|
46
|
+
ASYNC_QUEUE.push(args)
|
47
|
+
# Puma raises some ugly warnings if you start up a new thread in the
|
48
|
+
# background during initialization, so start the async error-reporting
|
49
|
+
# thread lazily.
|
50
|
+
async_error_thread
|
51
|
+
true
|
52
|
+
else
|
53
|
+
false
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def async_error_thread
|
58
|
+
CONFIG_MUTEX.synchronize do
|
59
|
+
@async_error_thread ||=
|
60
|
+
Thread.new do
|
61
|
+
Thread.current.abort_on_exception = true
|
62
|
+
loop { Que.notify_error(*ASYNC_QUEUE.pop) }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Helper method for recursively freezing a data structure.
|
4
|
+
|
5
|
+
module Que
|
6
|
+
module Utils
|
7
|
+
module Freeze
|
8
|
+
def recursively_freeze(thing)
|
9
|
+
case thing
|
10
|
+
when Array
|
11
|
+
thing.each { |e| recursively_freeze(e) }
|
12
|
+
when Hash
|
13
|
+
thing.each { |k, v| recursively_freeze(k); recursively_freeze(v) }
|
14
|
+
end
|
15
|
+
|
16
|
+
thing.freeze
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Tools for introspecting the state of the job queue.
|
4
|
+
|
5
|
+
module Que
|
6
|
+
module Utils
|
7
|
+
module Introspection
|
8
|
+
SQL[:job_stats] =
|
9
|
+
%{
|
10
|
+
SELECT job_class,
|
11
|
+
count(*) AS count,
|
12
|
+
count(locks.id) AS count_working,
|
13
|
+
sum((error_count > 0)::int) AS count_errored,
|
14
|
+
max(error_count) AS highest_error_count,
|
15
|
+
min(run_at) AS oldest_run_at
|
16
|
+
FROM public.que_jobs
|
17
|
+
LEFT JOIN (
|
18
|
+
SELECT (classid::bigint << 32) + objid::bigint AS id
|
19
|
+
FROM pg_locks
|
20
|
+
WHERE locktype = 'advisory'
|
21
|
+
) locks USING (id)
|
22
|
+
WHERE finished_at IS NULL AND expired_at IS NULL
|
23
|
+
GROUP BY job_class
|
24
|
+
ORDER BY count(*) DESC
|
25
|
+
}
|
26
|
+
|
27
|
+
def job_stats
|
28
|
+
execute :job_stats
|
29
|
+
end
|
30
|
+
|
31
|
+
SQL[:job_states] =
|
32
|
+
%{
|
33
|
+
SELECT que_jobs.*,
|
34
|
+
pg.ruby_hostname,
|
35
|
+
pg.ruby_pid
|
36
|
+
FROM public.que_jobs
|
37
|
+
JOIN (
|
38
|
+
SELECT (classid::bigint << 32) + objid::bigint AS id, que_lockers.*
|
39
|
+
FROM pg_locks
|
40
|
+
JOIN public.que_lockers USING (pid)
|
41
|
+
WHERE locktype = 'advisory'
|
42
|
+
) pg USING (id)
|
43
|
+
}
|
44
|
+
|
45
|
+
def job_states
|
46
|
+
execute :job_states
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Logic for serializing to/from JSON. We assume that the standard library's JSON
|
4
|
+
# module is good enough for our purposes.
|
5
|
+
|
6
|
+
require 'json'
|
7
|
+
|
8
|
+
module Que
|
9
|
+
module Utils
|
10
|
+
module JSONSerialization
|
11
|
+
def serialize_json(object)
|
12
|
+
JSON.dump(object)
|
13
|
+
end
|
14
|
+
|
15
|
+
def deserialize_json(json)
|
16
|
+
# Allowing `create_additions` would be a security vulnerability.
|
17
|
+
JSON.parse(json, symbolize_names: true, create_additions: false)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Tools for logging from Que.
|
4
|
+
|
5
|
+
module Que
|
6
|
+
module Utils
|
7
|
+
module Logging
|
8
|
+
attr_accessor :logger, :log_formatter, :internal_logger
|
9
|
+
|
10
|
+
def log(event:, level: :info, **extra)
|
11
|
+
data = _default_log_data
|
12
|
+
data[:event] = Que.assert(Symbol, event)
|
13
|
+
data.merge!(extra)
|
14
|
+
|
15
|
+
if l = get_logger
|
16
|
+
begin
|
17
|
+
if output = log_formatter.call(data)
|
18
|
+
l.send level, output
|
19
|
+
end
|
20
|
+
rescue => e
|
21
|
+
msg =
|
22
|
+
"Error raised from Que.log_formatter proc:" +
|
23
|
+
" #{e.class}: #{e.message}\n#{e.backtrace}"
|
24
|
+
|
25
|
+
l.error(msg)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Logging method used specifically to instrument Que's internals. There's
|
31
|
+
# usually not an internal logger set up, so this method is generally a no-
|
32
|
+
# op unless the specs are running or someone turns on internal logging so
|
33
|
+
# we can debug an issue.
|
34
|
+
def internal_log(event, object = nil)
|
35
|
+
if l = get_logger(internal: true)
|
36
|
+
data = _default_log_data
|
37
|
+
|
38
|
+
data[:internal_event] = Que.assert(Symbol, event)
|
39
|
+
data[:object_id] = object.object_id if object
|
40
|
+
data[:t] = Time.now.utc.iso8601(6)
|
41
|
+
|
42
|
+
additional = Que.assert(Hash, yield)
|
43
|
+
|
44
|
+
# Make sure that none of our log contents accidentally overwrite our
|
45
|
+
# default data contents.
|
46
|
+
expected_length = data.length + additional.length
|
47
|
+
data.merge!(additional)
|
48
|
+
Que.assert(expected_length == data.length) do
|
49
|
+
"Bad internal logging keys in: #{additional.keys.inspect}"
|
50
|
+
end
|
51
|
+
|
52
|
+
l.info(JSON.dump(data))
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def get_logger(internal: false)
|
57
|
+
if l = internal ? internal_logger : logger
|
58
|
+
l.respond_to?(:call) ? l.call : l
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def log_formatter
|
63
|
+
@log_formatter ||= JSON.method(:dump)
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def _default_log_data
|
69
|
+
{
|
70
|
+
lib: :que,
|
71
|
+
hostname: CURRENT_HOSTNAME,
|
72
|
+
pid: Process.pid,
|
73
|
+
thread: Thread.current.object_id,
|
74
|
+
}
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Logic for middleware to wrap jobs.
|
4
|
+
|
5
|
+
module Que
|
6
|
+
module Utils
|
7
|
+
module Middleware
|
8
|
+
def run_middleware(job, &block)
|
9
|
+
invoke_middleware(
|
10
|
+
middleware: middleware.dup,
|
11
|
+
job: job,
|
12
|
+
block: block,
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
def middleware
|
17
|
+
@middleware ||= []
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def invoke_middleware(middleware:, job:, block:)
|
23
|
+
if m = middleware.shift
|
24
|
+
m.call(job) do
|
25
|
+
invoke_middleware(middleware: middleware, job: job, block: block)
|
26
|
+
end
|
27
|
+
else
|
28
|
+
block.call
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|