canvas-jobs 0.10.6 → 0.11.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: c17b53cc20eeba1d6def3d6defed59ec2f4e6786
4
- data.tar.gz: 462039f0631c4c8446e485ff09543c2430958fd9
3
+ metadata.gz: 1682d0b7ae6c1e398bab7b3ed4e93b45c5f667f6
4
+ data.tar.gz: 9bcfe44b61d28af0f81f9e9036832468d5aad5a8
5
5
  SHA512:
6
- metadata.gz: 479c5177f7f37fa4c2f84caf850f214a316def2cc182f815bb59ef63f905490912e6b87396f57df6e87eee6af77b45047f4a2051ecf1076d467ebf963a0a01a7
7
- data.tar.gz: 779faa887354c56f5dcba06e16e1bc52f20d0084c0b432d59c282afc0908a74d32eb51ca83e1067cdf833daa1d5461e44c1e78607bf0d9443aa16e02f5e27ccf
6
+ metadata.gz: 258d4db18ca5cdc41940d03488b3537ef39f5c95385bad4fe6a007013d3785746bea9cc13218db45ed04fcc0d6eb157cc0b052039e294e1d4a7a5bd4615acd76
7
+ data.tar.gz: 9e085133db62875654a88b6955a85e53f7e6aaeec14d067d04e912cf3728fcaacd65036b08266fb97f90ec9c53a32d7368f1ae753b2e2ea3a793e3b4ed481b67
@@ -4,10 +4,8 @@ module Delayed
4
4
  class CLI
5
5
  def initialize(args = ARGV)
6
6
  @args = args
7
- # config that will be applied on Settings
7
+ # config that will be applied on Settings and passed to the created Pool
8
8
  @config = {}
9
- # worker configs that will be passed to the created Pool
10
- @worker_configs = []
11
9
  # CLI options that will be kept to this class
12
10
  @options = {
13
11
  :config_file => Settings.default_worker_config_name,
@@ -56,7 +54,6 @@ class CLI
56
54
 
57
55
  def load_and_apply_config!
58
56
  @config = Settings.worker_config(@options[:config_file])
59
- @worker_configs = @config.delete(:workers)
60
57
  Settings.apply_worker_config!(@config)
61
58
  end
62
59
 
@@ -88,7 +85,7 @@ class CLI
88
85
  def start
89
86
  load_rails
90
87
  tail_rails_log unless daemon.daemonized?
91
- Delayed::Pool.new(@worker_configs).start
88
+ Delayed::Pool.new(@config).start
92
89
  end
93
90
 
94
91
  def load_rails
@@ -10,6 +10,7 @@ module Delayed
10
10
  :loop => [:worker],
11
11
  :perform => [:worker, :job],
12
12
  :pop => [:worker],
13
+ :work_queue_pop => [:work_queue],
13
14
  }
14
15
 
15
16
  def initialize
@@ -6,13 +6,12 @@ class Pool
6
6
  attr_reader :workers
7
7
 
8
8
  def initialize(*args)
9
- if args.size == 1 && args.first.is_a?(Array)
10
- worker_configs = args.first
9
+ if args.first.is_a?(Hash)
10
+ @config = args.first
11
11
  else
12
12
  warn "Calling Delayed::Pool.new directly is deprecated. Use `Delayed::CLI.new.run()` instead."
13
13
  end
14
14
  @workers = {}
15
- @config = { workers: worker_configs }
16
15
  end
17
16
 
18
17
  def run
@@ -66,16 +65,31 @@ class Pool
66
65
  def spawn_all_workers
67
66
  ActiveRecord::Base.connection_handler.clear_all_connections!
68
67
 
68
+ if @config[:work_queue] == 'parent_process'
69
+ @work_queue = WorkQueue::ParentProcess.new
70
+ spawn_work_queue
71
+ end
72
+
69
73
  @config[:workers].each do |worker_config|
70
74
  (worker_config[:workers] || 1).times { spawn_worker(worker_config) }
71
75
  end
72
76
  end
73
77
 
