inst-jobs 0.12.3 → 0.13.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/delayed/backend/active_record.rb +40 -3
- data/lib/delayed/backend/base.rb +7 -0
- data/lib/delayed/backend/redis/job.rb +13 -3
- data/lib/delayed/logging.rb +32 -0
- data/lib/delayed/settings.rb +28 -2
- data/lib/delayed/version.rb +1 -1
- data/lib/delayed/work_queue/parent_process.rb +24 -180
- data/lib/delayed/work_queue/parent_process/client.rb +54 -0
- data/lib/delayed/work_queue/parent_process/server.rb +200 -0
- data/lib/delayed_job.rb +1 -0
- data/spec/active_record_job_spec.rb +28 -0
- data/spec/delayed/settings_spec.rb +7 -0
- data/spec/delayed/work_queue/parent_process/client_spec.rb +102 -0
- data/spec/delayed/work_queue/parent_process/server_spec.rb +162 -0
- data/spec/delayed/work_queue/parent_process_spec.rb +29 -164
- data/spec/gemfiles/42.gemfile.lock +44 -46
- data/spec/gemfiles/50.gemfile.lock +48 -48
- data/spec/shared/shared_backend.rb +17 -0
- metadata +9 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 59c2643528ac5d09160a35e1d4fd338d058b593d
|
4
|
+
data.tar.gz: 69e5d68901df56c91fc10df1eaafb973877ef9fa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e9b5d4be7a15c770a825beffc9d175a0f6b6318e6df899af14995f90f52dc24f23adc7b4f90321e8cb9eba2818ed0ac7889394d382ce86b18fe58d61aef91f60
|
7
|
+
data.tar.gz: 31d950b9b5bc5d843f9ed04f316f0ce076b3ba4e32f5284c499a1ed1cd4d4ab36d674246fdb31fdefde8d25b02a42e84e3568f5af567fb35637250a91e37f1c3
|
@@ -16,6 +16,13 @@ module Delayed
|
|
16
16
|
class Job < ::ActiveRecord::Base
|
17
17
|
include Delayed::Backend::Base
|
18
18
|
self.table_name = :delayed_jobs
|
19
|
+
# Rails hasn't completely loaded yet, and setting the table name will cache some stuff
|
20
|
+
# so reset that cache so that it will load correctly after Rails is all loaded
|
21
|
+
# It's fixed in Rails 5 to not cache anything when you set the table_name
|
22
|
+
if Rails.version < '5' && Rails.version >= '4.2'
|
23
|
+
@arel_engine = nil
|
24
|
+
@arel_table = nil
|
25
|
+
end
|
19
26
|
|
20
27
|
def self.reconnect!
|
21
28
|
clear_all_connections!
|
@@ -201,7 +208,9 @@ module Delayed
|
|
201
208
|
def self.get_and_lock_next_available(worker_names,
|
202
209
|
queue = Delayed::Settings.queue,
|
203
210
|
min_priority = nil,
|
204
|
-
max_priority = nil
|
211
|
+
max_priority = nil,
|
212
|
+
extra_jobs: 0,
|
213
|
+
extra_jobs_owner: nil)
|
205
214
|
|
206
215
|
check_queue(queue)
|
207
216
|
check_priorities(min_priority, max_priority)
|
@@ -216,7 +225,7 @@ module Delayed
|
|
216
225
|
effective_worker_names = Array(worker_names)
|
217
226
|
|
218
227
|
target_jobs = all_available(queue, min_priority, max_priority).
|
219
|
-
limit(effective_worker_names.length).
|
228
|
+
limit(effective_worker_names.length + extra_jobs).
|
220
229
|
lock
|
221
230
|
jobs_with_row_number = all.from(target_jobs).
|
222
231
|
select("id, ROW_NUMBER() OVER () AS row_number")
|
@@ -224,6 +233,9 @@ module Delayed
|
|
224
233
|
effective_worker_names.each_with_index do |worker, i|
|
225
234
|
updates << "WHEN #{i + 1} THEN #{connection.quote(worker)} "
|
226
235
|
end
|
236
|
+
if extra_jobs_owner
|
237
|
+
updates << "ELSE #{connection.quote(extra_jobs_owner)} "
|
238
|
+
end
|
227
239
|
updates << "END, locked_at = #{connection.quote(db_time_now)}"
|
228
240
|
# joins and returning in an update! just bypass AR
|
229
241
|
query = "UPDATE #{quoted_table_name} SET #{updates} FROM (#{jobs_with_row_number.to_sql}) j2 WHERE j2.id=delayed_jobs.id RETURNING delayed_jobs.*"
|
@@ -231,7 +243,14 @@ module Delayed
|
|
231
243
|
# because this is an atomic query, we don't have to return more jobs than we needed
|
232
244
|
# to try and lock them, nor is there a possibility we need to try again because
|
233
245
|
# all of the jobs we tried to lock had already been locked by someone else
|
234
|
-
|
246
|
+
if worker_names.is_a?(Array)
|
247
|
+
result = jobs.index_by(&:locked_by)
|
248
|
+
# all of the extras can come back as an array
|
249
|
+
result[extra_jobs_owner] = jobs.select { |j| j.locked_by == extra_jobs_owner } if extra_jobs_owner
|
250
|
+
return result
|
251
|
+
else
|
252
|
+
return jobs.first
|
253
|
+
end
|
235
254
|
else
|
236
255
|
batch_size = Settings.fetch_batch_size
|
237
256
|
batch_size *= worker_names.length if worker_names.is_a?(Array)
|
@@ -314,6 +333,12 @@ module Delayed
|
|
314
333
|
end
|
315
334
|
end
|
316
335
|
|
336
|
+
def self.unlock(jobs)
|
337
|
+
unlocked = where(id: jobs).update_all(locked_at: nil, locked_by: nil)
|
338
|
+
jobs.each(&:unlock)
|
339
|
+
unlocked
|
340
|
+
end
|
341
|
+
|
317
342
|
# Lock this job for this worker.
|
318
343
|
# Returns true if we have the lock, false otherwise.
|
319
344
|
#
|
@@ -332,6 +357,18 @@ module Delayed
|
|
332
357
|
end
|
333
358
|
end
|
334
359
|
|
360
|
+
def transfer_lock!(from:, to:)
|
361
|
+
now = self.class.db_time_now
|
362
|
+
# We don't own this job so we will update the locked_by name and the locked_at
|
363
|
+
affected_rows = self.class.where(id: self, locked_by: from).update_all(locked_at: now, locked_by: to)
|
364
|
+
if affected_rows == 1
|
365
|
+
mark_as_locked!(now, to)
|
366
|
+
return true
|
367
|
+
else
|
368
|
+
return false
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
335
372
|
def mark_as_locked!(time, worker)
|
336
373
|
self.locked_at = time
|
337
374
|
self.locked_by = worker
|
data/lib/delayed/backend/base.rb
CHANGED
@@ -118,6 +118,13 @@ module Delayed
|
|
118
118
|
Time.now.utc
|
119
119
|
end
|
120
120
|
|
121
|
+
def unlock_orphaned_pending_jobs
|
122
|
+
horizon = db_time_now - Settings.parent_process[:pending_jobs_idle_timeout] * 4
|
123
|
+
orphaned_jobs = running_jobs.select { |job| job.locked_by.start_with?('work_queue:') && job.locked_at < horizon }
|
124
|
+
return 0 if orphaned_jobs.empty?
|
125
|
+
unlock(orphaned_jobs)
|
126
|
+
end
|
127
|
+
|
121
128
|
def unlock_orphaned_jobs(pid = nil, name = nil)
|
122
129
|
begin
|
123
130
|
name ||= Socket.gethostname
|
@@ -221,7 +221,9 @@ class Job
|
|
221
221
|
def self.get_and_lock_next_available(worker_name,
|
222
222
|
queue = Delayed::Settings.queue,
|
223
223
|
min_priority = Delayed::MIN_PRIORITY,
|
224
|
-
max_priority = Delayed::MAX_PRIORITY
|
224
|
+
max_priority = Delayed::MAX_PRIORITY,
|
225
|
+
extra_jobs: nil,
|
226
|
+
extra_jobs_owner: nil)
|
225
227
|
|
226
228
|
check_queue(queue)
|
227
229
|
check_priorities(min_priority, max_priority)
|
@@ -352,9 +354,18 @@ class Job
|
|
352
354
|
self.create!(options.merge(:singleton => true))
|
353
355
|
end
|
354
356
|
|
357
|
+
def self.unlock(jobs)
|
358
|
+
jobs.each(&:unlock!)
|
359
|
+
jobs.length
|
360
|
+
end
|
361
|
+
|
355
362
|
# not saved, just used as a marker when creating
|
356
363
|
attr_accessor :singleton
|
357
364
|
|
365
|
+
def transfer_lock!(from:, to:)
|
366
|
+
lock_in_redis!(to)
|
367
|
+
end
|
368
|
+
|
358
369
|
def lock_in_redis!(worker_name)
|
359
370
|
self.locked_at = self.class.db_time_now
|
360
371
|
self.locked_by = worker_name
|
@@ -362,8 +373,7 @@ class Job
|
|
362
373
|
end
|
363
374
|
|
364
375
|
def unlock!
|
365
|
-
|
366
|
-
self.locked_by = nil
|
376
|
+
unlock
|
367
377
|
save!
|
368
378
|
end
|
369
379
|
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
module Delayed
|
4
|
+
module Logging
|
5
|
+
TIMESTAMP_FORMAT = '%Y-%m-%dT%H:%M:%S.%6N'.freeze
|
6
|
+
private_constant :TIMESTAMP_FORMAT
|
7
|
+
|
8
|
+
FORMAT = '%s - %s'
|
9
|
+
private_constant :FORMAT
|
10
|
+
|
11
|
+
|
12
|
+
def self.logger
|
13
|
+
return @logger if @logger
|
14
|
+
if defined?(Rails.logger) && Rails.logger
|
15
|
+
@logger = Rails.logger
|
16
|
+
else
|
17
|
+
@logger = ::Logger.new(STDOUT).tap do |logger|
|
18
|
+
logger.formatter = ->(_, time, _, msg) {
|
19
|
+
FORMAT % [
|
20
|
+
time.strftime(TIMESTAMP_FORMAT),
|
21
|
+
msg
|
22
|
+
]
|
23
|
+
}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def logger
|
29
|
+
Delayed::Logging.logger
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/delayed/settings.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'yaml'
|
2
2
|
require 'erb'
|
3
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
3
4
|
|
4
5
|
module Delayed
|
5
6
|
module Settings
|
@@ -17,7 +18,6 @@ module Delayed
|
|
17
18
|
:disable_periodic_jobs,
|
18
19
|
:disable_automatic_orphan_unlocking,
|
19
20
|
:last_ditch_logfile,
|
20
|
-
:parent_process_client_timeout,
|
21
21
|
]
|
22
22
|
SETTINGS_WITH_ARGS = [ :num_strands ]
|
23
23
|
|
@@ -32,6 +32,22 @@ module Delayed
|
|
32
32
|
|
33
33
|
mattr_accessor(*SETTINGS_WITH_ARGS)
|
34
34
|
|
35
|
+
PARENT_PROCESS_DEFAULTS = {
|
36
|
+
server_receive_timeout: 10.0,
|
37
|
+
server_socket_timeout: 10.0,
|
38
|
+
pending_jobs_idle_timeout: 30.0,
|
39
|
+
|
40
|
+
client_connect_timeout: 2.0,
|
41
|
+
client_receive_timeout: 10.0,
|
42
|
+
|
43
|
+
# We'll accept a partial, relative path and assume we want it inside
|
44
|
+
# Rails.root with inst-jobs.sock appended if provided a directory.
|
45
|
+
server_address: 'tmp',
|
46
|
+
}.with_indifferent_access.freeze
|
47
|
+
|
48
|
+
mattr_reader(:parent_process)
|
49
|
+
@@parent_process = PARENT_PROCESS_DEFAULTS.dup
|
50
|
+
|
35
51
|
def self.queue=(queue_name)
|
36
52
|
raise(ArgumentError, "queue_name must not be blank") if queue_name.blank?
|
37
53
|
@@queue = queue_name
|
@@ -44,7 +60,6 @@ module Delayed
|
|
44
60
|
self.fetch_batch_size = 5
|
45
61
|
self.select_random_from_batch = false
|
46
62
|
self.silence_periodic_log = false
|
47
|
-
self.parent_process_client_timeout = 10.0
|
48
63
|
|
49
64
|
self.num_strands = ->(strand_name){ nil }
|
50
65
|
self.default_job_options = ->{ Hash.new }
|
@@ -71,6 +86,8 @@ module Delayed
|
|
71
86
|
SETTINGS.each do |setting|
|
72
87
|
self.send("#{setting}=", config[setting.to_s]) if config.key?(setting.to_s)
|
73
88
|
end
|
89
|
+
parent_process.client_timeout = config['parent_process_client_timeout'] if config.key?('parent_process_client_timeout')
|
90
|
+
parent_process = config['parent_process'] if config.key?('parent_process')
|
74
91
|
end
|
75
92
|
|
76
93
|
def self.default_worker_config_name
|
@@ -86,5 +103,14 @@ module Delayed
|
|
86
103
|
end
|
87
104
|
File.expand_path("../#{path}", root)
|
88
105
|
end
|
106
|
+
|
107
|
+
def self.parent_process_client_timeout=(val)
|
108
|
+
parent_process['server_socket_timeout'] = Integer(val)
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.parent_process=(new_config)
|
112
|
+
raise 'Parent process configurations must be a hash!' unless Hash === new_config
|
113
|
+
@@parent_process = PARENT_PROCESS_DEFAULTS.merge(new_config)
|
114
|
+
end
|
89
115
|
end
|
90
116
|
end
|
data/lib/delayed/version.rb
CHANGED
@@ -1,7 +1,10 @@
|
|
1
|
+
require 'pathname'
|
1
2
|
require 'socket'
|
2
|
-
require 'tempfile'
|
3
3
|
require 'timeout'
|
4
4
|
|
5
|
+
require_relative 'parent_process/client'
|
6
|
+
require_relative 'parent_process/server'
|
7
|
+
|
5
8
|
module Delayed
|
6
9
|
module WorkQueue
|
7
10
|
# ParentProcess is a WorkQueue implementation that spawns a separate worker
|
@@ -24,200 +27,41 @@ class ParentProcess
|
|
24
27
|
class ProtocolError < RuntimeError
|
25
28
|
end
|
26
29
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
+
attr_reader :server_address
|
31
|
+
|
32
|
+
DEFAULT_SOCKET_NAME = 'inst-jobs.sock'.freeze
|
33
|
+
private_constant :DEFAULT_SOCKET_NAME
|
30
34
|
|
31
|
-
def
|
32
|
-
|
33
|
-
|
34
|
-
# write a unix socket file to the same location, we lose the hard uniqueness
|
35
|
-
# guarantees of Tempfile. This is OK for this use case, we only generate one
|
36
|
-
# Tempfile with this prefix.
|
37
|
-
tmp = Tempfile.new("inst-jobs-#{Process.pid}-")
|
38
|
-
path = tmp.path
|
39
|
-
tmp.close!
|
40
|
-
path
|
35
|
+
def initialize(config = Settings.parent_process)
|
36
|
+
@config = config
|
37
|
+
@server_address = generate_socket_path(config['server_address'])
|
41
38
|
end
|
42
39
|
|
43
40
|
def server(parent_pid: nil)
|
44
41
|
# The unix_server_socket method takes care of cleaning up any existing
|
45
42
|
# socket for us if the work queue process dies and is restarted.
|
46
|
-
listen_socket = Socket.unix_server_socket(@
|
47
|
-
Server.new(listen_socket, parent_pid: parent_pid)
|
43
|
+
listen_socket = Socket.unix_server_socket(@server_address)
|
44
|
+
Server.new(listen_socket, parent_pid: parent_pid, config: @config)
|
48
45
|
end
|
49
46
|
|
50
47
|
def client
|
51
|
-
Client.new(Addrinfo.unix(@
|
52
|
-
end
|
53
|
-
|
54
|
-
module SayUtil
|
55
|
-
def say(msg, level = :debug)
|
56
|
-
if defined?(Rails.logger) && Rails.logger
|
57
|
-
message = -> { "[#{Process.pid}]Q #{msg}" }
|
58
|
-
Rails.logger.send(level, self.class.name, &message)
|
59
|
-
else
|
60
|
-
puts(msg)
|
61
|
-
end
|
62
|
-
end
|
48
|
+
Client.new(Addrinfo.unix(@server_address), config: @config)
|
63
49
|
end
|
64
50
|
|
65
|
-
|
66
|
-
attr_reader :addrinfo
|
51
|
+
private
|
67
52
|
|
68
|
-
|
53
|
+
def generate_socket_path(supplied_path)
|
54
|
+
pathname = Pathname.new(supplied_path)
|
69
55
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
say("Requesting work using #{@socket.inspect}")
|
77
|
-
Marshal.dump([worker_name, worker_config], @socket)
|
78
|
-
response = Marshal.load(@socket)
|
79
|
-
unless response.nil? || (response.is_a?(Delayed::Job) && response.locked_by == worker_name)
|
80
|
-
say("Received invalid response from server: #{response.inspect}")
|
81
|
-
raise(ProtocolError, "response is not a locked job: #{response.inspect}")
|
82
|
-
end
|
83
|
-
say("Received work from server: #{response.inspect}")
|
84
|
-
response
|
85
|
-
rescue SystemCallError, IOError => ex
|
86
|
-
say("Work queue connection lost, reestablishing on next poll. (#{ex})", :error)
|
87
|
-
# The work queue process died. Return nil to signal the worker
|
88
|
-
# process should sleep as if no job was found, and then retry.
|
89
|
-
@socket = nil
|
90
|
-
nil
|
56
|
+
if pathname.absolute? && pathname.directory?
|
57
|
+
pathname.join(DEFAULT_SOCKET_NAME).to_s
|
58
|
+
elsif pathname.absolute?
|
59
|
+
supplied_path
|
60
|
+
else
|
61
|
+
generate_socket_path(Settings.expand_rails_path(supplied_path))
|
91
62
|
end
|
92
63
|
end
|
93
|
-
|
94
|
-
class Server
|
95
|
-
attr_reader :listen_socket
|
96
|
-
|
97
|
-
include SayUtil
|
98
|
-
|
99
|
-
def initialize(listen_socket, parent_pid: nil)
|
100
|
-
@listen_socket = listen_socket
|
101
|
-
@parent_pid = parent_pid
|
102
|
-
@clients = {}
|
103
|
-
@waiting_clients = {}
|
104
|
-
end
|
105
|
-
|
106
|
-
def connected_clients
|
107
|
-
@clients.size
|
108
|
-
end
|
109
|
-
|
110
|
-
def all_workers_idle?
|
111
|
-
!@clients.any? { |_, c| c.working }
|
112
|
-
end
|
113
|
-
|
114
|
-
# run the server queue worker
|
115
|
-
# this method does not return, only exits or raises an exception
|
116
|
-
def run
|
117
|
-
say "Starting work queue process"
|
118
|
-
|
119
|
-
while !exit?
|
120
|
-
run_once
|
121
|
-
end
|
122
|
-
|
123
|
-
rescue => e
|
124
|
-
say "WorkQueue Server died: #{e.inspect}", :error
|
125
|
-
raise
|
126
|
-
end
|
127
|
-
|
128
|
-
def run_once
|
129
|
-
handles = @clients.keys + [@listen_socket]
|
130
|
-
timeout = Settings.sleep_delay + (rand * Settings.sleep_delay_stagger)
|
131
|
-
readable, _, _ = IO.select(handles, nil, nil, timeout)
|
132
|
-
if readable
|
133
|
-
readable.each { |s| handle_read(s) }
|
134
|
-
end
|
135
|
-
check_for_work
|
136
|
-
end
|
137
|
-
|
138
|
-
def handle_read(socket)
|
139
|
-
if socket == @listen_socket
|
140
|
-
handle_accept
|
141
|
-
else
|
142
|
-
handle_request(socket)
|
143
|
-
end
|
144
|
-
end
|
145
|
-
|
146
|
-
# Any error on the listen socket other than WaitReadable will bubble up
|
147
|
-
# and terminate the work queue process, to be restarted by the parent daemon.
|
148
|
-
def handle_accept
|
149
|
-
socket, _addr = @listen_socket.accept_nonblock
|
150
|
-
if socket
|
151
|
-
@clients[socket] = ClientState.new(false, socket)
|
152
|
-
end
|
153
|
-
rescue IO::WaitReadable
|
154
|
-
say("Server attempted to read listen_socket but failed with IO::WaitReadable", :error)
|
155
|
-
# ignore and just try accepting again next time through the loop
|
156
|
-
end
|
157
|
-
|
158
|
-
def handle_request(socket)
|
159
|
-
# There is an assumption here that the client will never send a partial
|
160
|
-
# request and then leave the socket open. Doing so would leave us hanging
|
161
|
-
# here forever. This is only a reasonable assumption because we control
|
162
|
-
# the client.
|
163
|
-
worker_name, worker_config = client_timeout { Marshal.load(socket) }
|
164
|
-
client = @clients[socket]
|
165
|
-
client.name = worker_name
|
166
|
-
client.working = false
|
167
|
-
(@waiting_clients[worker_config] ||= []) << client
|
168
|
-
|
169
|
-
rescue SystemCallError, IOError, Timeout::Error => ex
|
170
|
-
say("Receiving message from client (#{socket}) failed: #{ex.inspect}", :error)
|
171
|
-
drop_socket(socket)
|
172
|
-
end
|
173
|
-
|
174
|
-
def check_for_work
|
175
|
-
@waiting_clients.each do |(worker_config, workers)|
|
176
|
-
next if workers.empty?
|
177
|
-
|
178
|
-
Delayed::Worker.lifecycle.run_callbacks(:work_queue_pop, self, worker_config) do
|
179
|
-
response = Delayed::Job.get_and_lock_next_available(
|
180
|
-
workers.map(&:name),
|
181
|
-
worker_config[:queue],
|
182
|
-
worker_config[:min_priority],
|
183
|
-
worker_config[:max_priority])
|
184
|
-
response.each do |(worker_name, job)|
|
185
|
-
client = workers.find { |worker| worker.name == worker_name }
|
186
|
-
client.working = true
|
187
|
-
@waiting_clients[worker_config].delete(client)
|
188
|
-
begin
|
189
|
-
client_timeout { Marshal.dump(job, client.socket) }
|
190
|
-
rescue SystemCallError, IOError, Timeout::Error
|
191
|
-
drop_socket(client.socket)
|
192
|
-
end
|
193
|
-
end
|
194
|
-
end
|
195
|
-
end
|
196
|
-
end
|
197
|
-
|
198
|
-
def drop_socket(socket)
|
199
|
-
# this socket went away
|
200
|
-
begin
|
201
|
-
socket.close
|
202
|
-
rescue IOError
|
203
|
-
end
|
204
|
-
@clients.delete(socket)
|
205
|
-
end
|
206
|
-
|
207
|
-
def exit?
|
208
|
-
parent_exited?
|
209
|
-
end
|
210
|
-
|
211
|
-
def parent_exited?
|
212
|
-
@parent_pid && @parent_pid != Process.ppid
|
213
|
-
end
|
214
|
-
|
215
|
-
def client_timeout
|
216
|
-
Timeout.timeout(Settings.parent_process_client_timeout) { yield }
|
217
|
-
end
|
218
|
-
|
219
|
-
ClientState = Struct.new(:working, :socket, :name)
|
220
|
-
end
|
221
64
|
end
|
222
65
|
end
|
223
66
|
end
|
67
|
+
|