inst-jobs 2.0.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/db/migrate/20101216224513_create_delayed_jobs.rb +9 -7
  3. data/db/migrate/20110531144916_cleanup_delayed_jobs_indexes.rb +8 -13
  4. data/db/migrate/20110610213249_optimize_delayed_jobs.rb +8 -8
  5. data/db/migrate/20110831210257_add_delayed_jobs_next_in_strand.rb +25 -25
  6. data/db/migrate/20120510004759_delayed_jobs_delete_trigger_lock_for_update.rb +4 -8
  7. data/db/migrate/20120531150712_drop_psql_jobs_pop_fn.rb +1 -3
  8. data/db/migrate/20120607164022_delayed_jobs_use_advisory_locks.rb +11 -15
  9. data/db/migrate/20120607181141_index_jobs_on_locked_by.rb +1 -1
  10. data/db/migrate/20120608191051_add_jobs_run_at_index.rb +2 -2
  11. data/db/migrate/20120927184213_change_delayed_jobs_handler_to_text.rb +1 -1
  12. data/db/migrate/20140505215510_copy_failed_jobs_original_id.rb +2 -3
  13. data/db/migrate/20150807133223_add_max_concurrent_to_jobs.rb +9 -13
  14. data/db/migrate/20151210162949_improve_max_concurrent.rb +4 -8
  15. data/db/migrate/20161206323555_add_back_default_string_limits_jobs.rb +3 -2
  16. data/db/migrate/20181217155351_speed_up_max_concurrent_triggers.rb +13 -17
  17. data/db/migrate/20200330230722_add_id_to_get_delayed_jobs_index.rb +8 -8
  18. data/db/migrate/20200824222232_speed_up_max_concurrent_delete_trigger.rb +72 -77
  19. data/db/migrate/20200825011002_add_strand_order_override.rb +93 -97
  20. data/db/migrate/20210809145804_add_n_strand_index.rb +12 -0
  21. data/db/migrate/20210812210128_add_singleton_column.rb +200 -0
  22. data/db/migrate/20210917232626_add_delete_conflicting_singletons_before_unlock_trigger.rb +27 -0
  23. data/db/migrate/20210928174754_fix_singleton_condition_in_before_insert.rb +56 -0
  24. data/db/migrate/20210929204903_update_conflicting_singleton_function_to_use_index.rb +27 -0
  25. data/exe/inst_jobs +3 -2
  26. data/lib/delayed/backend/active_record.rb +211 -168
  27. data/lib/delayed/backend/base.rb +110 -72
  28. data/lib/delayed/batch.rb +11 -9
  29. data/lib/delayed/cli.rb +98 -84
  30. data/lib/delayed/core_ext/kernel.rb +4 -2
  31. data/lib/delayed/daemon.rb +70 -74
  32. data/lib/delayed/job_tracking.rb +26 -25
  33. data/lib/delayed/lifecycle.rb +27 -23
  34. data/lib/delayed/log_tailer.rb +17 -17
  35. data/lib/delayed/logging.rb +13 -16
  36. data/lib/delayed/message_sending.rb +43 -52
  37. data/lib/delayed/performable_method.rb +6 -8
  38. data/lib/delayed/periodic.rb +72 -68
  39. data/lib/delayed/plugin.rb +2 -4
  40. data/lib/delayed/pool.rb +205 -168
  41. data/lib/delayed/server/helpers.rb +6 -6
  42. data/lib/delayed/server.rb +51 -54
  43. data/lib/delayed/settings.rb +94 -81
  44. data/lib/delayed/testing.rb +21 -22
  45. data/lib/delayed/version.rb +1 -1
  46. data/lib/delayed/work_queue/in_process.rb +21 -17
  47. data/lib/delayed/work_queue/parent_process/client.rb +55 -53
  48. data/lib/delayed/work_queue/parent_process/server.rb +245 -207
  49. data/lib/delayed/work_queue/parent_process.rb +52 -53
  50. data/lib/delayed/worker/consul_health_check.rb +32 -33
  51. data/lib/delayed/worker/health_check.rb +34 -26
  52. data/lib/delayed/worker/null_health_check.rb +3 -1
  53. data/lib/delayed/worker/process_helper.rb +8 -9
  54. data/lib/delayed/worker.rb +272 -241
  55. data/lib/delayed/yaml_extensions.rb +12 -10
  56. data/lib/delayed_job.rb +37 -37
  57. data/lib/inst-jobs.rb +1 -1
  58. data/spec/active_record_job_spec.rb +143 -139
  59. data/spec/delayed/cli_spec.rb +7 -7
  60. data/spec/delayed/daemon_spec.rb +10 -9
  61. data/spec/delayed/message_sending_spec.rb +16 -9
  62. data/spec/delayed/periodic_spec.rb +14 -21
  63. data/spec/delayed/server_spec.rb +38 -38
  64. data/spec/delayed/settings_spec.rb +26 -25
  65. data/spec/delayed/work_queue/in_process_spec.rb +7 -8
  66. data/spec/delayed/work_queue/parent_process/client_spec.rb +17 -12
  67. data/spec/delayed/work_queue/parent_process/server_spec.rb +117 -41
  68. data/spec/delayed/work_queue/parent_process_spec.rb +21 -23
  69. data/spec/delayed/worker/consul_health_check_spec.rb +37 -50
  70. data/spec/delayed/worker/health_check_spec.rb +60 -52
  71. data/spec/delayed/worker_spec.rb +44 -21
  72. data/spec/sample_jobs.rb +45 -15
  73. data/spec/shared/delayed_batch.rb +74 -67
  74. data/spec/shared/delayed_method.rb +143 -102
  75. data/spec/shared/performable_method.rb +39 -38
  76. data/spec/shared/shared_backend.rb +550 -437
  77. data/spec/shared/testing.rb +14 -14
  78. data/spec/shared/worker.rb +156 -148
  79. data/spec/shared_jobs_specs.rb +13 -13
  80. data/spec/spec_helper.rb +53 -55
  81. metadata +148 -82
  82. data/lib/delayed/backend/redis/bulk_update.lua +0 -50
  83. data/lib/delayed/backend/redis/destroy_job.lua +0 -2
  84. data/lib/delayed/backend/redis/enqueue.lua +0 -29
  85. data/lib/delayed/backend/redis/fail_job.lua +0 -5
  86. data/lib/delayed/backend/redis/find_available.lua +0 -3
  87. data/lib/delayed/backend/redis/functions.rb +0 -59
  88. data/lib/delayed/backend/redis/get_and_lock_next_available.lua +0 -17
  89. data/lib/delayed/backend/redis/includes/jobs_common.lua +0 -203
  90. data/lib/delayed/backend/redis/job.rb +0 -535
  91. data/lib/delayed/backend/redis/set_running.lua +0 -5
  92. data/lib/delayed/backend/redis/tickle_strand.lua +0 -2
  93. data/spec/gemfiles/42.gemfile +0 -7
  94. data/spec/gemfiles/50.gemfile +0 -7
  95. data/spec/gemfiles/51.gemfile +0 -7
  96. data/spec/gemfiles/52.gemfile +0 -7
  97. data/spec/gemfiles/60.gemfile +0 -7
  98. data/spec/redis_job_spec.rb +0 -148