78
+ def spawn_work_queue
79
+ parent_pid = Process.pid
80
+ pid = fork_with_reconnects do
81
+ $0 = "delayed_jobs_work_queue#{Settings.pool_procname_suffix}"
82
+ @work_queue.server(parent_pid: parent_pid).run
83
+ end
84
+ workers[pid] = :work_queue
85
+ end
86
+
74
87
  def spawn_worker(worker_config)
75
88
  if worker_config[:periodic]
76
89
  return # backwards compat
77
90
  else
78
91
  worker_config[:parent_pid] = Process.pid
92
+ worker_config[:work_queue] = @work_queue.client if @work_queue
79
93
  worker = Delayed::Worker.new(worker_config)
80
94
  end
81
95
 
@@ -125,8 +139,12 @@ class Pool
125
139
  child = Process.wait
126
140
  if workers.include?(child)
127
141
  worker = workers.delete(child)
128
- if worker.is_a?(Symbol)
142
+ case worker
143
+ when :periodic_audit
129
144
  say "ran auditor: #{worker}"
145
+ when :work_queue
146
+ say "work queue exited, restarting", :info
147
+ spawn_work_queue
130
148
  else
131
149
  say "child exited: #{child}, restarting", :info
132
150
  # fork to handle unlocking (to prevent polluting the parent with worker objects)
@@ -17,6 +17,7 @@ module Delayed
17
17
  :disable_periodic_jobs,
18
18
  :disable_automatic_orphan_unlocking,
19
19
  :last_ditch_logfile,
20
+ :parent_process_client_timeout,
20
21
  ]
21
22
  SETTINGS_WITH_ARGS = [ :num_strands ]
22
23
 
@@ -43,6 +44,7 @@ module Delayed
43
44
  self.fetch_batch_size = 5
44
45
  self.select_random_from_batch = false
45
46
  self.silence_periodic_log = false
47
+ self.parent_process_client_timeout = 10.0
46
48
 
47
49
  self.num_strands = ->(strand_name){ nil }
48
50
  self.default_job_options = ->{ Hash.new }
@@ -1,3 +1,3 @@
1
1
  module Delayed
2
- VERSION = "0.10.6"
2
+ VERSION = "0.11.0"
3
3
  end
