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.
@@ -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
- # @param [ThreadPoolExecutor] pool
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
- # Only changed in the worker thread
15
- @state = :inactive
16
- end
6
+ attr_reader :completed_task_count
7
+ attr_reader :thread
17
8
 
18
- # @return [undefined]
19
- def start
20
- return unless inactive?
9
+ def initialize(thread_factory, first_task)
10
+ @mutex = Mutex.new
21
11
 
22
- @thread = Thread.new do
23
- start_worker_loop
24
- end
12
+ @thread_factory = thread_factory
13
+ @first_task = first_task
14
+ @completed_task_count = 0
25
15
  end
26
16
 
27
- # @return [undefined]
28
- def join
29
- return unless @thread
30
- @thread.join
17
+ def first_task!
18
+ @first_task.tap {
19
+ @first_task = nil
20
+ }
31
21
  end
32
22
 
33
- # @return [undefined]
34
- def interrupt
35
- return unless @thread
36
- @thread.raise OperationInterrupted
23
+ def on_task_completion
24
+ @completed_task_count += 1
37
25
  end
38
26
 
39
- # Returns true if the pool worker is inactive
40
- # @return [Boolean]
41
- def inactive?
42
- :inactive == @state
27
+ def start(&worker_loop)
28
+ @thread = @thread_factory.create &worker_loop
43
29
  end
44
30
 
45
- # Returns true if the pool worker is waiting for tasks
46
- # @return [Boolean]
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
- protected
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
@@ -1,3 +1,3 @@
1
1
  module Contender
2
- VERSION = '0.1.1'
2
+ VERSION = '0.2.0'
3
3
  end
@@ -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