@@ -1,243 +1,281 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "activerecord-pg-extensions"
4
+
3
5
  module Delayed
4
- module WorkQueue
5
- class ParentProcess
6
- class Server
7
- attr_reader :clients, :listen_socket
8
-
9
- include Delayed::Logging
10
- SIGNALS = %i{INT TERM QUIT}
11
-
12
- def initialize(listen_socket, parent_pid: nil, config: Settings.parent_process)
13
- @listen_socket = listen_socket
14
- @parent_pid = parent_pid
15
- @clients = {}
16
- @waiting_clients = {}
17
- @prefetched_jobs = {}
18
-
19
- @config = config
20
- @client_timeout = config['server_socket_timeout'] || 10.0 # left for backwards compat
21
-
22
- @exit = false
23
- @self_pipe = IO.pipe
24
- end
6
+ module WorkQueue
7
+ class ParentProcess
8
+ class Server
9
+ attr_reader :clients, :listen_socket
25
10
 
26
- def connected_clients
27
- @clients.size
28
- end
11
+ include Delayed::Logging
12
+ SIGNALS = %i[INT TERM QUIT].freeze
29
13
 
30
- def all_workers_idle?
31
- !@clients.any? { |_, c| c.working }
32
- end
14
+ def initialize(listen_socket, parent_pid: nil, config: Settings.parent_process)
15
+ @listen_socket = listen_socket
16
+ @parent_pid = parent_pid
17
+ @clients = {}
18
+ @waiting_clients = {}
19
+ @prefetched_jobs = {}
33
20
 
