juggler 0.0.2 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -1
- data/Gemfile +4 -0
- data/README.md +38 -5
- data/Rakefile +8 -15
- data/VERSION +1 -1
- data/examples/app.rb +5 -0
- data/examples/test_consumer.rb +3 -4
- data/examples/test_producer.rb +17 -11
- data/juggler.gemspec +21 -0
- data/lib/juggler.rb +48 -14
- data/lib/juggler/job_runner.rb +144 -0
- data/lib/juggler/runner.rb +162 -51
- data/lib/juggler/state_machine.rb +81 -0
- data/spec/integration/juggler_spec.rb +100 -0
- data/spec/spec_helper.rb +50 -0
- data/spec/unit/job_runner_spec.rb +192 -0
- metadata +71 -43
- data/spec/juggler_spec.rb +0 -7
- data/spec/runner_spec.rb +0 -21
data/.gitignore
CHANGED
data/Gemfile
ADDED
data/README.md
CHANGED
@@ -2,17 +2,17 @@ Add jobs for asynchronous processing
|
|
2
2
|
|
3
3
|
Juggler.throw(:method, params)
|
4
4
|
|
5
|
-
Add handlers, with optional concurrency, inside
|
5
|
+
Add handlers, with optional concurrency, inside an EM loop
|
6
6
|
|
7
7
|
EM.run {
|
8
|
-
Juggler.juggle(:method, 10) do |params|
|
9
|
-
#
|
8
|
+
Juggler.juggle(:method, 10) do |deferrable, params|
|
9
|
+
# Succeed the deferrable when the job is done
|
10
10
|
end
|
11
11
|
}
|
12
12
|
|
13
13
|
For example
|
14
14
|
|
15
|
-
Juggler.juggle(:download, 10) do |params|
|
15
|
+
Juggler.juggle(:download, 10) do |df, params|
|
16
16
|
http = EM::Protocols::HttpClient.request({
|
17
17
|
:host => params[:host],
|
18
18
|
:port => 80,
|
@@ -20,6 +20,39 @@ For example
|
|
20
20
|
})
|
21
21
|
http.callback do |response|
|
22
22
|
puts "Got response status #{response[:status]} for #{a}"
|
23
|
+
df.success
|
23
24
|
end
|
24
|
-
http
|
25
25
|
end
|
26
|
+
|
27
|
+
The job is considered to have failed in the following cases:
|
28
|
+
|
29
|
+
* If the block raises an error
|
30
|
+
* If the block fails the passed deferrable
|
31
|
+
* If the job timeout is exceeded. In this case the passed deferrable will be failed by juggler. If you need to clean up any state in this case (for example you might want to cancel a HTTP request) then you should bind to `df.errback`.
|
32
|
+
|
33
|
+
## Customising behaviour
|
34
|
+
|
35
|
+
### Customising the back-off for failed jobs
|
36
|
+
|
37
|
+
By default, juggler will backoff jobs which failed exponentially using an exponent of 1.3, up to a maximum delay of 1 day, at which point the job will be buried. It's possible to customise this behaviour:
|
38
|
+
|
39
|
+
Juggler.backoff_function = lambda { |job_runner, job_stats|
|
40
|
+
# job_stats is a hash with string keys, as returned by beanstalkd's
|
41
|
+
# stats-job command. Particularly useful stats in this context are:
|
42
|
+
#
|
43
|
+
# job_stats["age"] - the time since the put command that created this job
|
44
|
+
# job_stats["delay"] - the previous amount of time delayed
|
45
|
+
|
46
|
+
new_delay = ([1, job_stats["delay"] * 2].max).ceil
|
47
|
+
if job_stats["age"] > 300
|
48
|
+
job_runner.delete
|
49
|
+
# Or you could bury the job
|
50
|
+
# job_runner.bury
|
51
|
+
else
|
52
|
+
job_runner.release(new_delay)
|
53
|
+
end
|
54
|
+
}
|
55
|
+
|
56
|
+
## Important points to note
|
57
|
+
|
58
|
+
* If your deferrable code raises errors, this will not be handled by juggler.
|
data/Rakefile
CHANGED
@@ -1,17 +1,10 @@
|
|
1
|
-
require
|
2
|
-
require 'rake'
|
1
|
+
require "bundler/gem_tasks"
|
3
2
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
gem.summary = %Q{Juggling background jobs with EventMachine and Beanstalkd}
|
9
|
-
gem.description = %Q{Juggling background jobs with EventMachine and Beanstalkd}
|
10
|
-
gem.email = "me@mloughran.com"
|
11
|
-
gem.homepage = "http://github.com/mloughran/juggler"
|
12
|
-
gem.authors = ["Martyn Loughran"]
|
13
|
-
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
14
|
-
end
|
15
|
-
rescue LoadError
|
16
|
-
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
3
|
+
require "rspec/core/rake_task"
|
4
|
+
|
5
|
+
RSpec::Core::RakeTask.new(:spec) do |s|
|
6
|
+
s.pattern = 'spec/**/*.rb'
|
17
7
|
end
|
8
|
+
|
9
|
+
task :default => :spec
|
10
|
+
task :test => :spec
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0
|
1
|
+
0.1.0
|
data/examples/app.rb
CHANGED
data/examples/test_consumer.rb
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
$:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
3
|
require 'juggler'
|
4
|
-
require 'eventmachine'
|
5
4
|
|
6
|
-
Juggler.logger = Logger
|
5
|
+
Juggler.logger.level = Logger::DEBUG
|
7
6
|
|
8
7
|
EM.run {
|
9
8
|
Juggler.juggle(:http, 3) do |path|
|
10
9
|
http = EM::Protocols::HttpClient.request({
|
11
|
-
:host => "
|
10
|
+
:host => "0.0.0.0",
|
12
11
|
:port => 4567,
|
13
12
|
:request => path
|
14
13
|
})
|
@@ -22,7 +21,7 @@ EM.run {
|
|
22
21
|
Juggler.juggle(:timer, 5) do |params|
|
23
22
|
defer = EM::DefaultDeferrable.new
|
24
23
|
|
25
|
-
EM::Timer.new(
|
24
|
+
EM::Timer.new(10) do
|
26
25
|
defer.set_deferred_status :succeeded, nil
|
27
26
|
# defer.set_deferred_status :failed, nil
|
28
27
|
end
|
data/examples/test_producer.rb
CHANGED
@@ -2,19 +2,25 @@ require 'rubygems'
|
|
2
2
|
$:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
3
|
require 'juggler'
|
4
4
|
|
5
|
-
Juggler.logger = Logger.new(STDOUT)
|
6
|
-
|
7
5
|
# Throw some jobs
|
8
6
|
|
9
|
-
|
7
|
+
# EM.run {
|
8
|
+
# # 10.times do |i|
|
9
|
+
# # path = ['/fast', '/slow'][i % 2]
|
10
|
+
# # Juggler.throw(:http, path, :ttr => 40)
|
11
|
+
# # end
|
12
|
+
# 1.times { Juggler.throw(:timer, {:foo => 'bar'}, :ttr => 5) }
|
13
|
+
# }
|
10
14
|
|
11
|
-
|
12
|
-
|
13
|
-
Juggler.throw(:http, path, {
|
14
|
-
:ttr => 1
|
15
|
-
})
|
15
|
+
Thread.new do
|
16
|
+
EM.run
|
16
17
|
end
|
17
|
-
10.times { Juggler.throw(:timer, {:foo => 'bar'}) }
|
18
18
|
|
19
|
-
|
20
|
-
|
19
|
+
loop do
|
20
|
+
puts "Choose your ttr!"
|
21
|
+
time = gets.to_i
|
22
|
+
puts "Creating job with ttr #{time}"
|
23
|
+
EM.next_tick {
|
24
|
+
Juggler.throw(:timer, {:foo => 'bar'}, :ttr => time)
|
25
|
+
}
|
26
|
+
end
|
data/juggler.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = "juggler"
|
5
|
+
s.version = "0.1.0"
|
6
|
+
s.authors = ["Martyn Loughran"]
|
7
|
+
s.email = ["me@mloughran.com"]
|
8
|
+
s.homepage = "https://github.com/mloughran/juggler"
|
9
|
+
s.summary = %q{Juggling background jobs with EventMachine and Beanstalkd}
|
10
|
+
s.description = %q{Juggling background jobs with EventMachine and Beanstalkd}
|
11
|
+
|
12
|
+
s.add_runtime_dependency "em-jack", "~> 0.1.0"
|
13
|
+
|
14
|
+
s.add_development_dependency "em-spec"
|
15
|
+
s.add_development_dependency "rake"
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
end
|
data/lib/juggler.rb
CHANGED
@@ -1,17 +1,31 @@
|
|
1
|
-
require '
|
2
|
-
|
3
|
-
|
1
|
+
require 'em-jack'
|
2
|
+
require 'eventmachine'
|
3
|
+
require 'uri'
|
4
4
|
|
5
5
|
class Juggler
|
6
6
|
class << self
|
7
|
-
attr_writer :
|
7
|
+
attr_writer :logger
|
8
|
+
attr_writer :shutdown_grace_timeout
|
9
|
+
attr_accessor :exception_handler
|
10
|
+
attr_accessor :backoff_function
|
11
|
+
|
12
|
+
def server=(uri)
|
13
|
+
@server = URI.parse(uri)
|
14
|
+
end
|
8
15
|
|
9
|
-
def
|
10
|
-
@
|
16
|
+
def server
|
17
|
+
@server ||= URI.parse("beanstalk://localhost:11300")
|
18
|
+
end
|
19
|
+
|
20
|
+
# By default after receiving QUIT juggler will wait up to 2s for running
|
21
|
+
# jobs to complete before killing them
|
22
|
+
def shutdown_grace_timeout
|
23
|
+
@shutdown_grace_timeout || 2
|
11
24
|
end
|
12
25
|
|
13
26
|
def logger
|
14
27
|
@logger ||= begin
|
28
|
+
require 'logger'
|
15
29
|
logger = Logger.new(STDOUT)
|
16
30
|
logger.level = Logger::WARN
|
17
31
|
logger.debug("Created logger")
|
@@ -22,15 +36,14 @@ class Juggler
|
|
22
36
|
def throw(method, params, options = {})
|
23
37
|
# TODO: Do some checking on the method
|
24
38
|
connection.use(method.to_s)
|
25
|
-
|
26
|
-
priority = options[:priority] || 50
|
27
|
-
delay = 0
|
28
|
-
# Add 2s because we want to handle the timeout before beanstalk does
|
29
|
-
ttr = (options[:ttr] || 60) + 2
|
30
|
-
|
31
|
-
connection.put(Marshal.dump(params), priority, delay, ttr)
|
39
|
+
connection.put(Marshal.dump(params), options)
|
32
40
|
end
|
33
41
|
|
42
|
+
# Strategy block: should return a deferrable object (so that juggler can
|
43
|
+
# apply callbacks and errbacks). You should note that this deferrable may
|
44
|
+
# be failed by juggler if the job timeout is exceeded, and therefore you
|
45
|
+
# are responsible for cleaning up your state (for example cancelling any
|
46
|
+
# timers which you have created)
|
34
47
|
def juggle(method, concurrency = 1, &strategy)
|
35
48
|
Runner.new(method, concurrency, strategy).run
|
36
49
|
end
|
@@ -38,9 +51,30 @@ class Juggler
|
|
38
51
|
private
|
39
52
|
|
40
53
|
def connection
|
41
|
-
@connection ||=
|
54
|
+
@connection ||= EMJack::Connection.new({
|
55
|
+
:host => server.host,
|
56
|
+
:port => server.port
|
57
|
+
})
|
42
58
|
end
|
43
59
|
end
|
44
60
|
end
|
45
61
|
|
62
|
+
# Default exception handler
|
63
|
+
Juggler.exception_handler = Proc.new do |e|
|
64
|
+
Juggler.logger.error "Error running job: #{e.message} (#{e.class})"
|
65
|
+
Juggler.logger.debug e.backtrace.join("\n")
|
66
|
+
end
|
67
|
+
|
68
|
+
# Default backoff function
|
69
|
+
Juggler.backoff_function = Proc.new do |job_runner, job_stats|
|
70
|
+
# 2, 3, 4, 6, 8, 11, 15, 20, ..., 72465
|
71
|
+
delay = ([1, job_stats["delay"] * 1.3].max).ceil
|
72
|
+
if delay > 60 * 60 * 24
|
73
|
+
job_runner.bury
|
74
|
+
else
|
75
|
+
job_runner.release(delay)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
46
79
|
Juggler.autoload 'Runner', 'juggler/runner'
|
80
|
+
Juggler.autoload 'JobRunner', 'juggler/job_runner'
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require 'juggler/state_machine'
|
2
|
+
|
3
|
+
class Juggler
|
4
|
+
class JobRunner
|
5
|
+
include StateMachine
|
6
|
+
|
7
|
+
state :new
|
8
|
+
state :running, :pre => :fetch_stats, :enter => :run_strategy
|
9
|
+
state :succeeded, :enter => :delete
|
10
|
+
state :timed_out, :enter => [:timeout_strategy, :backoff]
|
11
|
+
state :failed, :enter => :delete
|
12
|
+
state :retried, :enter => :backoff
|
13
|
+
state :done
|
14
|
+
|
15
|
+
attr_reader :job
|
16
|
+
|
17
|
+
def initialize(job, params, strategy)
|
18
|
+
@job = job
|
19
|
+
@params = params
|
20
|
+
@strategy = strategy
|
21
|
+
Juggler.logger.debug {
|
22
|
+
"#{to_s}: New job with body: #{params.inspect}"
|
23
|
+
}
|
24
|
+
@_state = :new
|
25
|
+
end
|
26
|
+
|
27
|
+
def run
|
28
|
+
change_state(:running)
|
29
|
+
end
|
30
|
+
|
31
|
+
def check_for_timeout
|
32
|
+
if state == :running
|
33
|
+
if (time_left = @end_time - Time.now) < 1
|
34
|
+
Juggler.logger.info("#{to_s}: Timed out (#{time_left}s left)")
|
35
|
+
change_state(:timed_out)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_s
|
41
|
+
"Job #{@job.jobid}"
|
42
|
+
end
|
43
|
+
|
44
|
+
def release(delay = 0)
|
45
|
+
Juggler.logger.debug { "#{to_s}: releasing" }
|
46
|
+
release_def = job.release(:delay => delay)
|
47
|
+
release_def.callback {
|
48
|
+
Juggler.logger.info { "#{to_s}: released for retry in #{delay}s" }
|
49
|
+
change_state(:done)
|
50
|
+
}
|
51
|
+
release_def.errback {
|
52
|
+
Juggler.logger.error { "#{to_s}: release failed (could not release)" }
|
53
|
+
change_state(:done)
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
def bury
|
58
|
+
Juggler.logger.warn { "#{to_s}: burying" }
|
59
|
+
release_def = job.bury(100000) # Set priority till em-jack fixed
|
60
|
+
release_def.callback {
|
61
|
+
change_state(:done)
|
62
|
+
}
|
63
|
+
release_def.errback {
|
64
|
+
change_state(:done)
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
def delete
|
69
|
+
dd = job.delete
|
70
|
+
dd.callback do
|
71
|
+
Juggler.logger.debug "#{to_s}: deleted"
|
72
|
+
change_state(:done)
|
73
|
+
end
|
74
|
+
dd.errback do
|
75
|
+
Juggler.logger.debug "#{to_s}: delete operation failed"
|
76
|
+
change_state(:done)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
# Retrives job stats from beanstalkd
|
83
|
+
def fetch_stats
|
84
|
+
dd = EM::DefaultDeferrable.new
|
85
|
+
|
86
|
+
Juggler.logger.debug { "#{to_s}: Fetching stats" }
|
87
|
+
|
88
|
+
stats_def = job.stats
|
89
|
+
stats_def.callback do |stats|
|
90
|
+
@stats = stats
|
91
|
+
@end_time = Time.now + stats["time-left"]
|
92
|
+
Juggler.logger.debug { "#{to_s} stats: #{stats.inspect}"}
|
93
|
+
dd.succeed
|
94
|
+
end
|
95
|
+
stats_def.errback {
|
96
|
+
Juggler.logger.error { "#{to_s}: Fetching stats failed" }
|
97
|
+
dd.fail
|
98
|
+
}
|
99
|
+
|
100
|
+
dd
|
101
|
+
end
|
102
|
+
|
103
|
+
# Wraps running the actual job.
|
104
|
+
# Returns a deferrable that fails if there is an exception calling the
|
105
|
+
# strategy or if the strategy triggers errback
|
106
|
+
def run_strategy
|
107
|
+
begin
|
108
|
+
sd = EM::DefaultDeferrable.new
|
109
|
+
@strategy.call(sd, @params)
|
110
|
+
sd.callback {
|
111
|
+
change_state(:succeeded)
|
112
|
+
}
|
113
|
+
sd.errback { |e|
|
114
|
+
# timed_out error is already handled
|
115
|
+
next if e == :timed_out
|
116
|
+
|
117
|
+
if e == :no_retry
|
118
|
+
# Do not schedule the job to be retried
|
119
|
+
change_state(:failed)
|
120
|
+
elsif e.kind_of?(Exception)
|
121
|
+
# Handle exception and schedule for retry
|
122
|
+
Juggler.exception_handler.call(e)
|
123
|
+
change_state(:retried)
|
124
|
+
else
|
125
|
+
Juggler.logger.debug { "#{to_s}: failed with #{e.inspect}" }
|
126
|
+
change_state(:retried)
|
127
|
+
end
|
128
|
+
}
|
129
|
+
@strategy_deferrable = sd
|
130
|
+
rescue => e
|
131
|
+
Juggler.exception_handler.call(e)
|
132
|
+
change_state(:retried)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def timeout_strategy
|
137
|
+
@strategy_deferrable.fail(:timed_out)
|
138
|
+
end
|
139
|
+
|
140
|
+
def backoff
|
141
|
+
Juggler.backoff_function.call(self, @stats)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
data/lib/juggler/runner.rb
CHANGED
@@ -1,90 +1,201 @@
|
|
1
1
|
class Juggler
|
2
|
+
# Stopping: This is rather complex. The point of the __STOP__ malarkey it to
|
3
|
+
# unblock a blocking reserve so that delete and release commands can be
|
4
|
+
# actioned on the currently running jobs before shutdown. Also a
|
5
|
+
# Juggler.shutdown_grace_timeout period is availble for jobs to complete before the
|
6
|
+
# eventmachine is stopped
|
7
|
+
#
|
2
8
|
class Runner
|
3
9
|
class << self
|
4
|
-
def start
|
5
|
-
@
|
6
|
-
|
7
|
-
|
10
|
+
def start(runner)
|
11
|
+
@runners ||= []
|
12
|
+
@runners << runner
|
13
|
+
|
14
|
+
@signals_setup ||= begin
|
15
|
+
%w{INT TERM}.each do |sig|
|
16
|
+
Signal.trap(sig) {
|
17
|
+
stop_all_runners_with_grace
|
18
|
+
}
|
19
|
+
end
|
8
20
|
true
|
9
21
|
end
|
10
22
|
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def stop_all_runners_with_grace
|
27
|
+
# Trigger each runner to shut down
|
28
|
+
@runners.each { |r| r.stop }
|
29
|
+
|
30
|
+
Juggler.logger.info {
|
31
|
+
"Giving processes #{Juggler.shutdown_grace_timeout}s grace period to exit"
|
32
|
+
}
|
33
|
+
|
34
|
+
EM::PeriodicTimer.new(0.1) {
|
35
|
+
if !@runners.any? { |r| r.running? }
|
36
|
+
Juggler.logger.info "Exited cleanly"
|
37
|
+
EM.stop
|
38
|
+
end
|
39
|
+
}
|
40
|
+
|
41
|
+
EM::Timer.new(Juggler.shutdown_grace_timeout) do
|
42
|
+
Juggler.logger.info {
|
43
|
+
"Force exited after #{Juggler.shutdown_grace_timeout}s with tasks running"
|
44
|
+
}
|
45
|
+
EM.stop
|
46
|
+
end
|
47
|
+
end
|
11
48
|
end
|
12
49
|
|
13
50
|
def initialize(method, concurrency, strategy)
|
14
51
|
@strategy = strategy
|
15
52
|
@concurrency = concurrency
|
16
53
|
@queue = method.to_s
|
54
|
+
|
17
55
|
@running = []
|
56
|
+
@reserved = false
|
57
|
+
end
|
58
|
+
|
59
|
+
# We potentially need to issue a new reserve call after a job is reserved
|
60
|
+
# (if we're not at the concurrency limit), and after a job completes
|
61
|
+
# (unless we're already reserving)
|
62
|
+
def reserve_if_necessary
|
63
|
+
if @on && @connection.connected? && !@reserved && @running.size < @concurrency
|
64
|
+
Juggler.logger.debug "#{to_s}: Reserving"
|
65
|
+
reserve
|
66
|
+
end
|
18
67
|
end
|
19
68
|
|
20
69
|
def reserve
|
21
|
-
|
70
|
+
@reserved = true
|
71
|
+
|
72
|
+
reserve_call = connection.reserve
|
73
|
+
|
74
|
+
reserve_call.callback do |job|
|
75
|
+
@reserved = false
|
22
76
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
77
|
+
begin
|
78
|
+
params = Marshal.load(job.body)
|
79
|
+
rescue => e
|
80
|
+
handle_exception(e, "#{to_s}: Exception unmarshaling #{@queue} job")
|
81
|
+
connection.delete(job)
|
82
|
+
next
|
83
|
+
end
|
84
|
+
|
85
|
+
if params == "__STOP__"
|
86
|
+
connection.delete(job)
|
87
|
+
next
|
88
|
+
end
|
89
|
+
|
90
|
+
job_runner = JobRunner.new(job, params, @strategy)
|
91
|
+
|
92
|
+
@running << job_runner
|
30
93
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
handle_exception(e, "Exception calling #{@queue} strategy")
|
35
|
-
beanstalk_job.decay
|
36
|
-
return
|
37
|
-
end
|
94
|
+
Juggler.logger.debug {
|
95
|
+
"#{to_s}: Excecuting #{@running.size} jobs"
|
96
|
+
}
|
38
97
|
|
39
|
-
|
40
|
-
|
41
|
-
|
98
|
+
# We may reserve after job is running (after fetching stats)
|
99
|
+
job_runner.bind(:running) {
|
100
|
+
reserve_if_necessary
|
101
|
+
}
|
42
102
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
103
|
+
# Also may reserve when a job is done
|
104
|
+
job_runner.bind(:done) {
|
105
|
+
@running.delete(job_runner)
|
106
|
+
reserve_if_necessary
|
107
|
+
}
|
108
|
+
|
109
|
+
job_runner.run
|
47
110
|
end
|
48
|
-
|
49
|
-
|
50
|
-
@
|
51
|
-
|
52
|
-
|
111
|
+
|
112
|
+
reserve_call.errback do |error|
|
113
|
+
@reserved = false
|
114
|
+
|
115
|
+
if error == :deadline_soon
|
116
|
+
# This doesn't necessarily mean that a job has taken too long, it is
|
117
|
+
# quite likely that the blocking reserve is just stopping jobs from
|
118
|
+
# being deleted
|
119
|
+
Juggler.logger.debug "#{to_s}: Reserve terminated (deadline_soon)"
|
120
|
+
|
121
|
+
check_all_reserved_jobs.callback {
|
122
|
+
reserve_if_necessary
|
123
|
+
}
|
124
|
+
elsif error == :disconnected
|
125
|
+
Juggler.logger.warn "#{to_s}: Reserve terminated (beanstalkd disconnected)"
|
126
|
+
else
|
127
|
+
Juggler.logger.error "#{to_s}: Unexpected error: #{error}"
|
128
|
+
reserve_if_necessary
|
129
|
+
end
|
53
130
|
end
|
54
|
-
rescue Beanstalk::TimedOut
|
55
|
-
rescue Beanstalk::NotConnected
|
56
|
-
Juggler.logger.fatal "Could not connect any beanstalk hosts. " \
|
57
|
-
"Retrying in 1s."
|
58
|
-
sleep 1
|
59
|
-
rescue => e
|
60
|
-
handle_exception(e, "Unhandled exception")
|
61
|
-
beanstalk_job.delete if beanstalk_job
|
62
131
|
end
|
63
132
|
|
64
133
|
def run
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
134
|
+
@on = true
|
135
|
+
Runner.start(self)
|
136
|
+
# Creates beanstalkd connection - reserve happens on connect
|
137
|
+
connection
|
69
138
|
end
|
70
139
|
|
71
|
-
|
140
|
+
def stop
|
141
|
+
@on = false
|
72
142
|
|
73
|
-
|
74
|
-
|
143
|
+
# See class documentation on stopping
|
144
|
+
if @reserved
|
145
|
+
Juggler.throw(@queue, "__STOP__")
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def running?
|
150
|
+
@running.size > 0
|
151
|
+
end
|
152
|
+
|
153
|
+
def to_s
|
154
|
+
"Tube #{@queue}"
|
75
155
|
end
|
76
156
|
|
157
|
+
private
|
158
|
+
|
77
159
|
def handle_exception(e, message)
|
78
|
-
Juggler.logger.error "#{message}: #{e.
|
160
|
+
Juggler.logger.error "#{message}: #{e.message} (#{e.class})"
|
79
161
|
Juggler.logger.debug e.backtrace.join("\n")
|
80
162
|
end
|
81
163
|
|
82
164
|
def connection
|
83
|
-
@
|
84
|
-
|
85
|
-
|
86
|
-
|
165
|
+
@connection ||= begin
|
166
|
+
c = EMJack::Connection.new({
|
167
|
+
:host => Juggler.server.host,
|
168
|
+
:port => Juggler.server.port,
|
169
|
+
})
|
170
|
+
c.on_connect {
|
171
|
+
c.watch(@queue)
|
172
|
+
reserve_if_necessary
|
173
|
+
}
|
174
|
+
c
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# Iterates over all jobs reserved on this connection and fails them if
|
179
|
+
# they're within 1s of their timeout. Returns a callback which completes
|
180
|
+
# when all jobs have been checked
|
181
|
+
def check_all_reserved_jobs
|
182
|
+
dd = EM::DefaultDeferrable.new
|
183
|
+
|
184
|
+
@running.each do |job_runner|
|
185
|
+
job_runner.check_for_timeout
|
186
|
+
end
|
187
|
+
|
188
|
+
# Wait 1s before reserving or we'll just get DEALINE_SOON again
|
189
|
+
# "If the client issues a reserve command during the safety margin,
|
190
|
+
# <snip>, the server will respond with: DEADLINE_SOON"
|
191
|
+
#
|
192
|
+
# In theory, one should not need to do this since reserve will already
|
193
|
+
# be triggered as a callback on the job that has timed out
|
194
|
+
EM::Timer.new(1) do
|
195
|
+
dd.succeed
|
87
196
|
end
|
197
|
+
|
198
|
+
dd
|
88
199
|
end
|
89
200
|
end
|
90
201
|
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# Special eventmachine state machine
|
2
|
+
#
|
3
|
+
# Callbacks are defined for before :exit, :pre enter and after :enter
|
4
|
+
# Asynchrous callbacks are not properly supported yet - only :pre must return
|
5
|
+
# a deferrable which will continue the callback chain on complete
|
6
|
+
#
|
7
|
+
# state :foobar, :enter => 'get_http'
|
8
|
+
#
|
9
|
+
module Juggler::StateMachine
|
10
|
+
def self.included(klass)
|
11
|
+
klass.extend(ClassMethods)
|
12
|
+
end
|
13
|
+
|
14
|
+
def state
|
15
|
+
@_state
|
16
|
+
end
|
17
|
+
|
18
|
+
def change_state(new_state)
|
19
|
+
old_state = @_state
|
20
|
+
|
21
|
+
Juggler.logger.debug "#{to_s}: Changing state: #{old_state} to #{new_state}"
|
22
|
+
|
23
|
+
return nil if old_state == new_state
|
24
|
+
|
25
|
+
raise "#{to_s}: Invalid state #{new_state}" unless self.class.states[new_state]
|
26
|
+
|
27
|
+
if method = self.class.states[new_state][:pre]
|
28
|
+
deferable = self.send(method)
|
29
|
+
deferable.callback {
|
30
|
+
run_synchronous_callbacks(old_state, new_state)
|
31
|
+
}
|
32
|
+
deferable.errback {
|
33
|
+
Juggler.logger.warn "#{to_s}: State change aborted - pre failed"
|
34
|
+
}
|
35
|
+
else
|
36
|
+
run_synchronous_callbacks(old_state, new_state)
|
37
|
+
end
|
38
|
+
|
39
|
+
return true
|
40
|
+
end
|
41
|
+
|
42
|
+
def bind(state, &callback)
|
43
|
+
@on_state ||= Hash.new { |h, k| h[k] = [] }
|
44
|
+
@on_state[state] << callback
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def run_synchronous_callbacks(old_state, new_state)
|
50
|
+
catch :halt do
|
51
|
+
if callbacks = self.class.states[old_state][:exit]
|
52
|
+
[callbacks].flatten.each { |c| self.send(c) }
|
53
|
+
end
|
54
|
+
|
55
|
+
set_state(new_state)
|
56
|
+
|
57
|
+
if callbacks = self.class.states[new_state][:enter]
|
58
|
+
[callbacks].flatten.each { |c| self.send(c) }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def set_state(new_state)
|
64
|
+
@_state = new_state
|
65
|
+
|
66
|
+
if @on_state && (callbacks = @on_state[new_state])
|
67
|
+
callbacks.each { |c| c.call(self) }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
module ClassMethods
|
72
|
+
def states
|
73
|
+
@_states
|
74
|
+
end
|
75
|
+
|
76
|
+
def state(name, callbacks = {})
|
77
|
+
@_states ||= {}
|
78
|
+
@_states[name] = callbacks
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require File.expand_path('../../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
describe "Juggler" do
|
4
|
+
include EM::SpecHelper
|
5
|
+
|
6
|
+
before :each do
|
7
|
+
# Reset state
|
8
|
+
Juggler.instance_variable_set(:@connection, nil)
|
9
|
+
Juggler::Runner.instance_variable_set(:@runners, nil)
|
10
|
+
|
11
|
+
# Start clean beanstalk instance for each test
|
12
|
+
Juggler.server = "beanstalk://localhost:10001"
|
13
|
+
system "beanstalkd -p 10001 &"
|
14
|
+
sleep 0.1
|
15
|
+
end
|
16
|
+
|
17
|
+
after :each do
|
18
|
+
# TODO: Use pid
|
19
|
+
system "killall beanstalkd"
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should successfully excecute one job" do
|
23
|
+
em(1) do
|
24
|
+
params_for_jobs_received = []
|
25
|
+
Juggler.juggle(:some_task, 1) { |df, params|
|
26
|
+
params_for_jobs_received << params
|
27
|
+
df.succeed_later_with(nil)
|
28
|
+
}
|
29
|
+
Juggler.throw(:some_task, {:some => "params"})
|
30
|
+
|
31
|
+
EM.add_timer(0.1) {
|
32
|
+
params_for_jobs_received.should == [{:some => "params"}]
|
33
|
+
done
|
34
|
+
}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should run correct number of jobs concurrently" do
|
39
|
+
em(1) do
|
40
|
+
params_for_jobs_received = []
|
41
|
+
Juggler.juggle(:some_task, 2) { |df, params|
|
42
|
+
params_for_jobs_received << params
|
43
|
+
df.succeed_later_with(nil, 0.2)
|
44
|
+
}
|
45
|
+
|
46
|
+
10.times { |i| Juggler.throw(:some_task, i) }
|
47
|
+
|
48
|
+
EM.add_timer(0.3) {
|
49
|
+
# After 0.3 seconds, 2 jobs should have completed, and 2 more started
|
50
|
+
params_for_jobs_received.should == [0, 1, 2, 3]
|
51
|
+
done
|
52
|
+
}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should stop em after jobs completed when signaled to QUIT" do
|
57
|
+
job_finished = false
|
58
|
+
job_started = false
|
59
|
+
em(1) do
|
60
|
+
Juggler.juggle(:some_task, 1) { |df, params|
|
61
|
+
job_started = true
|
62
|
+
EM.add_timer(0.2) {
|
63
|
+
job_finished = true
|
64
|
+
df.succeed
|
65
|
+
}
|
66
|
+
}
|
67
|
+
Juggler.throw(:some_task, 'foo')
|
68
|
+
EM.add_timer(0.1) {
|
69
|
+
job_started.should == true
|
70
|
+
job_finished.should == false
|
71
|
+
Juggler::Runner.send(:stop_all_runners_with_grace)
|
72
|
+
}
|
73
|
+
end
|
74
|
+
job_finished.should == true
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should kill jobs that do not complete within shutdown_grace_timeout" do
|
78
|
+
job_finished = false
|
79
|
+
job_started = false
|
80
|
+
em(1) do
|
81
|
+
Juggler.shutdown_grace_timeout = 0.1
|
82
|
+
Juggler.juggle(:some_task, 1) { |params|
|
83
|
+
dd = EM::DefaultDeferrable.new
|
84
|
+
job_started = true
|
85
|
+
EM.add_timer(1) {
|
86
|
+
job_finished = true
|
87
|
+
dd.succeed
|
88
|
+
}
|
89
|
+
dd
|
90
|
+
}
|
91
|
+
Juggler.throw(:some_task, 'foo')
|
92
|
+
EM.add_timer(0.1) {
|
93
|
+
job_started.should == true
|
94
|
+
job_finished.should == false
|
95
|
+
Juggler::Runner.send(:stop_all_runners_with_grace)
|
96
|
+
}
|
97
|
+
end
|
98
|
+
job_finished.should == false
|
99
|
+
end
|
100
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,3 +1,53 @@
|
|
1
1
|
$:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
2
|
|
3
3
|
require 'juggler'
|
4
|
+
|
5
|
+
Juggler.logger.level = Logger::FATAL
|
6
|
+
|
7
|
+
require 'em-spec/rspec'
|
8
|
+
|
9
|
+
module EM
|
10
|
+
class ChainableDeferrable
|
11
|
+
include Deferrable
|
12
|
+
|
13
|
+
def callback(&block)
|
14
|
+
super
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
def errback(&block)
|
19
|
+
super
|
20
|
+
self
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module Deferrable
|
25
|
+
def succeed_later_with(callback, time = 0)
|
26
|
+
EM.add_timer(time) {
|
27
|
+
succeed(callback)
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
def fail_later_with(callback, time = 0)
|
32
|
+
EM.add_timer(time) {
|
33
|
+
fail(callback)
|
34
|
+
}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def stub_deferrable(callback, time = 0)
|
40
|
+
d = EM::ChainableDeferrable.new
|
41
|
+
EM.add_timer(time) {
|
42
|
+
d.succeed(callback)
|
43
|
+
}
|
44
|
+
d
|
45
|
+
end
|
46
|
+
|
47
|
+
def stub_failing_deferrable(callback, time = 0)
|
48
|
+
d = EM::ChainableDeferrable.new
|
49
|
+
EM.add_timer(time) {
|
50
|
+
d.fail(callback)
|
51
|
+
}
|
52
|
+
d
|
53
|
+
end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
require File.expand_path('../../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
# This spec describes the behaviour of job runner by mocking the interface to
|
4
|
+
# beanstalk as exposed by em-jack
|
5
|
+
#
|
6
|
+
describe Juggler::JobRunner do
|
7
|
+
include EM::SpecHelper
|
8
|
+
|
9
|
+
it "should run job and delete in the success case" do
|
10
|
+
em(1) {
|
11
|
+
job = mock(:job, {
|
12
|
+
:jobid => 1,
|
13
|
+
:stats => stub_deferrable({"time-left" => 2})
|
14
|
+
})
|
15
|
+
|
16
|
+
job.should_receive(:delete).and_return(stub_deferrable(nil))
|
17
|
+
|
18
|
+
strategy = lambda { |df, params|
|
19
|
+
df.succeed_later_with(nil, 0.2)
|
20
|
+
}
|
21
|
+
|
22
|
+
jobrunner = Juggler::JobRunner.new(job, {}, strategy)
|
23
|
+
jobrunner.run
|
24
|
+
|
25
|
+
# To check that check_for_timeout doesn't timeout in the sucess case
|
26
|
+
EM.add_timer(0.1) {
|
27
|
+
jobrunner.check_for_timeout
|
28
|
+
}
|
29
|
+
|
30
|
+
jobrunner.bind(:succeeded) {
|
31
|
+
@state = :succeeded
|
32
|
+
}
|
33
|
+
|
34
|
+
jobrunner.bind(:done) {
|
35
|
+
@state.should == :succeeded
|
36
|
+
done
|
37
|
+
}
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should release job for retry if exception calling strategy" do
|
42
|
+
em(1) {
|
43
|
+
job = mock(:job, {
|
44
|
+
:jobid => 1,
|
45
|
+
:stats => stub_deferrable({"time-left" => 2, "delay" => 0})
|
46
|
+
})
|
47
|
+
|
48
|
+
job.should_receive(:release).with({:delay => 1}).
|
49
|
+
and_return(stub_deferrable(nil))
|
50
|
+
|
51
|
+
strategy = lambda { |df, params|
|
52
|
+
raise 'strategy blows up'
|
53
|
+
}
|
54
|
+
|
55
|
+
jobrunner = Juggler::JobRunner.new(job, {}, strategy)
|
56
|
+
jobrunner.run
|
57
|
+
|
58
|
+
jobrunner.bind(:retried) {
|
59
|
+
@state = :retried
|
60
|
+
}
|
61
|
+
|
62
|
+
jobrunner.bind(:done) {
|
63
|
+
@state.should == :retried
|
64
|
+
done
|
65
|
+
}
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should release job for retry and call exception handler if job deferrable fails with an exception" do
|
70
|
+
asserts = 0
|
71
|
+
|
72
|
+
em(1) {
|
73
|
+
job = mock(:job, {
|
74
|
+
:jobid => 1,
|
75
|
+
:stats => stub_deferrable({"time-left" => 2, "delay" => 0})
|
76
|
+
})
|
77
|
+
|
78
|
+
job.should_receive(:release).with({:delay => 1}).
|
79
|
+
and_return(stub_deferrable(nil))
|
80
|
+
|
81
|
+
Juggler.exception_handler = lambda { |e|
|
82
|
+
e.message.should == "FAIL"
|
83
|
+
asserts += 1
|
84
|
+
}
|
85
|
+
|
86
|
+
strategy = lambda { |df, params|
|
87
|
+
EM.next_tick {
|
88
|
+
df.fail(RuntimeError.new("FAIL"))
|
89
|
+
}
|
90
|
+
}
|
91
|
+
|
92
|
+
jobrunner = Juggler::JobRunner.new(job, {}, strategy)
|
93
|
+
jobrunner.run
|
94
|
+
|
95
|
+
jobrunner.bind(:retried) {
|
96
|
+
asserts += 1
|
97
|
+
}
|
98
|
+
|
99
|
+
jobrunner.bind(:done) {
|
100
|
+
asserts.should == 2
|
101
|
+
done
|
102
|
+
}
|
103
|
+
}
|
104
|
+
end
|
105
|
+
|
106
|
+
it "should release job for retry if job fails with no arguments" do
|
107
|
+
em(1) {
|
108
|
+
job = mock(:job, {
|
109
|
+
:jobid => 1,
|
110
|
+
:stats => stub_deferrable({"time-left" => 2, "delay" => 0})
|
111
|
+
})
|
112
|
+
|
113
|
+
job.should_receive(:release).with({:delay => 1}).
|
114
|
+
and_return(stub_deferrable(nil))
|
115
|
+
|
116
|
+
strategy = lambda { |df, params|
|
117
|
+
df.fail_later_with(nil)
|
118
|
+
}
|
119
|
+
|
120
|
+
jobrunner = Juggler::JobRunner.new(job, {}, strategy)
|
121
|
+
jobrunner.run
|
122
|
+
|
123
|
+
jobrunner.bind(:retried) {
|
124
|
+
@state = :retried
|
125
|
+
}
|
126
|
+
|
127
|
+
jobrunner.bind(:done) {
|
128
|
+
@state.should == :retried
|
129
|
+
done
|
130
|
+
}
|
131
|
+
}
|
132
|
+
end
|
133
|
+
|
134
|
+
it "should fail and delete job if job fails with :no_retry" do
|
135
|
+
em(1) {
|
136
|
+
job = mock(:job, {
|
137
|
+
:jobid => 1,
|
138
|
+
:stats => stub_deferrable({"time-left" => 2, "delay" => 0})
|
139
|
+
})
|
140
|
+
|
141
|
+
job.should_receive(:delete).and_return(stub_deferrable(nil))
|
142
|
+
|
143
|
+
strategy = lambda { |df, params|
|
144
|
+
df.fail_later_with(:no_retry)
|
145
|
+
}
|
146
|
+
|
147
|
+
jobrunner = Juggler::JobRunner.new(job, {}, strategy)
|
148
|
+
jobrunner.run
|
149
|
+
|
150
|
+
jobrunner.bind(:failed) {
|
151
|
+
@state = :failed
|
152
|
+
}
|
153
|
+
|
154
|
+
jobrunner.bind(:done) {
|
155
|
+
@state.should == :failed
|
156
|
+
done
|
157
|
+
}
|
158
|
+
}
|
159
|
+
end
|
160
|
+
|
161
|
+
it "should retry job if strategy deferrable exceeds timeout" do
|
162
|
+
em(1) {
|
163
|
+
job = mock(:job, {
|
164
|
+
:jobid => 1,
|
165
|
+
:stats => stub_deferrable({"time-left" => 1, "delay" => 0})
|
166
|
+
})
|
167
|
+
|
168
|
+
job.should_receive(:release).with({:delay => 1}).
|
169
|
+
and_return(stub_deferrable(nil))
|
170
|
+
|
171
|
+
strategy = lambda { |df, params|
|
172
|
+
df.succeed_later_with(nil, 2)
|
173
|
+
}
|
174
|
+
|
175
|
+
jobrunner = Juggler::JobRunner.new(job, {}, strategy)
|
176
|
+
jobrunner.run
|
177
|
+
|
178
|
+
EM.add_timer(0.1) {
|
179
|
+
jobrunner.check_for_timeout
|
180
|
+
}
|
181
|
+
|
182
|
+
jobrunner.bind(:timed_out) {
|
183
|
+
@state = :timed_out
|
184
|
+
}
|
185
|
+
|
186
|
+
jobrunner.bind(:done) {
|
187
|
+
@state.should == :timed_out
|
188
|
+
done
|
189
|
+
}
|
190
|
+
}
|
191
|
+
end
|
192
|
+
end
|
metadata
CHANGED
@@ -1,71 +1,99 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: juggler
|
3
|
-
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
5
6
|
platform: ruby
|
6
|
-
authors:
|
7
|
+
authors:
|
7
8
|
- Martyn Loughran
|
8
9
|
autorequire:
|
9
10
|
bindir: bin
|
10
11
|
cert_chain: []
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
12
|
+
date: 2012-08-14 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: em-jack
|
16
|
+
requirement: &70241134047560 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.1.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70241134047560
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: em-spec
|
27
|
+
requirement: &70241134047120 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70241134047120
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rake
|
38
|
+
requirement: &70241134046660 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70241134046660
|
16
47
|
description: Juggling background jobs with EventMachine and Beanstalkd
|
17
|
-
email:
|
48
|
+
email:
|
49
|
+
- me@mloughran.com
|
18
50
|
executables: []
|
19
|
-
|
20
51
|
extensions: []
|
21
|
-
|
22
|
-
|
23
|
-
- LICENSE
|
24
|
-
- README.md
|
25
|
-
files:
|
52
|
+
extra_rdoc_files: []
|
53
|
+
files:
|
26
54
|
- .gitignore
|
55
|
+
- Gemfile
|
27
56
|
- LICENSE
|
28
57
|
- README.md
|
29
58
|
- Rakefile
|
30
59
|
- VERSION
|
60
|
+
- examples/app.rb
|
31
61
|
- examples/test_consumer.rb
|
32
62
|
- examples/test_producer.rb
|
63
|
+
- juggler.gemspec
|
33
64
|
- lib/juggler.rb
|
65
|
+
- lib/juggler/job_runner.rb
|
34
66
|
- lib/juggler/runner.rb
|
35
|
-
-
|
67
|
+
- lib/juggler/state_machine.rb
|
68
|
+
- spec/integration/juggler_spec.rb
|
36
69
|
- spec/spec_helper.rb
|
37
|
-
|
38
|
-
homepage:
|
70
|
+
- spec/unit/job_runner_spec.rb
|
71
|
+
homepage: https://github.com/mloughran/juggler
|
39
72
|
licenses: []
|
40
|
-
|
41
73
|
post_install_message:
|
42
|
-
rdoc_options:
|
43
|
-
|
44
|
-
require_paths:
|
74
|
+
rdoc_options: []
|
75
|
+
require_paths:
|
45
76
|
- lib
|
46
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
77
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
78
|
+
none: false
|
79
|
+
requirements:
|
80
|
+
- - ! '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
84
|
+
none: false
|
85
|
+
requirements:
|
86
|
+
- - ! '>='
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
58
89
|
requirements: []
|
59
|
-
|
60
90
|
rubyforge_project:
|
61
|
-
rubygems_version: 1.
|
91
|
+
rubygems_version: 1.8.10
|
62
92
|
signing_key:
|
63
93
|
specification_version: 3
|
64
94
|
summary: Juggling background jobs with EventMachine and Beanstalkd
|
65
|
-
test_files:
|
66
|
-
- spec/juggler_spec.rb
|
67
|
-
- spec/runner_spec.rb
|
95
|
+
test_files:
|
96
|
+
- spec/integration/juggler_spec.rb
|
68
97
|
- spec/spec_helper.rb
|
69
|
-
-
|
70
|
-
|
71
|
-
- examples/test_producer.rb
|
98
|
+
- spec/unit/job_runner_spec.rb
|
99
|
+
has_rdoc:
|
data/spec/juggler_spec.rb
DELETED
data/spec/runner_spec.rb
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
-
|
3
|
-
describe Juggler::Runner do
|
4
|
-
it "should delete job from beanstalkd when successful" do
|
5
|
-
@mock_beanstalk = mock(Beanstalk::Pool)
|
6
|
-
@mock_job = mock(Beanstalk::Job, :body => "foo")
|
7
|
-
Beanstalk::Pool.should_receive(:new).and_return(@mock_beanstalk)
|
8
|
-
|
9
|
-
@mock_job.should_receive(:delete)
|
10
|
-
|
11
|
-
Juggler::Runner.new(:method, 1, lambda do
|
12
|
-
deferrable = EM::DefaultDeferrable.new
|
13
|
-
deferrable.set_deferred_status :succeeded, nil
|
14
|
-
deferrable
|
15
|
-
end).run
|
16
|
-
|
17
|
-
sleep 1
|
18
|
-
end
|
19
|
-
|
20
|
-
it "should retry job if not successful"
|
21
|
-
end
|