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.
Files changed (102) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +2 -0
  3. data/CHANGELOG.md +108 -14
  4. data/LICENSE.txt +1 -1
  5. data/README.md +49 -45
  6. data/bin/command_line_interface.rb +239 -0
  7. data/bin/que +8 -82
  8. data/docs/README.md +2 -0
  9. data/docs/active_job.md +6 -0
  10. data/docs/advanced_setup.md +7 -64
  11. data/docs/command_line_interface.md +45 -0
  12. data/docs/error_handling.md +65 -18
  13. data/docs/inspecting_the_queue.md +30 -80
  14. data/docs/job_helper_methods.md +27 -0
  15. data/docs/logging.md +3 -22
  16. data/docs/managing_workers.md +6 -61
  17. data/docs/middleware.md +15 -0
  18. data/docs/migrating.md +4 -7
  19. data/docs/multiple_queues.md +8 -4
  20. data/docs/shutting_down_safely.md +1 -1
  21. data/docs/using_plain_connections.md +39 -15
  22. data/docs/using_sequel.md +5 -3
  23. data/docs/writing_reliable_jobs.md +15 -24
  24. data/lib/que.rb +98 -182
  25. data/lib/que/active_job/extensions.rb +97 -0
  26. data/lib/que/active_record/connection.rb +51 -0
  27. data/lib/que/active_record/model.rb +48 -0
  28. data/lib/que/connection.rb +179 -0
  29. data/lib/que/connection_pool.rb +78 -0
  30. data/lib/que/job.rb +107 -156
  31. data/lib/que/job_cache.rb +240 -0
  32. data/lib/que/job_methods.rb +168 -0
  33. data/lib/que/listener.rb +176 -0
  34. data/lib/que/locker.rb +466 -0
  35. data/lib/que/metajob.rb +47 -0
  36. data/lib/que/migrations.rb +24 -17
  37. data/lib/que/migrations/4/down.sql +48 -0
  38. data/lib/que/migrations/4/up.sql +265 -0
  39. data/lib/que/poller.rb +267 -0
  40. data/lib/que/rails/railtie.rb +14 -0
  41. data/lib/que/result_queue.rb +35 -0
  42. data/lib/que/sequel/model.rb +51 -0
  43. data/lib/que/utils/assertions.rb +62 -0
  44. data/lib/que/utils/constantization.rb +19 -0
  45. data/lib/que/utils/error_notification.rb +68 -0
  46. data/lib/que/utils/freeze.rb +20 -0
  47. data/lib/que/utils/introspection.rb +50 -0
  48. data/lib/que/utils/json_serialization.rb +21 -0
  49. data/lib/que/utils/logging.rb +78 -0
  50. data/lib/que/utils/middleware.rb +33 -0
  51. data/lib/que/utils/queue_management.rb +18 -0
  52. data/lib/que/utils/transactions.rb +34 -0
  53. data/lib/que/version.rb +1 -1
  54. data/lib/que/worker.rb +128 -167
  55. data/que.gemspec +13 -2
  56. metadata +37 -80
  57. data/.rspec +0 -2
  58. data/.travis.yml +0 -64
  59. data/Gemfile +0 -24
  60. data/docs/customizing_que.md +0 -200
  61. data/lib/generators/que/install_generator.rb +0 -24
  62. data/lib/generators/que/templates/add_que.rb +0 -13
  63. data/lib/que/adapters/active_record.rb +0 -40
  64. data/lib/que/adapters/base.rb +0 -133
  65. data/lib/que/adapters/connection_pool.rb +0 -16
  66. data/lib/que/adapters/pg.rb +0 -21
  67. data/lib/que/adapters/pond.rb +0 -16
  68. data/lib/que/adapters/sequel.rb +0 -20
  69. data/lib/que/railtie.rb +0 -16
  70. data/lib/que/rake_tasks.rb +0 -59
  71. data/lib/que/sql.rb +0 -170
  72. data/spec/adapters/active_record_spec.rb +0 -175
  73. data/spec/adapters/connection_pool_spec.rb +0 -22
  74. data/spec/adapters/pg_spec.rb +0 -41
  75. data/spec/adapters/pond_spec.rb +0 -22
  76. data/spec/adapters/sequel_spec.rb +0 -57
  77. data/spec/gemfiles/Gemfile.current +0 -19
  78. data/spec/gemfiles/Gemfile.old +0 -19
  79. data/spec/gemfiles/Gemfile.older +0 -19
  80. data/spec/gemfiles/Gemfile.oldest +0 -19
  81. data/spec/spec_helper.rb +0 -129
  82. data/spec/support/helpers.rb +0 -25
  83. data/spec/support/jobs.rb +0 -35
  84. data/spec/support/shared_examples/adapter.rb +0 -42
  85. data/spec/support/shared_examples/multi_threaded_adapter.rb +0 -46
  86. data/spec/unit/configuration_spec.rb +0 -31
  87. data/spec/unit/connection_spec.rb +0 -14
  88. data/spec/unit/customization_spec.rb +0 -251
  89. data/spec/unit/enqueue_spec.rb +0 -245
  90. data/spec/unit/helper_spec.rb +0 -12
  91. data/spec/unit/logging_spec.rb +0 -101
  92. data/spec/unit/migrations_spec.rb +0 -84
  93. data/spec/unit/pool_spec.rb +0 -365
  94. data/spec/unit/run_spec.rb +0 -14
  95. data/spec/unit/states_spec.rb +0 -50
  96. data/spec/unit/stats_spec.rb +0 -46
  97. data/spec/unit/transaction_spec.rb +0 -36
  98. data/spec/unit/work_spec.rb +0 -596
  99. data/spec/unit/worker_spec.rb +0 -167
  100. data/tasks/benchmark.rb +0 -3
  101. data/tasks/rspec.rb +0 -14
  102. 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