34
- # run the server queue worker
35
- # this method does not return, only exits or raises an exception
36
- def run
37
- logger.debug "Starting work queue process"
21
+ @config = config
22
+ @client_timeout = config["server_socket_timeout"] || 10.0 # left for backwards compat
38
23
 
39
- SIGNALS.each do |sig|
40
- # We're not doing any aggressive exiting here since we really want
41
- # prefetched jobs to be unlocked and we're going to wake up the process
42
- # from the IO.select we're using to wait on clients.
43
- trap(sig) { @exit = true; @self_pipe[1].write_nonblock('.', exception: false) }
44
- end
24
+ @exit = false
25
+ @self_pipe = IO.pipe
26
+ end
45
27
 
46
- last_orphaned_prefetched_jobs_purge = Job.db_time_now - rand(15 * 60)
47
- while !exit?
48
- run_once
49
- if last_orphaned_prefetched_jobs_purge + 15 * 60 < Job.db_time_now
50
- Job.unlock_orphaned_prefetched_jobs
51
- last_orphaned_prefetched_jobs_purge = Job.db_time_now
28
+ def connected_clients
29
+ @clients.size
52
30
  end
53
- end
54
31
 
55
- rescue => e
56
- logger.error "WorkQueue Server died: #{e.inspect}"
57
- raise
58
- ensure
59
- unlock_all_prefetched_jobs
60
- end
32
+ def all_workers_idle?
33
+ @clients.none? { |_, c| c.working }
34
+ end
61
35
 
62
- def run_once
63
- handles = @clients.keys + [@listen_socket, @self_pipe[0]]
64
- # if we're currently idle, then force a "latency" to job fetching - don't
65
- # fetch recently queued jobs, allowing busier workers to fetch them first.
66
- # if they're not keeping up, the jobs will slip back in time, and suddenly we'll become
67
- # active and quickly pick up all the jobs we can. The latency is calculated to ensure that
68
- # an active worker is guaranteed to have attempted to fetch new jobs in the meantime
69
- forced_latency = Settings.sleep_delay + Settings.sleep_delay_stagger * 2 if all_workers_idle?
70
- timeout = Settings.sleep_delay + (rand * Settings.sleep_delay_stagger)
71
- readable, _, _ = IO.select(handles, nil, nil, timeout)
72
- if readable
73
- readable.each { |s| handle_read(s) }
74
- end
75
- Delayed::Worker.lifecycle.run_callbacks(:check_for_work, self) do
76
- check_for_work(forced_latency: forced_latency)
77
- end
78
- unlock_timed_out_prefetched_jobs
79
- end
36
+ # run the server queue worker
37
+ # this method does not return, only exits or raises an exception
38
+ def run
39
+ logger.debug "Starting work queue process"
80
40
 
81
- def handle_read(socket)
82
- if socket == @listen_socket
83
- handle_accept
84
- elsif socket == @self_pipe[0]
85
- # We really don't care about the contents of the pipe, we just need to
86
- # wake up.
87
- @self_pipe[0].read_nonblock(11, exception: false)
88
- else
89
- handle_request(socket)
90
- end
91
- end
41
+ SIGNALS.each do |sig|
42
+ # We're not doing any aggressive exiting here since we really want
43
+ # prefetched jobs to be unlocked and we're going to wake up the process
44
+ # from the IO.select we're using to wait on clients.
45
+ trap(sig) do
46
+ @exit = true
47
+ @self_pipe[1].write_nonblock(".", exception: false)
48
+ end
49
+ end
92
50
 
93
- # Any error on the listen socket other than WaitReadable will bubble up
94
- # and terminate the work queue process, to be restarted by the parent daemon.
95
- def handle_accept
96
- socket, _addr = @listen_socket.accept_nonblock
97
- if socket
98
- @clients[socket] = ClientState.new(false, socket)
99
- end
100
- rescue IO::WaitReadable
101
- logger.error("Server attempted to read listen_socket but failed with IO::WaitReadable")
102
- # ignore and just try accepting again next time through the loop
103
- end
51
+ last_orphaned_prefetched_jobs_purge = Job.db_time_now - rand(15 * 60)
52
+ until exit?
53
+ run_once
54
+ if last_orphaned_prefetched_jobs_purge + (15 * 60) < Job.db_time_now
55
+ Job.unlock_orphaned_prefetched_jobs
56
+ last_orphaned_prefetched_jobs_purge = Job.db_time_now
57
+ end
58
+ end
59
+ rescue => e
60
+ logger.error "WorkQueue Server died: #{e.inspect}"
61
+ raise
62
+ ensure
63
+ unlock_all_prefetched_jobs
64
+ end
104
65
 
105
- def handle_request(socket)
106
- # There is an assumption here that the client will never send a partial
107
- # request and then leave the socket open. Doing so would leave us hanging
108
- # in Marshal.load forever. This is only a reasonable assumption because we
109
- # control the client.
110
- client = @clients[socket]
111
- if socket.eof?
112
- logger.debug("Client #{client.name} closed connection")
113
- return drop_socket(socket)
114
- end
115
- worker_name, worker_config = Marshal.load(socket)
116
- client.name = worker_name
117
- client.working = false
118
- (@waiting_clients[worker_config] ||= []) << client
119
- rescue SystemCallError, IOError => ex
120
- logger.error("Receiving message from client (#{socket}) failed: #{ex.inspect}")
121
- drop_socket(socket)
122
- end
66
+ def run_once
67
+ handles = @clients.keys + [@listen_socket, @self_pipe[0]]
68
+ # if we're currently idle, then force a "latency" to job fetching - don't
69
+ # fetch recently queued jobs, allowing busier workers to fetch them first.
70
+ # if they're not keeping up, the jobs will slip back in time, and suddenly we'll become
71
+ # active and quickly pick up all the jobs we can. The latency is calculated to ensure that
72
+ # an active worker is guaranteed to have attempted to fetch new jobs in the meantime
73
+ forced_latency = Settings.sleep_delay + (Settings.sleep_delay_stagger * 2) if all_workers_idle?
74
+ timeout = Settings.sleep_delay + (rand * Settings.sleep_delay_stagger)
75
+ readable, = IO.select(handles, nil, nil, timeout)
76
+ readable&.each { |s| handle_read(s) }
77
+ Delayed::Worker.lifecycle.run_callbacks(:check_for_work, self) do
78
+ check_for_work(forced_latency: forced_latency)
79
+ end
80
+ unlock_timed_out_prefetched_jobs
81
+ end
123
82
 
124
- def check_for_work(forced_latency: nil)
125
- @waiting_clients.each do |(worker_config, workers)|
126
- prefetched_jobs = @prefetched_jobs[worker_config] ||= []
127
- logger.debug("I have #{prefetched_jobs.length} jobs for #{workers.length} waiting workers")
128
- while !prefetched_jobs.empty? && !workers.empty?
129
- job = prefetched_jobs.shift
130
- client = workers.shift
131
- # couldn't re-lock it for some reason
132
- logger.debug("Transferring prefetched job to #{client.name}")
133
- unless job.transfer_lock!(from: prefetch_owner, to: client.name)
134
- workers.unshift(client)
135
- next
83
+ def handle_read(socket)
84
+ if socket == @listen_socket
85
+ handle_accept
86
+ elsif socket == @self_pipe[0]
87
+ # We really don't care about the contents of the pipe, we just need to
88
+ # wake up.
89
+ @self_pipe[0].read_nonblock(11, exception: false)
90
+ else
91
+ handle_request(socket)
136
92
  end
137
- client.working = true
138
- begin
139
- logger.debug("Sending prefetched job #{job.id} to #{client.name}")
140
- client_timeout { Marshal.dump(job, client.socket) }
141
- rescue SystemCallError, IOError, Timeout::Error => ex
142
- logger.error("Failed to send pre-fetched job to #{client.name}: #{ex.inspect}")
143
- drop_socket(client.socket)
144
- Delayed::Job.unlock([job])
93
+ end
94
+
95
+ # Any error on the listen socket other than WaitReadable will bubble up
96
+ # and terminate the work queue process, to be restarted by the parent daemon.
97
+ def handle_accept
98
+ socket, _addr = @listen_socket.accept_nonblock
99
+ @clients[socket] = ClientState.new(false, socket) if socket
100
+ rescue IO::WaitReadable
101
+ logger.error("Server attempted to read listen_socket but failed with IO::WaitReadable")
102
+ # ignore and just try accepting again next time through the loop
103
+ end
104
+
105
+ def handle_request(socket)
106
+ # There is an assumption here that the client will never send a partial
107
+ # request and then leave the socket open. Doing so would leave us hanging
108
+ # in Marshal.load forever. This is only a reasonable assumption because we
109
+ # control the client.
110
+ client = @clients[socket]
111
+ if socket.eof?
112
+ logger.debug("Client #{client.name} closed connection")
113
+ return drop_socket(socket)
114
+ end
115
+ worker_name, worker_config = Marshal.load(socket)
116
+ client.name = worker_name
117
+ client.working = false
118
+ (@waiting_clients[worker_config] ||= []) << client
119
+ rescue SystemCallError, IOError => e
120
+ logger.error("Receiving message from client (#{socket}) failed: #{e.inspect}")
121
+ drop_socket(socket)
122
+ end
123
+
124
+ def check_for_work(forced_latency: nil)
125
+ @waiting_clients.each do |(worker_config, workers)|
126
+ prefetched_jobs = @prefetched_jobs[worker_config] ||= []
127
+ logger.debug("I have #{prefetched_jobs.length} jobs for #{workers.length} waiting workers")
128
+ while !prefetched_jobs.empty? && !workers.empty?
129
+ job = prefetched_jobs.shift
130
+ client = workers.shift
131
+ # couldn't re-lock it for some reason
132
+ logger.debug("Transferring prefetched job to #{client.name}")
133
+ unless job.transfer_lock!(from: prefetch_owner, to: client.name)
134
+ workers.unshift(client)
135
+ next
136
+ end
137
+ client.working = true
138
+ begin
139
+ logger.debug("Sending prefetched job #{job.id} to #{client.name}")
140
+ client_timeout { Marshal.dump(job, client.socket) }
141
+ rescue SystemCallError, IOError, Timeout::Error => e
142
+ logger.error("Failed to send pre-fetched job to #{client.name}: #{e.inspect}")
143
+ drop_socket(client.socket)
144
+ Delayed::Job.unlock([job])
145
+ end
146
+ end
147
+
148
+ next if workers.empty?
149
+
150
+ logger.debug("Fetching new work for #{workers.length} workers")
151
+ jobs_to_send = []
152
+
153
+ Delayed::Worker.lifecycle.run_callbacks(:work_queue_pop, self, worker_config) do
154
+ recipients = workers.map(&:name)
155
+
156
+ response = Delayed::Job.get_and_lock_next_available(
157
+ recipients,
158
+ worker_config[:queue],
159
+ worker_config[:min_priority],
160
+ worker_config[:max_priority],
161
+ prefetch: (Settings.fetch_batch_size * (worker_config[:workers] || 1)) - recipients.length,
162
+ prefetch_owner: prefetch_owner,
163
+ forced_latency: forced_latency
164
+ )
165
+ logger.debug(
166
+ "Fetched and locked #{response.values.flatten.size} new jobs for workers (#{response.keys.join(', ')})."
167
+ )
168
+ response.each do |(worker_name, locked_jobs)|
169
+ if worker_name == prefetch_owner
170
+ # it's actually an array of all the extra jobs
171
+ logger.debug(
172
+ "Adding prefetched jobs #{locked_jobs.length} to prefetched array (size: #{prefetched_jobs.count})"
173
+ )
174
+ prefetched_jobs.concat(locked_jobs)
175
+ next
176
+ end
177
+ client = workers.find { |worker| worker.name == worker_name }
178
+ client.working = true
179
+ jobs_to_send << [client, locked_jobs]
180
+ end
181
+ end
182
+
183
+ jobs_to_send.each do |(recipient, job_to_send)|
184
+ @waiting_clients[worker_config].delete(recipient)
185
+ begin
186
+ logger.debug("Sending job #{job_to_send.id} to #{recipient.name}")
187
+ client_timeout { Marshal.dump(job_to_send, recipient.socket) }
188
+ rescue SystemCallError, IOError, Timeout::Error => e
189
+ logger.error("Failed to send job to #{recipient.name}: #{e.inspect}")
190
+ drop_socket(recipient.socket)
191
+ Delayed::Job.unlock([job_to_send])
192
+ end
193
+ end
145
194
  end
