concurrent-ruby 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,69 @@
1
+ require 'thread'
2
+
3
+ require 'concurrent/global_thread_pool'
4
+
5
+ module Concurrent
6
+
7
+ IllegalMethodCallError = Class.new(StandardError)
8
+
9
+ class Defer
10
+
11
+ def initialize(operation = nil, callback = nil, errorback = nil, &block)
12
+ raise ArgumentError.new('no operation given') if operation.nil? && ! block_given?
13
+ raise ArgumentError.new('two operations given') if ! operation.nil? && block_given?
14
+
15
+ @operation = operation || block
16
+ @callback = callback
17
+ @errorback = errorback
18
+
19
+ if operation.nil?
20
+ @running = false
21
+ else
22
+ self.go
23
+ end
24
+ end
25
+
26
+ def then(&block)
27
+ raise IllegalMethodCallError.new('a callback has already been provided') unless @callback.nil?
28
+ raise IllegalMethodCallError.new('the defer is already running') if @running
29
+ raise ArgumentError.new('no block given') unless block_given?
30
+ @callback = block
31
+ return self
32
+ end
33
+
34
+ def rescue(&block)
35
+ raise IllegalMethodCallError.new('a errorback has already been provided') unless @errorback.nil?
36
+ raise IllegalMethodCallError.new('the defer is already running') if @running
37
+ raise ArgumentError.new('no block given') unless block_given?
38
+ @errorback = block
39
+ return self
40
+ end
41
+ alias_method :catch, :rescue
42
+ alias_method :on_error, :rescue
43
+
44
+ def go
45
+ return nil if @running
46
+ @running = true
47
+ $GLOBAL_THREAD_POOL.post { fulfill }
48
+ return nil
49
+ end
50
+
51
+ private
52
+
53
+ # @private
54
+ def fulfill # :nodoc:
55
+ result = @operation.call
56
+ @callback.call(result) unless @callback.nil?
57
+ rescue Exception => ex
58
+ @errorback.call(ex) unless @errorback.nil?
59
+ end
60
+ end
61
+ end
62
+
63
+ module Kernel
64
+
65
+ def defer(*args, &block)
66
+ return Concurrent::Defer.new(*args, &block)
67
+ end
68
+ module_function :defer
69
+ end
@@ -0,0 +1,60 @@
1
+ require 'thread'
2
+ require 'timeout'
3
+
4
+ module Concurrent
5
+
6
+ class Event
7
+
8
+ def initialize
9
+ @set = false
10
+ @notifier = Queue.new
11
+ @mutex = Mutex.new
12
+ @waiting = 0
13
+ end
14
+
15
+ def set?
16
+ return @set == true
17
+ end
18
+
19
+ def set(pulse = false)
20
+ return true if set?
21
+ @mutex.synchronize {
22
+ @set = true
23
+ while @waiting > 0
24
+ @notifier << :set
25
+ @waiting -= 1
26
+ end
27
+ @set = ! pulse
28
+ }
29
+ return true
30
+ end
31
+
32
+ def reset
33
+ @mutex.synchronize {
34
+ @set = false
35
+ }
36
+ return true
37
+ end
38
+
39
+ def pulse
40
+ return set(true)
41
+ end
42
+
43
+ def wait(timeout = nil)
44
+ return true if set?
45
+
46
+ if timeout.nil?
47
+ @waiting += 1
48
+ @notifier.pop
49
+ else
50
+ Timeout::timeout(timeout) do
51
+ @waiting += 1
52
+ @notifier.pop
53
+ end
54
+ end
55
+ return true
56
+ rescue Timeout::Error
57
+ return false
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,23 @@
1
+ require 'concurrent/global_thread_pool'
2
+
3
+ module Concurrent
4
+
5
+ class EventMachineDeferProxy
6
+ behavior(:global_thread_pool)
7
+
8
+ def post(*args, &block)
9
+ if args.empty?
10
+ EventMachine.defer(block)
11
+ else
12
+ new_block = proc{ block.call(*args) }
13
+ EventMachine.defer(new_block)
14
+ end
15
+ return true
16
+ end
17
+
18
+ def <<(block)
19
+ EventMachine.defer(block)
20
+ return self
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,89 @@
1
+ require 'thread'
2
+
3
+ require 'concurrent/thread_pool'
4
+ require 'concurrent/event'
5
+
6
+ module Concurrent
7
+
8
+ def self.new_fixed_thread_pool(size)
9
+ return FixedThreadPool.new(size)
10
+ end
11
+
12
+ class FixedThreadPool < ThreadPool
13
+ behavior(:thread_pool)
14
+
15
+ MIN_POOL_SIZE = 1
16
+ MAX_POOL_SIZE = 1024
17
+
18
+ def initialize(size)
19
+ super()
20
+ if size < MIN_POOL_SIZE || size > MAX_POOL_SIZE
21
+ raise ArgumentError.new("size must be between #{MIN_POOL_SIZE} and #{MAX_POOL_SIZE}")
22
+ end
23
+
24
+ @pool = size.times.collect{ create_worker_thread }
25
+ collect_garbage
26
+ end
27
+
28
+ def kill
29
+ @status = :killed
30
+ @pool.each{|t| Thread.kill(t) }
31
+ end
32
+
33
+ def size
34
+ if running?
35
+ return @pool.length
36
+ else
37
+ return 0
38
+ end
39
+ end
40
+
41
+ def post(*args, &block)
42
+ raise ArgumentError.new('no block given') unless block_given?
43
+ if running?
44
+ @queue << [args, block]
45
+ return true
46
+ else
47
+ return false
48
+ end
49
+ end
50
+
51
+ # @private
52
+ def status # :nodoc:
53
+ @pool.collect{|t| t.status }
54
+ end
55
+
56
+ private
57
+
58
+ # @private
59
+ def create_worker_thread # :nodoc:
60
+ Thread.new do
61
+ loop do
62
+ task = @queue.pop
63
+ if task == :stop
64
+ break
65
+ else
66
+ task.last.call(*task.first)
67
+ end
68
+ end
69
+ @pool.delete(Thread.current)
70
+ if @pool.empty?
71
+ @termination.set
72
+ @status = :shutdown unless killed?
73
+ end
74
+ end
75
+ end
76
+
77
+ # @private
78
+ def collect_garbage # :nodoc:
79
+ @collector = Thread.new do
80
+ sleep(1)
81
+ @pool.size.times do |i|
82
+ if @pool[i].status.nil?
83
+ @pool[i] = create_worker_thread
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,42 @@
1
+ require 'thread'
2
+
3
+ require 'concurrent/obligation'
4
+ require 'concurrent/global_thread_pool'
5
+
6
+ module Concurrent
7
+
8
+ class Future
9
+ include Obligation
10
+ behavior(:future)
11
+
12
+ def initialize(*args)
13
+
14
+ unless block_given?
15
+ @state = :fulfilled
16
+ else
17
+ @value = nil
18
+ @state = :pending
19
+ $GLOBAL_THREAD_POOL.post do
20
+ semaphore.synchronize do
21
+ Thread.pass
22
+ begin
23
+ @value = yield(*args)
24
+ @state = :fulfilled
25
+ rescue Exception => ex
26
+ @state = :rejected
27
+ @reason = ex
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ module Kernel
37
+
38
+ def future(*args, &block)
39
+ return Concurrent::Future.new(*args, &block)
40
+ end
41
+ module_function :future
42
+ end
@@ -0,0 +1,3 @@
1
+ require 'concurrent/cached_thread_pool'
2
+
3
+ $GLOBAL_THREAD_POOL ||= Concurrent::CachedThreadPool.new
@@ -0,0 +1,25 @@
1
+ require 'thread'
2
+
3
+ require 'concurrent/global_thread_pool'
4
+
5
+ module Kernel
6
+
7
+ # Post the given agruments and block to the Global Thread Pool.
8
+ #
9
+ # @param args [Array] zero or more arguments for the block
10
+ # @param block [Proc] operation to be performed concurrently
11
+ #
12
+ # @return [true,false] success/failre of thread creation
13
+ #
14
+ # @note Althought based on Go's goroutines and Erlang's spawn/1,
15
+ # Ruby has a vastly different runtime. Threads aren't nearly as
16
+ # efficient in Ruby. Use this function appropriately.
17
+ #
18
+ # @see http://golang.org/doc/effective_go.html#goroutines
19
+ # @see https://gobyexample.com/goroutines
20
+ def go(*args, &block)
21
+ return false unless block_given?
22
+ $GLOBAL_THREAD_POOL.post(*args, &block)
23
+ end
24
+ module_function :go
25
+ end
@@ -0,0 +1,121 @@
1
+ require 'thread'
2
+ require 'timeout'
3
+
4
+ require 'functional/behavior'
5
+
6
+ behavior_info(:future,
7
+ state: 0,
8
+ value: -1,
9
+ reason: 0,
10
+ pending?: 0,
11
+ fulfilled?: 0,
12
+ rejected?: 0)
13
+
14
+ behavior_info(:promise,
15
+ state: 0,
16
+ value: -1,
17
+ reason: 0,
18
+ pending?: 0,
19
+ fulfilled?: 0,
20
+ rejected?: 0,
21
+ then: 0,
22
+ rescue: -1)
23
+
24
+ module Concurrent
25
+
26
+ module Obligation
27
+
28
+ attr_reader :state
29
+ attr_reader :reason
30
+
31
+ # Has the obligation been fulfilled?
32
+ # @return [Boolean]
33
+ def fulfilled?() return(@state == :fulfilled); end
34
+ alias_method :realized?, :fulfilled?
35
+
36
+ # Is obligation completion still pending?
37
+ # @return [Boolean]
38
+ def pending?() return(!(fulfilled? || rejected?)); end
39
+
40
+ def value(timeout = nil)
41
+ if !pending? || timeout == 0
42
+ return @value
43
+ elsif timeout.nil?
44
+ return semaphore.synchronize { @value }
45
+ else
46
+ begin
47
+ return Timeout::timeout(timeout.to_f) {
48
+ semaphore.synchronize { @value }
49
+ }
50
+ rescue Timeout::Error => ex
51
+ return nil
52
+ end
53
+ end
54
+ end
55
+ alias_method :deref, :value
56
+
57
+ # Has the promise been rejected?
58
+ # @return [Boolean]
59
+ def rejected?() return(@state == :rejected); end
60
+
61
+ protected
62
+
63
+ def semaphore
64
+ @semaphore ||= Mutex.new
65
+ end
66
+ end
67
+ end
68
+
69
+ module Kernel
70
+
71
+ def deref(obligation, timeout = nil)
72
+ if obligation.respond_to?(:deref)
73
+ return obligation.deref(timeout)
74
+ elsif obligation.respond_to?(:value)
75
+ return obligation.deref(timeout)
76
+ else
77
+ return nil
78
+ end
79
+ end
80
+ module_function :deref
81
+
82
+ def pending?(obligation)
83
+ if obligation.respond_to?(:pending?)
84
+ return obligation.pending?
85
+ else
86
+ return false
87
+ end
88
+ end
89
+ module_function :pending?
90
+
91
+ def fulfilled?(obligation)
92
+ if obligation.respond_to?(:fulfilled?)
93
+ return obligation.fulfilled?
94
+ elsif obligation.respond_to?(:realized?)
95
+ return obligation.realized?
96
+ else
97
+ return false
98
+ end
99
+ end
100
+ module_function :fulfilled?
101
+
102
+ def realized?(obligation)
103
+ if obligation.respond_to?(:realized?)
104
+ return obligation.realized?
105
+ elsif obligation.respond_to?(:fulfilled?)
106
+ return obligation.fulfilled?
107
+ else
108
+ return false
109
+ end
110
+ end
111
+ module_function :realized?
112
+
113
+ def rejected?(obligation)
114
+ if obligation.respond_to?(:rejected?)
115
+ return obligation.rejected?
116
+ else
117
+ return false
118
+ end
119
+ end
120
+ module_function :rejected?
121
+ end
@@ -0,0 +1,194 @@
1
+ require 'thread'
2
+
3
+ require 'concurrent/obligation'
4
+ require 'concurrent/global_thread_pool'
5
+
6
+ module Concurrent
7
+
8
+ class Promise
9
+ include Obligation
10
+ behavior(:future)
11
+ behavior(:promise)
12
+
13
+ # Creates a new promise object. "A promise represents the eventual
14
+ # value returned from the single completion of an operation."
15
+ # Promises can be chained in a tree structure where each promise
16
+ # has zero or more children. Promises are resolved asynchronously
17
+ # in the order they are added to the tree. Parents are guaranteed
18
+ # to be resolved before their children. The result of each promise
19
+ # is passed to each of its children upon resolution. When
20
+ # a promise is rejected all its children will be summarily rejected.
21
+ # A promise that is neither resolved or rejected is pending.
22
+ #
23
+ # @param args [Array] zero or more arguments for the block
24
+ # @param block [Proc] the block to call when attempting fulfillment
25
+ #
26
+ # @see http://wiki.commonjs.org/wiki/Promises/A
27
+ # @see http://promises-aplus.github.io/promises-spec/
28
+ def initialize(*args, &block)
29
+ if args.first.is_a?(Promise)
30
+ @parent = args.first
31
+ else
32
+ @parent = nil
33
+ @chain = [self]
34
+ end
35
+
36
+ @mutex = Mutex.new
37
+ @handler = block || Proc.new{|result| result }
38
+ @state = :pending
39
+ @value = nil
40
+ @reason = nil
41
+ @children = []
42
+ @rescuers = []
43
+
44
+ realize(*args) if root?
45
+ end
46
+
47
+ # Create a new child Promise. The block argument for the child will
48
+ # be the result of fulfilling its parent. If the child will
49
+ # immediately be rejected if the parent has already been rejected.
50
+ #
51
+ # @param block [Proc] the block to call when attempting fulfillment
52
+ #
53
+ # @return [Promise] the new promise
54
+ def then(&block)
55
+ child = @mutex.synchronize do
56
+ block = Proc.new{|result| result } unless block_given?
57
+ @children << Promise.new(self, &block)
58
+ @children.last.on_reject(@reason) if rejected?
59
+ push(@children.last)
60
+ @children.last
61
+ end
62
+ return child
63
+ end
64
+
65
+ # Add a rescue handler to be run if the promise is rejected (via raised
66
+ # exception). Multiple rescue handlers may be added to a Promise.
67
+ # Rescue blocks will be checked in order and the first one with a
68
+ # matching Exception class will be processed. The block argument
69
+ # will be the exception that caused the rejection.
70
+ #
71
+ # @param clazz [Class] The class of exception to rescue
72
+ # @param block [Proc] the block to call if the rescue is matched
73
+ #
74
+ # @return [self] so that additional chaining can occur
75
+ def rescue(clazz = Exception, &block)
76
+ @mutex.synchronize do
77
+ @rescuers << Rescuer.new(clazz, block) if block_given?
78
+ end
79
+ return self
80
+ end
81
+ alias_method :catch, :rescue
82
+ alias_method :on_error, :rescue
83
+
84
+ protected
85
+
86
+ attr_reader :parent
87
+ attr_reader :handler
88
+ attr_reader :rescuers
89
+
90
+ # @private
91
+ Rescuer = Struct.new(:clazz, :block)
92
+
93
+ # @private
94
+ def root # :nodoc:
95
+ current = self
96
+ current = current.parent until current.root?
97
+ return current
98
+ end
99
+
100
+ # @private
101
+ def root? # :nodoc:
102
+ @parent.nil?
103
+ end
104
+
105
+ # @private
106
+ def push(promise) # :nodoc:
107
+ if root?
108
+ @chain << promise
109
+ else
110
+ @parent.push(promise)
111
+ end
112
+ end
113
+
114
+ # @private
115
+ def on_fulfill(value) # :nodoc:
116
+ @mutex.synchronize do
117
+ if pending?
118
+ @value = @handler.call(value)
119
+ @state = :fulfilled
120
+ @reason = nil
121
+ end
122
+ end
123
+ return @value
124
+ end
125
+
126
+ # @private
127
+ def on_reject(reason) # :nodoc:
128
+ @mutex.synchronize do
129
+ if pending?
130
+ @state = :rejected
131
+ @reason = reason
132
+ self.try_rescue(reason)
133
+ @value = nil
134
+ end
135
+ @children.each{|child| child.on_reject(reason) }
136
+ end
137
+ end
138
+
139
+ # @private
140
+ def try_rescue(ex) # :nodoc:
141
+ rescuer = @rescuers.find{|r| ex.is_a?(r.clazz) }
142
+ rescuer.block.call(ex) if rescuer
143
+ rescue Exception => e
144
+ # supress
145
+ end
146
+
147
+ # @private
148
+ def realize(*args) # :nodoc:
149
+ $GLOBAL_THREAD_POOL.post(@chain, @mutex, args) do |chain, mutex, args|
150
+ result = args.length == 1 ? args.first : args
151
+ index = 0
152
+ loop do
153
+ Thread.pass
154
+ current = mutex.synchronize{ chain[index] }
155
+ unless current.rejected?
156
+ current.semaphore.synchronize do
157
+ begin
158
+ result = current.on_fulfill(result)
159
+ rescue Exception => ex
160
+ current.on_reject(ex)
161
+ end
162
+ end
163
+ end
164
+ index += 1
165
+ sleep while index >= chain.length
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
171
+
172
+ module Kernel
173
+
174
+ # Creates a new promise object. "A promise represents the eventual
175
+ # value returned from the single completion of an operation."
176
+ # Promises can be chained in a tree structure where each promise
177
+ # has zero or more children. Promises are resolved asynchronously
178
+ # in the order they are added to the tree. Parents are guaranteed
179
+ # to be resolved before their children. The result of each promise
180
+ # is passes to each of its children when the child resolves. When
181
+ # a promise is rejected all its children will be summarily rejected.
182
+ # A promise added to a rejected promise will immediately be rejected.
183
+ # A promise that is neither resolved or rejected is pending.
184
+ #
185
+ # @param args [Array] zero or more arguments for the block
186
+ # @param block [Proc] the block to call when attempting fulfillment
187
+ #
188
+ # @see Promise
189
+ # @see http://wiki.commonjs.org/wiki/Promises/A
190
+ def promise(*args, &block)
191
+ return Concurrent::Promise.new(*args, &block)
192
+ end
193
+ module_function :promise
194
+ end