zeevex_concurrency 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
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
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ source 'https://rubygems.org'
2
+
3
+ group :development, :test do
4
+ gem 'pry'
5
+ gem 'pry-remote'
6
+ gem 'pry-doc'
7
+ gem 'pry-nav'
8
+ gem 'pry-buffers'
9
+ gem 'pry-syntax-hacks'
10
+ gem 'pry-git', :platform => :mri
11
+ gem 'jist'
12
+ gem 'ruby18_source_location', :platform => :mri_18
13
+ end
14
+
15
+ # Specify your gem's dependencies in zeevex_concurrency.gemspec
16
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Robert Sanders
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,29 @@
1
+ # ZeevexConcurrency
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'zeevex_concurrency'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install zeevex_concurrency
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,50 @@
1
+ require 'observer'
2
+ require 'thread'
3
+ require 'zeevex_concurrency/delayed'
4
+
5
+ class ZeevexConcurrency::Delay < ZeevexConcurrency::Delayed
6
+ include Observable
7
+ include ZeevexConcurrency::Delayed::Bindable
8
+
9
+ def initialize(computation = nil, options = {}, &block)
10
+ raise ArgumentError, "Must provide computation or block for a future" unless (computation || block)
11
+
12
+ @mutex = Mutex.new
13
+ @exec_mutex = Mutex.new
14
+ @exception = nil
15
+ @done = false
16
+ @result = false
17
+ @executed = false
18
+
19
+ # has to happen after exec_mutex initialized
20
+ bind(computation, &block)
21
+
22
+ Array(options.delete(:observer) || options.delete(:observers)).each do |observer|
23
+ add_observer observer
24
+ end
25
+ end
26
+
27
+ def self.create(callable = nil, options = {}, &block)
28
+ return callable if callable && callable.is_a?(ZeevexConcurrency::Delayed)
29
+ new(callable, options, &block)
30
+ end
31
+
32
+ def wait(timeout = nil)
33
+ true
34
+ end
35
+
36
+ def ready?
37
+ true
38
+ end
39
+
40
+ protected
41
+
42
+ def _fulfill(value, success = true)
43
+ @fulfilled_value = value
44
+ end
45
+
46
+ def _wait_for_value
47
+ _execute(binding)
48
+ @fulfilled_value
49
+ end
50
+ end
@@ -0,0 +1,233 @@
1
+ require 'thread'
2
+ require 'countdownlatch'
3
+
4
+ #
5
+ # base class for Promise, Future, etc.
6
+ #
7
+ class ZeevexConcurrency::Delayed
8
+
9
+ module ConvenienceMethods
10
+ def future(*args, &block)
11
+ ZeevexConcurrency::Future.__send__(:create, *args, &block)
12
+ end
13
+
14
+ def promise(*args, &block)
15
+ ZeevexConcurrency::Promise.__send__(:create, *args, &block)
16
+ end
17
+
18
+ def delay(*args, &block)
19
+ ZeevexConcurrency::Delay.__send__(:create, *args, &block)
20
+ end
21
+
22
+ def delayed?(obj)
23
+ obj.is_a?(ZeevexConcurrency::Delayed)
24
+ end
25
+
26
+ def delay?(obj)
27
+ obj.is_a?(ZeevexConcurrency::Delay)
28
+ end
29
+
30
+ def promise?(obj)
31
+ obj.is_a?(ZeevexConcurrency::Promise)
32
+ end
33
+
34
+ def future?(obj)
35
+ obj.is_a?(ZeevexConcurrency::Future)
36
+ end
37
+ end
38
+
39
+ def exception
40
+ @exception
41
+ end
42
+
43
+ def exception?
44
+ !! @exception
45
+ end
46
+
47
+ def executed?
48
+ @executed
49
+ end
50
+
51
+ def value(reraise = true)
52
+ @mutex.synchronize do
53
+ unless @done
54
+ @result = _wait_for_value
55
+ @done = true
56
+ end
57
+ end
58
+ if @exception && reraise
59
+ raise @exception
60
+ else
61
+ @result
62
+ end
63
+ end
64
+
65
+ def wait(timeout = nil)
66
+ Timeout::timeout(timeout) do
67
+ value(false)
68
+ true
69
+ end
70
+ rescue Timeout::Error
71
+ false
72
+ end
73
+
74
+ def set_result(&block)
75
+ @exec_mutex.synchronize do
76
+ raise ArgumentError, "Must supply block" unless block_given?
77
+ raise ArgumentError, "Already supplied block" if bound?
78
+ raise ArgumentError, "Promise already executed" if executed?
79
+
80
+ _execute(block)
81
+ end
82
+ end
83
+
84
+ protected
85
+
86
+ #
87
+ # not MT-safe; only to be called from executor thread
88
+ #
89
+ def _execute(computation)
90
+ raise "Already executed" if executed?
91
+ raise ArgumentError, "Cannot execute without computation" unless computation
92
+ success = false
93
+ begin
94
+ result = computation.call
95
+ success = true
96
+ rescue Exception
97
+ _smash($!)
98
+ end
99
+ @executed = true
100
+ # run this separately so we can report exceptions in _fulfill rather than capture them
101
+ _fulfill_and_notify(result) if (success)
102
+ rescue Exception
103
+ puts "*** exception in _fulfill: #{$!.inspect} ***"
104
+ ensure
105
+ @executed = true
106
+ end
107
+
108
+ def _fulfill_and_notify(value, success = true)
109
+ _fulfill(value, success)
110
+ if respond_to?(:notify_observers)
111
+ changed
112
+ begin
113
+ notify_observers(self, value, success)
114
+ rescue Exception
115
+ puts "Exception in notifying observers: #{$!.inspect}"
116
+ end
117
+ end
118
+ end
119
+ #
120
+ # not MT-safe; only to be called from executor thread
121
+ #
122
+ def _smash(ex)
123
+ @exception = ex
124
+ _fulfill_and_notify ex, false
125
+ end
126
+
127
+ ###
128
+
129
+ module LatchBased
130
+ def wait(timeout = nil)
131
+ @_latch.wait(timeout)
132
+ end
133
+
134
+ def ready?
135
+ @_latch.count == 0
136
+ end
137
+
138
+ protected
139
+
140
+ def _initialize_latch
141
+ @_latch = CountDownLatch.new(1)
142
+ end
143
+
144
+ def _fulfill(value, success = true)
145
+ @result = value
146
+ @_latch.countdown!
147
+ end
148
+
149
+ def _wait_for_value
150
+ @_latch.wait
151
+ @result
152
+ end
153
+ end
154
+
155
+ module QueueBased
156
+ def ready?
157
+ @exec_mutex.synchronize do
158
+ @queue.size > 0 || @executed
159
+ end
160
+ end
161
+
162
+ protected
163
+
164
+ def _initialize_queue
165
+ @queue = Queue.new
166
+ end
167
+
168
+ def _fulfill(value, success = true)
169
+ @queue << value
170
+ end
171
+
172
+ def _wait_for_value
173
+ @queue.pop
174
+ end
175
+ end
176
+
177
+ module Bindable
178
+ def bound?
179
+ !! @binding
180
+ end
181
+
182
+ def binding
183
+ @binding
184
+ end
185
+
186
+ def bind(proccy = nil, &block)
187
+ raise "Already bound" if bound?
188
+ if proccy && block
189
+ raise ArgumentError, "must supply a callable OR a block or neither, but not both"
190
+ end
191
+ raise ArgumentError, "Must provide computation as proc or block" unless (proccy || block)
192
+ @binding = proccy || block
193
+ end
194
+
195
+ def execute
196
+ @exec_mutex.synchronize do
197
+ return if executed?
198
+ return if respond_to?(:cancelled?) && cancelled?
199
+ _execute(binding)
200
+ end
201
+ end
202
+
203
+ def call
204
+ execute
205
+ end
206
+ end
207
+
208
+ module Cancellable
209
+ def cancelled?
210
+ @cancelled
211
+ end
212
+
213
+ def cancel
214
+ @exec_mutex.synchronize do
215
+ return false if executed?
216
+ return true if cancelled?
217
+ @cancelled = true
218
+ _smash CancelledException.new
219
+ true
220
+ end
221
+ end
222
+
223
+ def ready?
224
+ cancelled? || super
225
+ end
226
+ end
227
+
228
+ class CancelledException < StandardError; end
229
+ end
230
+
231
+ module ZeevexConcurrency
232
+ extend(ZeevexConcurrency::Delayed::ConvenienceMethods)
233
+ end
@@ -0,0 +1,154 @@
1
+ require 'thread'
2
+ require 'zeevex_concurrency/promise'
3
+
4
+ module ZeevexConcurrency
5
+ class EventLoop
6
+ def initialize(options = {})
7
+ @options = options
8
+ @mutex = options.delete(:mutex) || Mutex.new
9
+ @queue = options.delete(:queue) || Queue.new
10
+ @state = :stopped
11
+ end
12
+
13
+ def running?
14
+ @state == :started
15
+ end
16
+
17
+ def start
18
+ return unless @state == :stopped
19
+ @stop_requested = false
20
+ @thread = Thread.new do
21
+ process
22
+ end
23
+
24
+ @state = :started
25
+ end
26
+
27
+ def stop
28
+ return unless @state == :started
29
+ enqueue { @stop_requested = true }
30
+ unless @thread.join(1)
31
+ @thread.kill
32
+ @thread.join(0)
33
+ end
34
+
35
+ @thread = nil
36
+ @state = :stopped
37
+ end
38
+
39
+ #
40
+ # Enqueue any callable object (including a Promise or Future or other Delayed class) to the event loop
41
+ # and return a Delayed object which can be used to fetch the return value.
42
+ #
43
+ # Strictly obeys ordering.
44
+ #
45
+ def enqueue(callable = nil, &block)
46
+ to_run = callable || block
47
+ raise ArgumentError, "Must provide proc or block arg" unless to_run
48
+
49
+ to_run = ZeevexConcurrency::Promise.new(to_run) unless to_run.is_a?(ZeevexConcurrency::Delayed)
50
+ @queue << to_run
51
+ to_run
52
+ end
53
+
54
+ def <<(callable)
55
+ enqueue(callable)
56
+ end
57
+
58
+ def flush
59
+ @queue.clear
60
+ end
61
+
62
+ def backlog
63
+ @queue.size
64
+ end
65
+
66
+ def reset
67
+ stop
68
+ flush
69
+ start
70
+ end
71
+
72
+ #
73
+ # Returns true if the method was called from code executing on the event loop's thread
74
+ #
75
+ def in_event_loop?
76
+ Thread.current.object_id == @thread.object_id
77
+ end
78
+
79
+ #
80
+ # Runs a computation on the event loop. Does not deadlock if currently on the event loop, but
81
+ # will not preserve ordering either - it runs the computation immediately despite other events
82
+ # in the queue
83
+ #
84
+ def on_event_loop(runnable = nil, &block)
85
+ return unless runnable || block_given?
86
+ promise = (runnable && runnable.is_a?(ZeevexConcurrency::Delayed)) ?
87
+ runnable :
88
+ ZeevexConcurrency::Promise.create(runnable, &block)
89
+ if in_event_loop?
90
+ promise.call
91
+ promise
92
+ else
93
+ enqueue promise, &block
94
+ end
95
+ end
96
+
97
+ #
98
+ # Returns the value from the computation rather than a Promise. Has similar semantics to
99
+ # `on_event_loop` - if this is called from the event loop, it just executes the
100
+ # computation synchronously ahead of any other queued computations
101
+ #
102
+ def run_and_wait(runnable = nil, &block)
103
+ promise = on_event_loop(runnable, &block)
104
+ promise.value
105
+ end
106
+
107
+ protected
108
+
109
+ def process
110
+ while !@stop_requested
111
+ begin
112
+ process_one
113
+ rescue
114
+ ZeevexConcurrency.logger.error %{Exception caught in event loop: #{$!.inspect}: #{$!.backtrace.join("\n")}}
115
+ end
116
+ end
117
+ end
118
+
119
+ def process_one
120
+ @queue.pop.call
121
+ end
122
+
123
+ public
124
+
125
+ # event loop which throws away all events without running, returning nil from all promises
126
+ class Null
127
+ def initialize(options = {}); end
128
+ def start; end
129
+ def stop; end
130
+ def enqueue(callable = nil, &block)
131
+ to_run = ZeevexConcurrency::Promise.new unless to_run.is_a?(ZeevexConcurrency::Delayed)
132
+ to_run.set_result { nil }
133
+ to_run
134
+ end
135
+ def in_event_loop?; false; end
136
+ def on_event_loop(runnable = nil, &block)
137
+ enqueue(runnable, &block)
138
+ end
139
+ end
140
+
141
+ # event loop which runs all events synchronously when enqueued
142
+ class Inline < ZeevexConcurrency::EventLoop
143
+ def start; end
144
+ def stop; end
145
+ def enqueue(callable = nil, &block)
146
+ res = super
147
+ process_one
148
+ res
149
+ end
150
+ def in_event_loop?; true; end
151
+ end
152
+
153
+ end
154
+ end
@@ -0,0 +1,60 @@
1
+ require 'observer'
2
+ require 'timeout'
3
+ require 'zeevex_concurrency/delayed'
4
+ require 'zeevex_concurrency/event_loop'
5
+ require 'zeevex_concurrency/thread_pool'
6
+
7
+ class ZeevexConcurrency::Future < ZeevexConcurrency::Delayed
8
+ include Observable
9
+ include ZeevexConcurrency::Delayed::Bindable
10
+ include ZeevexConcurrency::Delayed::LatchBased
11
+ include ZeevexConcurrency::Delayed::Cancellable
12
+
13
+ # @@worker_pool = ZeevexConcurrency::EventLoop.new
14
+ @@worker_pool = ZeevexConcurrency::ThreadPool::FixedPool.new
15
+ @@worker_pool.start
16
+
17
+ def initialize(computation = nil, options = {}, &block)
18
+ raise ArgumentError, "Must provide computation or block for a future" unless (computation || block)
19
+
20
+ @mutex = Mutex.new
21
+ @exec_mutex = Mutex.new
22
+ @exception = nil
23
+ @done = false
24
+ @result = false
25
+ @executed = false
26
+
27
+ _initialize_latch
28
+
29
+ # has to happen after exec_mutex initialized
30
+ bind(computation, &block) if (computation || block)
31
+
32
+ Array(options.delete(:observer) || options.delete(:observers)).each do |observer|
33
+ add_observer observer
34
+ end
35
+ end
36
+
37
+ def self.shutdown
38
+ self.worker_pool.stop
39
+ end
40
+
41
+ def self.create(callable=nil, options = {}, &block)
42
+ nfuture = ZeevexConcurrency::Future.new(callable, options, &block)
43
+ (options.delete(:event_loop) || worker_pool).enqueue nfuture
44
+
45
+ nfuture
46
+ end
47
+
48
+ def self.worker_pool
49
+ @@worker_pool
50
+ end
51
+
52
+ def self.worker_pool=(pool)
53
+ @@worker_pool = pool
54
+ end
55
+
56
+ class << self
57
+ alias_method :future, :create
58
+ end
59
+ end
60
+
@@ -0,0 +1,7 @@
1
+ module ZeevexConcurrency
2
+ module Logging
3
+ def logger
4
+ @logger || ZeevexConcurrency.logger
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module ZeevexConcurrency
2
+ class NilLogger
3
+ def method_missing(symbol, *args)
4
+ nil
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,32 @@
1
+ require 'observer'
2
+ require 'timeout'
3
+ require 'zeevex_concurrency/delayed'
4
+
5
+ class ZeevexConcurrency::Promise < ZeevexConcurrency::Delayed
6
+ include Observable
7
+ include ZeevexConcurrency::Delayed::Bindable
8
+ include ZeevexConcurrency::Delayed::LatchBased
9
+
10
+ def initialize(computation = nil, options = {}, &block)
11
+ @mutex = Mutex.new
12
+ @exec_mutex = Mutex.new
13
+ @exception = nil
14
+ @done = false
15
+ @result = false
16
+ @executed = false
17
+
18
+ _initialize_latch
19
+
20
+ # has to happen after exec_mutex initialized
21
+ bind(computation, &block) if (computation || block)
22
+
23
+ Array(options.delete(:observer) || options.delete(:observers)).each do |observer|
24
+ self.add_observer observer
25
+ end
26
+ end
27
+
28
+ def self.create(callable = nil, options = {}, &block)
29
+ return callable if callable && callable.is_a?(ZeevexConcurrency::Delayed)
30
+ new(callable, options, &block)
31
+ end
32
+ end
@@ -0,0 +1,46 @@
1
+ # Alex's Ruby threading utilities - taken from https://github.com/alexdowad/showcase
2
+
3
+ require 'thread'
4
+ require 'zeevex_proxy'
5
+
6
+ # Wraps an object, synchronizes all method calls
7
+ # The wrapped object can also be set and read out
8
+ # which means this can also be used as a thread-safe reference
9
+ # (like a 'volatile' variable in Java)
10
+ class ZeevexConcurrency::Synchronized < ZeevexProxy::Base
11
+ def initialize(obj)
12
+ super
13
+ @mutex = ::Mutex.new
14
+ end
15
+
16
+ def _set_synchronized_object(val)
17
+ @mutex.synchronize { @obj = val }
18
+ end
19
+ def _get_synchronized_object
20
+ @mutex.synchronize { @obj }
21
+ end
22
+
23
+ def respond_to?(method)
24
+ if [:_set_synchronized_object, :_get_synchronized_object].include?(method.to_sym)
25
+ true
26
+ else
27
+ @obj.respond_to?(method)
28
+ end
29
+ end
30
+
31
+ def method_missing(method, *args, &block)
32
+ result = @mutex.synchronize { @obj.__send__(method, *args, &block) }
33
+ # result.__id__ == @obj.__id__ ? self : result
34
+ end
35
+ end
36
+
37
+ #
38
+ # make object synchronized unless already synchronized
39
+ #
40
+ def ZeevexConcurrency.Synchronized(obj)
41
+ if obj.respond_to?(:_get_synchronized_object)
42
+ obj
43
+ else
44
+ ZeevexConcurrency::Synchronized.new(obj)
45
+ end
46
+ end