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/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
+