Class: Ione::Future

Inherits:
Object
  • Object
show all
Extended by:
Factories
Includes:
Callbacks, Combinators
Defined in:
lib/ione/future.rb

Overview

A future represents the value of a process that may not yet have completed.

A future is either pending or completed and there are two ways to complete a future: either by resolving it to a value, or by failing it.

A future is usually created by first creating a Promise and returning that promise's future to the caller. The promise can then be fulfilled which resolves the future – see below for an example of thi.

The key thing about futures is that they compose. If you have a future you can transform it and combine without waiting for its value to be available. This means that you can model a series of asynchronous operations without worrying about which order they will complete in, or what happens if any of them fail. You can describe the steps you want to happen if all goes well, and add a handler at the end to capture errors, like you can with synchronous code and exception handlers. You can also add steps that recover from failures. See below, and the docs for Combinators for examples on how to compose asynchronous operations.

The mixins Combinators, Callbacks and Factories contain most of the method you would use to work with futures, and can be used for creating bridges to other futures implementations.

Examples:

Creating a future for a blocking operation

def find_my_ip
  promise = Promise.new
  Thread.start do
    begin
      data = JSON.load(open('http://jsonip.org/').read)
      promise.fulfill(data['ip'])
    rescue => e
      promise.fail(e)
    end
  end
  promise.future
end

Transforming futures

# find_my_ip is the method from the example above
ip_future = find_my_ip
# Future#map returns a new future that resolves to the value returned by the
# block, but the block is not called immediately, but when the receiving
# future resolves – this means that we can descrbe the processing steps that
# should be performed without having to worry about when the value becomes
# available.
ipaddr_future = ip_future.map { |ip| IPAddr.new(ip) }

Composing asynchronous operations

# find_my_ip is the method from the example above
ip_future = find_my_ip
# Future#flat_map is a way of chaining asynchronous operations, the future
# it returns will resolve to the value of the future returned by the block,
# but the block is not called until the receiver future resolves
location_future = ip_future.flat_map do |ip|
  # assuming resolve_geoip is a method returning a future
  resolve_geoip(ip)
end
# scheduler here could be an instance of Ione::Io::IoReactor
timer_future = scheduler.schedule_timer(5)
# Future.first returns a future that will resolve to the value of the
# first of its children that completes, so you can use it in combination
# with a scheduler to make sure you don't wait forever
location_or_timeout_future = Future.first(location_future, timer_future)
location_or_timeout_future.on_value do |location|
  if location
    puts "My location is #{location}"
  end
end

Making requests in parallel and collecting the results

# assuming client is a client for a remote service and that #find returns
# a future, and that thing_ids is an array of IDs of things we want to load
futures = thing_idss.map { |id| client.find(id) }
# Future.all is a way to combine multiple futures into a future that resolves
# to an array of values, in other words, it takes an array of futures and
# resolves to an array of the values of those futures
future_of_all_things = Future.all(futures)
future_of_all_things.on_value do |things|
  things.each do |thing|
    puts "here's a thing: #{thing}"
  end
end

Another way of making requests in parallel and collecting the results

# the last example can be simplified by using Future.traverse, which combines
# Array#map with Future.all – the block will be called once per item in
# the array, and the returned future resolves to an array of the values of
# the futures returned by the block
future_of_all_things = Future.traverse(thing_ids) { |id| client.find(id) }
future_of_all_things.on_value do |things|
  things.each do |thing|
    puts "here's a thing: #{thing}"
  end
end

See Also:

  • Promise
  • FutureCallbacks
  • FutureCombinators
  • FutureFactories

Since:

  • v1.0.0

Defined Under Namespace

Modules: Callbacks, Combinators, Factories

Constant Summary collapse

PENDING_STATE =

Since:

  • v1.0.0

0
RESOLVED_STATE =

Since:

  • v1.0.0

1
FAILED_STATE =

Since:

  • v1.0.0

2

Instance Method Summary collapse

Methods included from Factories

after, all, failed, first, reduce, resolved, traverse

Methods included from Callbacks

#on_failure, #on_value

Methods included from Combinators

#fallback, #flat_map, #map, #recover, #then

Instance Method Details

#completed?Boolean

Returns true if this future is resolved or failed

Returns:

  • (Boolean)

Since:

  • v1.0.0



