que 0.11.3 → 2.2.0

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 (114) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/tests.yml +51 -0
  3. data/.gitignore +2 -0
  4. data/.ruby-version +1 -0
  5. data/CHANGELOG.md +502 -97
  6. data/Dockerfile +20 -0
  7. data/LICENSE.txt +1 -1
  8. data/README.md +205 -59
  9. data/auto/dev +21 -0
  10. data/auto/pre-push-hook +30 -0
  11. data/auto/psql +9 -0
  12. data/auto/test +5 -0
  13. data/auto/test-postgres-14 +17 -0
  14. data/bin/que +8 -81
  15. data/docker-compose.yml +47 -0
  16. data/docs/README.md +881 -0
  17. data/lib/que/active_job/extensions.rb +114 -0
  18. data/lib/que/active_record/connection.rb +51 -0
  19. data/lib/que/active_record/model.rb +48 -0
  20. data/lib/que/command_line_interface.rb +259 -0
  21. data/lib/que/connection.rb +198 -0
  22. data/lib/que/connection_pool.rb +78 -0
  23. data/lib/que/job.rb +210 -103
  24. data/lib/que/job_buffer.rb +255 -0
  25. data/lib/que/job_methods.rb +176 -0
  26. data/lib/que/listener.rb +176 -0
  27. data/lib/que/locker.rb +507 -0
  28. data/lib/que/metajob.rb +47 -0
  29. data/lib/que/migrations/4/down.sql +48 -0
  30. data/lib/que/migrations/4/up.sql +267 -0
  31. data/lib/que/migrations/5/down.sql +73 -0
  32. data/lib/que/migrations/5/up.sql +76 -0
  33. data/lib/que/migrations/6/down.sql +8 -0
  34. data/lib/que/migrations/6/up.sql +8 -0
  35. data/lib/que/migrations/7/down.sql +5 -0
  36. data/lib/que/migrations/7/up.sql +13 -0
  37. data/lib/que/migrations.rb +37 -18
  38. data/lib/que/poller.rb +274 -0
  39. data/lib/que/rails/railtie.rb +12 -0
  40. data/lib/que/result_queue.rb +35 -0
  41. data/lib/que/sequel/model.rb +52 -0
  42. data/lib/que/utils/assertions.rb +62 -0
  43. data/lib/que/utils/constantization.rb +19 -0
  44. data/lib/que/utils/error_notification.rb +68 -0
  45. data/lib/que/utils/freeze.rb +20 -0
  46. data/lib/que/utils/introspection.rb +50 -0
  47. data/lib/que/utils/json_serialization.rb +21 -0
  48. data/lib/que/utils/logging.rb +79 -0
  49. data/lib/que/utils/middleware.rb +46 -0
  50. data/lib/que/utils/queue_management.rb +18 -0
  51. data/lib/que/utils/ruby2_keywords.rb +19 -0
  52. data/lib/que/utils/transactions.rb +34 -0
  53. data/lib/que/version.rb +5 -1
  54. data/lib/que/worker.rb +145 -149
  55. data/lib/que.rb +103 -159
  56. data/que.gemspec +17 -4
  57. data/scripts/docker-entrypoint +14 -0
  58. data/scripts/test +6 -0
  59. metadata +59 -95
  60. data/.rspec +0 -2
  61. data/.travis.yml +0 -17
  62. data/Gemfile +0 -24
  63. data/docs/advanced_setup.md +0 -106
  64. data/docs/customizing_que.md +0 -200
  65. data/docs/error_handling.md +0 -47
  66. data/docs/inspecting_the_queue.md +0 -114
  67. data/docs/logging.md +0 -50
  68. data/docs/managing_workers.md +0 -80
  69. data/docs/migrating.md +0 -30
  70. data/docs/multiple_queues.md +0 -27
  71. data/docs/shutting_down_safely.md +0 -7
  72. data/docs/using_plain_connections.md +0 -41
  73. data/docs/using_sequel.md +0 -31
  74. data/docs/writing_reliable_jobs.md +0 -117
  75. data/lib/generators/que/install_generator.rb +0 -24
  76. data/lib/generators/que/templates/add_que.rb +0 -13
  77. data/lib/que/adapters/active_record.rb +0 -54
  78. data/lib/que/adapters/base.rb +0 -127
  79. data/lib/que/adapters/connection_pool.rb +0 -16
  80. data/lib/que/adapters/pg.rb +0 -21
  81. data/lib/que/adapters/pond.rb +0 -16
  82. data/lib/que/adapters/sequel.rb +0 -20
  83. data/lib/que/railtie.rb +0 -16
  84. data/lib/que/rake_tasks.rb +0 -59
  85. data/lib/que/sql.rb +0 -152
  86. data/spec/adapters/active_record_spec.rb +0 -152
  87. data/spec/adapters/connection_pool_spec.rb +0 -22
  88. data/spec/adapters/pg_spec.rb +0 -41
  89. data/spec/adapters/pond_spec.rb +0 -22
  90. data/spec/adapters/sequel_spec.rb +0 -57
  91. data/spec/gemfiles/Gemfile1 +0 -18
  92. data/spec/gemfiles/Gemfile2 +0 -18
  93. data/spec/spec_helper.rb +0 -118
  94. data/spec/support/helpers.rb +0 -19
  95. data/spec/support/jobs.rb +0 -35
  96. data/spec/support/shared_examples/adapter.rb +0 -37
  97. data/spec/support/shared_examples/multi_threaded_adapter.rb +0 -46
  98. data/spec/travis.rb +0 -23
  99. data/spec/unit/connection_spec.rb +0 -14
  100. data/spec/unit/customization_spec.rb +0 -251
  101. data/spec/unit/enqueue_spec.rb +0 -245
  102. data/spec/unit/helper_spec.rb +0 -12
  103. data/spec/unit/logging_spec.rb +0 -101
  104. data/spec/unit/migrations_spec.rb +0 -84
  105. data/spec/unit/pool_spec.rb +0 -365
  106. data/spec/unit/run_spec.rb +0 -14
  107. data/spec/unit/states_spec.rb +0 -50
  108. data/spec/unit/stats_spec.rb +0 -46
  109. data/spec/unit/transaction_spec.rb +0 -36
  110. data/spec/unit/work_spec.rb +0 -407
  111. data/spec/unit/worker_spec.rb +0 -167
  112. data/tasks/benchmark.rb +0 -3
  113. data/tasks/rspec.rb +0 -14
  114. data/tasks/safe_shutdown.rb +0 -67
