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,97 @@
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
+ Que.internal_log(:active_job_perform, self) do
16
+ {args: args}
17
+ end
18
+
19
+ _run(
20
+ args: Que.recursively_freeze(
21
+ que_filter_args(
22
+ args.map { |a| a.is_a?(Hash) ? a.deep_symbolize_keys : a }
23
+ )
24
+ )
25
+ )
26
+ end
27
+
28
+ private
29
+
30
+ # Have helper methods like `destroy` and `retry_in` delegate to the actual
31
+ # job object. If the current job is being run through an ActiveJob adapter
32
+ # other than Que's, this will return nil, which is fine.
33
+ def que_target
34
+ Thread.current[:que_current_job]
35
+ end
36
+
37
+ # Filter out :_aj_symbol_keys constructs so that keywords work as
38
+ # expected.
39
+ def que_filter_args(thing)
40
+ case thing
41
+ when Array
42
+ thing.map { |t| que_filter_args(t) }
43
+ when Hash
44
+ thing.each_with_object({}) do |(k, v), hash|
45
+ hash[k] = que_filter_args(v) unless k == :_aj_symbol_keys
46
+ end
47
+ else
48
+ thing
49
+ end
50
+ end
51
+ end
52
+
53
+ # A module that we mix into ActiveJob's wrapper for Que::Job, to maintain
54
+ # backwards-compatibility with internal changes we make.
55
+ module WrapperExtensions
56
+ # The Rails adapter (built against a pre-1.0 version of this gem)
57
+ # assumes that it can access a job's id via job.attrs["job_id"]. So,
58
+ # oblige it.
59
+ def attrs
60
+ {"job_id" => que_attrs[:id]}
61
+ end
62
+
63
+ def run(args)
64
+ # Our ActiveJob extensions expect to be able to operate on the actual
65
+ # job object, but there's no way to access it through ActiveJob. So,
66
+ # scope it to the current thread. It's a bit messy, but it's the best
67
+ # option under the circumstances (doesn't require hacking ActiveJob in
68
+ # any more extensive way).
69
+
70
+ # There's no reason this logic should ever nest, because it wouldn't
71
+ # make sense to run a worker inside of a job, but even so, assert that
72
+ # nothing absurd is going on.
73
+ Que.assert NilClass, Thread.current[:que_current_job]
74
+
75
+ begin
76
+ Thread.current[:que_current_job] = self
77
+
78
+ # We symbolize the args hash but ActiveJob doesn't like that :/
79
+ super(args.deep_stringify_keys)
80
+ ensure
81
+ # Also assert that the current job state was only removed now, but
82
+ # unset the job first so that an assertion failure doesn't mess up
83
+ # the state any more than it already has.
84
+ current = Thread.current[:que_current_job]
85
+ Thread.current[:que_current_job] = nil
86
+ Que.assert(self, current)
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ class ActiveJob::QueueAdapters::QueAdapter
94
+ class JobWrapper < Que::Job
95
+ prepend Que::ActiveJob::WrapperExtensions
96
+ end
97
+ 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/chanks/que/issues/166#issuecomment-274218910
21
+ def wrap_in_rails_executor
22
+ if defined?(::Rails.application.executor)
23
+ ::Rails.application.executor.wrap { yield }
24
+ else
25
+ yield
26
+ end
27
+ end
28
+ end
29
+
30
+ module Middleware
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 = :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("now()")) }
20
+ scope :not_scheduled, -> { where(t[:run_at].lteq("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("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)
43
+ where("que_jobs.args @> ?", JSON.dump(args))
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,179 @@
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
+ conn.instance_variable_get(:@que_wrapper) ||
34
+ conn.instance_variable_set(:@que_wrapper, new(conn))
35
+ else
36
+ raise Error, "Unsupported input for Connection.wrap: #{conn.class}"
37
+ end
38
+ end
39
+ end
40
+
41
+ def initialize(connection)
42
+ @wrapped_connection = connection
43
+ @prepared_statements = Set.new
44
+ end
45
+
46
+ def execute(command, params = nil)
47
+ sql =
48
+ case command
49
+ when Symbol then SQL[command]
50
+ when String then command
51
+ else raise Error, "Bad command! #{command.inspect}"
52
+ end
53
+
54
+ params = convert_params(params) if params
55
+ result = execute_sql(sql, params)
56
+
57
+ Que.internal_log :connection_execute, self do
58
+ {
59
+ backend_pid: backend_pid,
60
+ command: command,
61
+ params: params,
62
+ ntuples: result.ntuples,
63
+ }
64
+ end
65
+
66
+ convert_result(result)
67
+ end
68
+
69
+ def execute_prepared(command, params = nil)
70
+ Que.assert(Symbol, command)
71
+
72
+ if !Que.use_prepared_statements || in_transaction?
73
+ return execute(command, params)
74
+ end
75
+
76
+ name = "que_#{command}"
77
+
78
+ begin
79
+ unless @prepared_statements.include?(command)
80
+ wrapped_connection.prepare(name, SQL[command])
81
+ @prepared_statements.add(command)
82
+ prepared_just_now = true
83
+ end
84
+
85
+ convert_result(
86
+ wrapped_connection.exec_prepared(name, params)
87
+ )
88
+ rescue ::PG::InvalidSqlStatementName => error
89
+ # Reconnections on ActiveRecord can cause the same connection
90
+ # objects to refer to new backends, so recover as well as we can.
91
+
92
+ unless prepared_just_now
93
+ Que.log level: :warn, event: :reprepare_statement, command: command
94
+ @prepared_statements.delete(command)
95
+ retry
96
+ end
97
+
98
+ raise error
99
+ end
100
+ end
101
+
102
+ def next_notification
103
+ wrapped_connection.notifies
104
+ end
105
+
106
+ def drain_notifications
107
+ loop { break if next_notification.nil? }
108
+ end
109
+
110
+ def in_transaction?
111
+ wrapped_connection.transaction_status != ::PG::PQTRANS_IDLE
112
+ end
113
+
114
+ private
115
+
116
+ def convert_params(params)
117
+ params.map do |param|
118
+ case param
119
+ when Time
120
+ # The pg gem unfortunately doesn't convert fractions of time
121
+ # instances, so cast them to a string.
122
+ param.strftime('%Y-%m-%d %H:%M:%S.%6N %z')
123
+ when Array, Hash
124
+ # Handle JSON.
125
+ Que.serialize_json(param)
126
+ else
127
+ param
128
+ end
129
+ end
130
+ end
131
+
132
+ def execute_sql(sql, params)
133
+ # Some versions of the PG gem dislike an empty/nil params argument.
134
+ if params && !params.empty?
135
+ wrapped_connection.async_exec(sql, params)
136
+ else
137
+ wrapped_connection.async_exec(sql)
138
+ end
139
+ end
140
+
141
+ # Procs used to convert strings from Postgres into Ruby types.
142
+ CAST_PROCS = {
143
+ # Boolean
144
+ 16 => 't'.method(:==),
145
+ # Timestamp with time zone
146
+ 1184 => Time.method(:parse),
147
+ }
148
+
149
+ # JSON, JSONB
150
+ CAST_PROCS[114] = CAST_PROCS[3802] = -> (j) { Que.deserialize_json(j) }
151
+
152
+ # Integer, bigint, smallint
153
+ CAST_PROCS[23] = CAST_PROCS[20] = CAST_PROCS[21] = proc(&:to_i)
154
+
155
+ CAST_PROCS.freeze
156
+
157
+ def convert_result(result)
158
+ output = result.to_a
159
+
160
+ result.fields.each_with_index do |field, index|
161
+ symbol = field.to_sym
162
+
163
+ if converter = CAST_PROCS[result.ftype(index)]
164
+ output.each do |hash|
165
+ value = hash.delete(field)
166
+ value = converter.call(value) if value
167
+ hash[symbol] = value
168
+ end
169
+ else
170
+ output.each do |hash|
171
+ hash[symbol] = hash.delete(field)
172
+ end
173
+ end
174
+ end
175
+
176
+ output
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A wrapper around whatever connection pool we're using. Mainly just asserts
4
+ # that the source connection pool is reentrant and thread-safe.
5
+
6
+ module Que
7
+ class ConnectionPool
8
+ def initialize(&block)
9
+ @connection_proc = block
10
+ @checked_out = Set.new
11
+ @mutex = Mutex.new
12
+ @thread_key = "que_connection_pool_#{object_id}".to_sym
13
+ end
14
+
15
+ def checkout
16
+ # Do some asserting to ensure that the connection pool we're using is
17
+ # behaving properly.
18
+ @connection_proc.call do |conn|
19
+ # Did this pool already have a connection for this thread?
20
+ preexisting = wrapped = current_connection
21
+
22
+ begin
23
+ if preexisting
24
+ # If so, check that the connection we just got is the one we expect.
25
+ if preexisting.wrapped_connection.backend_pid != conn.backend_pid
26
+ raise Error, "Connection pool is not reentrant! previous: #{preexisting.wrapped_connection.inspect} now: #{conn.inspect}"
27
+ end
28
+ else
29
+ # If not, make sure that it wasn't promised to any other threads.
30
+ sync do
31
+ Que.assert(@checked_out.add?(conn.backend_pid)) do
32
+ "Connection pool didn't synchronize access properly! (entrance: #{conn.backend_pid})"
33
+ end
34
+ end
35
+
36
+ self.current_connection = wrapped = Connection.wrap(conn)
37
+ end
38
+
39
+ yield(wrapped)
40
+ ensure
41
+ if preexisting.nil?
42
+ # We're at the top level (about to return this connection to the
43
+ # pool we got it from), so mark it as no longer ours.
44
+ self.current_connection = nil
45
+
46
+ sync do
47
+ Que.assert(@checked_out.delete?(conn.backend_pid)) do
48
+ "Connection pool didn't synchronize access properly! (exit: #{conn.backend_pid})"
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ def execute(*args)
57
+ checkout { |conn| conn.execute(*args) }
58
+ end
59
+
60
+ def in_transaction?
61
+ checkout { |conn| conn.in_transaction? }
62
+ end
63
+
64
+ private
65
+
66
+ def sync
67
+ @mutex.synchronize { yield }
68
+ end
69
+
70
+ def current_connection
71
+ Thread.current[@thread_key]
72
+ end
73
+
74
+ def current_connection=(c)
75
+ Thread.current[@thread_key] = c
76
+ end
77
+ end
78
+ end