deferred 0.5.3

Sign up to get free protection for your applications and to get access to all the features.
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,3 @@
1
+ module Deferred
2
+ VERSION = "0.5.3"
3
+ end
@@ -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
+