sqewer 5.0.0 → 5.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -0
- data/ACTIVE_JOB.md +64 -0
- data/FAQ.md +0 -4
- data/Gemfile +4 -0
- data/README.md +4 -0
- data/Rakefile +1 -0
- data/bin/sqewer +7 -0
- data/bin/sqewer_rails +10 -0
- data/lib/sqewer.rb +18 -1
- data/lib/sqewer/atomic_counter.rb +2 -2
- data/lib/sqewer/cli.rb +3 -3
- data/lib/sqewer/connection.rb +16 -16
- data/lib/sqewer/connection_messagebox.rb +4 -4
- data/lib/sqewer/execution_context.rb +5 -5
- data/lib/sqewer/extensions/active_job_adapter.rb +78 -0
- data/lib/sqewer/{contrib → extensions}/appsignal_wrapper.rb +4 -4
- data/lib/sqewer/extensions/railtie.rb +12 -0
- data/lib/sqewer/middleware_stack.rb +7 -7
- data/lib/sqewer/null_logger.rb +1 -1
- data/lib/sqewer/resubmit.rb +17 -0
- data/lib/sqewer/serializer.rb +25 -25
- data/lib/sqewer/simple_job.rb +11 -11
- data/lib/sqewer/state_lock.rb +2 -2
- data/lib/sqewer/submitter.rb +17 -3
- data/lib/sqewer/version.rb +1 -1
- data/lib/sqewer/worker.rb +47 -47
- data/spec/spec_helper.rb +8 -2
- data/spec/sqewer/active_job_spec.rb +113 -0
- data/spec/sqewer/cli_spec.rb +48 -31
- data/spec/sqewer/serializer_spec.rb +51 -56
- data/spec/sqewer/submitter_spec.rb +18 -0
- data/spec/sqewer/worker_spec.rb +11 -9
- data/sqewer.gemspec +21 -5
- metadata +55 -5
- data/lib/sqewer/contrib/performable.rb +0 -23
data/spec/spec_helper.rb
CHANGED
@@ -15,12 +15,18 @@ RSpec.configure do |config|
|
|
15
15
|
config.order = 'random'
|
16
16
|
config.around :each do | example |
|
17
17
|
if example.metadata[:sqs]
|
18
|
-
queue_name = '
|
18
|
+
queue_name = 'sqewer-test-queue-%s' % SecureRandom.hex(6)
|
19
19
|
client = Aws::SQS::Client.new
|
20
20
|
resp = client.create_queue(queue_name: queue_name)
|
21
21
|
ENV['SQS_QUEUE_URL'] = resp.queue_url
|
22
|
+
|
22
23
|
example.run
|
23
|
-
|
24
|
+
|
25
|
+
# Sometimes the queue is already deleted before the example completes. If the test has passed,
|
26
|
+
# we do not really care whether this invocation raises an exception about a non-existent queue since
|
27
|
+
# all we care about is the queue _being gone_ at the end of the example.
|
28
|
+
client.delete_queue(queue_url: ENV.fetch('SQS_QUEUE_URL')) rescue Aws::SQS::Errors::NonExistentQueue
|
29
|
+
|
24
30
|
ENV.delete('SQS_QUEUE_URL')
|
25
31
|
else
|
26
32
|
example.run
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
require 'securerandom'
|
3
|
+
require 'active_job'
|
4
|
+
require 'active_record'
|
5
|
+
require 'global_id'
|
6
|
+
require_relative '../../lib/sqewer/extensions/active_job_adapter'
|
7
|
+
|
8
|
+
class CreateFileJob < ActiveJob::Base
|
9
|
+
def perform(file)
|
10
|
+
File.open(file, 'w') {}
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class DeleteFileJob < ActiveJob::Base
|
15
|
+
def perform(file)
|
16
|
+
File.unlink(file)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class ActivateUser < ActiveJob::Base
|
21
|
+
def perform(user)
|
22
|
+
user.active = true
|
23
|
+
user.save!
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
GlobalID.app = 'test-app'
|
28
|
+
class User < ActiveRecord::Base
|
29
|
+
include GlobalID::Identification
|
30
|
+
end
|
31
|
+
|
32
|
+
describe ActiveJob::QueueAdapters::SqewerAdapter, :sqs => true do
|
33
|
+
let(:file) { File.join(Dir.tmpdir, "file_active_job_test_1") }
|
34
|
+
let(:client) { ::Aws::SQS::Client.new }
|
35
|
+
|
36
|
+
after :all do
|
37
|
+
# Ensure database files get killed afterwards
|
38
|
+
File.unlink(ActiveRecord::Base.connection_config[:database]) rescue nil
|
39
|
+
end
|
40
|
+
|
41
|
+
before :all do
|
42
|
+
ActiveJob::Base.queue_adapter = ActiveJob::QueueAdapters::SqewerAdapter
|
43
|
+
|
44
|
+
test_seed_name = SecureRandom.hex(4)
|
45
|
+
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ('master_db_%s.sqlite3' % test_seed_name))
|
46
|
+
|
47
|
+
ActiveRecord::Migration.suppress_messages do
|
48
|
+
ActiveRecord::Schema.define(:version => 1) do
|
49
|
+
create_table :users do |t|
|
50
|
+
t.string :email, :null => true
|
51
|
+
t.boolean :active, default: false
|
52
|
+
t.timestamps :null => false
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
before do
|
59
|
+
@queue_url_hash = { queue_url: ENV['SQS_QUEUE_URL'] }
|
60
|
+
end
|
61
|
+
|
62
|
+
it "sends job to the queue" do
|
63
|
+
CreateFileJob.perform_later(file)
|
64
|
+
resp = client.get_queue_attributes(@queue_url_hash.merge(attribute_names: ["ApproximateNumberOfMessages"]))
|
65
|
+
expect(resp.attributes["ApproximateNumberOfMessages"].to_i).to eq(1)
|
66
|
+
end
|
67
|
+
|
68
|
+
it "correctly serializes the job into a Sqewer job" do
|
69
|
+
job = CreateFileJob.perform_later(file)
|
70
|
+
resp = client.receive_message(@queue_url_hash)
|
71
|
+
serialized_job = JSON.parse(resp.messages.last.body)
|
72
|
+
|
73
|
+
expect(serialized_job["_job_class"]).to eq("ActiveJob::QueueAdapters::SqewerAdapter::Performable")
|
74
|
+
expect(serialized_job["_job_params"]["job"]["job_id"]).to eq(job.job_id)
|
75
|
+
end
|
76
|
+
|
77
|
+
it "executes job from the queue" do
|
78
|
+
file_delayed = "#{file}_delayed"
|
79
|
+
CreateFileJob.perform_later(file)
|
80
|
+
CreateFileJob.perform_later(file_delayed)
|
81
|
+
w = Sqewer::Worker.default
|
82
|
+
w.start
|
83
|
+
begin
|
84
|
+
wait_for { File.exist?(file) }.to eq(true)
|
85
|
+
File.unlink(file)
|
86
|
+
|
87
|
+
wait_for { File.exist?(file_delayed) }.to eq(true)
|
88
|
+
DeleteFileJob.set(wait: 5.seconds).perform_later(file_delayed)
|
89
|
+
|
90
|
+
sleep 6
|
91
|
+
|
92
|
+
expect(File.exist?(file_delayed)).to eq(false)
|
93
|
+
ensure
|
94
|
+
w.stop
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
it "serializes and deserializes active record using GlobalID" do
|
99
|
+
user = User.create(email: 'test@wetransfer.com')
|
100
|
+
expect(user.active).to eq(false)
|
101
|
+
ActivateUser.perform_later(user)
|
102
|
+
w = Sqewer::Worker.default
|
103
|
+
begin
|
104
|
+
w.start
|
105
|
+
sleep 4
|
106
|
+
user.reload
|
107
|
+
expect(user.active).to eq(true)
|
108
|
+
ensure
|
109
|
+
w.stop
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
data/spec/sqewer/cli_spec.rb
CHANGED
@@ -4,63 +4,80 @@ describe Sqewer::CLI, :sqs => true, :wait => {timeout: 120} do
|
|
4
4
|
after :each do
|
5
5
|
Dir.glob('*-result').each{|path| File.unlink(path) }
|
6
6
|
end
|
7
|
-
|
7
|
+
|
8
|
+
describe 'with a mock Worker' do
|
9
|
+
it 'uses just three methods' do
|
10
|
+
mock_worker = Class.new do
|
11
|
+
def self.start; end
|
12
|
+
def self.stop; end
|
13
|
+
def self.debug_thread_information!; end
|
14
|
+
end
|
15
|
+
|
16
|
+
worker_pid = fork do
|
17
|
+
Sqewer::CLI.start(mock_worker)
|
18
|
+
end
|
19
|
+
sleep 1
|
20
|
+
|
21
|
+
begin
|
22
|
+
Process.kill('INFO', worker_pid) # Calls debug_thread_information!
|
23
|
+
rescue ArgumentError, Errno::ENOTSUP # on Linux
|
24
|
+
end
|
25
|
+
Process.kill('TERM', worker_pid) # Terminates the worker
|
26
|
+
|
27
|
+
wait_for {
|
28
|
+
_, status = Process.wait2(worker_pid)
|
29
|
+
expect(status.exitstatus).to be_zero # Must have quit cleanly
|
30
|
+
}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
8
34
|
describe 'runs the commandline app, executes jobs and then quits cleanly' do
|
9
35
|
it 'on a USR1 signal' do
|
10
36
|
submitter = Sqewer::Connection.default
|
11
37
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
pid = fork { $stderr.reopen(stderr); $stderr.sync = true; exec("ruby #{__dir__}/cli_app.rb") }
|
16
|
-
|
38
|
+
pid = fork { exec("ruby #{__dir__}/cli_app.rb") }
|
39
|
+
|
17
40
|
Thread.new do
|
18
41
|
20.times do
|
19
|
-
j = {
|
42
|
+
j = {"_job_class" => 'MyJob', "_job_params" => {first_name: 'John', last_name: 'Doe'}}
|
20
43
|
submitter.send_message(JSON.dump(j))
|
21
44
|
end
|
22
45
|
end
|
23
|
-
|
24
|
-
sleep
|
25
|
-
|
26
|
-
|
46
|
+
|
47
|
+
sleep 8
|
48
|
+
wait_for {
|
49
|
+
Process.kill("USR1", pid)
|
50
|
+
_, status = Process.wait2(pid)
|
51
|
+
expect(status.exitstatus).to be_zero # Must have quit cleanly
|
52
|
+
}
|
27
53
|
|
28
54
|
generated_files = Dir.glob('*-result')
|
29
55
|
expect(generated_files).not_to be_empty
|
30
|
-
|
31
|
-
stderr.rewind
|
32
|
-
log_output = stderr.read
|
33
|
-
# This assertion frequently fails (probably because STDERR doesn't get flushed properly)
|
34
|
-
# expect(log_output).to include('Stopping (clean shutdown)')
|
56
|
+
generated_files.each{|path| File.unlink(path) }
|
35
57
|
end
|
36
58
|
|
37
59
|
it 'on a TERM signal' do
|
38
60
|
submitter = Sqewer::Connection.default
|
39
61
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
pid = fork { $stderr.reopen(stderr); $stderr.sync; exec("ruby #{__dir__}/cli_app.rb") }
|
44
|
-
|
62
|
+
pid = fork { exec("ruby #{__dir__}/cli_app.rb") }
|
63
|
+
|
45
64
|
Thread.new do
|
46
65
|
20.times do
|
47
|
-
j = {
|
66
|
+
j = {"_job_class" => 'MyJob', "_job_params" => {first_name: 'John', last_name: 'Doe'}}
|
48
67
|
submitter.send_message(JSON.dump(j))
|
49
68
|
end
|
50
69
|
end
|
51
|
-
|
52
|
-
sleep
|
53
|
-
|
54
|
-
|
70
|
+
|
71
|
+
sleep 8
|
72
|
+
wait_for {
|
73
|
+
Process.kill("TERM", pid)
|
74
|
+
_, status = Process.wait2(pid)
|
75
|
+
expect(status.exitstatus).to be_zero # Must have quit cleanly
|
76
|
+
}
|
55
77
|
|
56
78
|
generated_files = Dir.glob('*-result')
|
57
79
|
expect(generated_files).not_to be_empty
|
58
80
|
generated_files.each{|path| File.unlink(path) }
|
59
|
-
|
60
|
-
stderr.rewind
|
61
|
-
log_output = stderr.read
|
62
|
-
# This assertion frequently fails (probably because STDERR doesn't get flushed properly)
|
63
|
-
# expect(log_output).to include('Stopping (clean shutdown)')
|
64
81
|
end
|
65
82
|
end
|
66
83
|
end
|
@@ -31,6 +31,17 @@ describe Sqewer::Serializer do
|
|
31
31
|
expect(described_class.new.serialize(job)).to eq("{\"_job_class\":\"SomeJob\",\"_job_params\":{\"one\":123,\"two\":[456]}}")
|
32
32
|
end
|
33
33
|
|
34
|
+
it 'adds _execute_after when the value is given' do
|
35
|
+
class ThirdJob < Struct.new :one, :two
|
36
|
+
end
|
37
|
+
|
38
|
+
job = ThirdJob.new(123, [456])
|
39
|
+
res = described_class.new.serialize(job, Time.now.to_i + 1500)
|
40
|
+
parsed = JSON.load(res)
|
41
|
+
|
42
|
+
expect(parsed["_execute_after"]).to be_within(10).of(Time.now.to_i + 1500)
|
43
|
+
end
|
44
|
+
|
34
45
|
it 'raises an exception if the object is of an anonymous class' do
|
35
46
|
s = Struct.new(:foo)
|
36
47
|
o = s.new(1)
|
@@ -58,66 +69,50 @@ describe Sqewer::Serializer do
|
|
58
69
|
end
|
59
70
|
|
60
71
|
describe '#unserialize' do
|
61
|
-
|
62
|
-
|
63
|
-
class VerySimpleJob; end
|
64
|
-
blob = '{"job_class": "VerySimpleJob"}'
|
65
|
-
built_job = described_class.new.unserialize(blob)
|
66
|
-
expect(built_job).to be_kind_of(VerySimpleJob)
|
67
|
-
end
|
72
|
+
it 'wraps the job with a Resubmit when the _execute_after key hints that it is too early' do
|
73
|
+
class EvenSimplerJob; end
|
68
74
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
}.to raise_error(described_class::ArityMismatch)
|
76
|
-
end
|
75
|
+
timestamp_way_in_the_future = Time.now.to_i + (60 * 60 * 24 * 3)
|
76
|
+
blob = '{"_job_class": "EvenSimplerJob", "_execute_after": %d}' % timestamp_way_in_the_future
|
77
|
+
built_job = described_class.new.unserialize(blob)
|
78
|
+
|
79
|
+
expect(built_job).to be_kind_of(Sqewer::Resubmit)
|
80
|
+
expect(built_job.execute_after).to eq(timestamp_way_in_the_future)
|
77
81
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
blob = '{"job_class": "ValidJob", "foo": 1}'
|
82
|
-
built_job = described_class.new.unserialize(blob)
|
83
|
-
|
84
|
-
expect(built_job).to be_kind_of(ValidJob)
|
85
|
-
expect(built_job.foo).to eq(1)
|
86
|
-
end
|
82
|
+
embedded_job = built_job.job
|
83
|
+
expect(embedded_job).to be_kind_of(EvenSimplerJob)
|
87
84
|
end
|
88
85
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
expect(built_job.foo).to eq(1)
|
120
|
-
end
|
86
|
+
it 'builds a job without keyword arguments if its constructor does not need any kwargs' do
|
87
|
+
class EvenSimplerJob; end
|
88
|
+
|
89
|
+
blob = '{"_job_class": "EvenSimplerJob"}'
|
90
|
+
built_job = described_class.new.unserialize(blob)
|
91
|
+
|
92
|
+
expect(built_job).to be_kind_of(EvenSimplerJob)
|
93
|
+
|
94
|
+
blob = '{"_job_class": "EvenSimplerJob", "_job_params": null}'
|
95
|
+
built_job = described_class.new.unserialize(blob)
|
96
|
+
|
97
|
+
expect(built_job).to be_kind_of(EvenSimplerJob)
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'raises an error if the job does not accept the keyword arguments given in the ticket' do
|
101
|
+
class MicroJob; end
|
102
|
+
blob = '{"_job_class": "MicroJob", "_job_params":{"foo": 1}}'
|
103
|
+
expect {
|
104
|
+
described_class.new.unserialize(blob)
|
105
|
+
}.to raise_error(ArgumentError)
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'instantiates the job with keyword arguments' do
|
109
|
+
OtherValidJob = Ks.strict(:foo)
|
110
|
+
|
111
|
+
blob = '{"_job_class": "OtherValidJob", "_job_params": {"foo": 1}}'
|
112
|
+
built_job = described_class.new.unserialize(blob)
|
113
|
+
|
114
|
+
expect(built_job).to be_kind_of(OtherValidJob)
|
115
|
+
expect(built_job.foo).to eq(1)
|
121
116
|
end
|
122
117
|
end
|
123
118
|
end
|
@@ -39,5 +39,23 @@ describe Sqewer::Submitter do
|
|
39
39
|
subject = described_class.new(fake_connection, fake_serializer)
|
40
40
|
subject.submit!(:some_object, delay_seconds: 5)
|
41
41
|
end
|
42
|
+
|
43
|
+
it 'handles the massively delayed execution by clamping the delay_seconds to the SQS maximum, and saving the _execute_after' do
|
44
|
+
fake_serializer = double('Some serializer')
|
45
|
+
allow(fake_serializer).to receive(:serialize) {|object_to_serialize, timestamp_seconds|
|
46
|
+
|
47
|
+
delay_by = Time.now.to_i + 4585659855
|
48
|
+
expect(timestamp_seconds).to be_within(20).of(delay_by)
|
49
|
+
|
50
|
+
expect(object_to_serialize).not_to be_nil
|
51
|
+
'serialized-object-data'
|
52
|
+
}
|
53
|
+
|
54
|
+
fake_connection = double('Some SQS connection')
|
55
|
+
expect(fake_connection).to receive(:send_message).with('serialized-object-data', {delay_seconds: 899})
|
56
|
+
|
57
|
+
subject = described_class.new(fake_connection, fake_serializer)
|
58
|
+
subject.submit!(:some_object, delay_seconds: 4585659855)
|
59
|
+
end
|
42
60
|
end
|
43
61
|
end
|
data/spec/sqewer/worker_spec.rb
CHANGED
@@ -22,6 +22,11 @@ describe Sqewer::Worker, :sqs => true do
|
|
22
22
|
expect(default_worker).to respond_to(:stop)
|
23
23
|
end
|
24
24
|
|
25
|
+
it 'instantiates a new worker object on every call to .default' do
|
26
|
+
workers = (1..10).map { described_class.default }
|
27
|
+
expect(workers.uniq.length).to eq(10)
|
28
|
+
end
|
29
|
+
|
25
30
|
it 'instantiates a Logger to STDERR by default' do
|
26
31
|
expect(Logger).to receive(:new).with(STDERR)
|
27
32
|
worker = described_class.new
|
@@ -57,7 +62,7 @@ describe Sqewer::Worker, :sqs => true do
|
|
57
62
|
|
58
63
|
context 'when the job cannot be instantiated due to an unknown class' do
|
59
64
|
it 'is able to cope with an exception when the job class is unknown (one of generic exceptions)' do
|
60
|
-
payload = JSON.dump({
|
65
|
+
payload = JSON.dump({_job_class: 'UnknownJobClass', _job_params: {arg1: 'some value'}})
|
61
66
|
|
62
67
|
client = Aws::SQS::Client.new
|
63
68
|
client.send_message(queue_url: ENV.fetch('SQS_QUEUE_URL'), message_body: payload)
|
@@ -77,18 +82,18 @@ describe Sqewer::Worker, :sqs => true do
|
|
77
82
|
it 'sets up the processing pipeline so that jobs can execute in sequence (with threads)' do
|
78
83
|
class SecondaryJob
|
79
84
|
def run
|
80
|
-
File.open('secondary-job-run','w') {}
|
85
|
+
File.open(File.join(Dir.tmpdir, 'secondary-job-run'),'w') {}
|
81
86
|
end
|
82
87
|
end
|
83
88
|
|
84
89
|
class InitialJob
|
85
90
|
def run(executor)
|
86
|
-
File.open('initial-job-run','w') {}
|
91
|
+
File.open(File.join(Dir.tmpdir, 'initial-job-run'),'w') {}
|
87
92
|
executor.submit!(SecondaryJob.new)
|
88
93
|
end
|
89
94
|
end
|
90
95
|
|
91
|
-
payload = JSON.dump({
|
96
|
+
payload = JSON.dump({_job_class: 'InitialJob'})
|
92
97
|
client = Aws::SQS::Client.new
|
93
98
|
client.send_message(queue_url: ENV.fetch('SQS_QUEUE_URL'), message_body: payload)
|
94
99
|
|
@@ -97,11 +102,8 @@ describe Sqewer::Worker, :sqs => true do
|
|
97
102
|
worker.start
|
98
103
|
|
99
104
|
begin
|
100
|
-
wait_for { File.exist?('initial-job-run') }.to eq(true)
|
101
|
-
wait_for { File.exist?('secondary-job-run') }.to eq(true)
|
102
|
-
|
103
|
-
File.unlink('initial-job-run')
|
104
|
-
File.unlink('secondary-job-run')
|
105
|
+
wait_for { File.exist?(File.join(Dir.tmpdir, 'initial-job-run')) }.to eq(true)
|
106
|
+
wait_for { File.exist?(File.join(Dir.tmpdir, 'secondary-job-run')) }.to eq(true)
|
105
107
|
ensure
|
106
108
|
worker.stop
|
107
109
|
end
|