concurrent-ruby 0.0.1 → 0.1.0

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