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.
- checksums.yaml +7 -0
- data/.gitignore +55 -0
- data/.ruby-gemset +1 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +128 -0
- data/README.md +4 -0
- data/Rakefile +2 -0
- data/Thorfile +42 -0
- data/exceptional_synchrony.gemspec +26 -0
- data/lib/exceptional_synchrony.rb +14 -0
- data/lib/exceptional_synchrony/callback_exceptions.rb +43 -0
- data/lib/exceptional_synchrony/event_machine_proxy.rb +96 -0
- data/lib/exceptional_synchrony/limited_work_queue.rb +52 -0
- data/lib/exceptional_synchrony/parallel_sync.rb +86 -0
- data/lib/exceptional_synchrony/version.rb +3 -0
- data/test/test_helper.rb +57 -0
- data/test/unit/callback_exceptions_test.rb +150 -0
- data/test/unit/event_machine_proxy_test.rb +100 -0
- data/test/unit/limited_work_queue_test.rb +139 -0
- data/test/unit/parallel_sync_test.rb +182 -0
- metadata +188 -0
@@ -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
|
data/test/test_helper.rb
ADDED
@@ -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
|