@@ -0,0 +1,13 @@
1
+ module Delayed
2
+ module WorkQueue
3
+ # The simplest possible implementation of a WorkQueue -- just turns around and
4
+ # queries the queue inline.
5
+ class InProcess
6
+ def get_and_lock_next_available(worker_name, queue_name, min_priority, max_priority)
7
+ Delayed::Worker.lifecycle.run_callbacks(:work_queue_pop, self) do
8
+ Delayed::Job.get_and_lock_next_available(worker_name, queue_name, min_priority, max_priority)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,180 @@
1
+ require 'socket'
2
+ require 'tempfile'
3
+ require 'timeout'
4
+
5
+ module Delayed
6
+ module WorkQueue
7
+ # ParentProcess is a WorkQueue implementation that spawns a separate worker
8
+ # process for querying the queue. Each Worker child process sends requests to
9
+ # the ParentProcess via IPC, and receives responses. This centralized queue
10
+ # querying cuts down on db queries and lock contention, and allows the
11
+ # possibility for other centralized logic such as notifications when all workers
12
+ # are idle.
13
+ #
14
+ # The IPC implementation uses Unix stream sockets and Ruby's built-in Marshal
15
+ # functionality. The ParentProcess creates a Unix socket on the filesystem in
16
+ # the tmp directory, so that if a worker process dies and is restarted it can
17
+ # reconnect to the socket.
18
+ #
19
+ # While Unix and IP sockets are API compatible, we take a lot of shortcuts
20
+ # because we know it's just a local Unix socket. If we ever wanted to swap this
21
+ # out for a TCP/IP socket and have the WorkQueue running on another host, we'd
22
+ # want to be a lot more robust about partial reads/writes and timeouts.
23
+ class ParentProcess
24
+ class ProtocolError < RuntimeError
25
+ end
26
+
27
+ def initialize
28
+ @path = self.class.generate_socket_path
29
+ end
30
+
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("canvas-jobs-#{Process.pid}-")
38
+ path = tmp.path
39
+ tmp.close!
40
+ path
41
+ end
42
+
43
+ def server(parent_pid: nil)
44
+ # The unix_server_socket method takes care of cleaning up any existing
45
+ # 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)
48
+ end
49
+
50
+ def client
51
+ Client.new(Addrinfo.unix(@path))
52
+ end
53
+
54
+ class Client
55
+ attr_reader :addrinfo
56
+
57
+ def initialize(addrinfo)
58
+ @addrinfo = addrinfo
59
+ end
60
+
61
+ def get_and_lock_next_available(name, queue_name, min_priority, max_priority)
62
+ @socket ||= @addrinfo.connect
63
+ Marshal.dump([name, queue_name, min_priority, max_priority], @socket)
64
+ response = Marshal.load(@socket)
65
+ unless response.nil? || (response.is_a?(Delayed::Job) && response.locked_by == name)
66
+ raise(ProtocolError, "response is not a locked job: #{response.inspect}")
67
+ end
68
+ response
69
+ rescue SystemCallError, IOError
70
+ # The work queue process died. Return nil to signal the worker
71
+ # process should sleep as if no job was found, and then retry.
72
+ @socket = nil
73
+ nil
74
+ end
75
+ end
76
+
77
+ class Server
78
+ attr_reader :listen_socket
79
+
80
+ def initialize(listen_socket, parent_pid: nil)
81
+ @listen_socket = listen_socket
82
+ @parent_pid = parent_pid
83
+ @clients = {}
84
+ end
85
+
86
+ def connected_clients
87
+ @clients.size
88
+ end
89
+
90
+ def all_workers_idle?
91
+ !@clients.any? { |_, c| c.working }
92
+ end
93
+
94
+ def say(msg, level = :debug)
95
+ if defined?(Rails.logger) && Rails.logger
96
+ Rails.logger.send(level, "[#{Process.pid}]Q #{msg}")
97
+ else
98
+ puts(msg)
99
+ end
100
+ end
101
+
102
+ # run the server queue worker
103
+ # this method does not return, only exits or raises an exception
104
+ def run
105
+ say "Starting work queue process"
106
+
107
+ while !exit?
108
+ run_once
109
+ end
110
+
111
+ rescue => e
112
+ say "WorkQueue Server died: #{e.inspect}"
113
+ raise
114
+ end
115
+
116
+ def run_once
117
+ handles = @clients.keys + [@listen_socket]
118
+ readable, _, _ = IO.select(handles, nil, nil, 1)
119
+ if readable
120
+ readable.each { |s| handle_read(s) }
121
+ end
122
+ end
123
+
124
+ def handle_read(socket)
125
+ if socket == @listen_socket
126
+ handle_accept
127
+ else
128
+ handle_request(socket)
129
+ end
130
+ end
131
+
132
+ # Any error on the listen socket other than WaitReadable will bubble up
133
+ # and terminate the work queue process, to be restarted by the parent daemon.
134
+ def handle_accept
135
+ client, _addr = @listen_socket.accept_nonblock
136
+ if client
137
+ @clients[client] = ClientState.new(false)
138
+ end
139
+ rescue IO::WaitReadable
140
+ # ignore and just try accepting again next time through the loop
141
+ end
142
+
143
+ def handle_request(socket)
144
+ # There is an assumption here that the client will never send a partial
145
+ # request and then leave the socket open. Doing so would leave us hanging
146
+ # here forever. This is only a reasonable assumption because we control
147
+ # the client.
148
+ request = client_timeout { Marshal.load(socket) }
149
+ response = nil
150
+ Delayed::Worker.lifecycle.run_callbacks(:work_queue_pop, self) do
151
+ response = Delayed::Job.get_and_lock_next_available(*request)
152
+ @clients[socket].working = !response.nil?
153
+ end
154
+ client_timeout { Marshal.dump(response, socket) }
155
+ rescue SystemCallError, IOError, Timeout::Error
156
+ # this socket went away
157
+ begin
158
+ socket.close
159
+ rescue IOError
160
+ end
161
+ @clients.delete(socket)
162
+ end
163
+
164
+ def exit?
165
+ parent_exited?
166
+ end
167
+
168
+ def parent_exited?
169
+ @parent_pid && @parent_pid != Process.ppid
170
+ end
171
+
172
+ def client_timeout
173
+ Timeout.timeout(Settings.parent_process_client_timeout) { yield }
174
+ end
175
+
176
+ ClientState = Struct.new(:working)
177
+ end
178
+ end
179
+ end
180
+ end
@@ -6,7 +6,7 @@ require 'tmpdir'
6
6
  require 'set'