146
195
  end
147
196
 
148
- next if workers.empty?
149
-
150
- logger.debug("Fetching new work for #{workers.length} workers")
151
- jobs_to_send = []
152
-
153
- Delayed::Worker.lifecycle.run_callbacks(:work_queue_pop, self, worker_config) do
154
- recipients = workers.map(&:name)
155
-
156
- response = Delayed::Job.get_and_lock_next_available(
157
- recipients,
158
- worker_config[:queue],
159
- worker_config[:min_priority],
160
- worker_config[:max_priority],
161
- prefetch: Settings.fetch_batch_size * (worker_config[:workers] || 1) - recipients.length,
162
- prefetch_owner: prefetch_owner,
163
- forced_latency: forced_latency)
164
- logger.debug("Fetched and locked #{response.values.flatten.size} new jobs for workers (#{response.keys.join(', ')}).")
165
- response.each do |(worker_name, job)|
166
- if worker_name == prefetch_owner
167
- # it's actually an array of all the extra jobs
168
- logger.debug("Adding prefetched jobs #{job.length} to prefetched array (size: #{prefetched_jobs.count})")
169
- prefetched_jobs.concat(job)
170
- next
197
+ def unlock_prefetched_jobs
198
+ @prefetched_jobs.each do |(worker_config, jobs)|
199
+ next if jobs.empty?
200
+ next if block_given? && !yield(jobs)
201
+
202
+ connection = Delayed::Job.connection
203
+ connection.transaction do
204
+ # make absolutely sure we don't get hung up and leave things
205
+ # locked in the database
206
+ if connection.postgresql_version >= 9_06_00 # rubocop:disable Style/NumericLiterals
207
+ connection.idle_in_transaction_session_timeout = 5
208
+ end
209
+ # relatively short timeout for acquiring the lock
210
+ connection.statement_timeout = Settings.sleep_delay
211
+ Delayed::Job.advisory_lock(Delayed::Job.prefetch_jobs_lock_name)
212
+
213
+ # this query might take longer, and we really want to get it
214
+ # done if we got the lock, but still don't want an inadvertent
215
+ # hang
216
+ connection.statement_timeout = 30
217
+ Delayed::Job.unlock(jobs)
218
+ @prefetched_jobs[worker_config] = []
171
219
  end
172
- client = workers.find { |worker| worker.name == worker_name }
173
- client.working = true
174
- jobs_to_send << [client, job]
220
+ rescue ActiveRecord::QueryCanceled
221
+ # ignore; we'll retry anyway
222
+ logger.warn("unable to unlock prefetched jobs; skipping for now")
223
+ rescue ActiveRecord::StatementInvalid
224
+ # see if we dropped the connection
225
+ raise if connection.active?
226
+
227
+ # otherwise just reconnect and let it retry
228
+ logger.warn("failed to unlock prefetched jobs - connection terminated; skipping for now")
229
+ Delayed::Job.clear_all_connections!
175
230
  end
176
231
  end
177
232
 
178
- jobs_to_send.each do |(client, job)|
179
- @waiting_clients[worker_config].delete(client)
180
- begin
181
- logger.debug("Sending job #{job.id} to #{client.name}")
182
- client_timeout { Marshal.dump(job, client.socket) }
183
- rescue SystemCallError, IOError, Timeout::Error => ex
184
- logger.error("Failed to send job to #{client.name}: #{ex.inspect}")
185
- drop_socket(client.socket)
186
- Delayed::Job.unlock([job])
233
+ def unlock_timed_out_prefetched_jobs
234
+ unlock_prefetched_jobs do |jobs|
235
+ jobs.first.locked_at < Time.now.utc - Settings.parent_process[:prefetched_jobs_timeout]
187
236
  end