712
713
714
715
716
717
718
719
720
# File 'lib/ione/future.rb', line 712

def completed?
  return true unless @state == PENDING_STATE
  @lock.lock
  begin
    @state != PENDING_STATE
  ensure
    @lock.unlock
  end
end

#failed?Boolean

Returns true if this future has failed

Returns:

  • (Boolean)

Since:

  • v1.0.0



734
735
736
737
738
739
740
741
742
# File 'lib/ione/future.rb', line 734

def failed?
  return @state == FAILED_STATE unless @state == PENDING_STATE
  @lock.lock
  begin
    @state == FAILED_STATE
  ensure
    @lock.unlock
  end
end

#on_complete {|value, error, future| ... } ⇒ Object

Note:

Depending on the arity of the listener it will be passed different arguments. When the listener takes one argument it will receive the future itself as argument (this is backwards compatible with the pre v1.2 behaviour), with two arguments the value and error are given, with three arguments the value, error and the future itself will be given. The listener can also take no arguments. See the tests to find out the nitty-gritty details, for example the behaviour with different combinations of variable arguments and default values.

Most of the time you will use Ione::Future::Callbacks#on_value and Ione::Future::Callbacks#on_failure, and not instead of this method.

Registers a listener that will be called when this future completes, i.e. resolves or fails. The listener will be called with the future as solve argument.

The order in which listeners are called is not defined and implementation dependent. The thread the listener will be called on is also not defined and implementation dependent. The default implementation calls listeners registered before completion on the thread that completed the future, and listeners registered after completions on the thread that registers the listener – but this may change in the future, and may be different in special circumstances.

When a listener raises an error it will be swallowed and not re-raised. The reason for this is that the processing of the callback may be done in a context that does not expect, nor can recover from, errors. Not swallowing errors would stop other listeners from being called. If it appears as if a listener is not called, first make sure it is not raising any errors (even a syntax error or a spelling mistake in a method or variable name will not be hidden).

Yield Parameters:

  • value (Object)

    the value that the future resolves to

  • error (Error)

    the error that failed this future

  • future (Ione::Future)

    the future itself

See Also:

Since:

  • v1.0.0



644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
# File 'lib/ione/future.rb', line 644

def on_complete(&listener)
  run_immediately = false
  if @state != PENDING_STATE
    run_immediately = true
  else
    @lock.lock
    begin
      if @state == PENDING_STATE
        @listeners << listener
      else
        run_immediately = true
      end
    ensure
      @lock.unlock
    end
  end
  if run_immediately
    call_listener(listener)
  end
  nil
end

#resolved?Boolean

Returns true if this future is resolved

Returns:

  • (Boolean)

Since:

  • v1.0.0



723
724
725
726
727
728
729
730
731
# File 'lib/ione/future.rb', line 723

def resolved?
  return @state == RESOLVED_STATE unless @state == PENDING_STATE
  @lock.lock
  begin
    @state == RESOLVED_STATE
  ensure
    @lock.unlock
  end
end

#valueObject Also known as: get

Note:

This is a blocking operation and should be used with caution. You should never call this method in a block given to any of the other methods on Ione::Future. Prefer using combinator methods like Ione::Future::Combinators#map and Ione::Future::Combinators#flat_map to compose operations asynchronously, or use Ione::Future::Callbacks#on_value, Ione::Future::Callbacks#on_failure or #on_complete to listen for values and/or failures.

Returns the value of this future, blocking until it is available if necessary.

If the future fails this method will raise the error that failed the future.

Returns:

  • (Object)

    the value of this future

Raises:

  • (Error)

    the error that failed this future

See Also:

Since:

  • v1.0.0



684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
# File 'lib/ione/future.rb', line 684

def value
  raise @error if @state == FAILED_STATE
  return @value if @state == RESOLVED_STATE
  semaphore = nil
  @lock.lock
  begin
    raise @error if @state == FAILED_STATE
    return @value if @state == RESOLVED_STATE
    semaphore = Queue.new
    u = proc { semaphore << :unblock }
    @listeners << u
  ensure
    @lock.unlock
  end
  while true
    @lock.lock
    begin
      raise @error if @state == FAILED_STATE
      return @value if @state == RESOLVED_STATE
    ensure
      @lock.unlock
    end
    semaphore.pop
  end
end