7
7
 
8
8
  class Worker
9
- attr_reader :config, :queue, :min_priority, :max_priority
9
+ attr_reader :config, :queue_name, :min_priority, :max_priority, :work_queue
10
10
 
11
11
  # Callback to fire when a delayed job fails max_attempts times. If this
12
12
  # callback is defined, then the value of destroy_failed_jobs is ignored, and
@@ -32,11 +32,12 @@ class Worker
32
32
  @exit = false
33
33
  @config = options
34
34
  @parent_pid = options[:parent_pid]
35
- @queue = options[:queue] || Settings.queue
35
+ @queue_name = options[:queue] || Settings.queue
36
36
  @min_priority = options[:min_priority]
37
37
  @max_priority = options[:max_priority]
38
38
  @max_job_count = options[:worker_max_job_count].to_i
39
39
  @max_memory_usage = options[:worker_max_memory_usage].to_i
40
+ @work_queue = options[:work_queue] || WorkQueue::InProcess.new
40
41
  @job_count = 0
41
42
 
42
43
  app = Rails.application
@@ -93,14 +94,9 @@ class Worker
93
94
 
94
95
  def run
95
96
  self.class.lifecycle.run_callbacks(:loop, self) do
96
- job =
97
- self.class.lifecycle.run_callbacks(:pop, self) do
98
- Delayed::Job.get_and_lock_next_available(
99
- name,
100
- queue,
101
- min_priority,
102
- max_priority)
103
- end
97
+ job = self.class.lifecycle.run_callbacks(:pop, self) do
98
+ work_queue.get_and_lock_next_available(name, queue_name, min_priority, max_priority)
99
+ end
104
100
 
105
101
  if job
106
102
  configure_for_job(job) do
@@ -122,7 +118,7 @@ class Worker
122
118
  end
123
119
  end
124
120
  else
125
- set_process_name("wait:#{Settings.worker_procname_prefix}#{@queue}:#{min_priority || 0}:#{max_priority || 'max'}")
121
+ set_process_name("wait:#{Settings.worker_procname_prefix}#{@queue_name}:#{min_priority || 0}:#{max_priority || 'max'}")
126
122
  sleep(Settings.sleep_delay + (rand * Settings.sleep_delay_stagger))
127
123
  end
128
124
  end
@@ -35,6 +35,8 @@ require 'delayed/periodic'
35
35
  require 'delayed/plugin'
36
36
  require 'delayed/pool'
37
37
  require 'delayed/worker'
38
+ require 'delayed/work_queue/in_process'
39
+ require 'delayed/work_queue/parent_process'
38
40
 
39
41
  require 'delayed/engine'
40
42
 
@@ -19,7 +19,7 @@ describe 'Delayed::Backed::ActiveRecord::Job' do
19
19
  allow(Delayed::Job::Failed).to receive(:create).and_raise(RuntimeError)
20
20
  job = "test".send_later_enqueue_args :reverse, no_delay: true
21
21
  job_id = job.id
22
- proc { job.fail! }.should raise_error
22
+ proc { job.fail! }.should raise_error(RuntimeError)
23
23
  proc { Delayed::Job.find(job_id) }.should raise_error(ActiveRecord::RecordNotFound)
24
24
  Delayed::Job.count.should == 0
25
25
  end
@@ -77,11 +77,11 @@ describe 'Delayed::Backed::ActiveRecord::Job' do
77
77
  end
78
78
 
