exceptional_synchrony 1.0.1

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.
@@ -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