contender 0.1.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.
- checksums.yaml +7 -0
- data/lib/contender.rb +10 -0
- data/lib/contender/countdown_latch.rb +45 -0
- data/lib/contender/direct_executor.rb +8 -0
- data/lib/contender/errors.rb +7 -0
- data/lib/contender/executor.rb +20 -0
- data/lib/contender/pool.rb +4 -0
- data/lib/contender/pool/pool_worker.rb +79 -0
- data/lib/contender/pool/task.rb +52 -0
- data/lib/contender/pool/task_queue.rb +80 -0
- data/lib/contender/pool/thread_pool_executor.rb +162 -0
- data/lib/contender/version.rb +3 -0
- metadata +55 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 4277ce364374a3fc397165e20f7775c84828225c
|
4
|
+
data.tar.gz: f8c915d431b9d64ae31a9a9470e78ee83ba8df7d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6f252c5ef6200b03e51f3a35edddc150d03ddd01f20cf77f7a7b9d102114f499df03a07db0a5c8833fa3af55a0ad27e0a39ce9aa616ca0edc229853e7741f56c
|
7
|
+
data.tar.gz: 9c65d1d8d32808f2afcb0a66ffa41d995b7af142faeda839fd588d2c833582d63f3421c981bb759f46842e74987950d2f1aed48bb9e3bf892c961f43f26a0a25
|
data/lib/contender.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
module Contender
|
2
|
+
# Synchronization aid that allows one or more threads to wait until a set of operations being
|
3
|
+
# performed in other threads complete
|
4
|
+
class CountdownLatch
|
5
|
+
# @param [Integer] initial
|
6
|
+
# @return [undefined]
|
7
|
+
def initialize(initial)
|
8
|
+
@count = initial
|
9
|
+
|
10
|
+
@mutex = Mutex.new
|
11
|
+
@condition = ConditionVariable.new
|
12
|
+
end
|
13
|
+
|
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
|
+
# Performs an ordered decrement operation for this latch
|
23
|
+
#
|
24
|
+
# @api public
|
25
|
+
# @return [undefined]
|
26
|
+
def countdown
|
27
|
+
@mutex.synchronize do
|
28
|
+
@count = @count.pred if @count > 0
|
29
|
+
@condition.broadcast
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Waits until either the latch reaches zero or the timeout is reached
|
34
|
+
#
|
35
|
+
# @api public
|
36
|
+
# @param [Float] timeout
|
37
|
+
# @return [undefined]
|
38
|
+
def await(timeout = nil)
|
39
|
+
@mutex.synchronize do
|
40
|
+
return if @count == 0
|
41
|
+
@condition.wait @mutex, timeout
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end # CountdownLatch
|
45
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Contender
|
2
|
+
# Represents a mechanism for executing submitted blocks
|
3
|
+
#
|
4
|
+
# This interface decouples task submission from the way that tasks will be run, including details
|
5
|
+
# of thread use, scheduling, etc.
|
6
|
+
#
|
7
|
+
# @abstract
|
8
|
+
class Executor
|
9
|
+
# Executes the given block at some time in the future
|
10
|
+
#
|
11
|
+
# The block may execute in a new thread, in a pooled thread, or in the calling thread, at the
|
12
|
+
# discretion of the implementation.
|
13
|
+
#
|
14
|
+
# @abstract
|
15
|
+
# @param [Object...] arguments
|
16
|
+
# @param [Proc] block
|
17
|
+
# @return [undefined]
|
18
|
+
def execute(*arguments, &block); end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Contender
|
2
|
+
module Pool
|
3
|
+
# Encapsulates a single worker thread in a thread pool
|
4
|
+
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
|
13
|
+
|
14
|
+
# Only changed in the worker thread
|
15
|
+
@state = :inactive
|
16
|
+
end
|
17
|
+
|
18
|
+
# @return [undefined]
|
19
|
+
def start
|
20
|
+
return unless inactive?
|
21
|
+
|
22
|
+
@thread = Thread.new do
|
23
|
+
start_worker_loop
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [undefined]
|
28
|
+
def join
|
29
|
+
return unless @thread
|
30
|
+
@thread.join
|
31
|
+
end
|
32
|
+
|
33
|
+
# @return [undefined]
|
34
|
+
def interrupt
|
35
|
+
return unless @thread
|
36
|
+
@thread.raise OperationInterrupted
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns true if the pool worker is inactive
|
40
|
+
# @return [Boolean]
|
41
|
+
def inactive?
|
42
|
+
:inactive == @state
|
43
|
+
end
|
44
|
+
|
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
|
55
|
+
end
|
56
|
+
|
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
|
77
|
+
end # PoolWorker
|
78
|
+
end # Pool
|
79
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Contender
|
2
|
+
module Pool
|
3
|
+
# Encapsulates a task that is either waiting for execution or has been executed
|
4
|
+
class Task
|
5
|
+
# @return [Array] The arguments that the block will be executed with
|
6
|
+
attr_reader :arguments
|
7
|
+
|
8
|
+
# @return [Proc] The block that will be executed by a pool worker
|
9
|
+
attr_reader :block
|
10
|
+
|
11
|
+
# @return [Exception] Exception that occured during task execution
|
12
|
+
attr_reader :exception
|
13
|
+
|
14
|
+
# @param [Proc] block
|
15
|
+
# @param [Array] arguments
|
16
|
+
# @return [undefined]
|
17
|
+
def initialize(block, arguments)
|
18
|
+
@block = block
|
19
|
+
@arguments = arguments
|
20
|
+
@executed = false
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns true if this task has been executed
|
24
|
+
#
|
25
|
+
# @api public
|
26
|
+
# @return [Boolean]
|
27
|
+
def executed?
|
28
|
+
@executed
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns true if the execution of this task resulted in an exception
|
32
|
+
#
|
33
|
+
# @api public
|
34
|
+
# @return [Boolean]
|
35
|
+
def failed?
|
36
|
+
!!@exception
|
37
|
+
end
|
38
|
+
|
39
|
+
# Executes the block with the provided arguments
|
40
|
+
# @return [undefined]
|
41
|
+
def execute
|
42
|
+
begin
|
43
|
+
@block.call *@arguments
|
44
|
+
rescue Exception => exception
|
45
|
+
@exception = exception
|
46
|
+
end
|
47
|
+
|
48
|
+
@executed = true
|
49
|
+
end
|
50
|
+
end # Task
|
51
|
+
end # Pool
|
52
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Contender
|
2
|
+
module Pool
|
3
|
+
# Simple thread-safe queue that holds tasks waiting for execution
|
4
|
+
class TaskQueue
|
5
|
+
# @return [undefined]
|
6
|
+
def initialize
|
7
|
+
@condition = ConditionVariable.new
|
8
|
+
@mutex = Mutex.new
|
9
|
+
@tasks = Array.new
|
10
|
+
end
|
11
|
+
|
12
|
+
# Performs a volatile read of the size of the task queue
|
13
|
+
#
|
14
|
+
# @api public
|
15
|
+
# @return [Integer]
|
16
|
+
def backlog
|
17
|
+
@tasks.size
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns true if the task queue is empty
|
21
|
+
#
|
22
|
+
# @api public
|
23
|
+
# @return [Boolean]
|
24
|
+
def empty?
|
25
|
+
@tasks.empty?
|
26
|
+
end
|
27
|
+
|
28
|
+
# Removes any tasks that are queued for execution
|
29
|
+
#
|
30
|
+
# @api public
|
31
|
+
# @return [undefined]
|
32
|
+
def purge
|
33
|
+
@mutex.synchronize do
|
34
|
+
@tasks.clear
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Enqueues the given task for execution
|
39
|
+
#
|
40
|
+
# @api public
|
41
|
+
# @param [Task] task
|
42
|
+
# @return [undefined]
|
43
|
+
def enqueue(task)
|
44
|
+
@mutex.synchronize do
|
45
|
+
@tasks.push task
|
46
|
+
@condition.signal
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Dequeues the first task in line
|
51
|
+
#
|
52
|
+
# If this operation is performed as non-blocking and the queue is empty, then the result
|
53
|
+
# of the dequeue will be +nil+.
|
54
|
+
#
|
55
|
+
# If this operation is performed as blocking, then it will wait until a task is added to the
|
56
|
+
# queue. In the case that the thread pool is shutdown during while a worker is waiting for
|
57
|
+
# the operation to complete, +nil+ will be returned immediately.
|
58
|
+
#
|
59
|
+
# @api public
|
60
|
+
# @param [Boolean] non_block
|
61
|
+
# @return [Task]
|
62
|
+
def dequeue(non_block = false)
|
63
|
+
@mutex.synchronize do
|
64
|
+
if @tasks.empty?
|
65
|
+
return if non_block
|
66
|
+
@condition.wait @mutex
|
67
|
+
end
|
68
|
+
|
69
|
+
@tasks.shift
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Wakes up any threads waiting for tasks to be queued
|
74
|
+
# @return [undefined]
|
75
|
+
def wakeup
|
76
|
+
@condition.broadcast
|
77
|
+
end
|
78
|
+
end # TaskQueue
|
79
|
+
end # Pool
|
80
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
module Contender
|
2
|
+
module Pool
|
3
|
+
# Executor that uses a cached thread pool to execute tasks in the background
|
4
|
+
class ThreadPoolExecutor < Executor
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
# @param [Hash] options
|
8
|
+
# @return [undefined]
|
9
|
+
def initialize(options = Hash.new)
|
10
|
+
@queue = TaskQueue.new
|
11
|
+
|
12
|
+
@options = {
|
13
|
+
size: 2,
|
14
|
+
non_block: false
|
15
|
+
}.merge options
|
16
|
+
|
17
|
+
@workers = Array.new
|
18
|
+
@mutex = Mutex.new
|
19
|
+
|
20
|
+
@state = :inactive
|
21
|
+
end
|
22
|
+
|
23
|
+
# Defers the execution of the given block until it can be processed by a worker in the
|
24
|
+
# thread pool
|
25
|
+
#
|
26
|
+
# @api public
|
27
|
+
# @param [Object...] arguments
|
28
|
+
# @param [Proc] block
|
29
|
+
# @return [undefined]
|
30
|
+
def execute(*arguments, &block)
|
31
|
+
unless active?
|
32
|
+
raise 'Thread pool has not been started or is shutting down'
|
33
|
+
end
|
34
|
+
|
35
|
+
@queue.enqueue(Task.new(block, arguments))
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns true if this thread pool is inactive
|
39
|
+
#
|
40
|
+
# @api public
|
41
|
+
# @return [Boolean]
|
42
|
+
def inactive?
|
43
|
+
:inactive == @state
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns true if this thread pool is active and accepting tasks
|
47
|
+
#
|
48
|
+
# @api public
|
49
|
+
# @return [Boolean]
|
50
|
+
def active?
|
51
|
+
:active == @state
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns true if this thread pool is waiting to shutdown
|
55
|
+
#
|
56
|
+
# @api public
|
57
|
+
# @return [Boolean]
|
58
|
+
def shutdown?
|
59
|
+
:shutdown == @state
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns true if this thread pool is shutting down now
|
63
|
+
#
|
64
|
+
# @api public
|
65
|
+
# @return [Boolean]
|
66
|
+
def shutdown_now?
|
67
|
+
:shutdown_now == @state
|
68
|
+
end
|
69
|
+
|
70
|
+
# Starts the thread pool
|
71
|
+
#
|
72
|
+
# @api public
|
73
|
+
# @return [undefined]
|
74
|
+
def start
|
75
|
+
return unless inactive?
|
76
|
+
|
77
|
+
start_workers @options.fetch(:size)
|
78
|
+
@state = :active
|
79
|
+
end
|
80
|
+
|
81
|
+
# Blocks until all queued tasks have been executed, then shuts down the thread pool
|
82
|
+
#
|
83
|
+
# @api public
|
84
|
+
# @return [undefined]
|
85
|
+
def shutdown
|
86
|
+
return unless active?
|
87
|
+
|
88
|
+
@state = :shutdown
|
89
|
+
|
90
|
+
# TODO This is not ideal
|
91
|
+
until @queue.empty?
|
92
|
+
# Wait until the task queue is empty
|
93
|
+
sleep 0.1
|
94
|
+
end
|
95
|
+
|
96
|
+
@state = :shutdown_now
|
97
|
+
|
98
|
+
@queue.wakeup
|
99
|
+
@workers.each do |worker|
|
100
|
+
worker.join
|
101
|
+
end
|
102
|
+
|
103
|
+
cleanup
|
104
|
+
end
|
105
|
+
|
106
|
+
# Shuts down the thread pool instantly, interrupting any executing tasks
|
107
|
+
#
|
108
|
+
# @api public
|
109
|
+
# @return [undefined]
|
110
|
+
def shutdown!
|
111
|
+
return unless active?
|
112
|
+
|
113
|
+
@state = :shutdown_now
|
114
|
+
|
115
|
+
@workers.each do |worker|
|
116
|
+
worker.interrupt
|
117
|
+
end
|
118
|
+
|
119
|
+
cleanup
|
120
|
+
end
|
121
|
+
|
122
|
+
# @!method backlog
|
123
|
+
# Performs a volatile read on the number of tasks in the queue
|
124
|
+
# @return [Integer]
|
125
|
+
# @!method empty?
|
126
|
+
# Returns true if the task queue is empty
|
127
|
+
# @return [Boolean]
|
128
|
+
# @!method purge
|
129
|
+
# Removes any tasks that are queued for execution
|
130
|
+
# @return [undefined]
|
131
|
+
def_delegators :@queue, :backlog, :empty?, :purge
|
132
|
+
|
133
|
+
protected
|
134
|
+
|
135
|
+
# Cleans up pool workers and changes the executor state to inactive
|
136
|
+
# @return [undefined]
|
137
|
+
def cleanup
|
138
|
+
@workers.clear
|
139
|
+
@state = :inactive
|
140
|
+
end
|
141
|
+
|
142
|
+
# Starts the given number of worker threads
|
143
|
+
# @return [undefined]
|
144
|
+
def start_workers(count)
|
145
|
+
@mutex.synchronize do
|
146
|
+
count.times do
|
147
|
+
start_worker
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Starts a new worker and adds it to pool
|
153
|
+
# @return [undefined]
|
154
|
+
def start_worker
|
155
|
+
worker = PoolWorker.new self, @queue, @options.fetch(:non_block)
|
156
|
+
@workers.push worker
|
157
|
+
|
158
|
+
worker.start
|
159
|
+
end
|
160
|
+
end # ThreadPoolExecutor
|
161
|
+
end # Pool
|
162
|
+
end
|
metadata
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: contender
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ian Unruh
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-06-13 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Simple framework for managing in-process execution
|
14
|
+
email: ianunruh@gmail.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- lib/contender/countdown_latch.rb
|
20
|
+
- lib/contender/direct_executor.rb
|
21
|
+
- lib/contender/errors.rb
|
22
|
+
- lib/contender/executor.rb
|
23
|
+
- lib/contender/pool/pool_worker.rb
|
24
|
+
- lib/contender/pool/task.rb
|
25
|
+
- lib/contender/pool/task_queue.rb
|
26
|
+
- lib/contender/pool/thread_pool_executor.rb
|
27
|
+
- lib/contender/pool.rb
|
28
|
+
- lib/contender/version.rb
|
29
|
+
- lib/contender.rb
|
30
|
+
homepage: https://github.com/ianunruh/contender
|
31
|
+
licenses:
|
32
|
+
- Apache 2.0
|
33
|
+
metadata: {}
|
34
|
+
post_install_message:
|
35
|
+
rdoc_options: []
|
36
|
+
require_paths:
|
37
|
+
- lib
|
38
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - '>='
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '0'
|
43
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
requirements: []
|
49
|
+
rubyforge_project:
|
50
|
+
rubygems_version: 2.0.3
|
51
|
+
signing_key:
|
52
|
+
specification_version: 4
|
53
|
+
summary: Simple framework for managing in-process execution
|
54
|
+
test_files: []
|
55
|
+
has_rdoc:
|