79
79
  it "should raise error when holding failed jobs" do
80
- expect { Delayed::Job.bulk_update('hold', :flavor => 'failed', :query => @query) }.to raise_error
80
+ expect { Delayed::Job.bulk_update('hold', :flavor => 'failed', :query => @query) }.to raise_error(RuntimeError)
81
81
  end
82
82
 
83
83
  it "should raise error unholding failed jobs" do
84
- expect { Delayed::Job.bulk_update('unhold', :flavor => 'failed', :query => @query) }.to raise_error
84
+ expect { Delayed::Job.bulk_update('unhold', :flavor => 'failed', :query => @query) }.to raise_error(RuntimeError)
85
85
  end
86
86
  end
87
87
 
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Delayed::WorkQueue::InProcess do
4
+ before :all do
5
+ Delayed.select_backend(Delayed::Backend::ActiveRecord::Job)
6
+ end
7
+
8
+ after :all do
9
+ Delayed.send(:remove_const, :Job)
10
+ end
11
+
12
+ after :each do
13
+ Delayed::Worker.lifecycle.reset!
14
+ end
15
+
16
+ let(:subject) { described_class.new }
17
+ let(:args) { ["worker_name", "queue_name", 1, 2] }
18
+
19
+ it 'triggers the lifecycle event around the pop' do
20
+ called = false
21
+ Delayed::Worker.lifecycle.around(:work_queue_pop) do |queue, &cb|
22
+ expect(queue).to eq(subject)
23
+ expect(Delayed::Job).to receive(:get_and_lock_next_available).with(*args).and_return(:job)
24
+ called = true
25
+ cb.call(queue)
26
+ end
27
+ job = subject.get_and_lock_next_available(*args)
28
+ expect(job).to eq(:job)
29
+ expect(called).to eq(true)
30
+ end
31
+ end
@@ -0,0 +1,159 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Delayed::WorkQueue::ParentProcess do
4
+ before :all do
5
+ Delayed.select_backend(Delayed::Backend::ActiveRecord::Job)
6
+ end
7
+
8
+ after :all do
9
+ Delayed.send(:remove_const, :Job)
10
+ end
11
+
12
+ after :each do
13
+ Delayed::Worker.lifecycle.reset!
14
+ end
15
+
16
+ let(:subject) { described_class.new }
17
+
18
+ it 'generates a server listening on a valid unix socket' do
19
+ server = subject.server
20
+ expect(server).to be_a(Delayed::WorkQueue::ParentProcess::Server)
21
+ expect(server.listen_socket.local_address.unix?).to be(true)
22
+ expect { server.listen_socket.accept_nonblock }.to raise_error(IO::WaitReadable)
23
+ end
24
+
25
+ it 'generates a client connected to the server unix socket' do
26
+ server = subject.server
27
+ client = subject.client
28
+ expect(client).to be_a(Delayed::WorkQueue::ParentProcess::Client)
29
+ expect(client.addrinfo.unix?).to be(true)
30
+ expect(client.addrinfo.unix_path).to eq(server.listen_socket.local_address.unix_path)
31
+ end
32
+
33
+ describe Delayed::WorkQueue::ParentProcess::Client do
34
+ let(:subject) { described_class.new(addrinfo) }
35
+ let(:addrinfo) { double('Addrinfo') }
36
+ let(:connection) { double('Socket') }
37
+ let(:args) { ["worker_name", "queue_name", 1, 2] }
38
+ let(:job) { Delayed::Job.new(locked_by: "worker_name") }
39
+
40
+ it 'marshals the given arguments to the server and returns the response' do
41
+ expect(addrinfo).to receive(:connect).once.and_return(connection)
42
+ expect(Marshal).to receive(:dump).with(args, connection).ordered
43
+ expect(Marshal).to receive(:load).with(connection).and_return(job).ordered
44
+ response = subject.get_and_lock_next_available(*args)
45
+ expect(response).to eq(job)
46
+ end
47
+
48
+ it 'returns nil and then reconnects on socket error' do
49
+ expect(addrinfo).to receive(:connect).once.and_return(connection)
50
+ expect(Marshal).to receive(:dump).and_raise(SystemCallError.new("failure"))
51
+ response = subject.get_and_lock_next_available(*args)
52
+ expect(response).to be_nil
53
+
54
+ expect(addrinfo).to receive(:connect).once.and_return(connection)
55
+ expect(Marshal).to receive(:dump).with(args, connection)
56
+ expect(Marshal).to receive(:load).with(connection).and_return(job)
57
+ response = subject.get_and_lock_next_available(*args)
58
+ expect(response).to eq(job)
59
+ end
60
+
61
+ it 'errors if the response is not a locked job' do
62
+ expect(addrinfo).to receive(:connect).once.and_return(connection)
63
+ expect(Marshal).to receive(:dump).with(args, connection)
64
+ expect(Marshal).to receive(:load).with(connection).and_return(:not_a_job)
65
+ expect { subject.get_and_lock_next_available(*args) }.to raise_error(Delayed::WorkQueue::ParentProcess::ProtocolError)
66
+ end
67
+
68
+ it 'errors if the response is a job not locked by this worker' do
69
+ expect(addrinfo).to receive(:connect).once.and_return(connection)
70
+ expect(Marshal).to receive(:dump).with(args, connection)
71
+ job.locked_by = "somebody_else"
72
+ expect(Marshal).to receive(:load).with(connection).and_return(job)
73
+ expect { subject.get_and_lock_next_available(*args) }.to raise_error(Delayed::WorkQueue::ParentProcess::ProtocolError)
74
+ end
75
+ end
76
+
77
+ describe Delayed::WorkQueue::ParentProcess::Server do
78
+ let(:subject) { described_class.new(listen_socket) }
79
+ let(:listen_socket) { Socket.unix_server_socket(Delayed::WorkQueue::ParentProcess.generate_socket_path) }
80
+ let(:args) { [1,2,3] }
81
+ let(:job) { :a_job }
82
+
83
+ it 'accepts new clients' do
84
+ client = Socket.unix(subject.listen_socket.local_address.unix_path)
85
+ expect { subject.run_once }.to change(subject, :connected_clients).by(1)
86
+ end
87
+
88
+ it 'queries the queue on client request' do
89
+ client = Socket.unix(subject.listen_socket.local_address.unix_path)
90
+ subject.run_once
91
+
92
+ expect(Delayed::Job).to receive(:get_and_lock_next_available).with(*args).and_return(job)
93
+ Marshal.dump(args, client)
94
+ subject.run_once
95
+ expect(Marshal.load(client)).to eq(job)
96
+ end
97
+
98
+ it 'drops the client on i/o error' do
99
+ client = Socket.unix(subject.listen_socket.local_address.unix_path)
100
+ subject.run_once
101
+
102
+ Marshal.dump(args, client)
103
+
104
+ expect(Marshal).to receive(:load).and_raise(IOError.new("socket went away"))
105
+ expect { subject.run_once }.to change(subject, :connected_clients).by(-1)
106
+ end
107
+
108
+ it 'drops the client on timeout' do
109
+ client = Socket.unix(subject.listen_socket.local_address.unix_path)
110
+ subject.run_once
111
+
112
+ Marshal.dump(args, client)
113
+
114
+ expect(Marshal).to receive(:load).and_raise(Timeout::Error.new("socket timed out"))
115
+ expect(Timeout).to receive(:timeout).with(Delayed::Settings.parent_process_client_timeout).and_yield
116
+ expect { subject.run_once }.to change(subject, :connected_clients).by(-1)
117
+ end
118
+
119
+ it 'tracks when clients are idle' do
120
+ expect(subject.all_workers_idle?).to be(true)
121
+
122
+ client = Socket.unix(subject.listen_socket.local_address.unix_path)
123
+ subject.run_once
124
+ expect(subject.all_workers_idle?).to be(true)
125
+
126
+ expect(Delayed::Job).to receive(:get_and_lock_next_available).with(*args).and_return(job)
127
+ Marshal.dump(args, client)
128
+ subject.run_once
129
+ expect(subject.all_workers_idle?).to be(false)
130
+
131
+ expect(Delayed::Job).to receive(:get_and_lock_next_available).with(*args).and_return(nil)
132
+ Marshal.dump(args, client)
133
+ subject.run_once
134
+ expect(subject.all_workers_idle?).to be(true)
135
+ end
136
+
137
+ it 'triggers the lifecycle event around the pop' do
138
+ called = false
139
+ client = Socket.unix(subject.listen_socket.local_address.unix_path)
140
+ subject.run_once
141
+
142
+ Delayed::Worker.lifecycle.around(:work_queue_pop) do |queue, &cb|
143
+ expect(subject.all_workers_idle?).to be(true)
144
+ expect(queue).to eq(subject)
145
+ expect(Delayed::Job).to receive(:get_and_lock_next_available).with(*args).and_return(job)
146
+ called = true
147
+ res = cb.call(queue)
148
+ expect(subject.all_workers_idle?).to be(false)
149
+ res
150
+ end
151
+
152
+ Marshal.dump(args, client)
153
+ subject.run_once
154
+
155
+ expect(Marshal.load(client)).to eq(job)
156
+ expect(called).to eq(true)
157
+ end
158
+ end
159
+ end
@@ -29,7 +29,7 @@ shared_examples_for 'Delayed::Batch' do
29
29
  }
