performer 1.0.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/.gitignore +22 -0
- data/.rspec +1 -0
- data/.travis.yml +13 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +33 -0
- data/Rakefile +14 -0
- data/lib/performer.rb +111 -0
- data/lib/performer/condition_variable.rb +52 -0
- data/lib/performer/queue.rb +137 -0
- data/lib/performer/task.rb +143 -0
- data/lib/performer/version.rb +4 -0
- data/performer.gemspec +26 -0
- data/spec/puddle/queue_spec.rb +135 -0
- data/spec/puddle/task_spec.rb +123 -0
- data/spec/puddle_spec.rb +98 -0
- data/spec/spec_helper.rb +24 -0
- metadata +130 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 2b01b56f2d0d88ec08d5176a59a2de1dfe8b025e
|
4
|
+
data.tar.gz: 78df656923eac0cad6786f66711c96bf7418b006
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 86dd766848014e7b269fe8cb8e913a3ce8dfd413c38495303938631dfe5f1aa99036e40540704199eea6eacfef25720cd2b1d3646fccd22de54c3ba84729f99e
|
7
|
+
data.tar.gz: b2e0f32c4e36c6f762585873137415aa059c80bebee8467759f56c1cab83bd7be9ef16d0042e5a65ac0bd7df677349ee0f24d88ff5dff0a5d70aa26f6e316d9b
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--require ./spec/spec_helper
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Kim Burgestrand
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# Performer
|
2
|
+
|
3
|
+
[](https://travis-ci.org/Burgestrand/performer)
|
4
|
+
[](https://codeclimate.com/github/Burgestrand/performer)
|
5
|
+
[](http://badge.fury.io/rb/performer)
|
6
|
+
|
7
|
+
```
|
8
|
+
gem install performer
|
9
|
+
```
|
10
|
+
|
11
|
+
Performer is a tiny gem for scheduling blocks in a background thread,
|
12
|
+
and optionally waiting for the return value.
|
13
|
+
|
14
|
+
## Usage
|
15
|
+
|
16
|
+
``` ruby
|
17
|
+
performer = Performer.new
|
18
|
+
|
19
|
+
result = performer.sync { 2 + 1 }
|
20
|
+
result # => 3
|
21
|
+
|
22
|
+
future = performer.async { 2 + 1 }
|
23
|
+
future.value # => 3
|
24
|
+
|
25
|
+
future = performer.shutdown do
|
26
|
+
puts "Performer has been properly shutdown."
|
27
|
+
end
|
28
|
+
|
29
|
+
future.value # wait for shutdown
|
30
|
+
```
|
31
|
+
|
32
|
+
See documentation for [Performer](http://rdoc.info/github/Burgestrand/performer/master/Performer)
|
33
|
+
and [Performer::Task](http://rdoc.info/github/Burgestrand/performer/master/Performer/Task).
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
|
3
|
+
require "rspec/core/rake_task"
|
4
|
+
RSpec::Core::RakeTask.new
|
5
|
+
|
6
|
+
require "yard"
|
7
|
+
YARD::Rake::YardocTask.new
|
8
|
+
|
9
|
+
desc "Start a console with the code loaded."
|
10
|
+
task :console do
|
11
|
+
exec "irb", "-Ilib", "-rperformer"
|
12
|
+
end
|
13
|
+
|
14
|
+
task default: :spec
|
data/lib/performer.rb
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
require "performer/version"
|
2
|
+
|
3
|
+
# Performer is the main entry point and namespace of the Performer gem.
|
4
|
+
# It provides methods for synchronously and asynchronously scheduling
|
5
|
+
# blocks for execution in the performer thread, and a way to shut down
|
6
|
+
# the performer cleanly.
|
7
|
+
#
|
8
|
+
# @note The Performer is thread-safe.
|
9
|
+
#
|
10
|
+
# @example usage
|
11
|
+
# performer = Performer.new
|
12
|
+
# performer.sync { 1 + 1 } # => 2
|
13
|
+
# performer.async { 1 + 1 } # => Performer::Task
|
14
|
+
class Performer
|
15
|
+
# All internal errors inherit from Performer::Error.
|
16
|
+
class Error < StandardError; end
|
17
|
+
|
18
|
+
# Raised by {Performer#shutdown}.
|
19
|
+
class ShutdownError < Error; end
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
@queue = Performer::Queue.new
|
23
|
+
@running = true
|
24
|
+
@thread = Thread.new(&method(:run_loop))
|
25
|
+
@shutdown_task = Task.new(lambda do
|
26
|
+
@running = false
|
27
|
+
nil
|
28
|
+
end)
|
29
|
+
end
|
30
|
+
|
31
|
+
# If you ever need to forcefully kill the Performer (don't do that),
|
32
|
+
# here's the thread you'll need to attack.
|
33
|
+
#
|
34
|
+
# @return [Thread]
|
35
|
+
attr_reader :thread
|
36
|
+
|
37
|
+
# Synchronously schedule a block for execution.
|
38
|
+
#
|
39
|
+
# If run from inside a task in the same performer, the block is executed
|
40
|
+
# immediately. You can avoid this behavior by using {#async} instead.
|
41
|
+
#
|
42
|
+
# @param [Integer, nil] timeout (see Task#value)
|
43
|
+
# @yield block to be executed
|
44
|
+
# @return whatever the block returned
|
45
|
+
# @raise [TimeoutError] if waiting for the task to finish timed out
|
46
|
+
# @raise [ShutdownError] if shutdown has been requested
|
47
|
+
def sync(timeout = nil, &block)
|
48
|
+
if Thread.current == @thread
|
49
|
+
yield
|
50
|
+
else
|
51
|
+
async(&block).value(timeout)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Asynchronously schedule a block for execution.
|
56
|
+
#
|
57
|
+
# @yield block to be executed
|
58
|
+
# @return [Performer::Task]
|
59
|
+
# @raise [ShutdownError] if shutdown has been requested
|
60
|
+
def async(&block)
|
61
|
+
task = Task.new(block)
|
62
|
+
@queue.enq(task) { raise ShutdownError, "performer is shut down" }
|
63
|
+
end
|
64
|
+
|
65
|
+
# Asynchronously schedule a shutdown, allowing all previously queued tasks to finish.
|
66
|
+
#
|
67
|
+
# @note No additional tasks will be accepted after shutdown.
|
68
|
+
#
|
69
|
+
# @return [Performer::Task]
|
70
|
+
# @raise [ShutdownError] if performer is already shutdown
|
71
|
+
def shutdown
|
72
|
+
@queue.close(@shutdown_task) do
|
73
|
+
raise ShutdownError, "performer is shut down"
|
74
|
+
end
|
75
|
+
|
76
|
+
@shutdown_task
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def run_loop
|
82
|
+
while @running
|
83
|
+
open = @queue.deq do |task|
|
84
|
+
begin
|
85
|
+
task.call
|
86
|
+
rescue Performer::Task::Error
|
87
|
+
# No op. Allows cancelling scheduled tasks.
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
if not open and @queue.empty?
|
92
|
+
@running = false
|
93
|
+
end
|
94
|
+
end
|
95
|
+
ensure
|
96
|
+
@queue.close
|
97
|
+
until @queue.empty?
|
98
|
+
@queue.deq do |task|
|
99
|
+
begin
|
100
|
+
task.cancel
|
101
|
+
rescue Performer::Task::Error
|
102
|
+
# Shutting down. Don't care.
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
require "performer/condition_variable"
|
110
|
+
require "performer/queue"
|
111
|
+
require "performer/task"
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require "forwardable"
|
2
|
+
|
3
|
+
class Performer
|
4
|
+
# A custom ConditionVariable.
|
5
|
+
#
|
6
|
+
# It delegates to ConditionVariable for #wait, #signal and #broadcast,
|
7
|
+
# but also provides a reliable {#wait_until}.
|
8
|
+
#
|
9
|
+
# @api private
|
10
|
+
class ConditionVariable
|
11
|
+
extend Forwardable
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@condvar = ::ConditionVariable.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def_delegators :@condvar, :wait, :signal, :broadcast
|
18
|
+
|
19
|
+
# Wait until a given condition is true, determined by
|
20
|
+
# calling the given block.
|
21
|
+
#
|
22
|
+
# @note This method will honor the timeout, even in the case
|
23
|
+
# of spurious wakeups.
|
24
|
+
#
|
25
|
+
# @example usage
|
26
|
+
# mutex.synchronize do
|
27
|
+
# condvar.wait_until(mutex, 1) { done? }
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# @param [Mutex] mutex
|
31
|
+
# @param [Integer, nil] timeout
|
32
|
+
# @yield for condition
|
33
|
+
def wait_until(mutex, timeout = nil)
|
34
|
+
unless block_given?
|
35
|
+
raise ArgumentError, "no block given"
|
36
|
+
end
|
37
|
+
|
38
|
+
if timeout
|
39
|
+
finished = Time.now + timeout
|
40
|
+
until yield
|
41
|
+
timeout = finished - Time.now
|
42
|
+
break unless timeout > 0
|
43
|
+
wait(mutex, timeout)
|
44
|
+
end
|
45
|
+
else
|
46
|
+
wait(mutex) until yield
|
47
|
+
end
|
48
|
+
|
49
|
+
return self
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
class Performer
|
2
|
+
# Similar to the stdlib Queue, but with a thread-safe way of closing it down.
|
3
|
+
class Queue
|
4
|
+
def initialize
|
5
|
+
@queue = []
|
6
|
+
@queue_mutex = Mutex.new
|
7
|
+
@queue_cond = Performer::ConditionVariable.new
|
8
|
+
@undefined = {}
|
9
|
+
@open = true
|
10
|
+
end
|
11
|
+
|
12
|
+
# Push an object into the queue, or yield if not possible.
|
13
|
+
#
|
14
|
+
# @example pushing an item onto the queue
|
15
|
+
# queue.enq(obj) do
|
16
|
+
# raise "Unable to push #{obj} into queue!"
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# @yield if obj could not be pushed onto the queue
|
20
|
+
# @param obj
|
21
|
+
# @return obj
|
22
|
+
# @raise [ArgumentError] if no block given
|
23
|
+
def enq(obj)
|
24
|
+
unless block_given?
|
25
|
+
raise ArgumentError, "no block given"
|
26
|
+
end
|
27
|
+
|
28
|
+
pushed = false
|
29
|
+
@queue_mutex.synchronize do
|
30
|
+
pushed = try_push(obj)
|
31
|
+
@queue_cond.signal
|
32
|
+
end
|
33
|
+
yield if not pushed
|
34
|
+
|
35
|
+
obj
|
36
|
+
end
|
37
|
+
|
38
|
+
# Retrieve an object from the queue, or block until one is available.
|
39
|
+
#
|
40
|
+
# The behaviour is as follows:
|
41
|
+
# - empty, open: block until queue is either not empty, or open
|
42
|
+
# - not empty, open: yield an item off the queue, return true
|
43
|
+
# - not empty, not open: yield an item off the queue, return false
|
44
|
+
# - empty, not open: return false
|
45
|
+
#
|
46
|
+
# @example
|
47
|
+
# open = queue.deq do |obj|
|
48
|
+
# # do something with obj
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# @yield [obj] an item retrieved from the queue, if available
|
52
|
+
# @return [Boolean] true if queue is open, false if open
|
53
|
+
# @raise [ArgumentError] if no block given
|
54
|
+
def deq
|
55
|
+
unless block_given?
|
56
|
+
raise ArgumentError, "no block given"
|
57
|
+
end
|
58
|
+
|
59
|
+
obj, was_open = @queue_mutex.synchronize do
|
60
|
+
while empty? and open?
|
61
|
+
@queue_cond.wait(@queue_mutex)
|
62
|
+
end
|
63
|
+
|
64
|
+
obj = if empty?
|
65
|
+
undefined
|
66
|
+
else
|
67
|
+
queue.shift
|
68
|
+
end
|
69
|
+
|
70
|
+
[obj, open?]
|
71
|
+
end
|
72
|
+
|
73
|
+
yield obj unless undefined.equal?(obj)
|
74
|
+
was_open
|
75
|
+
end
|
76
|
+
|
77
|
+
# Close the queue, optionally pushing an item onto the queue right before close.
|
78
|
+
#
|
79
|
+
# @example close and enqueue
|
80
|
+
# queue.close(object) do
|
81
|
+
# raise "Queue is was already closed!"
|
82
|
+
# end
|
83
|
+
#
|
84
|
+
# @example close without enqueue
|
85
|
+
# queue.close # => no need for block, since no argument
|
86
|
+
#
|
87
|
+
# @yield if obj could not be pushed onto the queue
|
88
|
+
# @param [Object, nil] obj
|
89
|
+
# @return [Object, nil] obj
|
90
|
+
# @raise [ArgumentError] if obj given, but no block given
|
91
|
+
def close(obj = undefined)
|
92
|
+
if undefined.equal?(obj)
|
93
|
+
@queue_mutex.synchronize do
|
94
|
+
@open = false
|
95
|
+
@queue_cond.broadcast
|
96
|
+
end
|
97
|
+
|
98
|
+
nil
|
99
|
+
elsif not block_given?
|
100
|
+
raise ArgumentError, "no block given"
|
101
|
+
else
|
102
|
+
pushed = false
|
103
|
+
@queue_mutex.synchronize do
|
104
|
+
pushed = try_push(obj)
|
105
|
+
@open = false
|
106
|
+
@queue_cond.broadcast
|
107
|
+
end
|
108
|
+
yield if not pushed
|
109
|
+
|
110
|
+
obj
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# @return [Boolean] true if queue is empty
|
115
|
+
def empty?
|
116
|
+
queue.empty?
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
attr_reader :undefined
|
122
|
+
attr_reader :queue
|
123
|
+
|
124
|
+
def try_push(obj)
|
125
|
+
if open?
|
126
|
+
queue.push(obj)
|
127
|
+
true
|
128
|
+
else
|
129
|
+
false
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def open?
|
134
|
+
@open
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
require "timeout"
|
2
|
+
|
3
|
+
class Performer
|
4
|
+
# A task is constructed with a callable object (like a proc), and provides ways to:
|
5
|
+
#
|
6
|
+
# - call the contained object, once and only once
|
7
|
+
# - retrieve the value from the execution of the callable, and wait if execution is not finished
|
8
|
+
# - cancel a task, and as such wake up all waiting for the task value
|
9
|
+
#
|
10
|
+
# Furthermore, the Task public API is thread-safe, and a resolved task will never change value.
|
11
|
+
#
|
12
|
+
# @example constructing a task
|
13
|
+
# task = Task.new(lambda { 1 + 1 })
|
14
|
+
# worker = Thread.new(task, &:call)
|
15
|
+
# task.value # => 2
|
16
|
+
class Task
|
17
|
+
# Used for the internal task state machine.
|
18
|
+
#
|
19
|
+
# @api private
|
20
|
+
Transitions = {
|
21
|
+
idle: { executing: true, cancelled: true },
|
22
|
+
executing: { error: true, value: true },
|
23
|
+
}
|
24
|
+
|
25
|
+
# Allows easy capturing of all task-specific errors.
|
26
|
+
class Error < Performer::Error; end
|
27
|
+
|
28
|
+
# Raised in {Task#value} if the task was cancelled.
|
29
|
+
class CancelledError < Error; end
|
30
|
+
|
31
|
+
# Raised from {Task#call} or {Task#cancel} on invariant errors.
|
32
|
+
class InvariantError < Error; end
|
33
|
+
|
34
|
+
# Create a new Task from a callable object.
|
35
|
+
#
|
36
|
+
# @param [#call] callable
|
37
|
+
def initialize(callable)
|
38
|
+
@callable = callable
|
39
|
+
|
40
|
+
@value_mutex = Mutex.new
|
41
|
+
@value_cond = Performer::ConditionVariable.new
|
42
|
+
|
43
|
+
@value = nil
|
44
|
+
@value_type = :idle
|
45
|
+
end
|
46
|
+
|
47
|
+
# Execute the task. Arguments and block are passed on to the callable.
|
48
|
+
#
|
49
|
+
# @note A task can only be called once.
|
50
|
+
# @note A task can not be called after it has been cancelled.
|
51
|
+
# @note A task swallows standard errors during execution, but all other errors are propagated.
|
52
|
+
#
|
53
|
+
# When execution finishes, all waiting for {#value} will be woken up with the result.
|
54
|
+
#
|
55
|
+
# @return [Task] self
|
56
|
+
def call(*args, &block)
|
57
|
+
set(:executing) { nil }
|
58
|
+
|
59
|
+
begin
|
60
|
+
value = @callable.call(*args, &block)
|
61
|
+
rescue => ex
|
62
|
+
set(:error) { ex }
|
63
|
+
rescue Exception => ex
|
64
|
+
set(:error) { ex }
|
65
|
+
raise ex
|
66
|
+
else
|
67
|
+
set(:value) { value }
|
68
|
+
end
|
69
|
+
|
70
|
+
self
|
71
|
+
end
|
72
|
+
|
73
|
+
# Cancel the task. All waiting for {#value} will be woken up with a {CancelledError}.
|
74
|
+
#
|
75
|
+
# @note This cannot be done while the task is executing.
|
76
|
+
# @note This cannot be done if the task has finished executing.
|
77
|
+
#
|
78
|
+
# @param [String] message for the cancellation error
|
79
|
+
def cancel(message = "task was cancelled")
|
80
|
+
set(:cancelled) { CancelledError.new(message) }
|
81
|
+
end
|
82
|
+
|
83
|
+
# Retrieve the value of the task. If the task is not finished, this will block.
|
84
|
+
#
|
85
|
+
# @example waiting with a timeout and a block
|
86
|
+
# task.value(1) { raise MyOwnError, "Timed out after 1s" }
|
87
|
+
#
|
88
|
+
# @param [Integer, nil] timeout how long to wait for value before timing out, nil if wait forever
|
89
|
+
# @yield if block given, yields instead of raising an error on timeout
|
90
|
+
# @raise [TimeoutError] if waiting timeout was reached, and no block was given
|
91
|
+
def value(timeout = nil)
|
92
|
+
unless done?
|
93
|
+
@value_mutex.synchronize do
|
94
|
+
@value_cond.wait_until(@value_mutex, timeout) { done? }
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
if value?
|
99
|
+
return @value
|
100
|
+
elsif error? or cancelled?
|
101
|
+
raise @value
|
102
|
+
elsif block_given?
|
103
|
+
yield
|
104
|
+
else
|
105
|
+
raise TimeoutError, "retrieving value timed out after #{timeout}s"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def error?
|
112
|
+
@value_type == :error
|
113
|
+
end
|
114
|
+
|
115
|
+
def value?
|
116
|
+
@value_type == :value
|
117
|
+
end
|
118
|
+
|
119
|
+
def cancelled?
|
120
|
+
@value_type == :cancelled
|
121
|
+
end
|
122
|
+
|
123
|
+
def done?
|
124
|
+
value? or error? or cancelled?
|
125
|
+
end
|
126
|
+
|
127
|
+
# @param [Symbol] type
|
128
|
+
# @yield to set the value
|
129
|
+
def set(type)
|
130
|
+
@value_mutex.synchronize do
|
131
|
+
unless Transitions.fetch(@value_type, {}).has_key?(type)
|
132
|
+
raise InvariantError, "transition from #{@value_type} to #{type} is not allowed"
|
133
|
+
end
|
134
|
+
|
135
|
+
@value_type = type
|
136
|
+
@value = yield
|
137
|
+
@value_cond.broadcast if done?
|
138
|
+
|
139
|
+
@value
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
data/performer.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'performer/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "performer"
|
8
|
+
spec.version = Performer::VERSION
|
9
|
+
spec.authors = ["Kim Burgestrand"]
|
10
|
+
spec.email = ["kim@burgestrand.se"]
|
11
|
+
spec.summary = %q{Schedule blocks in a background thread.}
|
12
|
+
spec.description = %q{Performer is a tiny gem for scheduling blocks in a background thread,
|
13
|
+
and optionally waiting for the return value.}
|
14
|
+
spec.homepage = ""
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0")
|
18
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
23
|
+
spec.add_development_dependency "rake"
|
24
|
+
spec.add_development_dependency "rspec", [">= 3.0.0.rc1", "< 4.0"]
|
25
|
+
spec.add_development_dependency "yard", "~> 0.8"
|
26
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
describe Performer::Queue do
|
2
|
+
let(:queue) { Performer::Queue.new }
|
3
|
+
let(:failer) { lambda { raise "This should not happen" } }
|
4
|
+
|
5
|
+
def dequeue(q)
|
6
|
+
yielded = false
|
7
|
+
yielded_value = nil
|
8
|
+
|
9
|
+
open = q.deq do |value|
|
10
|
+
yielded_value = value
|
11
|
+
yielded = true
|
12
|
+
end
|
13
|
+
|
14
|
+
if yielded
|
15
|
+
[open, yielded_value]
|
16
|
+
else
|
17
|
+
[open]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
it "maintains FIFO order" do
|
22
|
+
queue.enq(1, &failer)
|
23
|
+
queue.enq(2, &failer)
|
24
|
+
|
25
|
+
dequeue(queue).should eq([true, 1])
|
26
|
+
dequeue(queue).should eq([true, 2])
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#enq" do
|
30
|
+
it "raises an error if not given an error handling block" do
|
31
|
+
lambda { queue.enq(1) }.should raise_error(ArgumentError, "no block given")
|
32
|
+
end
|
33
|
+
|
34
|
+
it "enqueues an object" do
|
35
|
+
queue.should be_empty
|
36
|
+
queue.enq(1) { raise "enq failed" }
|
37
|
+
queue.should_not be_empty
|
38
|
+
end
|
39
|
+
|
40
|
+
it "yields if equeueing failed" do
|
41
|
+
queue.close
|
42
|
+
|
43
|
+
error = RuntimeError.new("Enqueue failed!")
|
44
|
+
lambda { queue.enq(1) { raise error } }.should raise_error(error)
|
45
|
+
queue.should be_empty
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe "#deq" do
|
50
|
+
it "raises an error if not given an error handling block" do
|
51
|
+
lambda { queue.deq }.should raise_error(ArgumentError, "no block given")
|
52
|
+
end
|
53
|
+
|
54
|
+
context "empty, open" do
|
55
|
+
let(:waiter) do
|
56
|
+
Thread.new(queue) { |q| dequeue(q) }
|
57
|
+
end
|
58
|
+
|
59
|
+
before { wait_until_sleep(waiter) }
|
60
|
+
|
61
|
+
specify "is awoken when an object is added" do
|
62
|
+
queue.enq(1) { raise "enq failed" }
|
63
|
+
|
64
|
+
waiter.value.should eq([true, 1])
|
65
|
+
queue.should be_empty
|
66
|
+
end
|
67
|
+
|
68
|
+
specify "is awoken when closed without an object" do
|
69
|
+
queue.close
|
70
|
+
|
71
|
+
waiter.value.should eq([false])
|
72
|
+
queue.should be_empty
|
73
|
+
end
|
74
|
+
|
75
|
+
specify "is awoken when closed with an object" do
|
76
|
+
queue.close(:thingy) { raise "close failed" }
|
77
|
+
|
78
|
+
waiter.value.should eq([false, :thingy])
|
79
|
+
queue.should be_empty
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
specify "not empty, open" do
|
84
|
+
queue.enq(1) { raise "enq failed" }
|
85
|
+
|
86
|
+
dequeue(queue).should eq([true, 1])
|
87
|
+
queue.should be_empty
|
88
|
+
end
|
89
|
+
|
90
|
+
context "not empty, not open" do
|
91
|
+
before do
|
92
|
+
queue.enq(1) { raise "enq failed" }
|
93
|
+
end
|
94
|
+
|
95
|
+
specify "when closed without an object" do
|
96
|
+
queue.close
|
97
|
+
|
98
|
+
dequeue(queue).should eq([false, 1])
|
99
|
+
queue.should be_empty
|
100
|
+
end
|
101
|
+
|
102
|
+
specify "when closed with an object" do
|
103
|
+
queue.close(:thingy) { raise "close failed" }
|
104
|
+
|
105
|
+
dequeue(queue).should eq([false, 1])
|
106
|
+
queue.should_not be_empty
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
specify "empty, not open" do
|
111
|
+
queue.close
|
112
|
+
dequeue(queue).should eq([false])
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
describe "#close" do
|
117
|
+
it "can be closed multiple times without argument" do
|
118
|
+
queue.close # no errors
|
119
|
+
queue.close # no errors
|
120
|
+
end
|
121
|
+
|
122
|
+
it "yields if closed with an argument that cannot be pushed" do
|
123
|
+
lambda { |b| queue.close(:thingy, &b) }.should_not yield_control
|
124
|
+
lambda { |b| queue.close(:thingy, &b) }.should yield_control
|
125
|
+
end
|
126
|
+
|
127
|
+
it "returns the object queued" do
|
128
|
+
queue.close(:thingy) { raise "close failed" }.should eq(:thingy)
|
129
|
+
end
|
130
|
+
|
131
|
+
it "returns nil if not closed with an object" do
|
132
|
+
queue.close.should eq(nil)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
describe Performer::Task do
|
2
|
+
let(:task) { Performer::Task.new(noop) }
|
3
|
+
let(:noop) { lambda { :ok } }
|
4
|
+
|
5
|
+
describe "#call" do
|
6
|
+
it "raises an error if called after a result is available" do
|
7
|
+
task.call
|
8
|
+
|
9
|
+
lambda { task.call }.should raise_error(Performer::Task::InvariantError)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "raises an error when called during execution" do
|
13
|
+
task = Performer::Task.new(lambda { sleep })
|
14
|
+
thread = Thread.new(task, &:call)
|
15
|
+
wait_until_sleep(thread)
|
16
|
+
|
17
|
+
lambda { task.call }.should raise_error(Performer::Task::InvariantError)
|
18
|
+
end
|
19
|
+
|
20
|
+
it "raises an error if called after task was cancelled" do
|
21
|
+
task.cancel
|
22
|
+
|
23
|
+
lambda { task.call }.should raise_error(Performer::Task::InvariantError)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "swallows standard errors" do
|
27
|
+
error = StandardError.new("Hello!")
|
28
|
+
noop.should_receive(:call).and_raise(error)
|
29
|
+
|
30
|
+
task.call.should eql(task)
|
31
|
+
lambda { task.value }.should raise_error(error)
|
32
|
+
end
|
33
|
+
|
34
|
+
it "re-raises non-standard errors" do
|
35
|
+
error = SyntaxError.new("Hello!")
|
36
|
+
noop.should_receive(:call).and_raise(error)
|
37
|
+
|
38
|
+
lambda { task.call }.should raise_error(error)
|
39
|
+
lambda { task.value }.should raise_error(error)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "#cancel" do
|
44
|
+
it "raises an error if called after a result is available" do
|
45
|
+
task.call
|
46
|
+
|
47
|
+
lambda { task.cancel }.should raise_error(Performer::Task::InvariantError)
|
48
|
+
end
|
49
|
+
|
50
|
+
it "raises an error when called during execution" do
|
51
|
+
task = Performer::Task.new(lambda { sleep; :ok })
|
52
|
+
thread = Thread.new(task, &:call)
|
53
|
+
wait_until_sleep(thread)
|
54
|
+
|
55
|
+
lambda { task.cancel }.should raise_error(Performer::Task::InvariantError)
|
56
|
+
|
57
|
+
thread.wakeup
|
58
|
+
task.value.should eq(:ok)
|
59
|
+
end
|
60
|
+
|
61
|
+
it "raises an error if called after task was cancelled" do
|
62
|
+
task.cancel
|
63
|
+
|
64
|
+
lambda { task.cancel }.should raise_error(Performer::Task::InvariantError)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "#value" do
|
69
|
+
context "task has a result available" do
|
70
|
+
it "returns the value" do
|
71
|
+
task.call
|
72
|
+
|
73
|
+
task.value.should eq(:ok)
|
74
|
+
end
|
75
|
+
|
76
|
+
it "raises the error if the task is an error" do
|
77
|
+
error = RuntimeError.new("Some error")
|
78
|
+
noop.should_receive(:call).and_raise(error)
|
79
|
+
task.call rescue nil
|
80
|
+
|
81
|
+
lambda { task.value }.should raise_error(error)
|
82
|
+
end
|
83
|
+
|
84
|
+
it "raises the error if the task is cancelled" do
|
85
|
+
task.cancel
|
86
|
+
|
87
|
+
lambda { task.value }.should raise_error(Performer::Task::CancelledError)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
context "task receives a result later on" do
|
92
|
+
let(:waiter) { Thread.new(task, &:value) }
|
93
|
+
before(:each) { wait_until_sleep(waiter) }
|
94
|
+
|
95
|
+
it "is woken up once a value is available" do
|
96
|
+
task.call
|
97
|
+
waiter.value.should eq(:ok)
|
98
|
+
end
|
99
|
+
|
100
|
+
it "is woken up once an error is available" do
|
101
|
+
error = RuntimeError.new("Some error")
|
102
|
+
noop.should_receive(:call).and_raise(error)
|
103
|
+
task.call rescue nil
|
104
|
+
|
105
|
+
lambda { waiter.value }.should raise_error(error)
|
106
|
+
end
|
107
|
+
|
108
|
+
it "is woken up once task is cancelled" do
|
109
|
+
task.cancel
|
110
|
+
|
111
|
+
lambda { waiter.value }.should raise_error(Performer::Task::CancelledError)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
it "yields to the given block on timeout" do
|
116
|
+
task.value(0) { :what }.should eq(:what)
|
117
|
+
end
|
118
|
+
|
119
|
+
it "raises an error on timeout" do
|
120
|
+
lambda { task.value(0) }.should raise_error(TimeoutError)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
data/spec/puddle_spec.rb
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
describe Performer do
|
2
|
+
let(:performer) { Performer.new }
|
3
|
+
|
4
|
+
specify "VERSION" do
|
5
|
+
Performer::VERSION.should_not be_nil
|
6
|
+
end
|
7
|
+
|
8
|
+
describe "errors" do
|
9
|
+
specify "standard errors in tasks do not crash the performer" do
|
10
|
+
lambda { performer.sync { raise "Hell" } }.should raise_error(/Hell/)
|
11
|
+
performer.sync { 1 + 1 }.should eq(2)
|
12
|
+
end
|
13
|
+
|
14
|
+
specify "cancelled tasks do not crash the performer" do
|
15
|
+
stopgap = Queue.new
|
16
|
+
|
17
|
+
performer.async { stopgap.pop }
|
18
|
+
task = performer.async { 1 + 1 }
|
19
|
+
task.cancel
|
20
|
+
stopgap.push :go
|
21
|
+
|
22
|
+
performer.sync { 1 + 1 }.should eq(2)
|
23
|
+
end
|
24
|
+
|
25
|
+
specify "if the performer crashes, it brings all queued tasks with it" do
|
26
|
+
stopgap = Queue.new
|
27
|
+
|
28
|
+
performer.async { stopgap.pop }
|
29
|
+
performer.async { raise Exception, "Hell" }
|
30
|
+
task = performer.async { :not_ok }
|
31
|
+
|
32
|
+
stopgap.push :go
|
33
|
+
|
34
|
+
lambda { task.value }.should raise_error(Performer::Task::CancelledError)
|
35
|
+
end
|
36
|
+
|
37
|
+
specify "if the performer crashes, it no longer accepts tasks" do
|
38
|
+
lambda { performer.sync { raise Exception, "Hell" } }.should raise_error
|
39
|
+
|
40
|
+
lambda { performer.sync { 1 + 1 } }.should raise_error(Performer::ShutdownError)
|
41
|
+
lambda { performer.shutdown }.should raise_error(Performer::ShutdownError)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe "#sync" do
|
46
|
+
specify "with timeout" do
|
47
|
+
lambda { performer.sync(0) { sleep } }.should raise_error(TimeoutError)
|
48
|
+
end
|
49
|
+
|
50
|
+
it "yields directly to the task when executed from within the performer" do
|
51
|
+
value = performer.sync do
|
52
|
+
performer.sync { 1 + 2 }
|
53
|
+
end
|
54
|
+
|
55
|
+
value.should eq(3)
|
56
|
+
end
|
57
|
+
|
58
|
+
it "executes a task synchronously in another thread" do
|
59
|
+
thread = performer.sync { Thread.current }
|
60
|
+
thread.should_not eq(Thread.current)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe "#async" do
|
65
|
+
it "executes a task asynchronously in another thread" do
|
66
|
+
task = performer.async { Thread.current }
|
67
|
+
thread = task.value
|
68
|
+
thread.should_not eq(Thread.current)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe "#shutdown" do
|
73
|
+
it "performs a clean shutdown, allowing scheduled tasks to finish" do
|
74
|
+
stopgap = Queue.new
|
75
|
+
waiter = Thread.new(Thread.current) do |thread|
|
76
|
+
wait_until_sleep(thread)
|
77
|
+
stopgap.push :go
|
78
|
+
end
|
79
|
+
|
80
|
+
performer.async { stopgap.pop }
|
81
|
+
task = performer.async { :done }
|
82
|
+
term = performer.shutdown
|
83
|
+
|
84
|
+
task.value.should eq(:done)
|
85
|
+
term.value.should eq(nil)
|
86
|
+
end
|
87
|
+
|
88
|
+
it "prevents scheduling additional tasks" do
|
89
|
+
performer.shutdown
|
90
|
+
lambda { performer.sync { 1 + 1 } }.should raise_error(Performer::ShutdownError)
|
91
|
+
end
|
92
|
+
|
93
|
+
it "raises an error if shutdown is already underway" do
|
94
|
+
performer.shutdown
|
95
|
+
lambda { performer.shutdown }.should raise_error(Performer::ShutdownError)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require "performer"
|
2
|
+
require "timeout"
|
3
|
+
|
4
|
+
module ConcurrencyUtilities
|
5
|
+
def wait_until_sleep(thread)
|
6
|
+
Thread.pass until thread.status == "sleep"
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
RSpec.configure do |config|
|
11
|
+
config.include(ConcurrencyUtilities)
|
12
|
+
|
13
|
+
config.expect_with :rspec do |c|
|
14
|
+
c.syntax = :should
|
15
|
+
end
|
16
|
+
|
17
|
+
config.mock_with :rspec do |c|
|
18
|
+
c.syntax = :should
|
19
|
+
end
|
20
|
+
|
21
|
+
config.around(:each) do |example|
|
22
|
+
Timeout.timeout(1, &example)
|
23
|
+
end
|
24
|
+
end
|
metadata
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: performer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Kim Burgestrand
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-06-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.6'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.6'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 3.0.0.rc1
|
48
|
+
- - "<"
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: '4.0'
|
51
|
+
type: :development
|
52
|
+
prerelease: false
|
53
|
+
version_requirements: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: 3.0.0.rc1
|
58
|
+
- - "<"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '4.0'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: yard
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0.8'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0.8'
|
75
|
+
description: |-
|
76
|
+
Performer is a tiny gem for scheduling blocks in a background thread,
|
77
|
+
and optionally waiting for the return value.
|
78
|
+
email:
|
79
|
+
- kim@burgestrand.se
|
80
|
+
executables: []
|
81
|
+
extensions: []
|
82
|
+
extra_rdoc_files: []
|
83
|
+
files:
|
84
|
+
- ".gitignore"
|
85
|
+
- ".rspec"
|
86
|
+
- ".travis.yml"
|
87
|
+
- Gemfile
|
88
|
+
- LICENSE.txt
|
89
|
+
- README.md
|
90
|
+
- Rakefile
|
91
|
+
- lib/performer.rb
|
92
|
+
- lib/performer/condition_variable.rb
|
93
|
+
- lib/performer/queue.rb
|
94
|
+
- lib/performer/task.rb
|
95
|
+
- lib/performer/version.rb
|
96
|
+
- performer.gemspec
|
97
|
+
- spec/puddle/queue_spec.rb
|
98
|
+
- spec/puddle/task_spec.rb
|
99
|
+
- spec/puddle_spec.rb
|
100
|
+
- spec/spec_helper.rb
|
101
|
+
homepage: ''
|
102
|
+
licenses:
|
103
|
+
- MIT
|
104
|
+
metadata: {}
|
105
|
+
post_install_message:
|
106
|
+
rdoc_options: []
|
107
|
+
require_paths:
|
108
|
+
- lib
|
109
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
110
|
+
requirements:
|
111
|
+
- - ">="
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: '0'
|
114
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - ">="
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
requirements: []
|
120
|
+
rubyforge_project:
|
121
|
+
rubygems_version: 2.2.1
|
122
|
+
signing_key:
|
123
|
+
specification_version: 4
|
124
|
+
summary: Schedule blocks in a background thread.
|
125
|
+
test_files:
|
126
|
+
- spec/puddle/queue_spec.rb
|
127
|
+
- spec/puddle/task_spec.rb
|
128
|
+
- spec/puddle_spec.rb
|
129
|
+
- spec/spec_helper.rb
|
130
|
+
has_rdoc:
|