contender 0.1.1 → 0.2.0
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/contender.rb +45 -1
- data/lib/contender/copy_on_write_array.rb +19 -0
- data/lib/contender/copy_on_write_set.rb +17 -0
- data/lib/contender/copy_on_write_structure.rb +91 -0
- data/lib/contender/countdown_latch.rb +7 -10
- data/lib/contender/counter.rb +30 -0
- data/lib/contender/direct_executor.rb +5 -2
- data/lib/contender/errors.rb +23 -4
- data/lib/contender/executor.rb +5 -4
- data/lib/contender/executor_service.rb +94 -0
- data/lib/contender/future.rb +37 -0
- data/lib/contender/future_task.rb +99 -0
- data/lib/contender/linked_queue.rb +338 -0
- data/lib/contender/pool.rb +3 -3
- data/lib/contender/pool/pool_executor.rb +697 -0
- data/lib/contender/pool/pool_worker.rb +19 -60
- data/lib/contender/pool/rejection_policy.rb +61 -0
- data/lib/contender/queue.rb +87 -0
- data/lib/contender/thread_factory.rb +37 -0
- data/lib/contender/version.rb +1 -1
- data/spec/copy_on_write_array_spec.rb +20 -0
- data/spec/copy_on_write_set_spec.rb +25 -0
- data/spec/countdown_latch_spec.rb +136 -0
- data/spec/counter_spec.rb +26 -0
- data/spec/direct_executor_spec.rb +21 -0
- data/spec/future_task_spec.rb +170 -0
- data/spec/linked_queue_spec.rb +25 -0
- data/spec/pool/executor_spec.rb +436 -0
- data/spec/pool/executor_stress_spec.rb +31 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/wait_helper.rb +14 -0
- metadata +149 -22
- data/lib/contender/pool/task.rb +0 -52
- data/lib/contender/pool/task_queue.rb +0 -80
- data/lib/contender/pool/thread_pool_executor.rb +0 -167
@@ -1,79 +1,38 @@
|
|
1
1
|
module Contender
|
2
2
|
module Pool
|
3
|
-
# Encapsulates a single worker thread in a thread pool
|
4
3
|
class PoolWorker
|
5
|
-
|
6
|
-
# @param [TaskQueue] queue
|
7
|
-
# @param [Boolean] non_block
|
8
|
-
# @return [undefined]
|
9
|
-
def initialize(pool, queue, non_block = false)
|
10
|
-
@pool = pool
|
11
|
-
@queue = queue
|
12
|
-
@non_block = non_block
|
4
|
+
extend Forwardable
|
13
5
|
|
14
|
-
|
15
|
-
|
16
|
-
end
|
6
|
+
attr_reader :completed_task_count
|
7
|
+
attr_reader :thread
|
17
8
|
|
18
|
-
|
19
|
-
|
20
|
-
return unless inactive?
|
9
|
+
def initialize(thread_factory, first_task)
|
10
|
+
@mutex = Mutex.new
|
21
11
|
|
22
|
-
@
|
23
|
-
|
24
|
-
|
12
|
+
@thread_factory = thread_factory
|
13
|
+
@first_task = first_task
|
14
|
+
@completed_task_count = 0
|
25
15
|
end
|
26
16
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
17
|
+
def first_task!
|
18
|
+
@first_task.tap {
|
19
|
+
@first_task = nil
|
20
|
+
}
|
31
21
|
end
|
32
22
|
|
33
|
-
|
34
|
-
|
35
|
-
return unless @thread
|
36
|
-
@thread.raise OperationInterrupted
|
23
|
+
def on_task_completion
|
24
|
+
@completed_task_count += 1
|
37
25
|
end
|
38
26
|
|
39
|
-
|
40
|
-
|
41
|
-
def inactive?
|
42
|
-
:inactive == @state
|
27
|
+
def start(&worker_loop)
|
28
|
+
@thread = @thread_factory.create &worker_loop
|
43
29
|
end
|
44
30
|
|
45
|
-
|
46
|
-
|
47
|
-
def waiting?
|
48
|
-
:waiting == @state
|
49
|
-
end
|
50
|
-
|
51
|
-
# Returns true if the pool worker is executing a task
|
52
|
-
# @return [Boolean]
|
53
|
-
def executing?
|
54
|
-
:executing == @state
|
31
|
+
def interrupt
|
32
|
+
@thread.raise Interrupt
|
55
33
|
end
|
56
34
|
|
57
|
-
|
58
|
-
|
59
|
-
# Loop that is executed in another thread
|
60
|
-
# @return [undefined]
|
61
|
-
def start_worker_loop
|
62
|
-
@state = :waiting
|
63
|
-
|
64
|
-
loop do
|
65
|
-
break if @pool.shutdown_now?
|
66
|
-
|
67
|
-
task = @queue.dequeue @non_block
|
68
|
-
next unless task
|
69
|
-
|
70
|
-
@state = :executing
|
71
|
-
task.execute
|
72
|
-
@state = :waiting
|
73
|
-
end
|
74
|
-
ensure
|
75
|
-
@state = :inactive
|
76
|
-
end
|
35
|
+
def_delegators :@mutex, :lock, :unlock, :try_lock, :locked?
|
77
36
|
end # PoolWorker
|
78
37
|
end # Pool
|
79
38
|
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Contender
|
2
|
+
module Pool
|
3
|
+
# Represents the strategy to use when the pool executor rejects a task
|
4
|
+
class RejectionPolicy
|
5
|
+
# @abstract
|
6
|
+
# @param [Object] task
|
7
|
+
# @param [PoolExecutor] executor
|
8
|
+
# @return [undefined]
|
9
|
+
def on_rejection(task, executor)
|
10
|
+
raise NotImplementedError
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Rejection policy that executes the task in the calling thread
|
15
|
+
class ExecutionByCallerPolicy < RejectionPolicy
|
16
|
+
# @param [Object] task
|
17
|
+
# @param [PoolExecutor] executor
|
18
|
+
# @return [undefined]
|
19
|
+
def on_rejection(task, executor)
|
20
|
+
return if executor.shutdown?
|
21
|
+
task.call
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Rejection policy that simply raises an exception
|
26
|
+
class AbortPolicy < RejectionPolicy
|
27
|
+
# @param [Object] task
|
28
|
+
# @param [PoolExecutor] executor
|
29
|
+
# @return [undefined]
|
30
|
+
def on_rejection(task, executor)
|
31
|
+
raise TaskRejectionError, "Task #{task} rejected by thread pool executor"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
TaskRejectionError = Class.new RuntimeError
|
36
|
+
|
37
|
+
# Rejection policy that silently discards a task
|
38
|
+
class DiscardPolicy < RejectionPolicy
|
39
|
+
# @param [Object] task
|
40
|
+
# @param [PoolExecutor] executor
|
41
|
+
# @return [undefined]
|
42
|
+
def on_rejection(task, executor)
|
43
|
+
# Just ignore the task
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Rejection policy that discards the first task in the queue and then enqueues the
|
48
|
+
# task
|
49
|
+
class DiscardOldestPolicy < RejectionPolicy
|
50
|
+
# @param [Object] task
|
51
|
+
# @param [PoolExecutor] executor
|
52
|
+
# @return [undefined]
|
53
|
+
def on_rejection(task, executor)
|
54
|
+
return if executor.shutdown?
|
55
|
+
|
56
|
+
executor.queue.poll
|
57
|
+
executor.execute task
|
58
|
+
end
|
59
|
+
end # DiscardOldestPolicy
|
60
|
+
end # Pool
|
61
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module Contender
|
2
|
+
# @abstract
|
3
|
+
class Queue
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
# @abstract
|
7
|
+
# @param [Object] element
|
8
|
+
# @param [Float] timeout
|
9
|
+
# @return [Boolean]
|
10
|
+
def offer(element, timeout = nil)
|
11
|
+
raise NotImplementedError
|
12
|
+
end
|
13
|
+
|
14
|
+
# @abstract
|
15
|
+
# @param [Object] element
|
16
|
+
# @return [undefined]
|
17
|
+
def put(element)
|
18
|
+
raise NotImplementedError
|
19
|
+
end
|
20
|
+
|
21
|
+
# @abstract
|
22
|
+
# @param [Float] timeout
|
23
|
+
# @return [Object]
|
24
|
+
def poll(timeout = nil)
|
25
|
+
raise NotImplementedError
|
26
|
+
end
|
27
|
+
|
28
|
+
# @abstract
|
29
|
+
# @return [Object]
|
30
|
+
def take
|
31
|
+
raise NotImplementedError
|
32
|
+
end
|
33
|
+
|
34
|
+
# @abstract
|
35
|
+
# @return [Object]
|
36
|
+
def peek
|
37
|
+
raise NotImplementedError
|
38
|
+
end
|
39
|
+
|
40
|
+
# @abstract
|
41
|
+
# @return [undefined]
|
42
|
+
def clear
|
43
|
+
raise NotImplementedError
|
44
|
+
end
|
45
|
+
|
46
|
+
# @abstract
|
47
|
+
# @yield [Object]
|
48
|
+
# @return [undefined]
|
49
|
+
def each
|
50
|
+
raise NotImplementedError
|
51
|
+
end
|
52
|
+
|
53
|
+
# @abstract
|
54
|
+
# @param [Object] element
|
55
|
+
# @return [Boolean]
|
56
|
+
def delete(element)
|
57
|
+
raise NotImplementedError
|
58
|
+
end
|
59
|
+
|
60
|
+
# @abstract
|
61
|
+
# @param [Array] target
|
62
|
+
# @param [Integer] max_elements
|
63
|
+
# @return [Integer]
|
64
|
+
def drain_to(target, max_elements = FIXNUM_MAX)
|
65
|
+
raise NotImplementedError
|
66
|
+
end
|
67
|
+
|
68
|
+
# @abstract
|
69
|
+
# @return [Integer]
|
70
|
+
def capacity_remaining
|
71
|
+
raise NotImplementedError
|
72
|
+
end
|
73
|
+
|
74
|
+
# @abstract
|
75
|
+
# @return [Integer]
|
76
|
+
def size
|
77
|
+
raise NotImplementedError
|
78
|
+
end
|
79
|
+
|
80
|
+
alias_method :length, :size
|
81
|
+
|
82
|
+
# @return [Boolean]
|
83
|
+
def empty?
|
84
|
+
size == 0
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Contender
|
2
|
+
# @abstract
|
3
|
+
class ThreadFactory
|
4
|
+
# @abstract
|
5
|
+
# @yield
|
6
|
+
# @return [Thread]
|
7
|
+
def create(&block)
|
8
|
+
raise NotImplementedError
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class SimpleThreadFactory < ThreadFactory
|
13
|
+
# @return [ThreadGroup]
|
14
|
+
attr_reader :group
|
15
|
+
|
16
|
+
# @return [Integer]
|
17
|
+
attr_accessor :priority
|
18
|
+
|
19
|
+
# @return [undefined]
|
20
|
+
def initialize
|
21
|
+
@group = ThreadGroup.new
|
22
|
+
@priority = 0
|
23
|
+
end
|
24
|
+
|
25
|
+
# @api public
|
26
|
+
# @yield
|
27
|
+
# @return [Thread]
|
28
|
+
def create(&block)
|
29
|
+
thread = Thread.new &block
|
30
|
+
thread.priority = @priority
|
31
|
+
|
32
|
+
@group.add thread
|
33
|
+
|
34
|
+
thread
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/contender/version.rb
CHANGED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Contender
|
4
|
+
describe CopyOnWriteArray do
|
5
|
+
it 'handles concurrent operations without errors' do
|
6
|
+
arr = CopyOnWriteArray.new
|
7
|
+
|
8
|
+
(1..3).map { |i|
|
9
|
+
Thread.new do
|
10
|
+
100.times do |j|
|
11
|
+
arr << i
|
12
|
+
arr.each { |x| x * 2 }
|
13
|
+
arr.shift
|
14
|
+
arr.last
|
15
|
+
end
|
16
|
+
end
|
17
|
+
}.map(&:join)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Contender
|
4
|
+
describe CopyOnWriteSet do
|
5
|
+
it 'handles concurrent operations without errors' do
|
6
|
+
set = CopyOnWriteSet.new
|
7
|
+
|
8
|
+
(1..3).map { |i|
|
9
|
+
Thread.new do
|
10
|
+
100.times do |j|
|
11
|
+
n = i * 1000 + j
|
12
|
+
|
13
|
+
set.add n
|
14
|
+
set.each { |e| e }
|
15
|
+
|
16
|
+
set.should include(n)
|
17
|
+
|
18
|
+
set.delete n
|
19
|
+
set.size
|
20
|
+
end
|
21
|
+
end
|
22
|
+
}.map(&:join)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Contender
|
4
|
+
|
5
|
+
describe CountdownLatch do
|
6
|
+
it 'requires a positive initial count' do
|
7
|
+
expect {
|
8
|
+
CountdownLatch.new -1
|
9
|
+
}.to raise_error ArgumentError
|
10
|
+
end
|
11
|
+
|
12
|
+
describe 'counting down from 1' do
|
13
|
+
before do
|
14
|
+
@latch = CountdownLatch.new 1
|
15
|
+
@name = :foo
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'blocks until counted down in another thread' do
|
19
|
+
Thread.new do
|
20
|
+
@name = :bar
|
21
|
+
@latch.countdown
|
22
|
+
end
|
23
|
+
|
24
|
+
@latch.await.should == true
|
25
|
+
@latch.count.should == 0
|
26
|
+
|
27
|
+
@name.should == :bar
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'blocks another thread until counted down' do
|
31
|
+
Thread.new do
|
32
|
+
@latch.await.should == true
|
33
|
+
@latch.count.should == 0
|
34
|
+
@name.should == :bar
|
35
|
+
end
|
36
|
+
|
37
|
+
@name = :bar
|
38
|
+
@latch.countdown
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'returns false if timed out' do
|
42
|
+
@latch.await(0).should == false
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe 'counting down from 0' do
|
47
|
+
before do
|
48
|
+
@latch = CountdownLatch.new 0
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'does not block' do
|
52
|
+
@latch.await.should == true
|
53
|
+
@latch.count.should == 0
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe 'counting down from 2' do
|
58
|
+
before do
|
59
|
+
@latch = CountdownLatch.new 2
|
60
|
+
@name = :foo
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'blocks until counted down in 1 parallel thread' do
|
64
|
+
Thread.new do
|
65
|
+
@latch.countdown
|
66
|
+
@name = :bar
|
67
|
+
@latch.countdown
|
68
|
+
end
|
69
|
+
|
70
|
+
@latch.await
|
71
|
+
@latch.count.should == 0
|
72
|
+
@name.should == :bar
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'blocks until counted down in 2 parallel threads' do
|
76
|
+
Thread.new do
|
77
|
+
@latch.countdown
|
78
|
+
end
|
79
|
+
|
80
|
+
Thread.new do
|
81
|
+
@name = :bar
|
82
|
+
@latch.countdown
|
83
|
+
end
|
84
|
+
|
85
|
+
@latch.await
|
86
|
+
@latch.count.should == 0
|
87
|
+
@name.should == :bar
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'blocks until counted down in 2 chained threads' do
|
91
|
+
Thread.new do
|
92
|
+
@latch.countdown
|
93
|
+
|
94
|
+
Thread.new do
|
95
|
+
@name = :bar
|
96
|
+
@latch.countdown
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
@latch.await
|
101
|
+
@latch.count.should == 0
|
102
|
+
@name.should == :bar
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
describe 'with interleaved latches' do
|
107
|
+
before do
|
108
|
+
@check_latch = CountdownLatch.new 1
|
109
|
+
@change_latch1 = CountdownLatch.new 1
|
110
|
+
@change_latch2 = CountdownLatch.new 1
|
111
|
+
@name = :foo
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'blocks the correct thread' do
|
115
|
+
Thread.new do
|
116
|
+
@name = :bar
|
117
|
+
@change_latch1.countdown
|
118
|
+
|
119
|
+
@check_latch.await
|
120
|
+
|
121
|
+
@name = :baz
|
122
|
+
@change_latch2.countdown
|
123
|
+
end
|
124
|
+
|
125
|
+
@change_latch1.await
|
126
|
+
@name.should == :bar
|
127
|
+
|
128
|
+
@check_latch.countdown
|
129
|
+
|
130
|
+
@change_latch2.await
|
131
|
+
@name.should == :baz
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|