canvas-jobs 0.10.6 → 0.11.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.
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