30
30
  Delayed::Job.jobs_count(:current).should == 1
31
31
  job = Delayed::Job.find_available(1).first
32
- expect{ job.invoke_job }.to raise_error
32
+ expect{ job.invoke_job }.to raise_error(RuntimeError)
33
33
  end
34
34
 
35
35
  it "should create valid jobs" do
@@ -1,12 +1,12 @@
1
1
  shared_examples_for 'Delayed::PerformableMethod' do
2
-
2
+
3
3
  it "should not ignore ActiveRecord::RecordNotFound errors because they are not always permanent" do
4
4
  story = Story.create :text => 'Once upon...'
5
5
  p = Delayed::PerformableMethod.new(story, :tell, [])
6
6
  story.destroy
7
- lambda { YAML.load(p.to_yaml) }.should raise_error
7
+ lambda { YAML.load(p.to_yaml) }.should raise_error(Delayed::Backend::RecordNotFound)
8
8
  end
9
-
9
+
10
10
  it "should store the object using native YAML even if its an active record" do
11
11
  story = Story.create :text => 'Once upon...'
12
12
  p = Delayed::PerformableMethod.new(story, :tell, [])
@@ -16,7 +16,7 @@ shared_examples_for 'Delayed::PerformableMethod' do
16
16
  p.args.should == []
