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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fb490099d8b25310bcfd0c7f419dd095277f9d11
4
- data.tar.gz: 2be27c6ea2e6d37b6739d6ef19dcf971a0bcf81e
3
+ metadata.gz: 59c2643528ac5d09160a35e1d4fd338d058b593d
4
+ data.tar.gz: 69e5d68901df56c91fc10df1eaafb973877ef9fa
5
5
  SHA512:
6
- metadata.gz: 935a70be1f89625da0a07d96a02cbc4b21bfcd414d93e260582c266a987be59f947ff7084fa723ff8edebbca6262a4619e41ca88cac95c125b4c81c289e8cf5c
7
- data.tar.gz: 4930328d5b9c06f595425f11ae11c03113a4cfaa96327062e6c085e4da354d86c4bc1fcd4f54bbbe983e1213acf4d85fd14aaee5ce37f826ce7b22837f95b594
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
- return worker_names.is_a?(Array) ? jobs.index_by(&:locked_by) : jobs.first
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
@@ -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
- self.locked_at = nil
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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Delayed
2
- VERSION = "0.12.3"
2
+ VERSION = "0.13.0"
3
3
  end
@@ -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
- def initialize
28
- @path = self.class.generate_socket_path
29
- end
30
+ attr_reader :server_address
31
+
32
+ DEFAULT_SOCKET_NAME = 'inst-jobs.sock'.freeze
33
+ private_constant :DEFAULT_SOCKET_NAME
30
34
 
31
- def self.generate_socket_path
32
- # We utilize Tempfile as a convenient way to get a socket filename in the
33
- # writeable temp directory. However, since we destroy the normal file and
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(@path)
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(@path))
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
- class Client
66
- attr_reader :addrinfo
51
+ private
67
52
 
68
- include SayUtil
53
+ def generate_socket_path(supplied_path)
54
+ pathname = Pathname.new(supplied_path)
69
55
 
70
- def initialize(addrinfo)
71
- @addrinfo = addrinfo
72
- end
73
-
74
- def get_and_lock_next_available(worker_name, worker_config)
75
- @socket ||= @addrinfo.connect
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
+