sqewer 5.0.0 → 5.0.1
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 +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
|