deferred 0.5.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/MIT_LICENSE +19 -0
- data/README.markdown +215 -0
- data/Rakefile +1 -0
- data/deferred.gemspec +23 -0
- data/examples/threadpool_job_subtask.rb +203 -0
- data/lib/deferred.rb +32 -0
- data/lib/deferred/accessors.rb +170 -0
- data/lib/deferred/default.rb +36 -0
- data/lib/deferred/extensions.rb +22 -0
- data/lib/deferred/instance_methods.rb +82 -0
- data/lib/deferred/threadpool_job.rb +63 -0
- data/lib/deferred/version.rb +3 -0
- data/spec/deferred/accessors_spec.rb +95 -0
- data/spec/deferred/instance_methods_spec.rb +297 -0
- data/spec/deferred/threadpool_job_spec.rb +172 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/support/accessorized.rb +8 -0
- data/spec/support/bollocks_error.rb +2 -0
- data/spec/support/deferred_state_xtn.rb +22 -0
- metadata +166 -0
data/lib/deferred.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.expand_path('..', __FILE__)).uniq!
|
2
|
+
|
3
|
+
require "deferred/version"
|
4
|
+
require 'eventmachine'
|
5
|
+
|
6
|
+
module Deferred
|
7
|
+
class DeferredError < StandardError; end
|
8
|
+
class OnRunBlockNotRegisteredError < DeferredError; end
|
9
|
+
|
10
|
+
# convenience to return a new Deferred::Default instance
|
11
|
+
# this would be .new but that might have strange effects on
|
12
|
+
# classes that mix in this module
|
13
|
+
#
|
14
|
+
def self.new_default
|
15
|
+
Deferred::Default.new
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
require "deferred/extensions"
|
20
|
+
require "deferred/accessors"
|
21
|
+
require "deferred/instance_methods"
|
22
|
+
require "deferred/threadpool_job"
|
23
|
+
require "deferred/default"
|
24
|
+
|
25
|
+
module Deferred
|
26
|
+
include InstanceMethods
|
27
|
+
|
28
|
+
def self.included(base)
|
29
|
+
base.send(:include, Accessors)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
@@ -0,0 +1,170 @@
|
|
1
|
+
module Deferred
|
2
|
+
module Accessors
|
3
|
+
def self.included(base)
|
4
|
+
base.extend(Deferred::Accessors::ClassMethods)
|
5
|
+
base.send(:include, Deferred::Accessors::InstanceMethods)
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
# creates a DefaultDeferred and attr_reader on the given class
|
10
|
+
# the method created allows for a block to be given, which will be
|
11
|
+
# added to the callback chain.
|
12
|
+
#
|
13
|
+
# Also defines a "reset_#{event_name}" that lets you create a new deferred
|
14
|
+
# for that event. The reset_event method will return the deferred being
|
15
|
+
# replaced.
|
16
|
+
#
|
17
|
+
# @option opts [bool] :prefix ('on') what prefix should we use for the
|
18
|
+
# main accessor. if :prefix is false, then no prefix will be used
|
19
|
+
#
|
20
|
+
# @example usage
|
21
|
+
#
|
22
|
+
# class Foo
|
23
|
+
# include Deferred::Accessors
|
24
|
+
#
|
25
|
+
# deferred_event :stop
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# f = Foo.new
|
29
|
+
#
|
30
|
+
# # you can use on_stop as Deferred object
|
31
|
+
#
|
32
|
+
# f.on_stop.callback do
|
33
|
+
# puts "called back"
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# f.on_stop.errback do
|
37
|
+
# puts "erred back"
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# # or you can just hand a block that will be added to the callback
|
41
|
+
# # chain
|
42
|
+
#
|
43
|
+
# f.on_stop do
|
44
|
+
# puts "this is the callback chain"
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# @example reset method
|
48
|
+
#
|
49
|
+
# class Foo
|
50
|
+
# include Deferred::Accessors
|
51
|
+
#
|
52
|
+
# deferred_event :stop
|
53
|
+
#
|
54
|
+
#
|
55
|
+
# # this method fires the stop callbacks and re-arms them
|
56
|
+
# def restart
|
57
|
+
# dfr = reset_event_stop
|
58
|
+
#
|
59
|
+
# # do stuff that requires stop
|
60
|
+
# dfr.succeed
|
61
|
+
# end
|
62
|
+
# end
|
63
|
+
#
|
64
|
+
#
|
65
|
+
def deferred_event(*event_names)
|
66
|
+
opts = event_names.extract_options!
|
67
|
+
opts = {:before => false, :after => false}.merge(opts)
|
68
|
+
|
69
|
+
if opts[:prefix] != false
|
70
|
+
opts[:prefix] ||= 'on'
|
71
|
+
end
|
72
|
+
|
73
|
+
event_names.each do |event_name|
|
74
|
+
event_name = event_name.to_s
|
75
|
+
|
76
|
+
main_accessor_name = (opts[:prefix] == false) ? event_name : "#{opts[:prefix]}_#{event_name}"
|
77
|
+
|
78
|
+
define_method(:"reset_#{event_name}_event") do
|
79
|
+
_reset_deferred_event(event_name, opts.merge(:main_accessor_name => main_accessor_name))
|
80
|
+
end
|
81
|
+
|
82
|
+
create_deferred_event_reader(main_accessor_name)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# @private
|
87
|
+
def create_deferred_event_reader(accessor_name)
|
88
|
+
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
89
|
+
def #{accessor_name}(&blk)
|
90
|
+
_deferred_events['#{accessor_name}'].tap do |d|
|
91
|
+
d.callback(&blk) if blk
|
92
|
+
end
|
93
|
+
end
|
94
|
+
EOS
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
module InstanceMethods
|
100
|
+
# Like deferred_event, but called from within a method to ensure that the
|
101
|
+
# state transition happens only once.
|
102
|
+
#
|
103
|
+
# @example on_stop with deferred_state_transition
|
104
|
+
#
|
105
|
+
# class Foo
|
106
|
+
# deferred_event :stop
|
107
|
+
#
|
108
|
+
# def stop(&event_callback)
|
109
|
+
# deferred_state_transition(:on_stop, event_callback) do
|
110
|
+
# # handle stop cases
|
111
|
+
# end
|
112
|
+
# end
|
113
|
+
# end
|
114
|
+
#
|
115
|
+
# # which is equivalent to
|
116
|
+
#
|
117
|
+
# class Foo
|
118
|
+
# deferred_event :stop
|
119
|
+
#
|
120
|
+
# def stop(&blk)
|
121
|
+
# on_stop(&blk)
|
122
|
+
#
|
123
|
+
# return on_stop if @state_transition[:on_stop]
|
124
|
+
# @state_transition[:on_stop] = true
|
125
|
+
#
|
126
|
+
# # handle stop cases
|
127
|
+
#
|
128
|
+
# on_stop
|
129
|
+
# end
|
130
|
+
# end
|
131
|
+
#
|
132
|
+
#
|
133
|
+
# It's essentially wrapping the logic of a guard and returning a callback.
|
134
|
+
#
|
135
|
+
# @note This method is *not* threadsafe!
|
136
|
+
#
|
137
|
+
def deferred_state_transition(event_name, blk=nil)
|
138
|
+
event_name = event_name.to_sym
|
139
|
+
dfr = __send__(event_name)
|
140
|
+
|
141
|
+
dfr.callback(&blk) if blk
|
142
|
+
|
143
|
+
return dfr if _deferred_states[event_name]
|
144
|
+
_deferred_states[event_name] = true
|
145
|
+
|
146
|
+
yield
|
147
|
+
|
148
|
+
return dfr
|
149
|
+
end
|
150
|
+
|
151
|
+
protected
|
152
|
+
def _deferred_events
|
153
|
+
@_deferred_events ||= Hash.new { |h,k| h[k.to_s] = Deferred.new_default }
|
154
|
+
end
|
155
|
+
|
156
|
+
def _deferred_states
|
157
|
+
@_deferred_states ||= {}
|
158
|
+
end
|
159
|
+
|
160
|
+
# always returns a deferred, even one that has no callbacks
|
161
|
+
def _reset_deferred_event(event_name, opts={})
|
162
|
+
main_accessor_name = opts[:main_accessor_name]
|
163
|
+
|
164
|
+
_deferred_events.delete(main_accessor_name) { Deferred.new_default }
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Deferred
|
2
|
+
# This acts like the EventMachine::DefaultDeferrable, a blank class
|
3
|
+
# that includes the functionality of a Deferred module. Best used for
|
4
|
+
# cases where you need to follow the deferred result pattern, but don't need
|
5
|
+
# special behavior
|
6
|
+
#
|
7
|
+
class Default
|
8
|
+
include Accessors
|
9
|
+
include InstanceMethods
|
10
|
+
end
|
11
|
+
|
12
|
+
# Like Default but includes the ThreadpoolJob module.
|
13
|
+
class DefaultThreadpoolJob < Default
|
14
|
+
include ThreadpoolJob
|
15
|
+
|
16
|
+
# if you pass a block it will be used as the on_run block
|
17
|
+
#
|
18
|
+
# @example
|
19
|
+
#
|
20
|
+
# DefaultThreadpoolJob.new do
|
21
|
+
# # do stuff
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# # is the equivalent of
|
25
|
+
#
|
26
|
+
# dtj = DefaultThreadpoolJob.new
|
27
|
+
# dtj.on_run do
|
28
|
+
# # do stuff
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
def initialize(&blk)
|
32
|
+
on_run(&blk)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# stolen from active_support, but won't clobber ActiveSupport's definitions
|
2
|
+
|
3
|
+
class ::Hash
|
4
|
+
unless method_defined?(:extractable_options?)
|
5
|
+
def extractable_options?
|
6
|
+
instance_of?(Hash)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class ::Array
|
12
|
+
unless method_defined?(:extract_options!)
|
13
|
+
def extract_options!
|
14
|
+
if last.is_a?(Hash) && last.extractable_options?
|
15
|
+
pop
|
16
|
+
else
|
17
|
+
{}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module Deferred
|
2
|
+
module InstanceMethods
|
3
|
+
include EM::Deferrable
|
4
|
+
|
5
|
+
def callback(*a, &b)
|
6
|
+
super(*a, &b)
|
7
|
+
self
|
8
|
+
end
|
9
|
+
|
10
|
+
def errback(*a, &b)
|
11
|
+
super(*a, &b)
|
12
|
+
self
|
13
|
+
end
|
14
|
+
|
15
|
+
# add block to both callback and errback
|
16
|
+
def ensure_that(&b)
|
17
|
+
callback(&b)
|
18
|
+
errback(&b)
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
# call this deferred with the result (callback or errback) of the +other_dfr+
|
23
|
+
#
|
24
|
+
# @option opts [true,false] :ignore_errors (false) if true, then don't hook
|
25
|
+
# up the errback.
|
26
|
+
#
|
27
|
+
# @option opts [true,false] :only_errors (false) if true, then don't hook
|
28
|
+
# up the callback.
|
29
|
+
#
|
30
|
+
# @return self
|
31
|
+
#
|
32
|
+
# @example deferred chaining
|
33
|
+
#
|
34
|
+
# # this:
|
35
|
+
#
|
36
|
+
# d.chain_to(other_dfr)
|
37
|
+
#
|
38
|
+
# # is the equivalent of:
|
39
|
+
#
|
40
|
+
# other_dfr.callback { |*a| d.succeed(*a) }
|
41
|
+
# other_dfr.errback { |*a| d.fail(*a) }
|
42
|
+
#
|
43
|
+
#
|
44
|
+
def chain_to(other_dfr, opts={})
|
45
|
+
other_dfr.callback { |*a| self.succeed(*a) } unless opts[:only_errors]
|
46
|
+
other_dfr.errback { |*a| self.fail(*a) } unless opts[:ignore_errors]
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
50
|
+
# syntactic sugar equivalent to other_dfr.errback { |*a| self.fail(*a) }
|
51
|
+
def chain_err(other_dfr)
|
52
|
+
chain_to(other_dfr, :only_errors => true)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Like EM::Deferrable's timeout method, but optionally allows you to specify an
|
56
|
+
# exception class. An instance of that exception class will be created and
|
57
|
+
# used as the argument to #fail. If an exception_klass is not given, then #fail
|
58
|
+
# is called without arguments.
|
59
|
+
#
|
60
|
+
def timeout(seconds, exception_klass=nil)
|
61
|
+
cancel_timeout
|
62
|
+
me = self
|
63
|
+
@deferred_timeout = EventMachine::Timer.new(seconds) do
|
64
|
+
if exception_klass
|
65
|
+
e = exception_klass.new("timeout after %0.3f seconds" % [seconds.to_f]).tap { |e| e.set_backtrace([]) }
|
66
|
+
me.fail(*e)
|
67
|
+
else
|
68
|
+
me.fail
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# takes a block, if an exception is raised inside of the block
|
74
|
+
# we do self.fail(exc)
|
75
|
+
def errback_on_exception
|
76
|
+
yield
|
77
|
+
rescue Exception => e
|
78
|
+
self.fail(*e)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Deferred
|
2
|
+
# Adds several methods that simplify using a Deferred as a mechanism for
|
3
|
+
# handling the results of an EM.defer call. see #defer!
|
4
|
+
module ThreadpoolJob
|
5
|
+
include Deferred::InstanceMethods
|
6
|
+
include Deferred::Accessors
|
7
|
+
|
8
|
+
deferred_event :before_run, :prefix => false
|
9
|
+
|
10
|
+
attr_accessor :on_run_block
|
11
|
+
|
12
|
+
# Used with the defer! method. the block given will be the block run
|
13
|
+
# in the EM.threadpool (via EM.defer). see #defer!
|
14
|
+
def on_run(&block)
|
15
|
+
@on_run_block = block
|
16
|
+
end
|
17
|
+
|
18
|
+
# Calls the @on_run_block as the first argument to EM.defer, and uses the
|
19
|
+
# result (the value returned from calling the block) as the deferred result
|
20
|
+
# that will be used to call the registered callbacks. If the block raises an
|
21
|
+
# exception, that exception instance will be the argument used to call the
|
22
|
+
# registered errbacks.
|
23
|
+
#
|
24
|
+
# @raise [OnRunBlockNotRegisteredError] when defer! has been called without an
|
25
|
+
# on_run block assigned (i.e. with no work to defer to a threadpool)
|
26
|
+
#
|
27
|
+
def defer!
|
28
|
+
raise OnRunBlockNotRegisteredError unless @on_run_block
|
29
|
+
|
30
|
+
EM.next_tick do
|
31
|
+
before_run.succeed
|
32
|
+
EM.defer(method(:call_on_run_block).to_proc, method(:handle_completion).to_proc)
|
33
|
+
end
|
34
|
+
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
protected
|
39
|
+
def call_on_run_block
|
40
|
+
@on_run_block.call
|
41
|
+
rescue Exception => exc
|
42
|
+
exc
|
43
|
+
end
|
44
|
+
|
45
|
+
# after cb above runs, this method is called in the reactor
|
46
|
+
def handle_completion(*tp_result)
|
47
|
+
first = tp_result.first
|
48
|
+
|
49
|
+
if first.kind_of?(Exception)
|
50
|
+
self.fail(first)
|
51
|
+
elsif first.respond_to?(:callback) and first.respond_to?(:errback)
|
52
|
+
first.callback { |*a| handle_completion(*a) }
|
53
|
+
first.errback { |*a| handle_completion(*a) }
|
54
|
+
if first.respond_to?(:defer!)
|
55
|
+
EM.schedule { first.defer! }
|
56
|
+
end
|
57
|
+
else
|
58
|
+
self.succeed(*tp_result)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Deferred::Accessors' do
|
4
|
+
describe 'an Accessorized class' do
|
5
|
+
it %[should respond_to deferred_event] do
|
6
|
+
Accessorized.should be_respond_to(:deferred_event)
|
7
|
+
end
|
8
|
+
|
9
|
+
describe 'instance' do
|
10
|
+
before do
|
11
|
+
@acc = Accessorized.new
|
12
|
+
end
|
13
|
+
|
14
|
+
it %[should define an on_plain reader] do
|
15
|
+
@acc.should be_respond_to(:on_plain)
|
16
|
+
end
|
17
|
+
|
18
|
+
it %[should return a Deferred::Default instance] do
|
19
|
+
@acc.on_plain.should be_instance_of(Deferred::Default)
|
20
|
+
end
|
21
|
+
|
22
|
+
it %[should use a block passed to the reader as a callback] do
|
23
|
+
@callback_called = nil
|
24
|
+
|
25
|
+
@acc.on_plain do
|
26
|
+
@callback_called = true
|
27
|
+
end
|
28
|
+
|
29
|
+
@acc.on_plain.succeed
|
30
|
+
|
31
|
+
@callback_called.should be_true
|
32
|
+
end
|
33
|
+
|
34
|
+
describe 'generated methods' do
|
35
|
+
it %[should have a 'no_prefix' deferred defined] do
|
36
|
+
@acc.no_prefix.should be_instance_of(Deferred::Default)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe 'reset_event' do
|
41
|
+
before do
|
42
|
+
@orig_deferred = @acc.on_plain
|
43
|
+
@rval = @acc.reset_plain_event
|
44
|
+
end
|
45
|
+
|
46
|
+
it %[should return the original deferred] do
|
47
|
+
@rval.should be_kind_of(Deferred::Default)
|
48
|
+
@rval.should == @orig_deferred
|
49
|
+
end
|
50
|
+
|
51
|
+
it %[should return new deferreds after reset] do
|
52
|
+
@rval.should_not == @acc.on_plain
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe 'deferred_state_transition' do
|
59
|
+
before do
|
60
|
+
@dsx = DeferredStateXtn.new
|
61
|
+
end
|
62
|
+
|
63
|
+
it %[should return the deferred when called] do
|
64
|
+
@dsx.start.should == @dsx.on_start
|
65
|
+
end
|
66
|
+
|
67
|
+
it %[should add the given block as a callback to the deferred] do
|
68
|
+
callback_called = false
|
69
|
+
|
70
|
+
@dsx.start { callback_called = true }
|
71
|
+
|
72
|
+
@dsx.on_start.succeed
|
73
|
+
|
74
|
+
callback_called.should be_true
|
75
|
+
end
|
76
|
+
|
77
|
+
it %[should add the given block as a callback to the deferred after the callback has fired] do
|
78
|
+
callback_called = false
|
79
|
+
|
80
|
+
@dsx.on_start.succeed
|
81
|
+
|
82
|
+
@dsx.start { callback_called = true }
|
83
|
+
|
84
|
+
callback_called.should be_true
|
85
|
+
end
|
86
|
+
|
87
|
+
it %[should only allow the contained block to be called once] do
|
88
|
+
@dsx.start
|
89
|
+
@dsx.start
|
90
|
+
|
91
|
+
@dsx.start_run_times.should == 1
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|