188
237
  end
189
- end
190
- end
191
238
 
192
- def unlock_timed_out_prefetched_jobs
193
- @prefetched_jobs.each do |(worker_config, jobs)|
194
- next if jobs.empty?
195
- if jobs.first.locked_at < Time.now.utc - Settings.parent_process[:prefetched_jobs_timeout]
196
- Delayed::Job.unlock(jobs)
197
- @prefetched_jobs[worker_config] = []
239
+ def unlock_all_prefetched_jobs
240
+ # we try really hard; it may not have done any work if it timed out
241
+ 10.times do
242
+ unlock_prefetched_jobs
243
+ break if @prefetched_jobs.each_value.all?(&:empty?)
244
+ end
198
245
  end
199
- end
200
- end
201
246
 
202
- def unlock_all_prefetched_jobs
203
- @prefetched_jobs.each do |(_worker_config, jobs)|
204
- next if jobs.empty?
205
- Delayed::Job.unlock(jobs)
206
- end
207
- @prefetched_jobs = {}
208
- end
247
+ def drop_socket(socket)
248
+ # this socket went away
249
+ begin
250
+ socket.close
251
+ rescue IOError
252
+ nil
253
+ end
254
+ client = @clients[socket]
255
+ @clients.delete(socket)
256
+ @waiting_clients.each do |(_config, workers)|
257
+ workers.delete(client)
258
+ end
259
+ end
209
260
 
210
- def drop_socket(socket)
211
- # this socket went away
212
- begin
213
- socket.close
214
- rescue IOError
215
- end
216
- client = @clients[socket]
217
- @clients.delete(socket)
218
- @waiting_clients.each do |(_config, workers)|
219
- workers.delete(client)
220
- end
221
- end
261
+ def exit?
262
+ !!@exit || parent_exited?
263
+ end
222
264
 
223
- def exit?
224
- !!@exit || parent_exited?
225
- end
265
+ def prefetch_owner
266
+ "prefetch:#{Socket.gethostname rescue 'X'}"
267
+ end
226
268
 
227
- def prefetch_owner
228
- "prefetch:#{Socket.gethostname rescue 'X'}"
229
- end
269
+ def parent_exited?
270
+ @parent_pid && @parent_pid != Process.ppid
271
+ end
230
272
 
231
- def parent_exited?
232
- @parent_pid && @parent_pid != Process.ppid
233
- end
273
+ def client_timeout(&block)
274
+ Timeout.timeout(@client_timeout, &block)
275
+ end
234
276
 
235
- def client_timeout
236
- Timeout.timeout(@client_timeout) { yield }
277
+ ClientState = Struct.new(:working, :socket, :name)
278
+ end
237
279
  end
238
-
239
- ClientState = Struct.new(:working, :socket, :name)
240
280
  end
241
281
  end
242
- end
243
- end
@@ -1,69 +1,68 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'pathname'
4
- require 'socket'
5
- require 'timeout'
3
+ require "pathname"
4
+ require "socket"
5
+ require "timeout"
6
6
 
7
- require_relative 'parent_process/client'
8
- require_relative 'parent_process/server'
7
+ require_relative "parent_process/client"
8
+ require_relative "parent_process/server"
9
9
 
10
10
  module Delayed