17
17
  p.perform.should == 'Once upon...'
18
18
  end
19
-
19
+
20
20
  it "should allow class methods to be called on ActiveRecord models" do
21
21
  Story.create!(:text => 'Once upon a...')
22
22
  p = Delayed::PerformableMethod.new(Story, :count, [])
@@ -32,7 +32,7 @@ shared_examples_for 'Delayed::PerformableMethod' do
32
32
  p = Delayed::PerformableMethod.new(MyReverser, :reverse, ["ohai"])
33
33
  lambda { p.send(:perform).should == "iaho" }.should_not raise_error
34
34
  end
35
-
35
+
36
36
  it "should store arguments as native YAML if they are active record objects" do
37
37
  story = Story.create :text => 'Once upon...'
38
38
  reader = StoryReader.new
@@ -429,10 +429,10 @@ shared_examples_for 'a backend' do
429
429
  Delayed::Periodic.scheduled = {}
430
430
  expect { Delayed::Periodic.cron('my ChangedJob', '*/5 * * * * *') do
431
431
  Delayed::Job.enqueue(SimpleJob.new)
432
- end }.to raise_error
432
+ end }.to raise_error(ArgumentError)
433
433
  end
434
434
 
435
- expect { Delayed::Periodic.add_overrides({ 'my ChangedJob' => '*/10 * * * * * *' }) }.to raise_error
435
+ expect { Delayed::Periodic.add_overrides({ 'my ChangedJob' => '*/10 * * * * * *' }) }.to raise_error(ArgumentError)
436
436
  end
