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 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
@@ -0,0 +1,13 @@
1
+ language: ruby
2
+ cache: bundler
3
+ rvm:
4
+ - 2.0.0
5
+ - 2.1.0
6
+ - rbx-2
7
+ - jruby-19mode
8
+ notifications:
9
+ email:
10
+ recipients:
11
+ - kim@burgestrand.se
12
+ on_success: change
13
+ on_failure: change
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in performer.gemspec
4
+ gemspec
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
+ [![Build Status](https://travis-ci.org/Burgestrand/performer.svg?branch=master)](https://travis-ci.org/Burgestrand/performer)
4
+ [![Code Climate](https://codeclimate.com/github/Burgestrand/performer.png)](https://codeclimate.com/github/Burgestrand/performer)
5
+ [![Gem Version](https://badge.fury.io/rb/performer.png)](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
@@ -0,0 +1,4 @@
1
+ class Performer
2
+ # Current version of Performer.
3
+ VERSION = "1.0.0"
4
+ 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
@@ -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
@@ -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: