shoryuken-later 0.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 +15 -0
- data/.gitignore +22 -0
- data/Gemfile +4 -0
- data/LICENSE +166 -0
- data/README.md +63 -0
- data/Rakefile +13 -0
- data/bin/shoryuken-later +12 -0
- data/lib/shoryuken/later/active_job_adapter.rb +73 -0
- data/lib/shoryuken/later/cli.rb +281 -0
- data/lib/shoryuken/later/client.rb +26 -0
- data/lib/shoryuken/later/launcher.rb +42 -0
- data/lib/shoryuken/later/manager.rb +138 -0
- data/lib/shoryuken/later/poller.rb +91 -0
- data/lib/shoryuken/later/version.rb +5 -0
- data/lib/shoryuken/later/worker.rb +31 -0
- data/lib/shoryuken/later.rb +52 -0
- data/lib/shoryuken-later.rb +1 -0
- data/shoryuken-later.gemspec +32 -0
- data/spec/shoryuken/later/client_spec.rb +42 -0
- data/spec/shoryuken/later/poller_spec.rb +75 -0
- data/spec/shoryuken/worker_spec.rb +33 -0
- data/spec/spec_helper.rb +74 -0
- metadata +161 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# All of this has been "borrowed" from Shoryuken.
|
|
2
|
+
|
|
3
|
+
# @see Shoryuken::Launcher
|
|
4
|
+
module Shoryuken
|
|
5
|
+
module Later
|
|
6
|
+
class Launcher
|
|
7
|
+
include Celluloid
|
|
8
|
+
include Shoryuken::Util
|
|
9
|
+
|
|
10
|
+
trap_exit :actor_died
|
|
11
|
+
|
|
12
|
+
attr_accessor :manager
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
@manager = Shoryuken::Later::Manager.new_link
|
|
16
|
+
|
|
17
|
+
@done = false
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def stop(options = {})
|
|
21
|
+
watchdog('Later::Launcher#stop') do
|
|
22
|
+
@done = true
|
|
23
|
+
|
|
24
|
+
manager.async.stop(shutdown: !!options[:shutdown], timeout: Shoryuken::Later.options[:timeout])
|
|
25
|
+
manager.wait(:shutdown)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def run
|
|
30
|
+
watchdog('Later::Launcher#run') do
|
|
31
|
+
manager.async.start
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def actor_died(actor, reason)
|
|
36
|
+
return if @done
|
|
37
|
+
logger.warn 'Shoryuken::Later died due to the following error, cannot recover, process exiting'
|
|
38
|
+
exit 1
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Most of this has been "borrowed" from Shoryuken, but then repurposed for periodic polling.
|
|
2
|
+
|
|
3
|
+
# @see Shoryuken::Manager
|
|
4
|
+
require 'set'
|
|
5
|
+
require 'shoryuken/later/poller'
|
|
6
|
+
|
|
7
|
+
module Shoryuken
|
|
8
|
+
module Later
|
|
9
|
+
class Manager
|
|
10
|
+
include Celluloid
|
|
11
|
+
include Shoryuken::Util
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@tables = Shoryuken::Later.tables.dup.uniq
|
|
15
|
+
|
|
16
|
+
@done = false
|
|
17
|
+
|
|
18
|
+
@idle = Set.new([])
|
|
19
|
+
@busy = Set.new([])
|
|
20
|
+
@timers = {}
|
|
21
|
+
|
|
22
|
+
@tables.each{|table| Poller.supervise_as :"poller-#{table}", current_actor, table }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def start
|
|
26
|
+
logger.info 'Starting'
|
|
27
|
+
|
|
28
|
+
# Start a poller for every table being polled.
|
|
29
|
+
@tables.each do |table|
|
|
30
|
+
dispatch table
|
|
31
|
+
|
|
32
|
+
# Save the timer so it can be cancelled at shutdown.
|
|
33
|
+
@timers[table] = every(Shoryuken::Later.poll_delay) { dispatch table }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def stop(options = {})
|
|
38
|
+
watchdog('Later::Manager#stop died') do
|
|
39
|
+
@done = true
|
|
40
|
+
|
|
41
|
+
@timers.each_value{|timer| timer.cancel if timer }
|
|
42
|
+
@timers.clear
|
|
43
|
+
|
|
44
|
+
logger.info { "Shutting down #{@idle.size} idle poller(s)" }
|
|
45
|
+
|
|
46
|
+
@idle.each do |name|
|
|
47
|
+
poller = Actor[name] and poller.alive? and poller.terminate
|
|
48
|
+
end
|
|
49
|
+
@idle.clear
|
|
50
|
+
|
|
51
|
+
if @busy.empty?
|
|
52
|
+
return after(0) { signal(:shutdown) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
if options[:shutdown]
|
|
56
|
+
hard_shutdown_in(options[:timeout])
|
|
57
|
+
else
|
|
58
|
+
soft_shutdown(options[:timeout])
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def poller_done(table, poller)
|
|
64
|
+
watchdog('Later::Manager#poller_done died') do
|
|
65
|
+
logger.debug { "Poller done for '#{table}'" }
|
|
66
|
+
|
|
67
|
+
name = :"poller-#{table}"
|
|
68
|
+
@busy.delete name
|
|
69
|
+
|
|
70
|
+
if stopped?
|
|
71
|
+
poller.terminate if poller.alive?
|
|
72
|
+
else
|
|
73
|
+
@idle << name
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def poller_ready(table, poller)
|
|
79
|
+
watchdog('Later::Manager#poller_ready died') do
|
|
80
|
+
logger.debug { "Poller for '#{table}' ready" }
|
|
81
|
+
|
|
82
|
+
name = :"poller-#{table}"
|
|
83
|
+
@busy.delete name
|
|
84
|
+
@idle << name
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def stopped?
|
|
89
|
+
@done
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def dispatch(table)
|
|
95
|
+
name = :"poller-#{table}"
|
|
96
|
+
|
|
97
|
+
# Only start polling if the poller is idle.
|
|
98
|
+
if ! stopped? && @idle.include?(name)
|
|
99
|
+
@idle.delete(name)
|
|
100
|
+
@busy << name
|
|
101
|
+
|
|
102
|
+
Actor[name].async.poll
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def soft_shutdown(delay)
|
|
107
|
+
logger.info { "Waiting for #{@busy.size} busy pollers" }
|
|
108
|
+
|
|
109
|
+
if @busy.size > 0
|
|
110
|
+
after(delay) { soft_shutdown(delay) }
|
|
111
|
+
else
|
|
112
|
+
after(0) { signal(:shutdown) }
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def hard_shutdown_in(delay)
|
|
117
|
+
logger.info { "Waiting for #{@busy.size} busy pollers" }
|
|
118
|
+
logger.info { "Pausing up to #{delay} seconds to allow pollers to finish..." }
|
|
119
|
+
|
|
120
|
+
after(delay) do
|
|
121
|
+
watchdog("Later::Manager#hard_shutdown_in died") do
|
|
122
|
+
if @busy.size > 0
|
|
123
|
+
logger.info { "Hard shutting down #{@busy.size} busy pollers" }
|
|
124
|
+
|
|
125
|
+
@busy.each do |busy|
|
|
126
|
+
if poller = Actor[busy]
|
|
127
|
+
t = poller.bare_object.actual_work_thread
|
|
128
|
+
t.raise Shutdown if poller.alive?
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
after(0) { signal(:shutdown) }
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
module Shoryuken
|
|
4
|
+
module Later
|
|
5
|
+
class Poller
|
|
6
|
+
include Celluloid
|
|
7
|
+
include Shoryuken::Util
|
|
8
|
+
|
|
9
|
+
attr_reader :table_name
|
|
10
|
+
|
|
11
|
+
def initialize(manager, table_name)
|
|
12
|
+
@manager = manager
|
|
13
|
+
@table_name = table_name
|
|
14
|
+
|
|
15
|
+
@manager.async.poller_ready(@table_name, self)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def poll
|
|
19
|
+
watchdog('Later::Poller#poll died') do
|
|
20
|
+
started_at = Time.now
|
|
21
|
+
|
|
22
|
+
logger.debug { "Polling for scheduled messages in '#{@table_name}'" }
|
|
23
|
+
|
|
24
|
+
begin
|
|
25
|
+
while item = next_item
|
|
26
|
+
id = item.attributes['id']
|
|
27
|
+
logger.info "Found message #{id} from '#{@table_name}'"
|
|
28
|
+
defer do
|
|
29
|
+
if sent_msg = process_item(item)
|
|
30
|
+
logger.debug { "Enqueued message #{id} from '#{@table_name}' as #{sent_msg.id}" }
|
|
31
|
+
else
|
|
32
|
+
logger.debug { "Skipping already queued message #{id} from '#{@table_name}'" }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
logger.debug { "Poller for '#{@table_name}' completed in #{elapsed(started_at)} ms" }
|
|
38
|
+
rescue => ex
|
|
39
|
+
logger.error "Error fetching message: #{ex}"
|
|
40
|
+
logger.error ex.backtrace.first
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
@manager.async.poller_done(@table_name, self)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def table
|
|
50
|
+
Shoryuken::Later::Client.tables(@table_name)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Fetches the next available item from the schedule table.
|
|
54
|
+
def next_item
|
|
55
|
+
table.items.where(:perform_at).less_than((Time.now + Shoryuken::Later::MAX_QUEUE_DELAY).to_i).first
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Processes an item and enqueues it (unless another actor has already enqueued it).
|
|
59
|
+
def process_item(item)
|
|
60
|
+
time, worker_class, args, id = item.attributes.values_at('perform_at','shoryuken_class','shoryuken_args','id')
|
|
61
|
+
|
|
62
|
+
worker_class = worker_class.constantize
|
|
63
|
+
args = JSON.parse(args)
|
|
64
|
+
time = Time.at(time)
|
|
65
|
+
queue_name = item.attributes['shoryuken_queue']
|
|
66
|
+
|
|
67
|
+
# Conditionally delete an item prior to enqueuing it, ensuring only one actor may enqueue it.
|
|
68
|
+
begin item.delete(:if => {id: id})
|
|
69
|
+
rescue AWS::DynamoDB::Errors::ConditionalCheckFailedException => e
|
|
70
|
+
# Item was already deleted, so it does not need to be queued.
|
|
71
|
+
return
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Now the item is safe to be enqueued, since the conditional delete succeeded.
|
|
75
|
+
body, options = args.values_at('body','options')
|
|
76
|
+
if queue_name.nil?
|
|
77
|
+
worker_class.perform_in(time, body, options)
|
|
78
|
+
|
|
79
|
+
# For compatibility with Shoryuken's ActiveJob adapter, support an explicit queue name.
|
|
80
|
+
else
|
|
81
|
+
delay = (time - Time.now).to_i
|
|
82
|
+
options[:delay_seconds] = delay if delay > 0
|
|
83
|
+
options[:message_attributes] ||= {}
|
|
84
|
+
options[:message_attributes]['shoryuken_class'] = { string_value: worker_class.to_s, data_type: 'String' }
|
|
85
|
+
Shoryuken::Client.send_message(queue_name, body, options)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require 'time'
|
|
2
|
+
require 'json'
|
|
3
|
+
|
|
4
|
+
module Shoryuken
|
|
5
|
+
module Later
|
|
6
|
+
module Worker
|
|
7
|
+
module ClassMethods
|
|
8
|
+
|
|
9
|
+
def perform_later(time, body, options = {})
|
|
10
|
+
time = Time.now + time.to_i if Numeric===time
|
|
11
|
+
time = time.to_time if time.respond_to?(:to_time)
|
|
12
|
+
raise ArgumentError, 'expected Numeric, Time but got '+time.class.name unless Time===time
|
|
13
|
+
|
|
14
|
+
# Times that are less than 15 minutes in the future can be queued immediately.
|
|
15
|
+
if time < Time.now + Shoryuken::Later::MAX_QUEUE_DELAY
|
|
16
|
+
perform_in(time, body, options)
|
|
17
|
+
|
|
18
|
+
# Otherwise, the message is inserted into a DynamoDB table with the same name as the queue.
|
|
19
|
+
else
|
|
20
|
+
table = get_shoryuken_options['schedule_table'] || Shoryuken::Later.default_table
|
|
21
|
+
args = JSON.dump(body: body, options: options)
|
|
22
|
+
Shoryuken::Later::Client.put_item(table, perform_at: time.to_i, shoryuken_args: args,
|
|
23
|
+
shoryuken_class: self.to_s)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
Worker::ClassMethods.send :include, Later::Worker::ClassMethods
|
|
31
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
require 'shoryuken'
|
|
2
|
+
require 'shoryuken/later/version'
|
|
3
|
+
require 'shoryuken/later/client'
|
|
4
|
+
require 'shoryuken/later/worker'
|
|
5
|
+
|
|
6
|
+
module Shoryuken
|
|
7
|
+
module Later
|
|
8
|
+
MAX_QUEUE_DELAY = 15 * 60
|
|
9
|
+
|
|
10
|
+
DEFAULT_POLL_DELAY = 5 * 60
|
|
11
|
+
|
|
12
|
+
DEFAULTS = {
|
|
13
|
+
aws: {},
|
|
14
|
+
later: {
|
|
15
|
+
tables: [],
|
|
16
|
+
delay: DEFAULT_POLL_DELAY,
|
|
17
|
+
},
|
|
18
|
+
timeout: 8
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@@tables = []
|
|
22
|
+
@@default_table = 'shoryuken_later'
|
|
23
|
+
|
|
24
|
+
class << self
|
|
25
|
+
def options
|
|
26
|
+
@options ||= DEFAULTS.dup
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def poll_delay
|
|
30
|
+
options[:later][:delay] || DEFAULT_POLL_DELAY
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def default_table
|
|
34
|
+
@@default_table
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def default_table=(table)
|
|
38
|
+
@@default_table = table
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def tables
|
|
42
|
+
@@tables
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def logger
|
|
46
|
+
Shoryuken::Logging.logger
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
require 'shoryuken/later/active_job_adapter' if defined?(::ActiveJob)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require 'shoryuken/later'
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
+
require "shoryuken/later/version"
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |spec|
|
|
7
|
+
spec.name = "shoryuken-later"
|
|
8
|
+
spec.version = Shoryuken::Later::VERSION
|
|
9
|
+
spec.authors = ["Joe Khoobyar"]
|
|
10
|
+
spec.email = ["joe@khoobyar.name"]
|
|
11
|
+
spec.homepage = "http://github.com/joekhoobyar/shoryuken-later"
|
|
12
|
+
spec.summary = 'A scheduling plugin (using Dynamo DB) for Shoryuken'
|
|
13
|
+
spec.description = %Q{
|
|
14
|
+
This gem provides a scheduling plugin (using Dynamo DB) for Shoryuken, as well as an ActiveJob adapter
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
spec.license = "LGPL-3.0"
|
|
18
|
+
spec.files = `git ls-files -z`.split("\x0")
|
|
19
|
+
spec.executables = %w[shoryuken-later]
|
|
20
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
|
21
|
+
spec.require_paths = ["lib"]
|
|
22
|
+
|
|
23
|
+
spec.required_ruby_version = '>= 1.9.3'
|
|
24
|
+
|
|
25
|
+
spec.add_development_dependency "bundler", '>= 1.3.5'
|
|
26
|
+
spec.add_development_dependency "rake", '~> 10.0'
|
|
27
|
+
spec.add_development_dependency "rspec", '~> 3.0', '< 3.1'
|
|
28
|
+
|
|
29
|
+
spec.add_dependency "aws-sdk-v1"
|
|
30
|
+
spec.add_dependency "celluloid", "~> 0.15.2"
|
|
31
|
+
spec.add_dependency "shoryuken", "~> 0.0.4"
|
|
32
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Shoryuken::Later::Client do
|
|
4
|
+
let(:ddb) { double 'DynamoDB' }
|
|
5
|
+
let(:table_collection) { double 'Table Collection' }
|
|
6
|
+
let(:ddb_table) { double 'DynamoDb Table' }
|
|
7
|
+
let(:ddb_items) { double 'Table Items' }
|
|
8
|
+
let(:table) { 'shoryuken_later' }
|
|
9
|
+
|
|
10
|
+
before do
|
|
11
|
+
allow(described_class).to receive(:ddb).and_return(ddb)
|
|
12
|
+
allow(ddb).to receive(:tables).and_return(table_collection)
|
|
13
|
+
allow(table_collection).to receive(:[]).and_return(ddb_table)
|
|
14
|
+
allow(ddb_table).to receive(:items).and_return(ddb_items)
|
|
15
|
+
allow(ddb_table).to receive(:hash_key=)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
describe '.tables' do
|
|
19
|
+
it 'memoizes tables and sets the hash_key' do
|
|
20
|
+
expect(table_collection).to receive(:[]).once.with(table).and_return(ddb_table)
|
|
21
|
+
expect(ddb_table).to receive(:hash_key=).once.with([:id, :string])
|
|
22
|
+
|
|
23
|
+
expect(described_class.tables(table)).to eq(ddb_table)
|
|
24
|
+
expect(described_class.tables(table)).to eq(ddb_table)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
describe '.put_item' do
|
|
29
|
+
it 'creates an item with a supplied ID' do
|
|
30
|
+
expect(ddb_items).to receive(:create).with({'id' => 'fubar'}, {unless_exists: 'id'})
|
|
31
|
+
|
|
32
|
+
described_class.put_item(table,'id' => 'fubar')
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'creates an item with a auto-generated ID' do
|
|
36
|
+
expect(SecureRandom).to receive(:uuid).once.and_return('fubar')
|
|
37
|
+
expect(ddb_items).to receive(:create).with({'id' => 'fubar', 'perform_at' => 1234}, {unless_exists: 'id'})
|
|
38
|
+
|
|
39
|
+
described_class.put_item(table,'perform_at' => 1234)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'shoryuken/later/poller'
|
|
3
|
+
require 'shoryuken/later/manager'
|
|
4
|
+
|
|
5
|
+
describe Shoryuken::Later::Poller do
|
|
6
|
+
let(:manager) { double Shoryuken::Later::Manager, poller_ready: nil, poller_done: nil }
|
|
7
|
+
let(:ddb_table) { double 'DynamoDb Table' }
|
|
8
|
+
let(:ddb_items) { double 'Table Items' }
|
|
9
|
+
let(:table) { 'shoryuken_later' }
|
|
10
|
+
|
|
11
|
+
let(:body) { {'foo' => 'bar'} }
|
|
12
|
+
let(:json) { JSON.dump(body: body, options: {}) }
|
|
13
|
+
|
|
14
|
+
let(:ddb_item) do
|
|
15
|
+
double AWS::DynamoDB::Item, delete: nil,
|
|
16
|
+
attributes: {'id' => 'fubar', 'perform_at' => Time.now + 60, 'shoryuken_args' => json, 'shoryuken_class' => 'TestWorker'}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
before do
|
|
20
|
+
allow(manager).to receive(:async).and_return(manager)
|
|
21
|
+
allow(Shoryuken::Later::Client).to receive(:tables).with(table).and_return(ddb_table)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
subject do
|
|
25
|
+
described_class.new(manager, table)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
describe '#initialize' do
|
|
29
|
+
it 'informs the manager that the poller is ready' do
|
|
30
|
+
expect(manager).to receive(:poller_ready).once
|
|
31
|
+
|
|
32
|
+
subject.inspect
|
|
33
|
+
subject.inspect
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
describe '#poll' do
|
|
38
|
+
it 'pulls items from #next_item, and processes with #process_item' do
|
|
39
|
+
items = [ddb_item]
|
|
40
|
+
expect_any_instance_of(described_class).to receive(:next_item).twice { items.pop }
|
|
41
|
+
expect_any_instance_of(described_class).to receive(:process_item).once.with(ddb_item)
|
|
42
|
+
expect(manager).to receive(:poller_done).once
|
|
43
|
+
|
|
44
|
+
subject.poll
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'informs the manager after polling is done' do
|
|
48
|
+
items = []
|
|
49
|
+
expect_any_instance_of(described_class).to receive(:next_item).once { items.pop }
|
|
50
|
+
expect_any_instance_of(described_class).not_to receive(:process_item)
|
|
51
|
+
expect(manager).to receive(:poller_done).once
|
|
52
|
+
|
|
53
|
+
subject.poll
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
describe '#process_item' do
|
|
58
|
+
it 'enqueues a message if the item could be deleted' do
|
|
59
|
+
expect(TestWorker).to receive(:perform_in).once do |time,body,options|
|
|
60
|
+
expect(time ).to be > Time.now
|
|
61
|
+
expect(body ).to eq(body)
|
|
62
|
+
expect(options).to be_empty
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
subject.send(:process_item, ddb_item)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it 'does not enqueue a message if the item could not be deleted' do
|
|
69
|
+
expect(TestWorker).not_to receive(:perform_in)
|
|
70
|
+
allow(ddb_item).to receive(:delete) { raise AWS::DynamoDB::Errors::ConditionalCheckFailedException }
|
|
71
|
+
|
|
72
|
+
subject.send(:process_item, ddb_item)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe 'Shoryuken::Worker' do
|
|
4
|
+
let(:sqs_queue) { double 'SQS Queue' }
|
|
5
|
+
let(:queue) { 'shoryuken_later' }
|
|
6
|
+
let(:table) { 'shoryuken_later' }
|
|
7
|
+
let(:msg_attrs) { { 'shoryuken_class' => { string_value: TestWorker.to_s, data_type: 'String' } } }
|
|
8
|
+
|
|
9
|
+
before do
|
|
10
|
+
allow(Shoryuken::Client).to receive(:queues).with(queue).and_return(sqs_queue)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
describe '.perform_later' do
|
|
14
|
+
it 'delays a message for up to 15 minutes in the future' do
|
|
15
|
+
expect(sqs_queue).to receive(:send_message).with('message', { message_attributes: msg_attrs, delay_seconds: 15 * 60 })
|
|
16
|
+
|
|
17
|
+
TestWorker.perform_later(15 * 60, 'message')
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'schedules a message for over 15 minutes in the future' do
|
|
21
|
+
json = JSON.dump(body: 'message', options: {})
|
|
22
|
+
future = Time.now + 15 * 60 + 1
|
|
23
|
+
|
|
24
|
+
expect(Shoryuken::Later::Client).to receive(:put_item) do |table,attrs|
|
|
25
|
+
expect(attrs[:perform_at]).to be >= future.to_i
|
|
26
|
+
expect(attrs[:shoryuken_args]).to eq(json)
|
|
27
|
+
expect(attrs[:shoryuken_class]).to eq('TestWorker')
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
TestWorker.perform_later(15 * 60 + 1, 'message')
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
require 'bundler/setup'
|
|
2
|
+
Bundler.setup
|
|
3
|
+
|
|
4
|
+
require 'celluloid'
|
|
5
|
+
require 'shoryuken-later'
|
|
6
|
+
require 'json'
|
|
7
|
+
|
|
8
|
+
options_file = File.join(File.expand_path('../..', __FILE__), 'shoryuken.yml')
|
|
9
|
+
|
|
10
|
+
$options = {}
|
|
11
|
+
|
|
12
|
+
if File.exists? options_file
|
|
13
|
+
$options = YAML.load(File.read(options_file)).deep_symbolize_keys
|
|
14
|
+
|
|
15
|
+
AWS.config $options[:aws]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
Shoryuken.logger.level = Logger::UNKNOWN
|
|
19
|
+
Celluloid.logger.level = Logger::UNKNOWN
|
|
20
|
+
|
|
21
|
+
# For Ruby 1.9
|
|
22
|
+
module Kernel
|
|
23
|
+
def Hash(arg)
|
|
24
|
+
case arg
|
|
25
|
+
when NilClass
|
|
26
|
+
{}
|
|
27
|
+
when Hash
|
|
28
|
+
arg
|
|
29
|
+
when Array
|
|
30
|
+
Hash[*arg]
|
|
31
|
+
else
|
|
32
|
+
raise TypeError
|
|
33
|
+
end
|
|
34
|
+
end unless method_defined? :Hash
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# For Ruby 1.9
|
|
38
|
+
class Hash
|
|
39
|
+
def to_h
|
|
40
|
+
self
|
|
41
|
+
end unless method_defined? :to_h
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class TestWorker
|
|
45
|
+
include Shoryuken::Worker
|
|
46
|
+
|
|
47
|
+
shoryuken_options queue: 'shoryuken_later', schedule_table: 'shoryuken_later'
|
|
48
|
+
|
|
49
|
+
def perform(sqs_msg, body); end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
RSpec.configure do |config|
|
|
53
|
+
config.before do
|
|
54
|
+
Shoryuken::Later::Client.class_variable_set :@@tables, {}
|
|
55
|
+
|
|
56
|
+
Shoryuken::Later.options.clear
|
|
57
|
+
Shoryuken::Later.options.merge!($options)
|
|
58
|
+
|
|
59
|
+
Shoryuken::Later.tables.replace(['shoryuken_later'])
|
|
60
|
+
|
|
61
|
+
Shoryuken::Later.options[:later][:delay] = 60
|
|
62
|
+
Shoryuken::Later.options[:later][:tables] = ['shoryuken_later']
|
|
63
|
+
Shoryuken::Later.options[:timeout] = 1
|
|
64
|
+
|
|
65
|
+
Shoryuken::Later.options[:aws] = {}
|
|
66
|
+
|
|
67
|
+
TestWorker.get_shoryuken_options.clear
|
|
68
|
+
TestWorker.get_shoryuken_options['queue'] = 'shoryuken_later'
|
|
69
|
+
TestWorker.get_shoryuken_options['schedule_table'] = 'shoryuken_later'
|
|
70
|
+
|
|
71
|
+
Shoryuken.workers.clear
|
|
72
|
+
Shoryuken.register_worker('shoryuken_later', TestWorker)
|
|
73
|
+
end
|
|
74
|
+
end
|