437
437
  end
438
438
 
@@ -451,12 +451,12 @@ shared_examples_for 'a backend' do
451
451
 
452
452
  it "should fail on job creation if an unsaved AR object is used" do
453
453
  story = Story.new :text => "Once upon..."
454
- lambda { story.send_later(:text) }.should raise_error
454
+ lambda { story.send_later(:text) }.should raise_error(RuntimeError)
455
455
 
456
456
  reader = StoryReader.new
457
- lambda { reader.send_later(:read, story) }.should raise_error
457
+ lambda { reader.send_later(:read, story) }.should raise_error(RuntimeError)
458
458
 
459
- lambda { [story, 1, story, false].send_later(:first) }.should raise_error
459
+ lambda { [story, 1, story, false].send_later(:first) }.should raise_error(RuntimeError)
460
460
  end
461
461
 
462
462
  # the sort order of current_jobs and list_jobs depends on the back-end
@@ -285,14 +285,14 @@ shared_examples_for 'Delayed::Worker' do
285
285
  queue_name = "default_queue"
286
286
  Delayed::Settings.queue = queue_name
287
287
  worker = worker_create(:queue=>nil)
288
- worker.queue.should == queue_name
288
+ worker.queue_name.should == queue_name
289
289
  end
290
290
 
291
291
  it "should override default queue name if specified in initialize" do
292
292
  queue_name = "my_queue"
293
293
  Delayed::Settings.queue = "default_queue"
294
294
  worker = worker_create(:queue=>queue_name)
295
- worker.queue.should == queue_name
295
+ worker.queue_name.should == queue_name
296
296
  end
297
297
  end
298
298
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: canvas-jobs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.6
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tobias Luetke
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2016-03-04 00:00:00.000000000 Z
12
+ date: 2016-03-17 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: after_transaction_commit
@@ -157,14 +157,14 @@ dependencies:
157
157
  requirements:
158
158
  - - '='
159
159
  - !ruby/object:Gem::Version
160
- version: 3.1.0
160
+ version: 3.4.0
161
161
  type: :development
162
162
  prerelease: false
163
163
  version_requirements: !ruby/object:Gem::Requirement
164
164
  requirements:
165
165
  - - '='
166
166
  - !ruby/object:Gem::Version
167
- version: 3.1.0
167
+ version: 3.4.0
168
168
  - !ruby/object:Gem::Dependency
169
169
  name: test_after_commit
170
170
  requirement: !ruby/object:Gem::Requirement
@@ -313,6 +313,8 @@ files:
313
313
  - lib/delayed/settings.rb
314
314
  - lib/delayed/testing.rb
315
315
  - lib/delayed/version.rb
316
+ - lib/delayed/work_queue/in_process.rb
317
+ - lib/delayed/work_queue/parent_process.rb
316
318
  - lib/delayed/worker.rb
317
319
  - lib/delayed/yaml_extensions.rb
318
320
  - lib/delayed_job.rb
@@ -321,6 +323,8 @@ files:
321
323
  - spec/delayed/daemon_spec.rb
322
324
  - spec/delayed/server_spec.rb
323
325
  - spec/delayed/settings_spec.rb
326
+ - spec/delayed/work_queue/in_process_spec.rb
327
+ - spec/delayed/work_queue/parent_process_spec.rb
324
328
  - spec/delayed/worker_spec.rb
325
329
  - spec/gemfiles/32.gemfile
326
330
  - spec/gemfiles/40.gemfile
@@ -366,6 +370,8 @@ test_files:
366
370
  - spec/delayed/daemon_spec.rb
367
371
  - spec/delayed/server_spec.rb
368
372
  - spec/delayed/settings_spec.rb
373
+ - spec/delayed/work_queue/in_process_spec.rb
374
+ - spec/delayed/work_queue/parent_process_spec.rb
369
375
  - spec/delayed/worker_spec.rb
370
376
  - spec/gemfiles/32.gemfile
371
377
  - spec/gemfiles/40.gemfile