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.
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