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,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Que
4
+ SQL[:finish_job] =
5
+ %{
6
+ UPDATE public.que_jobs
7
+ SET finished_at = now()
8
+ WHERE id = $1::bigint
9
+ }
10
+
11
+ SQL[:expire_job] =
12
+ %{
13
+ UPDATE public.que_jobs
14
+ SET error_count = error_count + 1,
15
+ expired_at = now()
16
+ WHERE id = $1::bigint
17
+ }
18
+
19
+ SQL[:destroy_job] =
20
+ %{
21
+ DELETE FROM public.que_jobs
22
+ WHERE id = $1::bigint
23
+ }
24
+
25
+ SQL[:set_error] =
26
+ %{
27
+ UPDATE public.que_jobs
28
+ SET error_count = error_count + 1,
29
+ run_at = now() + $1::float * '1 second'::interval,
30
+ last_error_message = left($2::text, 500),
31
+ last_error_backtrace = left($3::text, 10000)
32
+ WHERE id = $4::bigint
33
+ }
34
+
35
+ module JobMethods
36
+ # Note that we delegate almost all methods to the result of the que_target
37
+ # method, which could be one of a few things, depending on the circumstance.
38
+
39
+ # Run the job with error handling and cleanup logic. Optionally support
40
+ # overriding the args, because it's necessary when jobs are invoked from
41
+ # ActiveJob.
42
+ def _run(args: nil, kwargs: nil, reraise_errors: false)
43
+ if args.nil? && que_target
44
+ args = que_target.que_attrs.fetch(:args)
45
+ end
46
+
47
+ if kwargs.nil? && que_target
48
+ kwargs = que_target.que_attrs.fetch(:kwargs)
49
+ end
50
+
51
+ run(*args, **kwargs)
52
+ default_resolve_action if que_target && !que_target.que_resolved
53
+ rescue => error
54
+ raise error unless que_target
55
+
56
+ que_target.que_error = error
57
+
58
+ run_error_notifier =
59
+ begin
60
+ handle_error(error)
61
+ rescue => error_2
62
+ Que.notify_error(error_2, que_target.que_attrs)
63
+ true
64
+ end
65
+
66
+ Que.notify_error(error, que_target.que_attrs) if run_error_notifier
67
+ retry_in_default_interval unless que_target.que_resolved
68
+
69
+ raise error if reraise_errors
70
+ end
71
+
72
+ def log_level(elapsed)
73
+ :debug
74
+ end
75
+
76
+ private
77
+
78
+ # This method defines the object on which the various job helper methods are
79
+ # acting. When using Que in the default configuration this will just be
80
+ # self, but when using the Que adapter for ActiveJob it'll be the actual
81
+ # underlying job object. When running an ActiveJob::Base subclass that
82
+ # includes this module through a separate adapter this will be nil - hence,
83
+ # the defensive coding in every method that no-ops if que_target is falsy.
84
+ def que_target
85
+ raise NotImplementedError
86
+ end
87
+
88
+ def resolve_que_setting(*args)
89
+ return unless que_target
90
+
91
+ que_target.class.resolve_que_setting(*args)
92
+ end
93
+
94
+ def default_resolve_action
95
+ return unless que_target
96
+
97
+ destroy
98
+ end
99
+
100
+ def expire
101
+ return unless que_target
102
+
103
+ if id = que_target.que_attrs[:id]
104
+ Que.execute :expire_job, [id]
105
+ end
106
+
107
+ que_target.que_resolved = true
108
+ end
109
+
110
+ def finish
111
+ return unless que_target
112
+
113
+ if id = que_target.que_attrs[:id]
114
+ Que.execute :finish_job, [id]
115
+ end
116
+
117
+ que_target.que_resolved = true
118
+ end
119
+
120
+ def error_count
121
+ return 0 unless que_target
122
+
123
+ count = que_target.que_attrs.fetch(:error_count)
124
+ que_target.que_error ? count + 1 : count
125
+ end
126
+
127
+ # To be overridden in subclasses.
128
+ def handle_error(error)
129
+ return unless que_target
130
+
131
+ max = resolve_que_setting(:maximum_retry_count)
132
+
133
+ if max && error_count > max
134
+ expire
135
+ else
136
+ retry_in_default_interval
137
+ end
138
+ end
139
+
140
+ def retry_in_default_interval
141
+ return unless que_target
142
+
143
+ retry_in(resolve_que_setting(:retry_interval, error_count))
144
+ end
145
+
146
+ # Explicitly check for the job id in these helpers, because it won't exist
147
+ # if we're running synchronously.
148
+ def retry_in(period)
149
+ return unless que_target
150
+
151
+ if id = que_target.que_attrs[:id]
152
+ values = [period]
153
+
154
+ if e = que_target.que_error
155
+ values << "#{e.class}: #{e.message}".slice(0, 500) << e.backtrace.join("\n").slice(0, 10000)
156
+ else
157
+ values << nil << nil
158
+ end
159
+
160
+ Que.execute :set_error, values << id
161
+ end
162
+
163
+ que_target.que_resolved = true
164
+ end
165
+
166
+ def destroy
167
+ return unless que_target
168
+
169
+ if id = que_target.que_attrs[:id]
170
+ Que.execute :destroy_job, [id]
171
+ end
172
+
173
+ que_target.que_resolved = true
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Que
4
+ class Listener
5
+ MESSAGE_FORMATS = {}
6
+
7
+ attr_reader :connection, :channel
8
+
9
+ def initialize(connection:, channel: nil)
10
+ @connection = connection
11
+ @channel = channel || "que_listener_#{connection.backend_pid}"
12
+
13
+ Que.internal_log :listener_instantiate, self do
14
+ {
15
+ backend_pid: connection.backend_pid,
16
+ }
17
+ end
18
+ end
19
+
20
+ def listen
21
+ connection.execute "LISTEN #{channel}"
22
+ end
23
+
24
+ def wait_for_grouped_messages(timeout)
25
+ messages = wait_for_messages(timeout)
26
+
27
+ output = {}
28
+
29
+ messages.each do |message|
30
+ message_type = message.delete(:message_type)
31
+
32
+ (output[message_type.to_sym] ||= []) << message.freeze
33
+ end
34
+
35
+ output
36
+ end
37
+
38
+ def wait_for_messages(timeout)
39
+ # Make sure we never pass nil to this method, so we don't hang the thread.
40
+ Que.assert(Numeric, timeout)
41
+
42
+ Que.internal_log :listener_waiting, self do
43
+ {
44
+ backend_pid: connection.backend_pid,
45
+ channel: channel,
46
+ timeout: timeout,
47
+ }
48
+ end
49
+
50
+ accumulated_messages = []
51
+
52
+ # Notifications often come in batches (especially when a transaction that
53
+ # inserted many jobs commits), so we want to loop and pick up all the
54
+ # received notifications before continuing.
55
+ loop do
56
+ notification_received =
57
+ connection.wait_for_notify(timeout) do |channel, pid, payload|
58
+ # We've received a notification, so zero out the timeout before we
59
+ # loop again to check for another message. This ensures that we
60
+ # don't wait an additional `timeout` seconds after processing the
61
+ # final message before this method returns.
62
+ timeout = 0
63
+
64
+ Que.internal_log(:listener_received_notification, self) do
65
+ {
66
+ channel: channel,
67
+ backend_pid: connection.backend_pid,
68
+ source_pid: pid,
69
+ payload: payload,
70
+ }
71
+ end
72
+
73
+ # Be very defensive about the message we receive - it may not be
74
+ # valid JSON or have the structure we expect.
75
+ next unless message = parse_payload(payload)
76
+
77
+ case message
78
+ when Array then accumulated_messages.concat(message)
79
+ when Hash then accumulated_messages << message
80
+ else raise Error, "Unexpected parse_payload output: #{message.class}"
81
+ end
82
+ end
83
+
84
+ break unless notification_received
85
+ end
86
+
87
+ return accumulated_messages if accumulated_messages.empty?
88
+
89
+ Que.internal_log(:listener_received_messages, self) do
90
+ {
91
+ backend_pid: connection.backend_pid,
92
+ channel: channel,
93
+ messages: accumulated_messages,
94
+ }
95
+ end
96
+
97
+ accumulated_messages.keep_if do |message|
98
+ next unless message.is_a?(Hash)
99
+ next unless type = message[:message_type]
100
+ next unless type.is_a?(String)
101
+ next unless format = MESSAGE_FORMATS[type.to_sym]
102
+
103
+ if message_matches_format?(message, format)
104
+ true
105
+ else
106
+ error_message = [
107
+ "Message of type '#{type}' doesn't match format!",
108
+ # Massage message and format a bit to make these errors more readable.
109
+ "Message: #{Hash[message.reject{|k,v| k == :message_type}.sort_by{|k,v| k}].inspect}",
110
+ "Format: #{Hash[format.sort_by{|k,v| k}].inspect}",
111
+ ].join("\n")
112
+
113
+ Que.notify_error_async(Error.new(error_message))
114
+ false
115
+ end
116
+ end
117
+
118
+ Que.internal_log(:listener_filtered_messages, self) do
119
+ {
120
+ backend_pid: connection.backend_pid,
121
+ channel: channel,
122
+ messages: accumulated_messages,
123
+ }
124
+ end
125
+
126
+ accumulated_messages
127
+ end
128
+
129
+ def unlisten
130
+ # Be sure to drain all notifications so that any code that uses this
131
+ # connection later doesn't receive any nasty surprises.
132
+ connection.execute "UNLISTEN *"
133
+ connection.drain_notifications
134
+
135
+ Que.internal_log :listener_unlisten, self do
136
+ {
137
+ backend_pid: connection.backend_pid,
138
+ channel: channel,
139
+ }
140
+ end
141
+ end
142
+
143
+ private
144
+
145
+ def parse_payload(payload)
146
+ Que.deserialize_json(payload)
147
+ rescue JSON::ParserError => e
148
+ Que.notify_error_async(e)
149
+ nil
150
+ end
151
+
152
+ def message_matches_format?(message, format)
153
+ message_has_all_keys?(message, format) &&
154
+ message_has_no_excess_keys?(message, format) &&
155
+ message_keys_all_valid?(message, format)
156
+ end
157
+
158
+ def message_has_all_keys?(message, format)
159
+ format.all? { |k,v| message.has_key?(k) }
160
+ end
161
+
162
+ def message_has_no_excess_keys?(message, format)
163
+ message.all? { |k,v| format.has_key?(k) || k == :message_type }
164
+ end
165
+
166
+ def message_keys_all_valid?(message, format)
167
+ message.all? do |key, value|
168
+ if type = format[key]
169
+ Que.assert?(type, value)
170
+ else
171
+ true
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end