performer 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: