• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jdantonio / concurrent-ruby / #812

23 May 2014 08:08PM UTC coverage: 39.344% (-57.9%) from 97.237%
#812

push

jdantonio
Merge pull request #96 from jdantonio/refactor/errors

Moved all custom errors into a single file and into the Concurrent module

17 of 18 new or added lines in 10 files covered. (94.44%)

1435 existing lines in 57 files now uncovered.

1187 of 3017 relevant lines covered (39.34%)

0.5 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

35.94
/lib/concurrent/async.rb
1
require 'thread'
1✔
2
require 'concurrent/configuration'
1✔
3
require 'concurrent/delay'
1✔
4
require 'concurrent/errors'
1✔
5
require 'concurrent/ivar'
1✔
6
require 'concurrent/future'
1✔
7
require 'concurrent/executor/thread_pool_executor'
1✔
8

9
module Concurrent
1✔
10

11
  # A mixin module that provides simple asynchronous behavior to any standard
12
  # class/object or object. 
13
  #
14
  #   Scenario:
15
  #     As a stateful, plain old Ruby class/object
16
  #     I want safe, asynchronous behavior
17
  #     So my long-running methods don't block the main thread
18
  #
19
  # Stateful, mutable objects must be managed carefully when used asynchronously.
20
  # But Ruby is an object-oriented language so designing with objects and classes
21
  # plays to Ruby's strengths and is often more natural to many Ruby programmers.
22
  # The `Async` module is a way to mix simple yet powerful asynchronous capabilities
23
  # into any plain old Ruby object or class. These capabilities provide a reasonable
24
  # level of thread safe guarantees when used correctly.
25
  #
26
  # When this module is mixed into a class or object it provides to new methods:
27
  # `async` and `await`. These methods are thread safe with respect to the enclosing
28
  # object. The former method allows methods to be called asynchronously by posting
29
  # to the global thread pool. The latter allows a method to be called synchronously
30
  # on the current thread but does so safely with respect to any pending asynchronous
31
  # method calls. Both methods return an `Obligation` which can be inspected for
32
  # the result of the method call. Calling a method with `async` will return a
33
  # `:pending` `Obligation` whereas `await` will return a `:complete` `Obligation`.
34
  #
35
  # Very loosely based on the `async` and `await` keywords in C#.
36
  #
37
  # @example Defining an asynchronous class
38
  #   class Echo
39
  #     include Concurrent::Async
40
  #
41
  #     def initialize
42
  #       init_mutex # initialize the internal synchronization objects
43
  #     end
44
  #
45
  #     def echo(msg)
46
  #       sleep(rand)
47
  #       print "#{msg}\n"
48
  #       nil
49
  #     end
50
  #   end
51
  #   
52
  #   horn = Echo.new
53
  #   horn.echo('zero') # synchronous, not thread-safe
54
  #
55
  #   horn.async.echo('one') # asynchronous, non-blocking, thread-safe
56
  #   horn.await.echo('two') # synchronous, blocking, thread-safe
57
  #
58
  # @example Monkey-patching an existing object
59
  #   numbers = 1_000_000.times.collect{ rand }
60
  #   numbers.extend(Concurrent::Async)
61
  #   numbers.init_mutex # initialize the internal synchronization objects
62
  #   
63
  #   future = numbers.async.max
64
  #   future.state #=> :pending
65
  #   
66
  #   sleep(2)
67
  #   
68
  #   future.state #=> :fulfilled
69
  #   future.value #=> 0.999999138918843
70
  #
71
  # @note This module depends on several internal synchronization objects that
72
  #       must be initialized prior to calling any of the async/await/executor methods.
73
  #       The best practice is to call `init_mutex` from within the constructor
74
  #       of the including class. A less ideal but acceptable practice is for the
75
  #       thread creating the asynchronous object to explicitly call the `init_mutex`
76
  #       method prior to calling any of the async/await/executor methods. If
