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