que 0.14.3 → 1.0.0.beta
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/.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
|