77
  #       `init_mutex` is *not* called explicitly the async/await/executor methods
78
  #       will raize a `Concurrent::InitializationError`. This is the only way 
79
  #       thread-safe initialization can be guaranteed.
80
  #
81
  # @note Thread safe guarantees can only be made when asynchronous method calls
82
  #       are not mixed with synchronous method calls. Use only synchronous calls
83
  #       when the object is used exclusively on a single thread. Use only
84
  #       `async` and `await` when the object is shared between threads. Once you
85
  #       call a method using `async`, you should no longer call any methods
86
  #       directly on the object. Use `async` and `await` exclusively from then on.
87
  #       With careful programming it is possible to switch back and forth but it's
88
  #       also very easy to create race conditions and break your application.
89
  #       Basically, it's "async all the way down."
90
  #
91
  # @since 0.6.0
92
  #
93
  # @see Concurrent::Obligation
94
  module Async
1✔
95

96
    # Check for the presence of a method on an object and determine if a given
97
    # set of arguments matches the required arity.
98
    #
99
    # @param [Object] obj the object to check against
100
    # @param [Symbol] method the method to check the object for
101
    # @param [Array] args zero or more arguments for the arity check
102
    #
103
    # @raise [NameError] the object does not respond to `method` method
104
    # @raise [ArgumentError] the given `args` do not match the arity of `method`
105
    #
106
    # @note This check is imperfect because of the way Ruby reports the arity of
107
    #   methods with a variable number of arguments. It is possible to determine
108
    #   if too few arguments are given but impossible to determine if too many
109
    #   arguments are given. This check may also fail to recognize dynamic behavior
110
    #   of the object, such as methods simulated with `method_missing`.
111
    #
112
    # @see http://www.ruby-doc.org/core-2.1.1/Method.html#method-i-arity Method#arity
113
    # @see http://ruby-doc.org/core-2.1.0/Object.html#method-i-respond_to-3F Object#respond_to?
114
    # @see http://www.ruby-doc.org/core-2.1.0/BasicObject.html#method-i-method_missing BasicObject#method_missing
115
    def validate_argc(obj, method, *args)
1✔
UNCOV
116
      argc = args.length
×
UNCOV
117
      arity = obj.method(method).arity
×
118

UNCOV
119
      if arity >= 0 && argc != arity
×
UNCOV
120
        raise ArgumentError.new("wrong number of arguments (#{argc} for #{arity})")
×
UNCOV
121
      elsif arity < 0 && (arity = (arity + 1).abs) > argc
×
UNCOV
122
        raise ArgumentError.new("wrong number of arguments (#{argc} for #{arity}..*)")
×
123
      end
124
    end
125
    module_function :validate_argc
1✔
126

127
    # Delegates synchronous, thread-safe method calls to the wrapped object.
128
    #
129
    # @!visibility private
130
    class AwaitDelegator # :nodoc:
1✔
131

132
      # Create a new delegator object wrapping the given `delegate` and
133
      # protecting it with the given `mutex`.
134
      #
135
      # @param [Object] delegate the object to wrap and delegate method calls to
136
      # @param [Mutex] mutex the mutex lock to use when delegating method calls
137
      def initialize(delegate, mutex)
1✔
UNCOV
138
        @delegate = delegate
×
UNCOV
139
        @mutex = mutex
×
140
      end
141

142
      # Delegates method calls to the wrapped object. For performance,
143
      # dynamically defines the given method on the delegator so that
144
      # all future calls to `method` will not be directed here.
145
      #
146
      # @param [Symbol] method the method being called
147
      # @param [Array] args zero or more arguments to the method
148
      #
149
      # @return [IVar] the result of the method call
150
      #
151
      # @raise [NameError] the object does not respond to `method` method
152
      # @raise [ArgumentError] the given `args` do not match the arity of `method`
153
      def method_missing(method, *args, &block)
1✔
UNCOV
154
        super unless @delegate.respond_to?(method)