11
- module WorkQueue
12
- # ParentProcess is a WorkQueue implementation that spawns a separate worker
13
- # process for querying the queue. Each Worker child process sends requests to
14
- # the ParentProcess via IPC, and receives responses. This centralized queue
15
- # querying cuts down on db queries and lock contention, and allows the
16
- # possibility for other centralized logic such as notifications when all workers
17
- # are idle.
18
- #
19
- # The IPC implementation uses Unix stream sockets and Ruby's built-in Marshal
20
- # functionality. The ParentProcess creates a Unix socket on the filesystem in
21
- # the tmp directory, so that if a worker process dies and is restarted it can
22
- # reconnect to the socket.
23
- #
24
- # While Unix and IP sockets are API compatible, we take a lot of shortcuts
25
- # because we know it's just a local Unix socket. If we ever wanted to swap this
26
- # out for a TCP/IP socket and have the WorkQueue running on another host, we'd
27
- # want to be a lot more robust about partial reads/writes and timeouts.
28
- class ParentProcess
29
- class ProtocolError < RuntimeError
30
- end
11
+ module WorkQueue
12
+ # ParentProcess is a WorkQueue implementation that spawns a separate worker
13
+ # process for querying the queue. Each Worker child process sends requests to
14
+ # the ParentProcess via IPC, and receives responses. This centralized queue
15
+ # querying cuts down on db queries and lock contention, and allows the
16
+ # possibility for other centralized logic such as notifications when all workers
17
+ # are idle.
18
+ #
19
+ # The IPC implementation uses Unix stream sockets and Ruby's built-in Marshal
20
+ # functionality. The ParentProcess creates a Unix socket on the filesystem in
21
+ # the tmp directory, so that if a worker process dies and is restarted it can
22
+ # reconnect to the socket.
23
+ #
24
+ # While Unix and IP sockets are API compatible, we take a lot of shortcuts
25
+ # because we know it's just a local Unix socket. If we ever wanted to swap this
26
+ # out for a TCP/IP socket and have the WorkQueue running on another host, we'd
27
+ # want to be a lot more robust about partial reads/writes and timeouts.
28
+ class ParentProcess
29
+ class ProtocolError < RuntimeError
30
+ end
31
31
 
32
- attr_reader :server_address
32
+ attr_reader :server_address
33
33
 
34
- DEFAULT_SOCKET_NAME = 'inst-jobs.sock'.freeze
35
- private_constant :DEFAULT_SOCKET_NAME
34
+ DEFAULT_SOCKET_NAME = "inst-jobs.sock"
35
+ private_constant :DEFAULT_SOCKET_NAME
36
36
 
37
- def initialize(config = Settings.parent_process)
38
- @config = config
39
- @server_address = generate_socket_path(config['server_address'])
40
- end
37
+ def initialize(config = Settings.parent_process)
38
+ @config = config
39
+ @server_address = generate_socket_path(config["server_address"])
40
+ end
41
41
 
42
- def server(parent_pid: nil)
43
- # The unix_server_socket method takes care of cleaning up any existing
44
- # socket for us if the work queue process dies and is restarted.
45
- listen_socket = Socket.unix_server_socket(@server_address)
46
- Server.new(listen_socket, parent_pid: parent_pid, config: @config)
47
- end
42
+ def server(parent_pid: nil)
43
+ # The unix_server_socket method takes care of cleaning up any existing
44
+ # socket for us if the work queue process dies and is restarted.
45
+ listen_socket = Socket.unix_server_socket(@server_address)
46
+ Server.new(listen_socket, parent_pid: parent_pid, config: @config)
47
+ end
48
48
 
49
- def client
50
- Client.new(Addrinfo.unix(@server_address), config: @config)
51
- end
49
+ def client
50
+ Client.new(Addrinfo.unix(@server_address), config: @config)
51
+ end
52
52
 
53
- private
53
+ private
54
54
 
55
- def generate_socket_path(supplied_path)
56
- pathname = Pathname.new(supplied_path)
55
+ def generate_socket_path(supplied_path)
56
+ pathname = Pathname.new(supplied_path)
57
57
 
58
- if pathname.absolute? && pathname.directory?
59
- pathname.join(DEFAULT_SOCKET_NAME).to_s
60
- elsif pathname.absolute?
61
- supplied_path
62
- else
63
- generate_socket_path(Settings.expand_rails_path(supplied_path))
58
+ if pathname.absolute? && pathname.directory?
59
+ pathname.join(DEFAULT_SOCKET_NAME).to_s
60
+ elsif pathname.absolute?
61
+ supplied_path
62
+ else
63
+ generate_socket_path(Settings.expand_rails_path(supplied_path))
64
+ end
65
+ end
64
66
  end
65
67
  end
66
68
  end
67
- end
68
- end
69
-