@@ -0,0 +1,114 @@
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
+ args, kwargs = Que.split_out_ruby2_keywords(args)
16
+
17
+ Que.internal_log(:active_job_perform, self) do
18
+ {args: args, kwargs: kwargs}
19
+ end
20
+
21
+ _run(
22
+ args: Que.recursively_freeze(
23
+ que_filter_args(
24
+ args.map { |a| a.is_a?(Hash) ? a.deep_symbolize_keys : a }
25
+ )
26
+ ),
27
+ kwargs: Que.recursively_freeze(
28
+ que_filter_args(
29
+ kwargs.deep_symbolize_keys,
30
+ )
31
+ ),
32
+ )
33
+ end
34
+
35
+ private
36
+
37
+ # Have helper methods like `destroy` and `retry_in` delegate to the actual
38
+ # job object. If the current job is being run through an ActiveJob adapter
39
+ # other than Que's, this will return nil, which is fine.
40
+ def que_target
41
+ Thread.current[:que_current_job]
42
+ end
43
+
44
+ # Filter out :_aj_symbol_keys constructs so that keywords work as
45
+ # expected.
46
+ def que_filter_args(thing)
47
+ case thing
48
+ when Array
49
+ thing.map { |t| que_filter_args(t) }
50
+ when Hash
51
+ thing.each_with_object({}) do |(k, v), hash|
52
+ hash[k] = que_filter_args(v) unless k == :_aj_symbol_keys
53
+ end
54
+ else
55
+ thing
56
+ end
57
+ end
58
+ end
59
+
60
+ # A module that we mix into ActiveJob's wrapper for Que::Job, to maintain
61
+ # backwards-compatibility with internal changes we make.
62
+ module WrapperExtensions
63
+ module ClassMethods
64
+ # We've dropped support for job options supplied as top-level keywords, but ActiveJob's QueAdapter still uses them. So we have to move them into the job_options hash ourselves.
65
+ def enqueue(args, priority:, queue:, run_at: nil)
66
+ super(args, job_options: { priority: priority, queue: queue, run_at: run_at })
67
+ end
68
+ end
69
+
70
+ module InstanceMethods
71
+ # The Rails adapter (built against a pre-1.0 version of this gem)
72
+ # assumes that it can access a job's id via job.attrs["job_id"]. So,
73
+ # oblige it.
74
+ def attrs
75
+ {"job_id" => que_attrs[:id]}
76
+ end
77
+
78
+ def run(args)
79
+ # Our ActiveJob extensions expect to be able to operate on the actual
80
+ # job object, but there's no way to access it through ActiveJob. So,
81
+ # scope it to the current thread. It's a bit messy, but it's the best
82
+ # option under the circumstances (doesn't require hacking ActiveJob in
83
+ # any more extensive way).
84
+
85
+ # There's no reason this logic should ever nest, because it wouldn't
86
+ # make sense to run a worker inside of a job, but even so, assert that
87
+ # nothing absurd is going on.
88
+ Que.assert NilClass, Thread.current[:que_current_job]
89
+
90
+ begin
91
+ Thread.current[:que_current_job] = self
92
+
93
+ # We symbolize the args hash but ActiveJob doesn't like that :/
94
+ super(args.deep_stringify_keys)
95
+ ensure
96
+ # Also assert that the current job state was only removed now, but
97
+ # unset the job first so that an assertion failure doesn't mess up
98
+ # the state any more than it already has.
99
+ current = Thread.current[:que_current_job]
100
+ Thread.current[:que_current_job] = nil
101
+ Que.assert(self, current)
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ class ActiveJob::QueueAdapters::QueAdapter
110
+ class JobWrapper < Que::Job
111
+ extend Que::ActiveJob::WrapperExtensions::ClassMethods
112
+ prepend Que::ActiveJob::WrapperExtensions::InstanceMethods
113
+ end
114
+ 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/que-rb/que/issues/166#issuecomment-274218910
21
+ def wrap_in_rails_executor(&block)
22
+ if defined?(::Rails.application.executor)
23
+ ::Rails.application.executor.wrap(&block)
24
+ else
25
+ yield
26
+ end
27
+ end
28
+ end
29
+
30
+ module JobMiddleware
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 = 'public.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 (Arel.sql("now()"))) }
20
+ scope :not_scheduled, -> { where(t[:run_at].lteq(Arel.sql("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(Arel.sql("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, **kwargs)
43
+ where("que_jobs.args @> ? AND que_jobs.kwargs @> ?", JSON.dump(args), JSON.dump(kwargs))
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,259 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+ require 'logger'
5
+ require 'uri'
6
+
7
+ module Que
8
+ module CommandLineInterface
9
+ # Have a sensible default require file for Rails.
10
+ RAILS_ENVIRONMENT_FILE = './config/environment.rb'
11
+
12
+ class << self
13
+ # Need to rely on dependency injection a bit to make this method cleanly
14
+ # testable :/
15
+ def parse(
16
+ args:,
17
+ output:,
18
+ default_require_file: RAILS_ENVIRONMENT_FILE
19
+ )
20
+
21
+ options = {}
22
+ queues = []
23
+ log_level = 'info'
24
+ log_internals = false
25
+ poll_interval = 5
26
+ connection_url = nil
27
+ worker_count = nil
28
+ worker_priorities = nil
29
+
30
+ parser =
31
+ OptionParser.new do |opts|
32
+ opts.banner = 'usage: que [options] [file/to/require] ...'
33
+
34
+ opts.on(
35
+ '-h',
36
+ '--help',
37
+ "Show this help text.",
38
+ ) do
39
+ output.puts opts.help
40
+ return 0
41
+ end
42
+
43
+ opts.on(
44
+ '-i',
45
+ '--poll-interval [INTERVAL]',
46
+ Float,
47
+ "Set maximum interval between polls for available jobs, " \
48
+ "in seconds (default: 5)",
49
+ ) do |i|
50
+ poll_interval = i
51
+ end
52
+
53
+ opts.on(
54
+ '-l',
55
+ '--log-level [LEVEL]',
56
+ String,
57
+ "Set level at which to log to STDOUT " \
58
+ "(debug, info, warn, error, fatal) (default: info)",
59
+ ) do |l|
60
+ log_level = l
61
+ end
62
+
63
+ opts.on(
64
+ '-p',
65
+ '--worker-priorities [LIST]',
66
+ Array,
67
+ "List of priorities to assign to workers (default: 10,30,50,any,any,any)",
68
+ ) do |priority_array|
69
+ worker_priorities =
70
+ priority_array.map do |p|
71
+ case p
72
+ when /\Aany\z/i
73
+ nil
74
+ when /\A\d+\z/
75
+ Integer(p)
76
+ else
77
+ output.puts "Invalid priority option: '#{p}'. Please use an integer or the word 'any'."
78
+ return 1
79
+ end
80
+ end
81
+ end
82
+
83
+ opts.on(
84
+ '-q',
85
+ '--queue-name [NAME]',
86
+ String,
87
+ "Set a queue name to work jobs from. " \
88
+ "Can be passed multiple times. " \
89
+ "(default: the default queue only)",
90
+ ) do |queue_name|
91
+ queues << queue_name
92
+ end
93
+
94
+ opts.on(
95
+ '-w',
96
+ '--worker-count [COUNT]',
97
+ Integer,
98
+ "Set number of workers in process (default: 6)",
99
+ ) do |w|
100
+ worker_count = w
101
+ end
102
+
103
+ opts.on(
104
+ '-v',
105
+ '--version',
106
+ "Print Que version and exit.",
107
+ ) do
108
+ require 'que'
109
+ output.puts "Que version #{Que::VERSION}"
110
+ return 0
111
+ end
112
+
113
+ opts.on(
114
+ '--connection-url [URL]',
115
+ String,
116
+ "Set a custom database url to connect to for locking purposes.",
117
+ ) do |url|
118
+ options[:connection_url] = url
119
+ end
120
+
121
+ opts.on(
122
+ '--log-internals',
123
+ "Log verbosely about Que's internal state. " \
124
+ "Only recommended for debugging issues",
125
+ ) do |l|
126
+ log_internals = true
127
+ end
128
+
129
+ opts.on(
130
+ '--maximum-buffer-size [SIZE]',
131
+ Integer,
132
+ "Set maximum number of jobs to be locked and held in this " \
133
+ "process awaiting a worker (default: 8)",
134
+ ) do |s|
135
+ options[:maximum_buffer_size] = s
136
+ end
137
+
138
+ opts.on(
139
+ '--minimum-buffer-size [SIZE]',
140
+ Integer,
141
+ "Unused (deprecated)",
142
+ ) do |s|
143
+ warn "The --minimum-buffer-size SIZE option has been deprecated and will be removed in v2.0 (it's actually been unused since v1.0.0.beta4)"
144
+ end
145
+
146
+ opts.on(
147
+ '--wait-period [PERIOD]',
148
+ Float,
149
+ "Set maximum interval between checks of the in-memory job queue, " \
150
+ "in milliseconds (default: 50)",
151
+ ) do |p|
152
+ options[:wait_period] = p
153
+ end
154
+ end
155
+
156
+ parser.parse!(args)
157
+
158
+ options[:worker_priorities] =
159
+ if worker_count && worker_priorities
160
+ worker_priorities.values_at(0...worker_count)
161
+ elsif worker_priorities
162
+ worker_priorities
163
+ elsif worker_count
164
+ Array.new(worker_count) { nil }
165
+ else
166
+ [10, 30, 50, nil, nil, nil]
167
+ end
168
+
169
+ if args.length.zero?
170
+ if File.exist?(default_require_file)
171
+ args << default_require_file
172
+ else
173
+ output.puts <<-OUTPUT
174
+ You didn't include any Ruby files to require!
175
+ Que needs to be able to load your application before it can process jobs.
176
+ (Or use `que -h` for a list of options)
177
+ OUTPUT
178
+ return 1
179
+ end
180
+ end
181
+
182
+ args.each do |file|
183
+ begin
184
+ require file
185
+ rescue LoadError => e
186
+ output.puts "Could not load file '#{file}': #{e}"
187
+ return 1
188
+ end
189
+ end
190
+
191
+ Que.logger ||= Logger.new(STDOUT)
192
+
193
+ if log_internals
194
+ Que.internal_logger = Que.logger
195
+ end
196
+
197
+ begin
198
+ Que.get_logger.level = Logger.const_get(log_level.upcase)
199
+ rescue NameError
200
+ output.puts "Unsupported logging level: #{log_level} (try debug, info, warn, error, or fatal)"
201
+ return 1
202
+ end
203
+
204
+ if queues.any?
205
+ queues_hash = {}
206
+
207
+ queues.each do |queue|
208
+ name, interval = queue.split('=')
209
+ p = interval ? Float(interval) : poll_interval
210
+
211
+ Que.assert(p > 0.01) { "Poll intervals can't be less than 0.01 seconds!" }
212
+
213
+ queues_hash[name] = p
214
+ end
215
+
216
+ options[:queues] = queues_hash
217
+ end
218
+
219
+ options[:poll_interval] = poll_interval
220
+
221
+ locker =
222
+ begin
223
+ Que::Locker.new(**options)
224
+ rescue => e
225
+ output.puts(e.message)
226
+ return 1
227
+ end
228
+
229
+ # It's a bit sloppy to use a global for this when a local variable would
230
+ # do, but we want to stop the locker from the CLI specs, so...
231
+ $stop_que_executable = false
232
+ %w[INT TERM].each { |signal| trap(signal) { $stop_que_executable = true } }
233
+
234
+ output.puts(
235
+ <<~STARTUP
236
+ Que #{Que::VERSION} started worker process with:
237
+ Worker threads: #{locker.workers.length} (priorities: #{locker.workers.map { |w| w.priority || 'any' }.join(', ')})
238
+ Buffer size: #{locker.job_buffer.maximum_size}
239
+ Queues:
240
+ #{locker.queues.map { |queue, interval| " - #{queue} (poll interval: #{interval}s)" }.join("\n")}
241
+ Que waiting for jobs...
242
+ STARTUP
243
+ )
244
+
245
+ loop do
246
+ sleep 0.01
247
+ break if $stop_que_executable || locker.stopping?
248
+ end
249
+
250
+ output.puts "\nFinishing Que's current jobs before exiting..."
251
+
252
+ locker.stop!
253
+
254
+ output.puts "Que's jobs finished, exiting..."
255
+ return 0
256
+ end
257
+ end
258
+ end
259
+ end
@@ -0,0 +1,198 @@
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
+ if conn.instance_variable_defined?(:@que_wrapper)
34
+ conn.instance_variable_get(:@que_wrapper)
35
+ else
36
+ conn.instance_variable_set(:@que_wrapper, new(conn))
37
+ end
38
+ else
39
+ raise Error, "Unsupported input for Connection.wrap: #{conn.class}"
40
+ end
41
+ end
42
+ end
43
+
44
+ def initialize(connection)
45
+ @wrapped_connection = connection
46
+ @prepared_statements = Set.new
47
+ end
48
+
49
+ def execute(command, params = [])
50
+ sql =
51
+ case command
52
+ when Symbol then SQL[command]
53
+ when String then command
54
+ else raise Error, "Bad command! #{command.inspect}"
55
+ end
56
+
57
+ params = convert_params(params)
58
+
59
+ result =
60
+ Que.run_sql_middleware(sql, params) do
61
+ # Some versions of the PG gem dislike an empty/nil params argument.
62
+ if params.empty?
63
+ wrapped_connection.async_exec(sql)
64
+ else
65
+ wrapped_connection.async_exec_params(sql, params)
66
+ end
67
+ end
68
+
69
+ Que.internal_log :connection_execute, self do
70
+ {
71
+ backend_pid: backend_pid,
72
+ command: command,
73
+ params: params,
74
+ ntuples: result.ntuples,
75
+ }
76
+ end
77
+
78
+ convert_result(result)
79
+ end
80
+
81
+ def execute_prepared(command, params = nil)
82
+ Que.assert(Symbol, command)
83
+
84
+ if !Que.use_prepared_statements || in_transaction?
85
+ return execute(command, params)
86
+ end
87
+
88
+ name = "que_#{command}"
89
+
90
+ begin
91
+ unless @prepared_statements.include?(command)
92
+ wrapped_connection.prepare(name, SQL[command])
93
+ @prepared_statements.add(command)
94
+ prepared_just_now = true
95
+ end
96
+
97
+ convert_result(
98
+ wrapped_connection.exec_prepared(name, params)
99
+ )
100
+ rescue ::PG::InvalidSqlStatementName => error
101
+ # Reconnections on ActiveRecord can cause the same connection
102
+ # objects to refer to new backends, so recover as well as we can.
103
+
104
+ unless prepared_just_now
105
+ Que.log level: :warn, event: :reprepare_statement, command: command
106
+ @prepared_statements.delete(command)
107
+ retry
108
+ end
109
+
110
+ raise error
111
+ end
112
+ end
113
+
114
+ def next_notification
115
+ wrapped_connection.notifies
116
+ end
117
+
118
+ def drain_notifications
119
+ loop { break if next_notification.nil? }
120
+ end
121
+
122
+ def server_version
123
+ wrapped_connection.server_version
124
+ end
125
+
126
+ def in_transaction?
127
+ wrapped_connection.transaction_status != ::PG::PQTRANS_IDLE
128
+ end
129
+
130
+ private
131
+
132
+ def convert_params(params)
133
+ params.map do |param|
134
+ case param
135
+ when Time
136
+ # The pg gem unfortunately doesn't convert fractions of time
137
+ # instances, so cast them to a string.
138
+ param.strftime('%Y-%m-%d %H:%M:%S.%6N %z')
139
+ when Array, Hash
140
+ # Handle JSON.
141
+ Que.serialize_json(param)
142
+ else
143
+ param
144
+ end
145
+ end
146
+ end
147
+
148
+ # Procs used to convert strings from Postgres into Ruby types.
149
+ CAST_PROCS = {
150
+ # Boolean
151
+ 16 => -> (value) {
152
+ case value
153
+ when String then value == 't'.freeze
154
+ else !!value
155
+ end
156
+ },
157
+
158
+ # Timestamp with time zone
159
+ 1184 => -> (value) {
160
+ case value
161
+ when Time then value
162
+ when String then Time.parse(value)
163
+ else raise "Unexpected time class: #{value.class} (#{value.inspect})"
164
+ end
165
+ }
166
+ }
167
+
168
+ # JSON, JSONB
169
+ CAST_PROCS[114] = CAST_PROCS[3802] = -> (j) { Que.deserialize_json(j) }
170
+
171
+ # Integer, bigint, smallint
172
+ CAST_PROCS[23] = CAST_PROCS[20] = CAST_PROCS[21] = proc(&:to_i)
173
+
174
+ CAST_PROCS.freeze
175
+
176
+ def convert_result(result)
177
+ output = result.to_a
178
+
179
+ result.fields.each_with_index do |field, index|
180
+ symbol = field.to_sym
181
+
182
+ if converter = CAST_PROCS[result.ftype(index)]
183
+ output.each do |hash|
184
+ value = hash.delete(field)
185
+ value = converter.call(value) if value
186
+ hash[symbol] = value
187
+ end
188
+ else
189
+ output.each do |hash|
190
+ hash[symbol] = hash.delete(field)
191
+ end
192
+ end
193
+ end
194
+
195
+ output
196
+ end
197
+ end
198
+ end