×
UNCOV
155
        Async::validate_argc(@delegate, method, *args)
×
156

UNCOV
157
        self.define_singleton_method(method) do |*args|
×
UNCOV
158
          Async::validate_argc(@delegate, method, *args)
×
UNCOV
159
          ivar = Concurrent::IVar.new
×
UNCOV
160
          value, reason = nil, nil
×
161
          begin
UNCOV
162
            @mutex.synchronize do
×
UNCOV
163
              value = @delegate.send(method, *args, &block)
×
164
            end
165
          rescue => reason
166
            # caught
167
          ensure
UNCOV
168
            return ivar.complete(reason.nil?, value, reason)
×
169
          end
170
        end
171

UNCOV
172
        self.send(method, *args)
×
173
      end
174
    end
175

176
    # Delegates asynchronous, thread-safe method calls to the wrapped object.
177
    #
178
    # @!visibility private
179
    class AsyncDelegator # :nodoc:
1✔
180

181
      # Create a new delegator object wrapping the given `delegate` and
182
      # protecting it with the given `mutex`.
183
      #
184
      # @param [Object] delegate the object to wrap and delegate method calls to
185
      # @param [Mutex] mutex the mutex lock to use when delegating method calls
186
      def initialize(delegate, executor, mutex)
1✔
UNCOV
187
        @delegate = delegate
×
UNCOV
188
        @executor = executor
×
UNCOV
189
        @mutex = mutex
×
190
      end
191

192
      # Delegates method calls to the wrapped object. For performance,
193
      # dynamically defines the given method on the delegator so that
194
      # all future calls to `method` will not be directed here.
195
      #
196
      # @param [Symbol] method the method being called
197
      # @param [Array] args zero or more arguments to the method
198
      #
199
      # @return [IVar] the result of the method call
200
      #
201
      # @raise [NameError] the object does not respond to `method` method
202
      # @raise [ArgumentError] the given `args` do not match the arity of `method`
203
      def method_missing(method, *args, &block)
1✔
UNCOV
204
        super unless @delegate.respond_to?(method)
×
UNCOV
205
        Async::validate_argc(@delegate, method, *args)
×
206

UNCOV
207
        self.define_singleton_method(method) do |*args|
×
UNCOV
208
          Async::validate_argc(@delegate, method, *args)
×
UNCOV
209
          Concurrent::Future.execute(executor: @executor.value) do
×
UNCOV
210
            @mutex.synchronize do
×
UNCOV
211
              @delegate.send(method, *args, &block)
×
212
            end
213
          end
214
        end
215

UNCOV
216
        self.send(method, *args)
×
217
      end
218
    end
219

220
    # Causes the chained method call to be performed asynchronously on the
221
    # global thread pool. The method called by this method will return a
222
    # `Future` object in the `:pending` state and the method call will have
223
    # been scheduled on the global thread pool. The final disposition of the
224
    # method call can be obtained by inspecting the returned `Future`.
225
    #
226
    # Before scheduling the method on the global thread pool a best-effort
227
    # attempt will be made to validate that the method exists on the object
228
    # and that the given arguments match the arity of the requested function.
229
    # Due to the dynamic nature of Ruby and limitations of its reflection
230
    # library, some edge cases will be missed. For more information see
231
    # the documentation for the `validate_argc` method.
232
    #
233
    # @note The method call is guaranteed to be thread safe  with respect to
234
    #   all other method calls against the same object that are called with
235
    #   either `async` or `await`. The mutable nature of Ruby references
236
    #   (and object orientation in general) prevent any other thread safety
237
    #   guarantees. Do NOT mix non-protected method calls with protected
238
    #   method call. Use *only* protected method calls when sharing the object
239
    #   between threads.
240
    #
241
    # @return [Concurrent::Future] the pending result of the asynchronous operation
242
    #
243
    # @raise [Concurrent::InitializationError] `#init_mutex` has not been called
244
    # @raise [NameError] the object does not respond to `method` method
