exceptional_synchrony 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|