sqewer 5.0.0 → 5.0.1

Sign up to get free protection for your applications and to get access to all the features.
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 = 'conveyor-belt-test-queue-%s' % SecureRandom.hex(6)
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
- resp = client.delete_queue(queue_url: ENV.fetch('SQS_QUEUE_URL'))
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
@@ -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
- stderr = Tempfile.new('worker-stderr')
13
- stderr.sync = true
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 = {job_class: 'MyJob', first_name: 'John', last_name: 'Doe'}
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 10 # Give it some time to process all the jobs
25
- Process.kill("USR1", pid)
26
- wait_for { Process.wait(pid) }
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
- stderr = Tempfile.new('worker-stderr')
41
- stderr.sync = true
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 = {job_class: 'MyJob', first_name: 'John', last_name: 'Doe'}
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 4
53
- Process.kill("TERM", pid)
54
- wait_for { Process.wait(pid) }
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
- describe 'with the old ticket format' do
62
- it 'builds a job without keyword arguments if its constructor does not need any kwargs' do
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
- it 'raises an error if the job does not accept the keyword arguments given in the ticket' do
70
- class OtherVerySimpleJob; end
71
- blob = '{"job_class": "OtherVerySimpleJob", "foo": 1}'
72
-
73
- expect {
74
- described_class.new.unserialize(blob)
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
- it 'instantiates the job with keyword arguments' do
79
- ValidJob = Ks.strict(:foo)
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
- describe 'with the new ticket format' do
90
- it 'builds a job without keyword arguments if its constructor does not need any kwargs' do
91
- class EvenSimplerJob; end
92
-
93
- blob = '{"_job_class": "EvenSimplerJob"}'
94
- built_job = described_class.new.unserialize(blob)
95
-
96
- expect(built_job).to be_kind_of(EvenSimplerJob)
97
-
98
- blob = '{"_job_class": "EvenSimplerJob", "_job_params": null}'
99
- built_job = described_class.new.unserialize(blob)
100
-
101
- expect(built_job).to be_kind_of(EvenSimplerJob)
102
- end
103
-
104
- it 'raises an error if the job does not accept the keyword arguments given in the ticket' do
105
- class MicroJob; end
106
- blob = '{"_job_class": "MicroJob", "_job_params":{"foo": 1}}'
107
- expect {
108
- described_class.new.unserialize(blob)
109
- }.to raise_error(described_class::ArityMismatch)
110
- end
111
-
112
- it 'instantiates the job with keyword arguments' do
113
- OtherValidJob = Ks.strict(:foo)
114
-
115
- blob = '{"_job_class": "OtherValidJob", "_job_params": {"foo": 1}}'
116
- built_job = described_class.new.unserialize(blob)
117
-
118
- expect(built_job).to be_kind_of(OtherValidJob)
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
@@ -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({job_class: 'UnknownJobClass', arg1: 'some value'})
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({job_class: 'InitialJob'})
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