exceptional_synchrony 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,86 @@
1
+ require 'fiber'
2
+ require 'set'
3
+
4
+ module ExceptionalSynchrony
5
+ class ParallelSync
6
+ def self.parallel(em, *args)
7
+ parallel_sync = new(em, *args)
8
+ yield parallel_sync
9
+ parallel_sync.run_all!
10
+ end
11
+
12
+ # em is the EventMachine proxy
13
+ # Downstream is an optional queue. If provided it will run the job for us. It must create the Fiber as well.
14
+ def initialize(em, downstream = nil)
15
+ @em = em
16
+ @downstream = downstream
17
+ @jobs = []
18
+ @finished = Set.new
19
+ end
20
+
21
+ def add(proc = nil, &block)
22
+ job = proc || block
23
+ @jobs << job
24
+ end
25
+
26
+ # Runs all the jobs that have been added.
27
+ # Returns the hash of responses where the key is their ordinal number, in order.
28
+ def run_all!
29
+ original_fiber = Fiber.current
30
+
31
+ @responses = (0...@jobs.size).build_hash { |key| [key, nil] } # initialize in sorted order so we don't have to sort later
32
+
33
+ @jobs.each_with_index do |job, index|
34
+ run_and_finish = lambda do |*args|
35
+ @responses[index] = CallbackExceptions.return_exception(*args, &job)
36
+ @finished.add(index)
37
+ check_progress(original_fiber)
38
+ end
39
+
40
+ if @downstream
41
+ if job.respond_to?(:encapsulate)
42
+ cancel_proc = -> do
43
+ @responses[index] = :cancelled
44
+ @finished.add(index)
45
+ end
46
+ @downstream.add(job.encapsulate(cancel: cancel_proc, &run_and_finish))
47
+ else
48
+ @downstream.add(&run_and_finish)
49
+ end
50
+ else
51
+ Fiber.new(&run_and_finish).resume
52
+ end
53
+ end
54
+
55
+ unless finished?
56
+ @yielded = true
57
+ Fiber.yield
58
+ end
59
+
60
+ raise_any_exceptions(@responses)
61
+
62
+ @responses
63
+ end
64
+
65
+ private
66
+ def check_progress(original_fiber)
67
+ if finished? && original_fiber.alive? && original_fiber != Fiber.current && @yielded
68
+ original_fiber.resume
69
+ end
70
+ end
71
+
72
+ def finished?
73
+ @finished.size == @jobs.size
74
+ end
75
+
76
+ def raise_any_exceptions(responses)
77
+ if (exceptions = responses.values.select { |response| response.is_a?(Exception) }).any?
78
+ master_exception, *remaining_exceptions = exceptions
79
+ remaining_exceptions.each do |ex|
80
+ master_exception.message << "\n====================================\n#{ex.class}: #{ex.to_s}"
81
+ end
82
+ raise master_exception
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,3 @@
1
+ module ExceptionalSynchrony
2
+ VERSION = '1.0.1'
3
+ end
@@ -0,0 +1,57 @@
1
+ require_relative '../lib/exceptional_synchrony.rb'
2
+
3
+ require 'minitest/autorun' or raise "Already loaded minitest?"
4
+ require 'minitest/pride'
5
+ require 'webmock'
6
+ require 'webmock/minitest'
7
+ require 'rr'
8
+
9
+ module TestHelper
10
+ @@constant_overrides = []
11
+
12
+ def self.included(base)
13
+ base.class_eval do
14
+ before do
15
+ unless @@constant_overrides.nil? || @@constant_overrides.empty?
16
+ raise "Uh-oh! constant_overrides left over: #{@@constant_overrides.inspect}"
17
+ end
18
+ # TODO:this code doesn't seem to be running. But the after code is. Weird. -Colin
19
+ end
20
+
21
+ after do
22
+ @@constant_overrides && @@constant_overrides.reverse.each do |parent_module, k, v|
23
+ ExceptionHandling.ensure_completely_safe "constant cleanup #{k.inspect}, #{parent_module}(#{parent_module.class})::#{v.inspect}(#{v.class})" do
24
+ silence_warnings do
25
+ if v == :never_defined
26
+ parent_module.send(:remove_const, k)
27
+ else
28
+ parent_module.const_set(k, v)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ @@constant_overrides = []
34
+
35
+ WebMock.reset!
36
+ end
37
+ end
38
+ end
39
+
40
+ def set_test_const(const_name, value)
41
+ const_name.is_a?(Symbol) and const_name = const_name.to_s
42
+ const_name.is_a?(String) or raise "Pass the constant name, not its value!"
43
+
44
+ final_parent_module = final_const_name = nil
45
+ original_value =
46
+ const_name.split('::').reduce(Object) do |parent_module, nested_const_name|
47
+ parent_module == :never_defined and raise "You need to set each parent constant earlier! #{nested_const_name}"
48
+ final_parent_module = parent_module
49
+ final_const_name = nested_const_name
50
+ parent_module.const_get(nested_const_name) rescue :never_defined
51
+ end
52
+
53
+ @@constant_overrides << [final_parent_module, final_const_name, original_value]
54
+
55
+ silence_warnings { final_parent_module.const_set(final_const_name, value) }
56
+ end
57
+ end
@@ -0,0 +1,150 @@
1
+ require_relative '../test_helper'
2
+
3
+ describe ExceptionalSynchrony::CallbackExceptions do
4
+ describe "ensure_callback" do
5
+ it "should execute succeed with return value" do
6
+ deferrable = EM::DefaultDeferrable.new
7
+ mock(deferrable).succeed(42)
8
+ ExceptionalSynchrony::CallbackExceptions.ensure_callback(deferrable) do
9
+ 42
10
+ end
11
+ end
12
+
13
+ it "should execute succeed by splatting an array return value" do
14
+ deferrable = EM::DefaultDeferrable.new
15
+ mock(deferrable).succeed(41, 42)
16
+ ExceptionalSynchrony::CallbackExceptions.ensure_callback(deferrable) do
17
+ [41, 42]
18
+ end
19
+ end
20
+
21
+ it "should execute succeed with a double array when you want to return an array value" do
22
+ deferrable = EM::DefaultDeferrable.new
23
+ mock(deferrable).succeed([41, 42])
24
+ ExceptionalSynchrony::CallbackExceptions.ensure_callback(deferrable) do
25
+ [[41, 42]]
26
+ end
27
+ end
28
+
29
+ it "should execute succeed with the exception that gets raised" do
30
+ deferrable = EM::DefaultDeferrable.new
31
+ exception = ArgumentError.new('Error message')
32
+ mock(deferrable).succeed(exception)
33
+ ExceptionalSynchrony::CallbackExceptions.ensure_callback(deferrable) do
34
+ raise exception
35
+ end
36
+ end
37
+
38
+ it "should execute succeed with the exception that gets raised" do
39
+ deferrable = EM::DefaultDeferrable.new
40
+ exception = ArgumentError.new('Error message')
41
+ mock(deferrable).succeed(exception)
42
+ ExceptionalSynchrony::CallbackExceptions.ensure_callback(deferrable) do
43
+ raise exception
44
+ end
45
+ end
46
+
47
+ it "should execute succeed even with a Ruby internal exception (not derived from StandardError)" do
48
+ deferrable = EM::DefaultDeferrable.new
49
+ exception = NoMemoryError.new('Error message')
50
+ mock(deferrable).succeed(exception)
51
+ ExceptionalSynchrony::CallbackExceptions.ensure_callback(deferrable) do
52
+ raise exception
53
+ end
54
+ end
55
+ end
56
+
57
+ describe "map_deferred_result" do
58
+ describe "success" do
59
+ it "should map success value" do
60
+ deferrable = EM::DefaultDeferrable.new
61
+ deferrable.succeed(12)
62
+ result = ExceptionalSynchrony::CallbackExceptions.map_deferred_result(deferrable)
63
+ result.must_equal 12
64
+ end
65
+
66
+ it "should map success values to an array" do
67
+ deferrable = EM::DefaultDeferrable.new
68
+ deferrable.succeed(12, 13, 14)
69
+ result = ExceptionalSynchrony::CallbackExceptions.map_deferred_result(deferrable)
70
+ result.must_equal [12, 13, 14]
71
+ end
72
+
73
+ it "should map success exception values to raise" do
74
+ deferrable = EM::DefaultDeferrable.new
75
+ exception = ArgumentError.new("Wrong argument!")
76
+ deferrable.succeed(exception)
77
+ result = assert_raises(ArgumentError) do
78
+ ExceptionalSynchrony::CallbackExceptions.map_deferred_result(deferrable)
79
+ end
80
+ result.must_equal exception
81
+ end
82
+ end
83
+
84
+ describe "failure" do
85
+ it "should map failure value to raise" do
86
+ deferrable = EM::DefaultDeferrable.new
87
+ deferrable.fail(first: "a", last: "b")
88
+ result = assert_raises(ExceptionalSynchrony::CallbackExceptions::Failure) do
89
+ ExceptionalSynchrony::CallbackExceptions.map_deferred_result(deferrable)
90
+ end
91
+ result.message.must_equal "{:first=>\"a\", :last=>\"b\"}"
92
+ end
93
+
94
+ it "should map failure exceptions to raise" do
95
+ deferrable = EM::DefaultDeferrable.new
96
+ exception = ArgumentError.new("Wrong argument!")
97
+ deferrable.fail(exception)
98
+ result = assert_raises(ExceptionalSynchrony::CallbackExceptions::Failure) do
99
+ ExceptionalSynchrony::CallbackExceptions.map_deferred_result(deferrable)
100
+ end
101
+ result.message.must_match /ArgumentError/
102
+ result.message.must_match /Wrong argument!/
103
+ end
104
+
105
+ it "should map timeout failure to raise TimeoutError" do
106
+ deferrable = EM::DefaultDeferrable.new
107
+
108
+ def deferrable.error
109
+ "Timeout"
110
+ end
111
+
112
+ deferrable.fail(deferrable)
113
+ assert_raises(Timeout::Error) do
114
+ ExceptionalSynchrony::CallbackExceptions.map_deferred_result(deferrable)
115
+ end
116
+ end
117
+ end
118
+
119
+ describe "no status" do
120
+ it "should raise ArgumentError if you try to map when there is no result yet" do
121
+ deferrable = EM::DefaultDeferrable.new
122
+ result = assert_raises(ArgumentError) do
123
+ ExceptionalSynchrony::CallbackExceptions.map_deferred_result(deferrable)
124
+ end
125
+ result.message.must_match /no deferred status set yet/i
126
+ end
127
+ end
128
+ end
129
+
130
+ describe "return_exception" do
131
+ it "should return the value if no exception" do
132
+ ExceptionalSynchrony::CallbackExceptions.return_exception do
133
+ 14
134
+ end.must_equal 14
135
+ end
136
+
137
+ it "should yield its args" do
138
+ ExceptionalSynchrony::CallbackExceptions.return_exception(0, 1) do |a, b|
139
+ assert_equal [0, 1], [a, b]
140
+ 14
141
+ end.must_equal 14
142
+ end
143
+
144
+ it "should rescue any exception that was raised and return it" do
145
+ ExceptionalSynchrony::CallbackExceptions.return_exception do
146
+ raise ArgumentError, "An argument error occurred"
147
+ end.inspect.must_equal ArgumentError.new("An argument error occurred").inspect
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,100 @@
1
+ require_relative '../test_helper'
2
+
3
+ describe ExceptionalSynchrony::EventMachineProxy do
4
+ include TestHelper
5
+
6
+ before do
7
+ @em = ExceptionalSynchrony::EventMachineProxy.new(EventMachine, nil)
8
+ @yielded_value = nil
9
+ @block = lambda { |value| @yielded_value = value }
10
+ end
11
+
12
+ it "should proxy add_timer" do
13
+ mock(EventMachine::Synchrony).add_timer(10)
14
+ @em.add_timer(10) { }
15
+ end
16
+
17
+ it "should proxy add_periodic_timer" do
18
+ mock(EventMachine::Synchrony).add_periodic_timer(10)
19
+ @em.add_periodic_timer(10) { }
20
+ end
21
+
22
+ it "should proxy sleep" do
23
+ mock(EventMachine::Synchrony).sleep(0.001)
24
+ @em.sleep(0.001)
25
+ end
26
+
27
+ it "should proxy next_tick" do
28
+ mock(EventMachine::Synchrony).next_tick
29
+ @em.next_tick { }
30
+ end
31
+
32
+ it "should proxy stop" do
33
+ mock(EventMachine).stop
34
+ mock(EventMachine).next_tick
35
+ @em.stop
36
+ end
37
+
38
+ it "should proxy connect" do
39
+ ServerClass = Class.new
40
+ mock(EventMachine).connect(ServerClass, 8080, :handler, :extra_arg).yields(:called)
41
+ @em.connect(ServerClass, 8080, :handler, :extra_arg, &@block)
42
+ @yielded_value.must_equal :called
43
+ end
44
+
45
+ EXCEPTION = ArgumentError.new('in block')
46
+
47
+ describe "blocks should be wrapped in ensure_completely_safe" do
48
+ before do
49
+ set_test_const('ExceptionalSynchrony::EventMachineProxy::WRAP_WITH_ENSURE_COMPLETELY_SAFE', true)
50
+ end
51
+
52
+ it "add_timer" do
53
+ mock(ExceptionHandling).log_error(EXCEPTION, "add_timer")
54
+ mock(EventMachine::Synchrony).add_timer(10) { |duration, *args| args.first.call }
55
+ @em.add_timer(10) { raise EXCEPTION }
56
+ end
57
+
58
+ it "add_periodic_timer" do
59
+ mock(ExceptionHandling).log_error(EXCEPTION, "add_periodic_timer")
60
+ mock(EventMachine::Synchrony).add_periodic_timer(10) { |duration, *args| args.first.call }
61
+ @em.add_periodic_timer(10) { raise EXCEPTION }
62
+ end
63
+
64
+ it "next_tick" do
65
+ mock(ExceptionHandling).log_error(EXCEPTION, "next_tick")
66
+ mock(EventMachine::Synchrony).next_tick { |*args| args.first.call }
67
+ @em.next_tick { raise EXCEPTION }
68
+ end
69
+ end
70
+
71
+ [false, true].each do |synchrony|
72
+ describe "synchrony = #{synchrony}" do
73
+ it "should dispatch to the proxy's synchrony method instead of run iff synchrony" do
74
+ proxy_mock = Struct.new(:proxy, :class_connection) do
75
+ if synchrony
76
+ def self.synchrony(&block)
77
+ block.(:synchrony)
78
+ end
79
+ end
80
+
81
+ def self.run(&block)
82
+ block.(:run)
83
+ end
84
+ end
85
+
86
+ mock(proxy_mock).error_handler
87
+
88
+ proxy = ExceptionalSynchrony::EventMachineProxy.new(proxy_mock, nil)
89
+
90
+ proxy.run(&@block)
91
+ @yielded_value.must_equal synchrony ? :synchrony : :run
92
+ end
93
+ end
94
+ end
95
+
96
+ it "should proxy reactor_running?" do
97
+ mock(EventMachine).reactor_running?
98
+ @em.reactor_running?
99
+ end
100
+ end
@@ -0,0 +1,139 @@
1
+ describe ExceptionalSynchrony::LimitedWorkQueue do
2
+ before do
3
+ @em = ExceptionalSynchrony::EventMachineProxy.new(EventMachine, EventMachine::HttpRequest)
4
+ end
5
+
6
+ it "should raise an exception if created with a limit < 1" do
7
+ assert_raises(ArgumentError) do
8
+ ExceptionalSynchrony::LimitedWorkQueue.new(@em, 0)
9
+ end.message.must_match /must be positive/
10
+
11
+ assert_raises(ArgumentError) do
12
+ ExceptionalSynchrony::LimitedWorkQueue.new(@em, -2)
13
+ end.message.must_match /must be positive/
14
+ end
15
+
16
+ describe "when created" do
17
+ before do
18
+ @queue = ExceptionalSynchrony::LimitedWorkQueue.new(@em, 2)
19
+ end
20
+
21
+ it "should run non-blocking jobs immediately" do
22
+ c = 0
23
+ ExceptionalSynchrony::EMP.run_and_stop do
24
+ @queue.add { c+=1 }
25
+ @queue.add { c+=1 }
26
+ @queue.add { c+=1 }
27
+ end
28
+ assert_equal 3, c
29
+ end
30
+
31
+ class LWQTestProc
32
+ def initialize(cancel_proc = nil, &block)
33
+ @cancel_proc = cancel_proc
34
+ @block = block
35
+ end
36
+
37
+ def call
38
+ @block.call
39
+ end
40
+
41
+ def cancel
42
+ @cancel_proc.call
43
+ end
44
+ end
45
+
46
+ it "should allow objects to be queued instead of Procs" do
47
+ c = 0
48
+ ExceptionalSynchrony::EMP.run_and_stop do
49
+ @queue.add(LWQTestProc.new { c+=1 })
50
+ @queue.add(LWQTestProc.new { c+=1 })
51
+ @queue.add(LWQTestProc.new { c+=1 })
52
+ end
53
+ assert_equal 3, c
54
+ end
55
+
56
+ class LWQTestProcWithMergeDrop < LWQTestProc
57
+ def merge(queue)
58
+ if queue.find { |entry| entry.is_a?(self.class) }
59
+ self.cancel
60
+ queue # leave it as is (self is dropped)
61
+ end
62
+ end
63
+ end
64
+
65
+ it "shouldn't bother with merge when the queue is empty" do
66
+ job_proc = LWQTestProc.new { }
67
+ class << job_proc
68
+ def merge(queue)
69
+ raise "merge should not be called!"
70
+ end
71
+ end
72
+
73
+ ExceptionalSynchrony::EMP.run_and_stop do
74
+ @queue.add(job_proc)
75
+ end
76
+ end
77
+
78
+ it "should allow objects to merge themselves into the queue (canceling itself)" do
79
+ c = 0
80
+ ExceptionalSynchrony::EMP.run_and_stop do
81
+ @queue.add(LWQTestProc.new { @em.sleep(0.001); c+=1 })
82
+ @queue.add(LWQTestProc.new { @em.sleep(0.001); c+=2 })
83
+ @queue.add(LWQTestProcWithMergeDrop.new(-> { c+=4 }) { @em.sleep(0.001); c+=8 })
84
+ @queue.add(LWQTestProcWithMergeDrop.new(-> { c+=16 }) { @em.sleep(0.001); c+=32 }) # will get merged (by canceling self)
85
+ @em.sleep(0.050)
86
+ end
87
+ assert_equal 1+2+8+16, c
88
+ end
89
+
90
+ class LWQTestProcWithMergeReplace < LWQTestProc
91
+ def merge(queue)
92
+ if same = queue.find { |entry| entry.is_a?(self.class) }
93
+ same.cancel
94
+ queue - [same] + [self]
95
+ end
96
+ end
97
+ end
98
+
99
+ it "should allow objects to merge themselves into the queue (canceling/replacing earlier)" do
100
+ c = 0
101
+ ExceptionalSynchrony::EMP.run_and_stop do
102
+ @queue.add(LWQTestProc.new { @em.sleep(0.001); c+=1 })
103
+ @queue.add(LWQTestProc.new { @em.sleep(0.001); c+=2 })
104
+ @queue.add(LWQTestProcWithMergeReplace.new(-> { c+=4 }) { @em.sleep(0.001); c+=8 })
105
+ @queue.add(LWQTestProcWithMergeReplace.new(-> { c+=16 }) { @em.sleep(0.001); c+=32 }) # will get merged with above (replacing above)
106
+ @em.sleep(0.050)
107
+ end
108
+ assert_equal 1+2+4+32, c
109
+ end
110
+
111
+ it "should run 2 blocking tasks in parallel and only start 3rd when one of the first 2 finishes" do
112
+ stub_request(:get, "http://www.google.com/").
113
+ to_return(:status => 200, :body => "1", :headers => {})
114
+
115
+ stub_request(:get, "http://www.cnn.com/").
116
+ to_return(:status => 402, :body => "2", :headers => {})
117
+
118
+ stub_request(:get, "http://news.ycombinator.com/").
119
+ to_return(:status => 200, :body => "3", :headers => {})
120
+
121
+ ExceptionalSynchrony::EMP.run_and_stop do
122
+ c = -1
123
+ started2 = nil; ended0 = nil; ended1 = nil
124
+ @queue.add { c+=1; @em.sleep(0.001); ExceptionalSynchrony::EMP.connection.new("http://www.google.com").get; ended0 = c+=1 }
125
+ @queue.add { c+=1; @em.sleep(0.001); ExceptionalSynchrony::EMP.connection.new("http://www.cnn.com").get; ended1 = c+=1 }
126
+ @queue.add { started2 = c+=1; ExceptionalSynchrony::EMP.connection.new("http://news.ycombinator.com").get; c+=1 }
127
+
128
+ 3.times do
129
+ @em.sleep(0.005)
130
+ break if c == 5
131
+ end
132
+
133
+ assert_equal 5, c
134
+
135
+ assert started2 > ended0 || started2 > ended1, [ended0, ended1, started2].inspect
136
+ end
137
+ end
138
+ end
139
+ end