245
    # @raise [ArgumentError] the given `args` do not match the arity of `method`
246
    #
247
    # @see Concurrent::Future
248
    def async
1✔
UNCOV
249
      raise InitializationError.new('#init_mutex was never called') unless @__async__mutex__
×
UNCOV
250
      @__async_delegator__.value
×
251
    end
252
    alias_method :future, :async
1✔
253

254
    # Causes the chained method call to be performed synchronously on the
255
    # current thread. The method called by this method will return an
256
    # `IVar` object in either the `:fulfilled` or `rejected` state and the
257
    # method call will have completed. The final disposition of the
258
    # method call can be obtained by inspecting the returned `IVar`.
259
    #
260
    # Before scheduling the method on the global thread pool a best-effort
261
    # attempt will be made to validate that the method exists on the object
262
    # and that the given arguments match the arity of the requested function.
263
    # Due to the dynamic nature of Ruby and limitations of its reflection
264
    # library, some edge cases will be missed. For more information see
265
    # the documentation for the `validate_argc` method.
266
    #
267
    # @note The method call is guaranteed to be thread safe  with respect to
268
    #   all other method calls against the same object that are called with
269
    #   either `async` or `await`. The mutable nature of Ruby references
270
    #   (and object orientation in general) prevent any other thread safety
271
    #   guarantees. Do NOT mix non-protected method calls with protected
272
    #   method call. Use *only* protected method calls when sharing the object
273
    #   between threads.
274
    #
275
    # @return [Concurrent::IVar] the completed result of the synchronous operation
276
    #
277
    # @raise [Concurrent::InitializationError] `#init_mutex` has not been called
278
    # @raise [NameError] the object does not respond to `method` method
279
    # @raise [ArgumentError] the given `args` do not match the arity of `method`
280
    #
281
    # @see Concurrent::IVar
282
    def await
1✔
UNCOV
283
      raise InitializationError.new('#init_mutex was never called') unless @__async__mutex__
×
UNCOV
284
      @__await_delegator__.value
×
285
    end
286
    alias_method :delay, :await
1✔
287

288
    # Set a new executor
289
    #
290
    # @raise [Concurrent::InitializationError] `#init_mutex` has not been called
291
    # @raise [ArgumentError] executor has already been set
292
    def executor=(executor)
1✔
UNCOV
293
      raise InitializationError.new('#init_mutex was never called') unless @__async__mutex__
×
UNCOV
294
      @__async__executor__.reconfigure { executor } or
×
295
        raise ArgumentError.new('executor has already been set')
296
    end
297

298
    # Initialize the internal mutex and other synchronization objects. This method
299
    # *must* be called from the constructor of the including class or explicitly
300
    # by the caller prior to calling any other methods. If `init_mutex` is *not*
301
    # called explicitly the async/await/executor methods will raize a
302
    # `Concurrent::InitializationError`. This is the only way thread-safe
303
    # initialization can be guaranteed.
304
    #
305
    # @note This method *must* be called from the constructor of the including
306
    #       class or explicitly by the caller prior to calling any other methods.
307
    #       This is the only way thread-safe initialization can be guaranteed.
308
    #
309
    # @raise [Concurrent::InitializationError] when called more than once
310
    def init_mutex
1✔
UNCOV
311
      raise InitializationError.new('#init_mutex was already called') if @__async__mutex__
×
UNCOV
312
      (@__async__mutex__ = Mutex.new).lock
×
UNCOV
313
      @__async__executor__ = Delay.new{ Concurrent.configuration.global_operation_pool }
×
UNCOV
314
      @__await_delegator__ = Delay.new{ AwaitDelegator.new(self, @__async__mutex__) }
×
UNCOV
315
      @__async_delegator__ = Delay.new{ AsyncDelegator.new(self, @__async__executor__, @__async__mutex__) }
×
UNCOV
316
      @__async__mutex__.unlock
×
317
    end
318
  end
319
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc