concurrent-ruby-edge 0.4.1 → 0.5.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 +4 -4
- data/CHANGELOG.md +26 -0
- data/README.md +36 -4
- data/lib-edge/concurrent-edge.rb +4 -0
- data/lib-edge/concurrent/actor/reference.rb +3 -0
- data/lib-edge/concurrent/edge/cancellation.rb +78 -112
- data/lib-edge/concurrent/edge/channel.rb +450 -0
- data/lib-edge/concurrent/edge/erlang_actor.rb +1545 -0
- data/lib-edge/concurrent/edge/processing_actor.rb +83 -64
- data/lib-edge/concurrent/edge/promises.rb +80 -110
- data/lib-edge/concurrent/edge/throttle.rb +167 -141
- data/lib-edge/concurrent/edge/version.rb +3 -0
- metadata +8 -5
@@ -15,9 +15,9 @@ module Concurrent
|
|
15
15
|
# values[-5, 5] # => [49996, 49997, 49998, 49999, 50000]
|
16
16
|
# @!macro warn.edge
|
17
17
|
class ProcessingActor < Synchronization::Object
|
18
|
-
|
19
|
-
# TODO (pitr-ch
|
20
|
-
#
|
18
|
+
|
19
|
+
# TODO (pitr-ch 29-Jan-2019): simplify as much as possible, maybe even do not delegate to mailbox, no ask linking etc
|
20
|
+
# TODO (pitr-ch 03-Feb-2019): remove completely
|
21
21
|
|
22
22
|
safe_initialization!
|
23
23
|
|
@@ -55,31 +55,33 @@ module Concurrent
|
|
55
55
|
# @param [Object] args Arguments passed to the process.
|
56
56
|
# @param [Promises::Channel] channel which serves as mailing box. The channel can have limited
|
57
57
|
# size to achieve backpressure.
|
58
|
-
# @yield args to the process to get back a future which represents the actors execution.
|
58
|
+
# @yield [actor, *args] to the process to get back a future which represents the actors execution.
|
59
|
+
# @yieldparam [ProcessingActor] actor
|
59
60
|
# @yieldparam [Object] *args
|
60
61
|
# @yieldreturn [Promises::Future(Object)] a future representing next step of execution
|
61
62
|
# @return [ProcessingActor]
|
62
|
-
# @example
|
63
|
-
# # TODO (pitr-ch 19-Jan-2017): actor with limited mailbox
|
64
63
|
def self.act_listening(channel, *args, &process)
|
65
|
-
|
66
|
-
Promises.
|
67
|
-
future(actor, *args, &process).
|
68
|
-
run.
|
69
|
-
chain_resolvable(actor.instance_variable_get(:@Terminated))
|
70
|
-
actor
|
64
|
+
ProcessingActor.new channel, *args, &process
|
71
65
|
end
|
72
66
|
|
73
|
-
# Receives a message when available, used in the actor's process.
|
74
|
-
# @return [Promises::Future(Object)] a future which will be fulfilled with a message from
|
75
|
-
# mailbox when it is available.
|
76
|
-
def receive(
|
77
|
-
|
78
|
-
|
67
|
+
# # Receives a message when available, used in the actor's process.
|
68
|
+
# # @return [Promises::Future(Object)] a future which will be fulfilled with a message from
|
69
|
+
# # mailbox when it is available.
|
70
|
+
# def receive(*channels)
|
71
|
+
# channels = [@Mailbox] if channels.empty?
|
72
|
+
# Promises::Channel.select(*channels)
|
73
|
+
# # TODO (pitr-ch 27-Dec-2016): support patterns
|
74
|
+
# # - put any received message aside if it does not match
|
75
|
+
# # - on each receive call check the messages put aside
|
76
|
+
# # - track where the message came from, cannot later receive m form other channel only because it matches
|
77
|
+
# end
|
78
|
+
|
79
|
+
def receive(channel = mailbox)
|
80
|
+
channel.pop_op
|
79
81
|
end
|
80
82
|
|
81
83
|
# Tells a message to the actor. May block current thread if the mailbox is full.
|
82
|
-
# {#
|
84
|
+
# {#tell_op} is a better option since it does not block. It's usually used to integrate with
|
83
85
|
# threading code.
|
84
86
|
# @example
|
85
87
|
# Thread.new(actor) do |actor|
|
@@ -91,7 +93,7 @@ module Concurrent
|
|
91
93
|
# @param [Object] message
|
92
94
|
# @return [self]
|
93
95
|
def tell!(message)
|
94
|
-
@Mailbox.push(message)
|
96
|
+
@Mailbox.push(message)
|
95
97
|
self
|
96
98
|
end
|
97
99
|
|
@@ -99,61 +101,78 @@ module Concurrent
|
|
99
101
|
# @param [Object] message
|
100
102
|
# @return [Promises::Future(ProcessingActor)] a future which will be fulfilled with the actor
|
101
103
|
# when the message is pushed to mailbox.
|
102
|
-
def
|
103
|
-
@Mailbox.
|
104
|
+
def tell_op(message)
|
105
|
+
@Mailbox.push_op(message).then(self) { |_ch, actor| actor }
|
104
106
|
end
|
105
107
|
|
106
|
-
# Simplifies common pattern when a message sender also requires an answer to the message
|
107
|
-
# from the actor. It appends a resolvable_future for the answer after the message.
|
108
|
-
# @todo has to be nice also on the receive side, cannot make structure like this [message = [...], answer]
|
109
|
-
# all receives should receive something friendly
|
110
|
-
# @param [Object] message
|
111
|
-
# @param [Promises::ResolvableFuture] answer
|
112
|
-
# @return [Promises::Future] a future which will be fulfilled with the answer to the message
|
113
|
-
# @example
|
114
|
-
# add_once_actor = Concurrent::ProcessingActor.act do |actor|
|
115
|
-
# actor.receive.then do |(a, b), answer|
|
116
|
-
# result = a + b
|
117
|
-
# answer.fulfill result
|
118
|
-
# # terminate with result value
|
119
|
-
# result
|
120
|
-
# end
|
121
|
-
# end
|
122
|
-
# # => <#Concurrent::ProcessingActor:0x7fcd1315f6e8 termination:pending>
|
108
|
+
# # Simplifies common pattern when a message sender also requires an answer to the message
|
109
|
+
# # from the actor. It appends a resolvable_future for the answer after the message.
|
110
|
+
# # @todo has to be nice also on the receive side, cannot make structure like this [message = [...], answer]
|
111
|
+
# # all receives should receive something friendly
|
112
|
+
# # @param [Object] message
|
113
|
+
# # @param [Promises::ResolvableFuture] answer
|
114
|
+
# # @return [Promises::Future] a future which will be fulfilled with the answer to the message
|
115
|
+
# # @example
|
116
|
+
# # add_once_actor = Concurrent::ProcessingActor.act do |actor|
|
117
|
+
# # actor.receive.then do |(a, b), answer|
|
118
|
+
# # result = a + b
|
119
|
+
# # answer.fulfill result
|
120
|
+
# # # terminate with result value
|
121
|
+
# # result
|
122
|
+
# # end
|
123
|
+
# # end
|
124
|
+
# # # => <#Concurrent::ProcessingActor:0x7fcd1315f6e8 termination:pending>
|
125
|
+
# #
|
126
|
+
# # add_once_actor.ask([1, 2]).value! # => 3
|
127
|
+
# # # fails the actor already added once
|
128
|
+
# # add_once_actor.ask(%w(ab cd)).reason
|
129
|
+
# # # => #<RuntimeError: actor terminated normally before answering with a value: 3>
|
130
|
+
# # add_once_actor.termination.value! # => 3
|
131
|
+
# def ask(message, answer = Promises.resolvable_future)
|
132
|
+
# raise 'to be removed'
|
123
133
|
#
|
124
|
-
#
|
125
|
-
#
|
126
|
-
#
|
127
|
-
#
|
128
|
-
#
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
134
|
+
# # TODO (pitr-ch 12-Dec-2018): REMOVE, the process ends up as another future not a value, no nice way to do ask in the actor
|
135
|
+
# tell [message, answer]
|
136
|
+
# # do not leave answers unanswered when actor terminates.
|
137
|
+
# Promises.any(
|
138
|
+
# Promises.fulfilled_future(:answer).zip(answer),
|
139
|
+
# Promises.fulfilled_future(:termination).zip(@Terminated)
|
140
|
+
# ).chain do |fulfilled, (which, value), (_, reason)|
|
141
|
+
# # TODO (pitr-ch 20-Jan-2017): we have to know which future was resolved
|
142
|
+
# # TODO (pitr-ch 20-Jan-2017): make the combinator programmable, so anyone can create what is needed
|
143
|
+
# # FIXME (pitr-ch 19-Jan-2017): ensure no callbacks are accumulated on @Terminated
|
144
|
+
# if which == :termination
|
145
|
+
# raise reason.nil? ? format('actor terminated normally before answering with a value: %s', value) : reason
|
146
|
+
# else
|
147
|
+
# fulfilled ? value : raise(reason)
|
148
|
+
# end
|
149
|
+
# end
|
150
|
+
# end
|
151
|
+
|
152
|
+
# actor.ask2 { |a| [:count, a] }
|
153
|
+
def ask_op(answer = Promises.resolvable_future, &message_provider)
|
154
|
+
# TODO (pitr-ch 12-Dec-2018): is it ok to let the answers be unanswered when the actor terminates
|
155
|
+
tell_op(message_provider.call(answer)).then(answer) { |_, a| a }
|
156
|
+
|
157
|
+
# answer.chain { |v| [true, v] } | @Terminated.then
|
145
158
|
end
|
146
159
|
|
147
160
|
# @return [String] string representation.
|
148
|
-
def
|
149
|
-
format '%s termination
|
161
|
+
def to_s
|
162
|
+
format '%s termination: %s>', super[0..-2], termination.state
|
163
|
+
end
|
164
|
+
|
165
|
+
alias_method :inspect, :to_s
|
166
|
+
|
167
|
+
def to_ary
|
168
|
+
[@Mailbox, @Terminated]
|
150
169
|
end
|
151
170
|
|
152
171
|
private
|
153
172
|
|
154
|
-
def initialize(channel
|
173
|
+
def initialize(channel, *args, &process)
|
155
174
|
@Mailbox = channel
|
156
|
-
@Terminated = Promises.
|
175
|
+
@Terminated = Promises.future(self, *args, &process).run
|
157
176
|
super()
|
158
177
|
end
|
159
178
|
|
@@ -12,7 +12,7 @@ module Concurrent
|
|
12
12
|
# Asks the actor with its value.
|
13
13
|
# @return [Future] new future with the response form the actor
|
14
14
|
def then_ask(actor)
|
15
|
-
self.then { |v|
|
15
|
+
self.then(actor) { |v, a| a.ask_op(v) }.flat
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
@@ -49,97 +49,6 @@ module Concurrent
|
|
49
49
|
include FlatShortcuts
|
50
50
|
end
|
51
51
|
|
52
|
-
# @!macro warn.edge
|
53
|
-
class Channel < Concurrent::Synchronization::Object
|
54
|
-
safe_initialization!
|
55
|
-
|
56
|
-
# Default size of the Channel, makes it accept unlimited number of messages.
|
57
|
-
UNLIMITED = ::Object.new
|
58
|
-
UNLIMITED.singleton_class.class_eval do
|
59
|
-
include Comparable
|
60
|
-
|
61
|
-
def <=>(other)
|
62
|
-
1
|
63
|
-
end
|
64
|
-
|
65
|
-
def to_s
|
66
|
-
'unlimited'
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
# A channel to pass messages between promises. The size is limited to support back pressure.
|
71
|
-
# @param [Integer, UNLIMITED] size the maximum number of messages stored in the channel.
|
72
|
-
def initialize(size = UNLIMITED)
|
73
|
-
super()
|
74
|
-
@Size = size
|
75
|
-
# TODO (pitr-ch 26-Dec-2016): replace with lock-free implementation
|
76
|
-
@Mutex = Mutex.new
|
77
|
-
@Probes = []
|
78
|
-
@Messages = []
|
79
|
-
@PendingPush = []
|
80
|
-
end
|
81
|
-
|
82
|
-
|
83
|
-
# Returns future which will fulfill when the message is added to the channel. Its value is the message.
|
84
|
-
# @param [Object] message
|
85
|
-
# @return [Future]
|
86
|
-
def push(message)
|
87
|
-
@Mutex.synchronize do
|
88
|
-
while true
|
89
|
-
if @Probes.empty?
|
90
|
-
if @Size > @Messages.size
|
91
|
-
@Messages.push message
|
92
|
-
return Promises.fulfilled_future message
|
93
|
-
else
|
94
|
-
pushed = Promises.resolvable_future
|
95
|
-
@PendingPush.push [message, pushed]
|
96
|
-
return pushed.with_hidden_resolvable
|
97
|
-
end
|
98
|
-
else
|
99
|
-
probe = @Probes.shift
|
100
|
-
if probe.fulfill [self, message], false
|
101
|
-
return Promises.fulfilled_future(message)
|
102
|
-
end
|
103
|
-
end
|
104
|
-
end
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
# Returns a future witch will become fulfilled with a value from the channel when one is available.
|
109
|
-
# @param [ResolvableFuture] probe the future which will be fulfilled with a channel value
|
110
|
-
# @return [Future] the probe, its value will be the message when available.
|
111
|
-
def pop(probe = Concurrent::Promises.resolvable_future)
|
112
|
-
# TODO (pitr-ch 26-Dec-2016): improve performance
|
113
|
-
pop_for_select(probe).then(&:last)
|
114
|
-
end
|
115
|
-
|
116
|
-
# @!visibility private
|
117
|
-
def pop_for_select(probe = Concurrent::Promises.resolvable_future)
|
118
|
-
@Mutex.synchronize do
|
119
|
-
if @Messages.empty?
|
120
|
-
@Probes.push probe
|
121
|
-
else
|
122
|
-
message = @Messages.shift
|
123
|
-
probe.fulfill [self, message]
|
124
|
-
|
125
|
-
unless @PendingPush.empty?
|
126
|
-
message, pushed = @PendingPush.shift
|
127
|
-
@Messages.push message
|
128
|
-
pushed.fulfill message
|
129
|
-
end
|
130
|
-
end
|
131
|
-
end
|
132
|
-
probe
|
133
|
-
end
|
134
|
-
|
135
|
-
# @return [String] Short string representation.
|
136
|
-
def to_s
|
137
|
-
format '%s size:%s>', super[0..-2], @Size
|
138
|
-
end
|
139
|
-
|
140
|
-
alias_method :inspect, :to_s
|
141
|
-
end
|
142
|
-
|
143
52
|
class Future < AbstractEventFuture
|
144
53
|
# @!macro warn.edge
|
145
54
|
module NewChannelIntegration
|
@@ -147,8 +56,8 @@ module Concurrent
|
|
147
56
|
# @param [Channel] channel to push to.
|
148
57
|
# @return [Future] a future which is fulfilled after the message is pushed to the channel.
|
149
58
|
# May take a moment if the channel is full.
|
150
|
-
def
|
151
|
-
self.then { |value|
|
59
|
+
def then_channel_push(channel)
|
60
|
+
self.then(channel) { |value, ch| ch.push_op value }.flat_future
|
152
61
|
end
|
153
62
|
|
154
63
|
end
|
@@ -157,22 +66,6 @@ module Concurrent
|
|
157
66
|
end
|
158
67
|
|
159
68
|
module FactoryMethods
|
160
|
-
# @!macro warn.edge
|
161
|
-
module NewChannelIntegration
|
162
|
-
|
163
|
-
# Selects a channel which is ready to be read from.
|
164
|
-
# @param [Channel] channels
|
165
|
-
# @return [Future] a future which is fulfilled with pair [channel, message] when one of the channels is
|
166
|
-
# available for reading
|
167
|
-
def select_channel(*channels)
|
168
|
-
probe = Promises.resolvable_future
|
169
|
-
channels.each { |ch| ch.pop_for_select probe }
|
170
|
-
probe
|
171
|
-
end
|
172
|
-
end
|
173
|
-
|
174
|
-
include NewChannelIntegration
|
175
|
-
|
176
69
|
# @!macro promises.shortcut.on
|
177
70
|
# @return [Future]
|
178
71
|
# @!macro warn.edge
|
@@ -200,5 +93,82 @@ module Concurrent
|
|
200
93
|
end
|
201
94
|
end
|
202
95
|
|
96
|
+
module Resolvable
|
97
|
+
include InternalStates
|
98
|
+
|
99
|
+
# Reserves the event or future, if reserved others are prevented from resolving it.
|
100
|
+
# Advanced feature.
|
101
|
+
# Be careful about the order of reservation to avoid deadlocks,
|
102
|
+
# the method blocks if the future or event is already reserved
|
103
|
+
# until it is released or resolved.
|
104
|
+
#
|
105
|
+
# @example
|
106
|
+
# f = Concurrent::Promises.resolvable_future
|
107
|
+
# reserved = f.reserve
|
108
|
+
# Thread.new { f.resolve true, :val, nil } # fails
|
109
|
+
# f.resolve true, :val, nil, true if reserved # must be called only if reserved
|
110
|
+
# @return [true, false] on successful reservation
|
111
|
+
def reserve
|
112
|
+
while true
|
113
|
+
return true if compare_and_set_internal_state(PENDING, RESERVED)
|
114
|
+
return false if resolved?
|
115
|
+
# FIXME (pitr-ch 17-Jan-2019): sleep until given up or resolved instead of busy wait
|
116
|
+
Thread.pass
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# @return [true, false] on successful release of the reservation
|
121
|
+
def release
|
122
|
+
compare_and_set_internal_state(RESERVED, PENDING)
|
123
|
+
end
|
124
|
+
|
125
|
+
# @return [Comparable] an item to sort the resolvable events or futures
|
126
|
+
# by to get the right global locking order of resolvable events or futures
|
127
|
+
# @see .atomic_resolution
|
128
|
+
def self.locking_order_by(resolvable)
|
129
|
+
resolvable.object_id
|
130
|
+
end
|
131
|
+
|
132
|
+
# Resolves all passed events and futures to the given resolutions
|
133
|
+
# if possible (all are unresolved) or none.
|
134
|
+
#
|
135
|
+
# @param [Hash{Resolvable=>resolve_arguments}, Array<Array(Resolvable, resolve_arguments)>] resolvable_map
|
136
|
+
# collection of resolvable events and futures which should be resolved all at once
|
137
|
+
# and what should they be resolved to, examples:
|
138
|
+
# ```ruby
|
139
|
+
# { a_resolvable_future1 => [true, :val, nil],
|
140
|
+
# a_resolvable_future2 => [false, nil, :err],
|
141
|
+
# a_resolvable_event => [] }
|
142
|
+
# ```
|
143
|
+
# or
|
144
|
+
# ```ruby
|
145
|
+
# [[a_resolvable_future1, [true, :val, nil]],
|
146
|
+
# [a_resolvable_future2, [false, nil, :err]],
|
147
|
+
# [a_resolvable_event, []]]
|
148
|
+
# ```
|
149
|
+
# @return [true, false] if success
|
150
|
+
def self.atomic_resolution(resolvable_map)
|
151
|
+
# atomic_resolution event => [], future => [true, :v, nil]
|
152
|
+
sorted = resolvable_map.to_a.sort_by { |resolvable, _| locking_order_by resolvable }
|
153
|
+
|
154
|
+
reserved = 0
|
155
|
+
while reserved < sorted.size && sorted[reserved].first.reserve
|
156
|
+
reserved += 1
|
157
|
+
end
|
158
|
+
|
159
|
+
if reserved == sorted.size
|
160
|
+
sorted.each { |resolvable, args| resolvable.resolve(*args, true, true) }
|
161
|
+
true
|
162
|
+
else
|
163
|
+
while reserved > 0
|
164
|
+
reserved -= 1
|
165
|
+
raise 'has to be reserved' unless sorted[reserved].first.release
|
166
|
+
end
|
167
|
+
false
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
|
203
173
|
end
|
204
174
|
end
|
@@ -1,102 +1,121 @@
|
|
1
1
|
module Concurrent
|
2
|
-
#
|
3
|
-
#
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
# max_two.throttled_block do
|
8
|
-
# # Only 2 at the same time
|
9
|
-
# do_stuff
|
10
|
-
# end
|
11
|
-
# end
|
12
|
-
# end
|
13
|
-
# @!macro throttle.example.throttled_future
|
14
|
-
# @example
|
15
|
-
# throttle.throttled_future(1) do |arg|
|
16
|
-
# arg.succ
|
17
|
-
# end
|
18
|
-
# @!macro throttle.example.throttled_future_chain
|
19
|
-
# @example
|
20
|
-
# throttle.throttled_future_chain do |trigger|
|
21
|
-
# trigger.
|
22
|
-
# # 2 throttled promises
|
23
|
-
# chain { 1 }.
|
24
|
-
# then(&:succ)
|
25
|
-
# end
|
26
|
-
# @!macro throttle.example.then_throttled_by
|
27
|
-
# @example
|
28
|
-
# data = (1..5).to_a
|
29
|
-
# db = data.reduce({}) { |h, v| h.update v => v.to_s }
|
30
|
-
# max_two = Throttle.new 2
|
2
|
+
# A tool managing concurrency level of tasks.
|
3
|
+
# The maximum capacity is set in constructor.
|
4
|
+
# Each acquire will lower the available capacity and release will increase it.
|
5
|
+
# When there is no available capacity the current thread may either be blocked or
|
6
|
+
# an event is returned which will be resolved when capacity becomes available.
|
31
7
|
#
|
32
|
-
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
#
|
37
|
-
#
|
38
|
-
#
|
39
|
-
# db[v]
|
40
|
-
# end
|
41
|
-
# end
|
8
|
+
# The more common usage of the Throttle is with a proxy executor
|
9
|
+
# `a_throttle.on(Concurrent.global_io_executor)`.
|
10
|
+
# Anything executed on the proxy executor will be throttled and
|
11
|
+
# execute on the given executor. There can be more than one proxy executors.
|
12
|
+
# All abstractions which execute tasks have option to specify executor,
|
13
|
+
# therefore the proxy executor can be injected to any abstraction
|
14
|
+
# throttling its concurrency level.
|
42
15
|
#
|
43
|
-
#
|
44
|
-
|
45
|
-
# A tool manage concurrency level of future tasks.
|
16
|
+
# {include:file:docs-source/throttle.out.md}
|
46
17
|
#
|
47
|
-
# @!macro throttle.example.then_throttled_by
|
48
|
-
# @!macro throttle.example.throttled_future
|
49
|
-
# @!macro throttle.example.throttled_future_chain
|
50
|
-
# @!macro throttle.example.throttled_block
|
51
18
|
# @!macro warn.edge
|
52
19
|
class Throttle < Synchronization::Object
|
53
|
-
# TODO (pitr-ch 21-Dec-2016): consider using sized channel for implementation instead when available
|
54
|
-
|
55
20
|
safe_initialization!
|
56
|
-
attr_atomic(:can_run)
|
57
|
-
private :can_run, :can_run=, :swap_can_run, :compare_and_set_can_run, :update_can_run
|
58
21
|
|
59
|
-
|
60
|
-
|
61
|
-
|
22
|
+
attr_atomic(:capacity)
|
23
|
+
private :capacity, :capacity=, :swap_capacity, :compare_and_set_capacity, :update_capacity
|
24
|
+
|
25
|
+
# @return [Integer] The available capacity.
|
26
|
+
def available_capacity
|
27
|
+
current_capacity = capacity
|
28
|
+
current_capacity >= 0 ? current_capacity : 0
|
29
|
+
end
|
30
|
+
|
31
|
+
# Create throttle.
|
32
|
+
# @param [Integer] capacity How many tasks using this throttle can run at the same time.
|
33
|
+
def initialize(capacity)
|
62
34
|
super()
|
63
|
-
@
|
64
|
-
|
65
|
-
@
|
35
|
+
@MaxCapacity = capacity
|
36
|
+
@Queue = LockFreeQueue.new
|
37
|
+
@executor_cache = [nil, nil]
|
38
|
+
self.capacity = capacity
|
66
39
|
end
|
67
40
|
|
68
|
-
# @return [Integer] The
|
69
|
-
def
|
70
|
-
@
|
41
|
+
# @return [Integer] The maximum capacity.
|
42
|
+
def max_capacity
|
43
|
+
@MaxCapacity
|
71
44
|
end
|
72
45
|
|
73
|
-
#
|
74
|
-
#
|
75
|
-
#
|
46
|
+
# Blocks current thread until there is capacity available in the throttle.
|
47
|
+
# The acquired capacity has to be returned to the throttle by calling {#release}.
|
48
|
+
# If block is passed then the block is called after the capacity is acquired and
|
49
|
+
# it is automatically released after the block is executed.
|
50
|
+
#
|
51
|
+
# @param [Numeric] timeout the maximum time in second to wait.
|
52
|
+
# @yield [] block to execute after the capacity is acquired
|
53
|
+
# @return [Object, self, true, false]
|
54
|
+
# * When no timeout and no block it returns self
|
55
|
+
# * When no timeout and with block it returns the result of the block
|
56
|
+
# * When with timeout and no block it returns true when acquired and false when timed out
|
57
|
+
# * When with timeout and with block it returns the result of the block of nil on timing out
|
76
58
|
# @see #release
|
77
|
-
def
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
59
|
+
def acquire(timeout = nil, &block)
|
60
|
+
event = acquire_or_event
|
61
|
+
if event
|
62
|
+
within_timeout = event.wait(timeout)
|
63
|
+
# release immediately when acquired later after the timeout since it is unused
|
64
|
+
event.on_resolution!(self, &:release) unless within_timeout
|
65
|
+
else
|
66
|
+
within_timeout = true
|
67
|
+
end
|
68
|
+
|
69
|
+
called = false
|
70
|
+
if timeout
|
71
|
+
if block
|
72
|
+
if within_timeout
|
73
|
+
called = true
|
74
|
+
block.call
|
83
75
|
else
|
84
|
-
|
85
|
-
@Queue.push event
|
86
|
-
return event
|
76
|
+
nil
|
87
77
|
end
|
78
|
+
else
|
79
|
+
within_timeout
|
80
|
+
end
|
81
|
+
else
|
82
|
+
if block
|
83
|
+
called = true
|
84
|
+
block.call
|
85
|
+
else
|
86
|
+
self
|
87
|
+
end
|
88
|
+
end
|
89
|
+
ensure
|
90
|
+
release if called
|
91
|
+
end
|
92
|
+
|
93
|
+
# Tries to acquire capacity from the throttle.
|
94
|
+
# Returns true when there is capacity available.
|
95
|
+
# The acquired capacity has to be returned to the throttle by calling {#release}.
|
96
|
+
# @return [true, false]
|
97
|
+
# @see #release
|
98
|
+
def try_acquire
|
99
|
+
while true
|
100
|
+
current_capacity = capacity
|
101
|
+
if current_capacity > 0
|
102
|
+
return true if compare_and_set_capacity(
|
103
|
+
current_capacity, current_capacity - 1)
|
104
|
+
else
|
105
|
+
return false
|
88
106
|
end
|
89
107
|
end
|
90
108
|
end
|
91
109
|
|
92
|
-
#
|
110
|
+
# Releases previously acquired capacity back to Throttle.
|
111
|
+
# Has to be called exactly once for each acquired capacity.
|
93
112
|
# @return [self]
|
94
|
-
# @see #
|
113
|
+
# @see #acquire_operation, #acquire, #try_acquire
|
95
114
|
def release
|
96
115
|
while true
|
97
|
-
|
98
|
-
if
|
99
|
-
if
|
116
|
+
current_capacity = capacity
|
117
|
+
if compare_and_set_capacity current_capacity, current_capacity + 1
|
118
|
+
if current_capacity < 0
|
100
119
|
# release called after trigger which pushed a trigger, busy wait is ok
|
101
120
|
Thread.pass until (trigger = @Queue.pop)
|
102
121
|
trigger.resolve
|
@@ -106,94 +125,101 @@ module Concurrent
|
|
106
125
|
end
|
107
126
|
end
|
108
127
|
|
109
|
-
# Blocks current thread until the block can be executed.
|
110
|
-
# @yield to throttled block
|
111
|
-
# @yieldreturn [Object] is used as a result of the method
|
112
|
-
# @return [Object] the result of the block
|
113
|
-
# @!macro throttle.example.throttled_block
|
114
|
-
def throttled_block(&block)
|
115
|
-
trigger.wait
|
116
|
-
block.call
|
117
|
-
ensure
|
118
|
-
release
|
119
|
-
end
|
120
|
-
|
121
128
|
# @return [String] Short string representation.
|
122
129
|
def to_s
|
123
|
-
format '%s
|
130
|
+
format '%s capacity available %d of %d>', super[0..-2], capacity, @MaxCapacity
|
124
131
|
end
|
125
132
|
|
126
133
|
alias_method :inspect, :to_s
|
127
134
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
135
|
+
# @!visibility private
|
136
|
+
def acquire_or_event
|
137
|
+
while true
|
138
|
+
current_capacity = capacity
|
139
|
+
if compare_and_set_capacity current_capacity, current_capacity - 1
|
140
|
+
if current_capacity > 0
|
141
|
+
return nil
|
142
|
+
else
|
143
|
+
event = Promises.resolvable_event
|
144
|
+
@Queue.push event
|
145
|
+
return event
|
146
|
+
end
|
147
|
+
end
|
139
148
|
end
|
149
|
+
end
|
140
150
|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
151
|
+
include Promises::FactoryMethods
|
152
|
+
|
153
|
+
# @param [ExecutorService] executor
|
154
|
+
# @return [ExecutorService] An executor which wraps given executor and allows to post tasks only
|
155
|
+
# as available capacity in the throttle allows.
|
156
|
+
# @example throttling future
|
157
|
+
# a_future.then_on(a_throttle.on(:io)) { a_throttled_task }
|
158
|
+
def on(executor = Promises::FactoryMethods.default_executor)
|
159
|
+
current_executor, current_cache = @executor_cache
|
160
|
+
return current_cache if current_executor == executor && current_cache
|
161
|
+
|
162
|
+
if current_executor.nil?
|
163
|
+
# cache first proxy
|
164
|
+
proxy_executor = ProxyExecutor.new(self, Concurrent.executor(executor))
|
165
|
+
@executor_cache = [executor, proxy_executor]
|
166
|
+
return proxy_executor
|
167
|
+
else
|
168
|
+
# do not cache more than 1 executor
|
169
|
+
ProxyExecutor.new(self, Concurrent.executor(executor))
|
147
170
|
end
|
148
171
|
end
|
149
172
|
|
150
|
-
|
151
|
-
|
173
|
+
# Uses executor provided by {#on} therefore
|
174
|
+
# all events and futures created using factory methods on this object will be throttled.
|
175
|
+
# Overrides {Promises::FactoryMethods#default_executor}.
|
176
|
+
#
|
177
|
+
# @return [ExecutorService]
|
178
|
+
# @see Promises::FactoryMethods#default_executor
|
179
|
+
def default_executor
|
180
|
+
on(super)
|
181
|
+
end
|
152
182
|
|
153
|
-
|
183
|
+
class ProxyExecutor < Synchronization::Object
|
184
|
+
safe_initialization!
|
154
185
|
|
155
|
-
|
156
|
-
module ThrottleIntegration
|
186
|
+
include ExecutorService
|
157
187
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
throttled_futures.call(a_trigger).on_resolution! { throttle.release }
|
164
|
-
end
|
188
|
+
def initialize(throttle, executor)
|
189
|
+
super()
|
190
|
+
@Throttle = throttle
|
191
|
+
@Executor = executor
|
192
|
+
end
|
165
193
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
194
|
+
def post(*args, &task)
|
195
|
+
if (event = @Throttle.acquire_or_event)
|
196
|
+
event.on_resolution! { inner_post(*args, &task) }
|
197
|
+
else
|
198
|
+
inner_post(*args, &task)
|
171
199
|
end
|
172
200
|
end
|
173
201
|
|
174
|
-
|
175
|
-
|
202
|
+
def can_overflow?
|
203
|
+
@Executor.can_overflow?
|
204
|
+
end
|
176
205
|
|
177
|
-
|
178
|
-
|
206
|
+
def serialized?
|
207
|
+
@Executor.serialized?
|
208
|
+
end
|
179
209
|
|
180
|
-
|
181
|
-
# @return [Future]
|
182
|
-
# @see Future#then
|
183
|
-
# @!macro throttle.example.then_throttled_by
|
184
|
-
def then_throttled_by(throttle, *args, &block)
|
185
|
-
throttled_by(throttle) { |trigger| trigger.then(*args, &block) }
|
186
|
-
end
|
210
|
+
private
|
187
211
|
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
212
|
+
def inner_post(*arguments, &task)
|
213
|
+
@Executor.post(*arguments) do |*args|
|
214
|
+
begin
|
215
|
+
task.call(*args)
|
216
|
+
ensure
|
217
|
+
@Throttle.release
|
218
|
+
end
|
193
219
|
end
|
194
220
|
end
|
195
|
-
|
196
|
-
include ThrottleIntegration
|
197
221
|
end
|
222
|
+
|
223
|
+
private_constant :ProxyExecutor
|
198
224
|
end
|
199
225
|
end
|