que 0.11.3 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
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