deferred 0.5.3
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.
- 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
|
+
|