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
data/lib/contender.rb
CHANGED
@@ -1,10 +1,54 @@
|
|
1
|
+
require 'atomic'
|
1
2
|
require 'forwardable'
|
3
|
+
require 'monitor'
|
4
|
+
require 'set'
|
2
5
|
require 'thread'
|
3
6
|
|
4
7
|
require 'contender/version'
|
5
|
-
|
6
8
|
require 'contender/errors'
|
9
|
+
|
10
|
+
require 'contender/copy_on_write_structure'
|
11
|
+
require 'contender/copy_on_write_array'
|
12
|
+
require 'contender/copy_on_write_set'
|
7
13
|
require 'contender/countdown_latch'
|
14
|
+
require 'contender/counter'
|
8
15
|
require 'contender/executor'
|
16
|
+
require 'contender/executor_service'
|
9
17
|
require 'contender/direct_executor'
|
18
|
+
require 'contender/future'
|
19
|
+
require 'contender/future_task'
|
10
20
|
require 'contender/pool'
|
21
|
+
require 'contender/thread_factory'
|
22
|
+
|
23
|
+
require 'contender/queue'
|
24
|
+
require 'contender/linked_queue'
|
25
|
+
|
26
|
+
module Contender
|
27
|
+
# http://stackoverflow.com/questions/535721/ruby-max-integer
|
28
|
+
FIXNUM_MAX = (2**(0.size * 8 - 2) - 1)
|
29
|
+
FIXNUM_MIN = -(2**(0.size * 8 - 2))
|
30
|
+
|
31
|
+
# @param [Integer] size
|
32
|
+
# @param [Boolean] allow_timeout
|
33
|
+
# @return [PoolExecutor]
|
34
|
+
def self.fixed_pool(size, allow_timeout = false)
|
35
|
+
executor = Pool::PoolExecutor.new size, size, 0, LinkedQueue.new, simple_thread_factory
|
36
|
+
|
37
|
+
if allow_timeout
|
38
|
+
executor.work_timeout = 60
|
39
|
+
executor.allow_core_timeout = true
|
40
|
+
end
|
41
|
+
|
42
|
+
executor
|
43
|
+
end
|
44
|
+
|
45
|
+
# @return [PoolExecutor]
|
46
|
+
def self.single_pool
|
47
|
+
fixed_pool 1
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.simple_thread_factory
|
51
|
+
SimpleThreadFactory.new
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Contender
|
2
|
+
# Array that uses copy-on-write to guarantee thread safety
|
3
|
+
#
|
4
|
+
# Use this when read throughput is the primary requirement; write operations will always be
|
5
|
+
# slow due to the need to create a shallow copy of the array to perform operations on.
|
6
|
+
class CopyOnWriteArray
|
7
|
+
include CopyOnWriteStructure
|
8
|
+
include Comparable
|
9
|
+
include Enumerable
|
10
|
+
|
11
|
+
delegate_to Array
|
12
|
+
|
13
|
+
mutators :<<, :[]=, :clear, :collect!, :compact!, :concat, :delete, :delete_at, :delete_if,
|
14
|
+
:fill, :flatten!, :insert, :keep_if, :map!, :pop, :push, :reject!, :replace, :reverse!,
|
15
|
+
:rotate!, :select!, :shift, :shuffle!, :slice!, :sort!, :sort_by!, :uniq!, :unshift
|
16
|
+
|
17
|
+
accessors!
|
18
|
+
end # CopyOnWriteArray
|
19
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Contender
|
2
|
+
# Set that uses copy-on-write to guarantee thread safety
|
3
|
+
#
|
4
|
+
# Use this when read throughput is the primary requirement; write operations will always be
|
5
|
+
# slow due to the need to create a shallow copy of the set to perform operations on.
|
6
|
+
class CopyOnWriteSet
|
7
|
+
include CopyOnWriteStructure
|
8
|
+
include Enumerable
|
9
|
+
|
10
|
+
delegate_to Set
|
11
|
+
|
12
|
+
mutators :<<, :add, :add?, :clear, :collect!, :delete, :delete?, :delete_if, :flatten!,
|
13
|
+
:keep_if, :map!, :merge, :reject!, :replace, :select!, :subtract
|
14
|
+
|
15
|
+
accessors!
|
16
|
+
end # CopyOnWriteSet
|
17
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module Contender
|
2
|
+
# @abstract
|
3
|
+
module CopyOnWriteStructure
|
4
|
+
extend Forwardable
|
5
|
+
|
6
|
+
def self.included(receiver)
|
7
|
+
receiver.send :extend, ClassMethods
|
8
|
+
receiver.send :extend, Forwardable
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def accessors(*args)
|
13
|
+
@accessors ||= Set.new
|
14
|
+
|
15
|
+
args.each do |method|
|
16
|
+
@accessors.add method
|
17
|
+
|
18
|
+
def_delegator :current_reference, method
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def accessors!
|
23
|
+
methods = Set.new(delegated_type.instance_methods(false))
|
24
|
+
|
25
|
+
methods = methods - @accessors if @accessors
|
26
|
+
methods = methods - @mutators if @mutators
|
27
|
+
|
28
|
+
methods.each do |method|
|
29
|
+
def_delegator :current_reference, method
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def mutators(*args)
|
34
|
+
@mutators ||= Set.new
|
35
|
+
|
36
|
+
args.each do |method|
|
37
|
+
@mutators.add method
|
38
|
+
|
39
|
+
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
40
|
+
def #{method}(*args, &block)
|
41
|
+
update_reference do |current|
|
42
|
+
current.#{method}(*args, &block)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
RUBY_EVAL
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
alias_method :accessor, :accessors
|
50
|
+
alias_method :mutator, :mutators
|
51
|
+
|
52
|
+
def delegate_to(type)
|
53
|
+
@delegated_type = type
|
54
|
+
end
|
55
|
+
|
56
|
+
attr_reader :delegated_type
|
57
|
+
end
|
58
|
+
|
59
|
+
def initialize(*args, &block)
|
60
|
+
@reference = Atomic.new(self.class.delegated_type.new(*args, &block))
|
61
|
+
end
|
62
|
+
|
63
|
+
protected
|
64
|
+
|
65
|
+
# @return [Object]
|
66
|
+
def current_reference
|
67
|
+
@reference.value
|
68
|
+
end
|
69
|
+
|
70
|
+
# @yield [Object]
|
71
|
+
# @return [Object]
|
72
|
+
def update_reference
|
73
|
+
loop do
|
74
|
+
current = current_reference
|
75
|
+
update = current.clone
|
76
|
+
|
77
|
+
result = yield update
|
78
|
+
|
79
|
+
if result.equal? update
|
80
|
+
# Most write operations return a reference to self
|
81
|
+
# Don't let this reference leak to the caller
|
82
|
+
result = self
|
83
|
+
end
|
84
|
+
|
85
|
+
return result if @reference.compare_and_swap current, update
|
86
|
+
|
87
|
+
# Concurrent modification, retry operation
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end # CopyOnWriteStructure
|
91
|
+
end
|
@@ -2,30 +2,27 @@ module Contender
|
|
2
2
|
# Synchronization aid that allows one or more threads to wait until a set of operations being
|
3
3
|
# performed in other threads complete
|
4
4
|
class CountdownLatch
|
5
|
+
# @return [Integer]
|
6
|
+
attr_reader :count
|
7
|
+
|
5
8
|
# @param [Integer] initial
|
6
9
|
# @return [undefined]
|
7
10
|
def initialize(initial)
|
11
|
+
raise ArgumentError if initial < 0
|
12
|
+
|
8
13
|
@count = initial
|
9
14
|
|
10
15
|
@mutex = Mutex.new
|
11
16
|
@condition = ConditionVariable.new
|
12
17
|
end
|
13
18
|
|
14
|
-
# Performs an ordered read of the count for this latch
|
15
|
-
# @return [Integer]
|
16
|
-
def count
|
17
|
-
@mutex.synchronize do
|
18
|
-
@count
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
19
|
# Performs an ordered decrement operation for this latch
|
23
20
|
#
|
24
21
|
# @api public
|
25
22
|
# @return [undefined]
|
26
23
|
def countdown
|
27
24
|
@mutex.synchronize do
|
28
|
-
@count
|
25
|
+
@count -= 1 if @count > 0
|
29
26
|
@condition.broadcast if @count == 0
|
30
27
|
end
|
31
28
|
end
|
@@ -37,7 +34,7 @@ module Contender
|
|
37
34
|
# @return [Boolean] True if the latch reached zero, false if the timeout was reached
|
38
35
|
def await(timeout = nil)
|
39
36
|
@mutex.synchronize do
|
40
|
-
return if @count == 0
|
37
|
+
return true if @count == 0
|
41
38
|
@condition.wait @mutex, timeout
|
42
39
|
|
43
40
|
@count == 0
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Contender
|
2
|
+
# Simplified interface to an atomic reference that acts as a counter
|
3
|
+
class Counter < Atomic
|
4
|
+
# @return [undefined]
|
5
|
+
def initialize(initial = 0)
|
6
|
+
super initial
|
7
|
+
end
|
8
|
+
|
9
|
+
# Increments the value of this counter by 1
|
10
|
+
# @return [Integer] The new value of this counter
|
11
|
+
def increment
|
12
|
+
update do |count|
|
13
|
+
count = count + 1
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Decrements the value of this counter by 1
|
18
|
+
# @return [Integer] The new value of this counter
|
19
|
+
def decrement
|
20
|
+
update do |count|
|
21
|
+
count = count - 1
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [String]
|
26
|
+
def inspect
|
27
|
+
"#<Contender::Counter = #{value}>"
|
28
|
+
end
|
29
|
+
end # Counter
|
30
|
+
end
|
@@ -1,8 +1,11 @@
|
|
1
1
|
module Contender
|
2
2
|
# Simple implementation of an executor that executes blocks in the calling thread
|
3
3
|
class DirectExecutor < Executor
|
4
|
-
|
5
|
-
|
4
|
+
# @api public
|
5
|
+
# @param [Object] task
|
6
|
+
# @return [undefined]
|
7
|
+
def execute(task = nil, &block)
|
8
|
+
(task || block).call
|
6
9
|
end
|
7
10
|
end
|
8
11
|
end
|
data/lib/contender/errors.rb
CHANGED
@@ -1,7 +1,26 @@
|
|
1
1
|
module Contender
|
2
|
-
# Raised when
|
3
|
-
class
|
2
|
+
# Raised when attempting to retrieve the result of a task that was cancelled
|
3
|
+
class CancellationError < RuntimeError; end
|
4
4
|
|
5
|
-
# Raised when
|
6
|
-
class
|
5
|
+
# Raised when a blocking operation times out
|
6
|
+
class TimeoutError < RuntimeError; end
|
7
|
+
|
8
|
+
# Raised when a thread is interrupted during the execution of a task
|
9
|
+
class InterruptError < ThreadError; end
|
10
|
+
|
11
|
+
# Raised when an operation can not be completed because the object is not in the correct state
|
12
|
+
class InvalidStateError < RuntimeError; end
|
13
|
+
|
14
|
+
# Raised when attempting to retrieve the result of a task that aborted by raising an exception
|
15
|
+
class ExecutionError < RuntimeError
|
16
|
+
# @return [Exception]
|
17
|
+
attr_reader :cause
|
18
|
+
|
19
|
+
# @param [Exception] cause
|
20
|
+
# @return [undefined]
|
21
|
+
def initialize(cause)
|
22
|
+
@cause = cause
|
23
|
+
set_backtrace cause.backtrace
|
24
|
+
end
|
25
|
+
end
|
7
26
|
end
|
data/lib/contender/executor.rb
CHANGED
@@ -12,9 +12,10 @@ module Contender
|
|
12
12
|
# discretion of the implementation.
|
13
13
|
#
|
14
14
|
# @abstract
|
15
|
-
# @param [Object
|
16
|
-
# @param [Proc] block
|
15
|
+
# @param [Object] task
|
17
16
|
# @return [undefined]
|
18
|
-
def execute(
|
19
|
-
|
17
|
+
def execute(task = nil, &block)
|
18
|
+
raise NotImplementedError
|
19
|
+
end
|
20
|
+
end # Executor
|
20
21
|
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module Contender
|
2
|
+
# @abstract
|
3
|
+
class ExecutorService < Executor
|
4
|
+
# @abstract
|
5
|
+
# @return [undefined]
|
6
|
+
def shutdown
|
7
|
+
raise NotImplementedError
|
8
|
+
end
|
9
|
+
|
10
|
+
# @abstract
|
11
|
+
# @return [Array]
|
12
|
+
def shutdown!
|
13
|
+
raise NotImplementedError
|
14
|
+
end
|
15
|
+
|
16
|
+
# @abstract
|
17
|
+
# @param [Float] timeout
|
18
|
+
# @return [Boolean]
|
19
|
+
def await_termination(timeout)
|
20
|
+
raise NotImplementedError
|
21
|
+
end
|
22
|
+
|
23
|
+
# @abstract
|
24
|
+
# @return [Boolean]
|
25
|
+
def shutdown?
|
26
|
+
raise NotImplementedError
|
27
|
+
end
|
28
|
+
|
29
|
+
# @abstract
|
30
|
+
# @return [Boolean]
|
31
|
+
def terminated?
|
32
|
+
raise NotImplementedError
|
33
|
+
end
|
34
|
+
|
35
|
+
# @api public
|
36
|
+
# @raise [ArgumentError] If neither a block nor callable were given
|
37
|
+
# @param [Object] callable
|
38
|
+
# @return [FutureTask]
|
39
|
+
def submit(callable = nil, &block)
|
40
|
+
callable ||= block
|
41
|
+
|
42
|
+
raise ArgumentError unless callable
|
43
|
+
|
44
|
+
future = future_for callable
|
45
|
+
execute future
|
46
|
+
future
|
47
|
+
end
|
48
|
+
|
49
|
+
# @api public
|
50
|
+
# @param [Array<Object>] tasks
|
51
|
+
# @return [Array<FutureTask>]
|
52
|
+
def invoke_all(tasks)
|
53
|
+
finished = false
|
54
|
+
futures = Array.new
|
55
|
+
|
56
|
+
begin
|
57
|
+
tasks.each do |task|
|
58
|
+
future = create_task_for task
|
59
|
+
|
60
|
+
futures.push future
|
61
|
+
execute future
|
62
|
+
end
|
63
|
+
|
64
|
+
futures.each do |future|
|
65
|
+
unless future.done?
|
66
|
+
begin
|
67
|
+
future.result
|
68
|
+
rescue CancellationError
|
69
|
+
rescue ExecutionError
|
70
|
+
# The caller can deal with these errors
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
finished = true
|
76
|
+
futures
|
77
|
+
ensure
|
78
|
+
unless finished
|
79
|
+
futures.each do |future|
|
80
|
+
future.cancel true
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# @api public
|
87
|
+
# @param [Object] task
|
88
|
+
# @return [FutureTask]
|
89
|
+
def future_for(task)
|
90
|
+
FutureTask.new task
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Contender
|
2
|
+
# @abstract
|
3
|
+
class Future
|
4
|
+
# @abstract
|
5
|
+
# @param [Boolean] should_interrupt
|
6
|
+
# @return [Boolean] True if this future was cancelled
|
7
|
+
def cancel(should_interrupt)
|
8
|
+
raise NotImplementedError
|
9
|
+
end
|
10
|
+
|
11
|
+
# Returns true if this future was cancelled before it could complete
|
12
|
+
#
|
13
|
+
# @abstract
|
14
|
+
# @return [Boolean]
|
15
|
+
def cancelled?
|
16
|
+
raise NotImplementedError
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns true if this future was either cancelled or completed
|
20
|
+
#
|
21
|
+
# @abstract
|
22
|
+
# @return [Boolean]
|
23
|
+
def done?
|
24
|
+
raise NotImplementedError
|
25
|
+
end
|
26
|
+
|
27
|
+
# @abstract
|
28
|
+
# @raise [ExecutionError] If the result of the operation was an exception
|
29
|
+
# @raise [TimeoutError] If the timeout was reached before the operation completed
|
30
|
+
# @raise [CancellationError] If the operation was cancelled
|
31
|
+
# @param [Integer] timeout Time to wait for the result
|
32
|
+
# @return [Object] The result of the future
|
33
|
+
def result(timeout = nil)
|
34
|
+
raise NotImplementedError
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|