cql-rb 1.1.0.pre3 → 1.1.0.pre6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/README.md +2 -2
  2. data/lib/cql/client.rb +9 -5
  3. data/lib/cql/client/asynchronous_client.rb +105 -192
  4. data/lib/cql/client/asynchronous_prepared_statement.rb +51 -9
  5. data/lib/cql/client/connection_helper.rb +155 -0
  6. data/lib/cql/client/connection_manager.rb +56 -0
  7. data/lib/cql/client/keyspace_changer.rb +27 -0
  8. data/lib/cql/client/null_logger.rb +21 -0
  9. data/lib/cql/client/request_runner.rb +5 -3
  10. data/lib/cql/client/synchronous_client.rb +5 -5
  11. data/lib/cql/client/synchronous_prepared_statement.rb +4 -8
  12. data/lib/cql/future.rb +320 -210
  13. data/lib/cql/io/connection.rb +5 -5
  14. data/lib/cql/io/io_reactor.rb +21 -23
  15. data/lib/cql/protocol/cql_protocol_handler.rb +69 -38
  16. data/lib/cql/protocol/encoding.rb +5 -1
  17. data/lib/cql/protocol/requests/register_request.rb +2 -0
  18. data/lib/cql/protocol/type_converter.rb +1 -0
  19. data/lib/cql/version.rb +1 -1
  20. data/spec/cql/client/asynchronous_client_spec.rb +368 -175
  21. data/spec/cql/client/asynchronous_prepared_statement_spec.rb +132 -22
  22. data/spec/cql/client/connection_helper_spec.rb +335 -0
  23. data/spec/cql/client/connection_manager_spec.rb +118 -0
  24. data/spec/cql/client/keyspace_changer_spec.rb +50 -0
  25. data/spec/cql/client/request_runner_spec.rb +12 -12
  26. data/spec/cql/client/synchronous_client_spec.rb +15 -15
  27. data/spec/cql/client/synchronous_prepared_statement_spec.rb +15 -11
  28. data/spec/cql/future_spec.rb +529 -301
  29. data/spec/cql/io/connection_spec.rb +12 -12
  30. data/spec/cql/io/io_reactor_spec.rb +61 -61
  31. data/spec/cql/protocol/cql_protocol_handler_spec.rb +26 -12
  32. data/spec/cql/protocol/encoding_spec.rb +5 -0
  33. data/spec/cql/protocol/type_converter_spec.rb +1 -1
  34. data/spec/cql/time_uuid_spec.rb +7 -7
  35. data/spec/integration/client_spec.rb +2 -2
  36. data/spec/integration/io_spec.rb +20 -20
  37. data/spec/integration/protocol_spec.rb +17 -17
  38. data/spec/integration/regression_spec.rb +6 -0
  39. data/spec/integration/uuid_spec.rb +4 -0
  40. data/spec/support/fake_io_reactor.rb +38 -8
  41. data/spec/support/fake_server.rb +3 -3
  42. metadata +12 -2
@@ -0,0 +1,155 @@
1
+ # encoding: utf-8
2
+
3
+ module Cql
4
+ module Client
5
+ # @private
6
+ class ConnectionHelper
7
+ def initialize(io_reactor, port, credentials, connection_timeout, logger)
8
+ @io_reactor = io_reactor
9
+ @port = port
10
+ @credentials = credentials
11
+ @connection_timeout = connection_timeout
12
+ @logger = logger
13
+ @request_runner = RequestRunner.new
14
+ @keyspace_changer = KeyspaceChanger.new
15
+ end
16
+
17
+ def connect(hosts, initial_keyspace)
18
+ f = @io_reactor.start.flat_map do
19
+ connect_to_hosts(hosts, initial_keyspace, true)
20
+ end
21
+ f = f.map do |connections|
22
+ connected_connections = connections.select(&:connected?)
23
+ if connected_connections.empty?
24
+ e = connections.first.error
25
+ if e.is_a?(Cql::QueryError) && e.code == 0x100
26
+ e = AuthenticationError.new(e.message)
27
+ end
28
+ raise e
29
+ end
30
+ connected_connections
31
+ end
32
+ f
33
+ end
34
+
35
+ def discover_peers(seed_connections, initial_keyspace)
36
+ @logger.debug('Looking for additional nodes')
37
+ connection = seed_connections.sample
38
+ return Future.resolved([]) unless connection
39
+ request = Protocol::QueryRequest.new('SELECT peer, data_center, host_id, rpc_address FROM system.peers', :one)
40
+ peer_info = @request_runner.execute(connection, request)
41
+ peer_info.flat_map do |result|
42
+ seed_dcs = seed_connections.map { |c| c[:data_center] }.uniq
43
+ unconnected_peers = result.select do |row|
44
+ seed_dcs.include?(row['data_center']) && seed_connections.none? { |c| c[:host_id] == row['host_id'] }
45
+ end
46
+ if unconnected_peers.empty?
47
+ @logger.debug('No additional nodes found')
48
+ else
49
+ @logger.debug('%d additional nodes found' % unconnected_peers.size)
50
+ end
51
+ node_addresses = unconnected_peers.map do |row|
52
+ rpc_address = row['rpc_address'].to_s
53
+ if rpc_address == '0.0.0.0'
54
+ row['peer'].to_s
55
+ else
56
+ rpc_address
57
+ end
58
+ end
59
+ if node_addresses.any?
60
+ connect_to_hosts(node_addresses, initial_keyspace, false)
61
+ else
62
+ Future.resolved([])
63
+ end
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ def connect_to_hosts(hosts, initial_keyspace, peer_discovery)
70
+ connection_futures = hosts.map do |host|
71
+ connect_to_host(host, initial_keyspace).recover do |error|
72
+ FailedConnection.new(error, host, @port)
73
+ end
74
+ end
75
+ connection_futures.each do |cf|
76
+ cf.on_value do |c|
77
+ if c.is_a?(FailedConnection)
78
+ @logger.warn('Failed connecting to node at %s:%d: %s' % [c.host, c.port, c.error.message])
79
+ else
80
+ @logger.info('Connected to node %s at %s:%d in data center %s' % [c[:host_id], c.host, c.port, c[:data_center]])
81
+ end
82
+ c.on_closed do
83
+ @logger.warn('Connection to node %s at %s:%d in data center %s unexpectedly closed' % [c[:host_id], c.host, c.port, c[:data_center]])
84
+ end
85
+ end
86
+ end
87
+ hosts_connected_future = Future.all(*connection_futures)
88
+ if peer_discovery
89
+ hosts_connected_future.flat_map do |connections|
90
+ discover_peers(connections.select(&:connected?), initial_keyspace).map do |peer_connections|
91
+ connections + peer_connections
92
+ end
93
+ end
94
+ else
95
+ hosts_connected_future
96
+ end
97
+ end
98
+
99
+ def connect_to_host(host, keyspace)
100
+ @logger.debug('Connecting to node at %s:%d' % [host, @port])
101
+ connected = @io_reactor.connect(host, @port, @connection_timeout)
102
+ connected.flat_map do |connection|
103
+ initialize_connection(connection, keyspace)
104
+ end
105
+ end
106
+
107
+ def initialize_connection(connection, keyspace)
108
+ started = @request_runner.execute(connection, Protocol::StartupRequest.new)
109
+ authenticated = started.flat_map { |response| maybe_authenticate(response, connection) }
110
+ identified = authenticated.flat_map { identify_node(connection) }
111
+ identified.flat_map { @keyspace_changer.use_keyspace(keyspace, connection) }
112
+ end
113
+
114
+ def identify_node(connection)
115
+ request = Protocol::QueryRequest.new('SELECT data_center, host_id FROM system.local', :one)
116
+ f = @request_runner.execute(connection, request)
117
+ f.on_value do |result|
118
+ unless result.empty?
119
+ connection[:host_id] = result.first['host_id']
120
+ connection[:data_center] = result.first['data_center']
121
+ end
122
+ end
123
+ f
124
+ end
125
+
126
+ def maybe_authenticate(response, connection)
127
+ case response
128
+ when AuthenticationRequired
129
+ if @credentials
130
+ credentials_request = Protocol::CredentialsRequest.new(@credentials)
131
+ @request_runner.execute(connection, credentials_request).map { connection }
132
+ else
133
+ Future.failed(AuthenticationError.new('Server requested authentication, but no credentials given'))
134
+ end
135
+ else
136
+ Future.resolved(connection)
137
+ end
138
+ end
139
+
140
+ class FailedConnection
141
+ attr_reader :error, :host, :port
142
+
143
+ def initialize(error, host, port)
144
+ @error = error
145
+ @host = host
146
+ @port = port
147
+ end
148
+
149
+ def connected?
150
+ false
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,56 @@
1
+ # encoding: utf-8
2
+
3
+ module Cql
4
+ module Client
5
+ # @private
6
+ class ConnectionManager
7
+ include Enumerable
8
+
9
+ def initialize
10
+ @connections = []
11
+ @lock = Mutex.new
12
+ end
13
+
14
+ def add_connections(connections)
15
+ @lock.synchronize do
16
+ @connections.concat(connections)
17
+ connections.each do |connection|
18
+ connection.on_closed do
19
+ @lock.synchronize do
20
+ @connections.delete(connection)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ def connected?
28
+ @lock.synchronize do
29
+ @connections.any?
30
+ end
31
+ end
32
+
33
+ def snapshot
34
+ @lock.synchronize do
35
+ @connections.dup
36
+ end
37
+ end
38
+
39
+ def random_connection
40
+ raise NotConnectedError unless connected?
41
+ @lock.synchronize do
42
+ @connections.sample
43
+ end
44
+ end
45
+
46
+ def each_connection(&callback)
47
+ return self unless block_given?
48
+ raise NotConnectedError unless connected?
49
+ @lock.synchronize do
50
+ @connections.each(&callback)
51
+ end
52
+ end
53
+ alias_method :each, :each_connection
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,27 @@
1
+ # encoding: utf-8
2
+
3
+ module Cql
4
+ module Client
5
+ # @private
6
+ class KeyspaceChanger
7
+ KEYSPACE_NAME_PATTERN = /^\w[\w\d_]*$|^"\w[\w\d_]*"$/
8
+
9
+ def initialize
10
+ @request_runner = RequestRunner.new
11
+ end
12
+
13
+ def use_keyspace(keyspace, connection)
14
+ return Future.resolved(connection) unless keyspace
15
+ return Future.failed(InvalidKeyspaceNameError.new(%("#{keyspace}" is not a valid keyspace name))) unless valid_keyspace_name?(keyspace)
16
+ request = Protocol::QueryRequest.new("USE #{keyspace}", :one)
17
+ @request_runner.execute(connection, request).map { connection }
18
+ end
19
+
20
+ private
21
+
22
+ def valid_keyspace_name?(name)
23
+ name =~ KEYSPACE_NAME_PATTERN
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,21 @@
1
+ # encoding: utf-8
2
+
3
+ module Cql
4
+ module Client
5
+ # @private
6
+ class NullLogger
7
+ def close(*); end
8
+ def debug(*); end
9
+ def debug?; false end
10
+ def error(*); end
11
+ def error?; false end
12
+ def fatal(*); end
13
+ def fatal?; false end
14
+ def info(*); end
15
+ def info?; false end
16
+ def unknown(*); end
17
+ def warn(*); end
18
+ def warn?; false end
19
+ end
20
+ end
21
+ end
@@ -9,8 +9,6 @@ module Cql
9
9
  case response
10
10
  when Protocol::RowsResultResponse
11
11
  QueryResult.new(response.metadata, response.rows)
12
- when Protocol::PreparedResultResponse
13
- AsynchronousPreparedStatement.new(connection, response.id, response.metadata)
14
12
  when Protocol::ErrorResponse
15
13
  cql = request.is_a?(Protocol::QueryRequest) ? request.cql : nil
16
14
  raise QueryError.new(response.code, response.message, cql)
@@ -19,7 +17,11 @@ module Cql
19
17
  when Protocol::AuthenticateResponse
20
18
  AuthenticationRequired.new(response.authentication_class)
21
19
  else
22
- nil
20
+ if block_given?
21
+ yield response
22
+ else
23
+ nil
24
+ end
23
25
  end
24
26
  end
25
27
  end
@@ -29,12 +29,12 @@ module Cql
29
29
  end
30
30
 
31
31
  def connect
32
- synchronous_backtrace { @async_client.connect.get }
32
+ synchronous_backtrace { @async_client.connect.value }
33
33
  self
34
34
  end
35
35
 
36
36
  def close
37
- synchronous_backtrace { @async_client.close.get }
37
+ synchronous_backtrace { @async_client.close.value }
38
38
  self
39
39
  end
40
40
 
@@ -47,15 +47,15 @@ module Cql
47
47
  end
48
48
 
49
49
  def use(keyspace)
50
- synchronous_backtrace { @async_client.use(keyspace).get }
50
+ synchronous_backtrace { @async_client.use(keyspace).value }
51
51
  end
52
52
 
53
53
  def execute(cql, consistency=nil)
54
- synchronous_backtrace { @async_client.execute(cql, consistency).get }
54
+ synchronous_backtrace { @async_client.execute(cql, consistency).value }
55
55
  end
56
56
 
57
57
  def prepare(cql)
58
- async_statement = synchronous_backtrace { @async_client.prepare(cql).get }
58
+ async_statement = synchronous_backtrace { @async_client.prepare(cql).value }
59
59
  SynchronousPreparedStatement.new(async_statement)
60
60
  end
61
61
 
@@ -12,13 +12,13 @@ module Cql
12
12
  end
13
13
 
14
14
  def execute(*args)
15
- synchronous_backtrace { @async_statement.execute(*args).get }
15
+ synchronous_backtrace { @async_statement.execute(*args).value }
16
16
  end
17
17
 
18
18
  def pipeline
19
19
  pl = Pipeline.new(@async_statement)
20
20
  yield pl
21
- synchronous_backtrace { pl.get }
21
+ synchronous_backtrace { pl.value }
22
22
  end
23
23
 
24
24
  def async
@@ -37,12 +37,8 @@ module Cql
37
37
  @futures << @async_statement.execute(*args)
38
38
  end
39
39
 
40
- def get
41
- if @futures.any?
42
- Future.combine(*@futures).get
43
- else
44
- []
45
- end
40
+ def value
41
+ Future.all(*@futures).value
46
42
  end
47
43
  end
48
44
  end
@@ -6,168 +6,128 @@ require 'thread'
6
6
  module Cql
7
7
  FutureError = Class.new(CqlError)
8
8
 
9
- # A future represents the value of a process that may not yet have completed.
9
+ # A promise of delivering a value some time in the future.
10
10
  #
11
- # @private
11
+ # A promise is the write end of a Promise/Future pair. It can be fulfilled
12
+ # with a value or failed with an error. The value can be read through the
13
+ # future returned by {#future}.
12
14
  #
13
- class Future
15
+ # @private
16
+ class Promise
17
+ attr_reader :future
18
+
14
19
  def initialize
15
- @complete_listeners = []
16
- @failure_listeners = []
17
- @value_barrier = Queue.new
18
- @state_lock = Mutex.new
20
+ @future = CompletableFuture.new
19
21
  end
20
22
 
21
- # Combine multiple futures into a new future which completes when all
22
- # constituent futures complete, or fail when one or more of them fails.
23
+ # Fulfills the promise.
23
24
  #
24
- # The value of the combined future is an array of the values of the
25
- # constituent futures.
25
+ # This will resolve this promise's future, and trigger all listeners of that
26
+ # future. The value of the future will be the specified value, or nil if
27
+ # no value is specified.
26
28
  #
27
- # @param [Array<Future>] futures the futures to combine
28
- # @return [Future<Array>] an array of the values of the constituent futures
29
- #
30
- def self.combine(*futures)
31
- if futures.any?
32
- CombinedFuture.new(*futures)
33
- else
34
- completed([])
35
- end
29
+ # @param [Object] value the value of the future
30
+ def fulfill(value=nil)
31
+ @future.resolve(value)
36
32
  end
37
33
 
38
- # Returns a future which will complete with the value of the first
39
- # (successful) of the specified futures. If all of the futures fail, the
40
- # returned future will also fail (with the error of the last failed future).
34
+ # Fails the promise.
41
35
  #
42
- # @param [Array<Future>] futures the futures to monitor
43
- # @return [Future] a future which represents the first completing future
36
+ # This will fail this promise's future, and trigger all listeners of that
37
+ # future.
44
38
  #
45
- def self.first(*futures)
46
- ff = Future.new
47
- futures.each do |f|
48
- f.on_complete do |value|
49
- ff.complete!(value) unless ff.complete?
50
- end
51
- f.on_failure do |e|
52
- ff.fail!(e) if futures.all?(&:failed?)
53
- end
54
- end
55
- ff
39
+ # @param [Error] error the error which prevented the promise to be fulfilled
40
+ def fail(error)
41
+ @future.fail(error)
56
42
  end
57
43
 
58
- # Creates a new future which is completed.
59
- #
60
- # @param [Object, nil] value the value of the created future
61
- # @return [Future] a completed future
44
+ # Observe a future and fulfill the promise with the future's value when the
45
+ # future resolves, or fail with the future's error when the future fails.
62
46
  #
63
- def self.completed(value=nil)
64
- CompletedFuture.new(value)
47
+ # @param [Cql::Future] future the future to observe
48
+ def observe(future)
49
+ future.on_value { |v| fulfill(v) }
50
+ future.on_failure { |e| fail(e) }
65
51
  end
66
52
 
67
- # Creates a new future which is failed.
53
+ # Run the given block and fulfill this promise with its result. If the block
54
+ # raises an error, fail this promise with the error.
68
55
  #
69
- # @param [Error] error the error of the created future
70
- # @return [Future] a failed future
71
- #
72
- def self.failed(error)
73
- FailedFuture.new(error)
74
- end
75
-
76
- # Completes the future.
56
+ # All arguments given will be passed onto the block.
77
57
  #
78
- # This will trigger all completion listeners in the calling thread.
58
+ # @example
59
+ # promise.try { 3 + 4 }
60
+ # promise.future.value # => 7
79
61
  #
80
- # @param [Object] v the value of the future
62
+ # @example
63
+ # promise.try do
64
+ # do_something_that_will_raise_an_error
65
+ # end
66
+ # promise.future.value # => (raises error)
81
67
  #
82
- def complete!(v=nil)
83
- @state_lock.synchronize do
84
- raise FutureError, 'Future already completed' if complete? || failed?
85
- @value = v
86
- @complete_listeners.each do |listener|
87
- listener.call(@value) rescue nil
88
- end
89
- end
90
- ensure
91
- @state_lock.synchronize do
92
- @value_barrier << :ping
93
- end
94
- end
95
-
96
- # Returns whether or not the future is complete
68
+ # @example
69
+ # promise.try('foo', 'bar', &proc_taking_two_arguments)
97
70
  #
98
- def complete?
99
- defined? @value
71
+ # @yieldparam [Array] ctx the arguments passed to {#try}
72
+ def try(*ctx)
73
+ fulfill(yield(*ctx))
74
+ rescue => e
75
+ fail(e)
100
76
  end
77
+ end
101
78
 
102
- # Registers a listener for when this future completes
79
+ # @private
80
+ module FutureFactories
81
+ # Combines multiple futures into a new future which resolves when all
82
+ # constituent futures complete, or fails when one or more of them fails.
103
83
  #
104
- # @yieldparam [Object] value the value of the completed future
84
+ # The value of the combined future is an array of the values of the
85
+ # constituent futures.
105
86
  #
106
- def on_complete(&listener)
107
- @state_lock.synchronize do
108
- if complete?
109
- listener.call(value) rescue nil
110
- else
111
- @complete_listeners << listener
112
- end
87
+ # @param [Array<Cql::Future>] futures the futures to combine
88
+ # @return [Cql::Future<Array>] an array of the values of the constituent
89
+ # futures
90
+ def all(*futures)
91
+ if futures.any?
92
+ CombinedFuture.new(futures)
93
+ else
94
+ resolved([])
113
95
  end
114
96
  end
115
97
 
116
- # Returns the value of this future, blocking until it is available, if necessary.
117
- #
118
- # If the future fails this method will raise the error that failed the future.
119
- #
120
- # @return [Object] the value of this future
121
- #
122
- def value
123
- raise @error if @error
124
- return @value if defined? @value
125
- @value_barrier.pop
126
- raise @error if @error
127
- return @value
128
- end
129
- alias_method :get, :value
130
-
131
- # Fails the future.
132
- #
133
- # This will trigger all failure listeners in the calling thread.
134
- #
135
- # @param [Error] error the error which prevented the value from being determined
98
+ # Returns a future which will be resolved with the value of the first
99
+ # (resolved) of the specified futures. If all of the futures fail, the
100
+ # returned future will also fail (with the error of the last failed future).
136
101
  #
137
- def fail!(error)
138
- @state_lock.synchronize do
139
- raise FutureError, 'Future already completed' if failed? || complete?
140
- @error = error
141
- @failure_listeners.each do |listener|
142
- listener.call(error) rescue nil
143
- end
144
- end
145
- ensure
146
- @state_lock.synchronize do
147
- @value_barrier << :ping
102
+ # @param [Array<Cql::Future>] futures the futures to monitor
103
+ # @return [Cql::Future] a future which represents the first completing future
104
+ def first(*futures)
105
+ if futures.any?
106
+ FirstFuture.new(futures)
107
+ else
108
+ resolved
148
109
  end
149
110
  end
150
111
 
151
- # Returns whether or not the future is failed.
112
+ # Creates a new pre-resolved future.
152
113
  #
153
- def failed?
154
- !!@error
114
+ # @param [Object, nil] value the value of the created future
115
+ # @return [Cql::Future] a resolved future
116
+ def resolved(value=nil)
117
+ ResolvedFuture.new(value)
155
118
  end
156
119
 
157
- # Registers a listener for when this future fails
158
- #
159
- # @yieldparam [Error] error the error that failed the future
120
+ # Creates a new pre-failed future.
160
121
  #
161
- def on_failure(&listener)
162
- @state_lock.synchronize do
163
- if failed?
164
- listener.call(@error) rescue nil
165
- else
166
- @failure_listeners << listener
167
- end
168
- end
122
+ # @param [Error] error the error of the created future
123
+ # @return [Cql::Future] a failed future
124
+ def failed(error)
125
+ FailedFuture.new(error)
169
126
  end
127
+ end
170
128
 
129
+ # @private
130
+ module FutureCombinators
171
131
  # Returns a new future representing a transformation of this future's value.
172
132
  #
173
133
  # @example
@@ -175,20 +135,14 @@ module Cql
175
135
  #
176
136
  # @yieldparam [Object] value the value of this future
177
137
  # @yieldreturn [Object] the transformed value
178
- # @return [Future] a new future representing the transformed value
179
- #
138
+ # @return [Cql::Future] a new future representing the transformed value
180
139
  def map(&block)
181
- fp = Future.new
182
- on_failure { |e| fp.fail!(e) }
183
- on_complete do |v|
184
- begin
185
- vv = block.call(v)
186
- fp.complete!(vv)
187
- rescue => e
188
- fp.fail!(e)
189
- end
140
+ p = Promise.new
141
+ on_failure { |e| p.fail(e) }
142
+ on_value do |v|
143
+ p.try(v, &block)
190
144
  end
191
- fp
145
+ p.future
192
146
  end
193
147
 
194
148
  # Returns a new future representing a transformation of this future's value,
@@ -200,24 +154,20 @@ module Cql
200
154
  # This method is useful when you want to chain asynchronous operations.
201
155
  #
202
156
  # @yieldparam [Object] value the value of this future
203
- # @yieldreturn [Future] a future representing the transformed value
204
- # @return [Future] a new future representing the transformed value
205
- #
157
+ # @yieldreturn [Cql::Future] a future representing the transformed value
158
+ # @return [Cql::Future] a new future representing the transformed value
206
159
  def flat_map(&block)
207
- fp = Future.new
208
- on_failure { |e| fp.fail!(e) }
209
- on_complete do |v|
160
+ p = Promise.new
161
+ on_failure { |e| p.fail(e) }
162
+ on_value do |v|
210
163
  begin
211
- fpp = block.call(v)
212
- fpp.on_failure { |e| fp.fail!(e) }
213
- fpp.on_complete do |vv|
214
- fp.complete!(vv)
215
- end
164
+ f = block.call(v)
165
+ p.observe(f)
216
166
  rescue => e
217
- fp.fail!(e)
167
+ p.fail(e)
218
168
  end
219
169
  end
220
- fp
170
+ p.future
221
171
  end
222
172
 
223
173
  # Returns a new future which represents either the value of the original
@@ -232,124 +182,284 @@ module Cql
232
182
  #
233
183
  # @example
234
184
  # future2 = future1.recover { |error| 'foo' }
235
- # future1.fail!(error)
236
- # future2.get # => 'foo'
185
+ # future1.fail(error)
186
+ # future2.value # => 'foo'
237
187
  #
238
188
  # @yieldparam [Object] error the error from the original future
239
189
  # @yieldreturn [Object] the value of the new future
240
- # @return [Future] a new future representing a value recovered from the error
241
- #
190
+ # @return [Cql::Future] a new future representing a value recovered from the error
242
191
  def recover(&block)
243
- fp = Future.new
192
+ p = Promise.new
244
193
  on_failure do |e|
245
- begin
246
- vv = block.call(e)
247
- fp.complete!(vv)
248
- rescue => e
249
- fp.fail!(e)
250
- end
194
+ p.try(e, &block)
251
195
  end
252
- on_complete do |v|
253
- fp.complete!(v)
196
+ on_value do |v|
197
+ p.fulfill(v)
254
198
  end
255
- fp
199
+ p.future
256
200
  end
257
201
 
258
202
  # Returns a new future which represents either the value of the original
259
203
  # future, or the value of the future returned by the given block.
260
204
  #
261
205
  # This is like {#recover} but for cases when the handling of an error is
262
- # itself asynchronous.
206
+ # itself asynchronous. In other words, {#fallback} is to {#recover} what
207
+ # {#flat_map} is to {#map}.
263
208
  #
264
209
  # If the block raises an error a failed future with that error will be
265
210
  # returned (this can be used to transform an error into another error,
266
211
  # instead of tranforming an error into a value).
267
212
  #
268
213
  # @example
269
- # future2 = future1.fallback { |error| perform_async_operation }
270
- # future1.fail!(error)
271
- # future2.get # => whatever the async operation resolved to
214
+ # result = http_get('/foo/bar').fallback do |error|
215
+ # http_get('/baz')
216
+ # end
217
+ # result.value # either the response to /foo/bar, or if that failed
218
+ # # the response to /baz
272
219
  #
273
220
  # @yieldparam [Object] error the error from the original future
274
221
  # @yieldreturn [Object] the value of the new future
275
- # @return [Future] a new future representing a value recovered from the error
276
- #
222
+ # @return [Cql::Future] a new future representing a value recovered from the
223
+ # error
277
224
  def fallback(&block)
278
- fp = Future.new
225
+ p = Promise.new
279
226
  on_failure do |e|
280
227
  begin
281
- fpp = block.call(e)
282
- fpp.on_failure do |e|
283
- fp.fail!(e)
284
- end
285
- fpp.on_complete do |vv|
286
- fp.complete!(vv)
287
- end
228
+ f = block.call(e)
229
+ p.observe(f)
288
230
  rescue => e
289
- fp.fail!(e)
231
+ p.fail(e)
290
232
  end
291
233
  end
292
- on_complete do |v|
293
- fp.complete!(v)
234
+ on_value do |v|
235
+ p.fulfill(v)
294
236
  end
295
- fp
237
+ p.future
296
238
  end
297
239
  end
298
240
 
299
241
  # @private
300
- class CompletedFuture < Future
301
- def initialize(value=nil)
302
- super()
303
- complete!(value)
242
+ module FutureCallbacks
243
+ # Registers a listener that will be called when this future completes,
244
+ # i.e. resolves or fails. The listener will be called with the future as
245
+ # solve argument
246
+ #
247
+ # @yieldparam [Cql::Future] future the future
248
+ def on_complete(&listener)
249
+ run_immediately = false
250
+ @lock.synchronize do
251
+ if @resolved || @failed
252
+ run_immediately = true
253
+ else
254
+ @complete_listeners << listener
255
+ end
256
+ end
257
+ if run_immediately
258
+ listener.call(self) rescue nil
259
+ end
260
+ nil
261
+ end
262
+
263
+ # Registers a listener that will be called when this future becomes
264
+ # resolved. The listener will be called with the value of the future as
265
+ # sole argument.
266
+ #
267
+ # @yieldparam [Object] value the value of the resolved future
268
+ def on_value(&listener)
269
+ run_immediately = false
270
+ @lock.synchronize do
271
+ if @resolved
272
+ run_immediately = true
273
+ elsif !@failed
274
+ @value_listeners << listener
275
+ end
276
+ end
277
+ if run_immediately
278
+ listener.call(value) rescue nil
279
+ end
280
+ nil
281
+ end
282
+
283
+ # Registers a listener that will be called when this future fails. The
284
+ # lisener will be called with the error that failed the future as sole
285
+ # argument.
286
+ #
287
+ # @yieldparam [Error] error the error that failed the future
288
+ def on_failure(&listener)
289
+ run_immediately = false
290
+ @lock.synchronize do
291
+ if @failed
292
+ run_immediately = true
293
+ elsif !@resolved
294
+ @failure_listeners << listener
295
+ end
296
+ end
297
+ if run_immediately
298
+ listener.call(@error) rescue nil
299
+ end
300
+ nil
304
301
  end
305
302
  end
306
303
 
304
+ # A future represents the value of a process that may not yet have completed.
305
+ #
306
+ # @see Cql::Promise
307
307
  # @private
308
- class FailedFuture < Future
309
- def initialize(error)
310
- super()
311
- fail!(error)
308
+ class Future
309
+ extend FutureFactories
310
+ include FutureCombinators
311
+ include FutureCallbacks
312
+
313
+ def initialize
314
+ @lock = Mutex.new
315
+ @resolved = false
316
+ @failed = false
317
+ @failure_listeners = []
318
+ @value_listeners = []
319
+ @complete_listeners = []
320
+ end
321
+
322
+ # Returns the value of this future, blocking until it is available if
323
+ # necessary.
324
+ #
325
+ # If the future fails this method will raise the error that failed the
326
+ # future.
327
+ #
328
+ # @return [Object] the value of this future
329
+ def value
330
+ @lock.synchronize do
331
+ raise @error if @failed
332
+ return @value if @resolved
333
+ t = Thread.current
334
+ u = proc { t.run }
335
+ @value_listeners << u
336
+ @failure_listeners << u
337
+ while true
338
+ raise @error if @failed
339
+ return @value if @resolved
340
+ @lock.sleep
341
+ end
342
+ end
343
+ end
344
+
345
+ # Returns true if this future is resolved or failed
346
+ def completed?
347
+ resolved? || failed?
348
+ end
349
+
350
+ # Returns true if this future is resolved
351
+ def resolved?
352
+ @lock.synchronize { @resolved }
353
+ end
354
+
355
+ # Returns true if this future has failed
356
+ def failed?
357
+ @lock.synchronize { @failed }
312
358
  end
313
359
  end
314
360
 
315
361
  # @private
316
- class CombinedFuture < Future
317
- def initialize(*futures)
362
+ class CompletableFuture < Future
363
+ def resolve(v=nil)
364
+ value_listeners = nil
365
+ complete_listeners = nil
366
+ @lock.synchronize do
367
+ raise FutureError, 'Future already completed' if @resolved || @failed
368
+ @resolved = true
369
+ @value = v
370
+ value_listeners = @value_listeners
371
+ complete_listeners = @complete_listeners
372
+ @value_listeners = nil
373
+ @failure_listeners = nil
374
+ @complete_listeners = nil
375
+ end
376
+ value_listeners.each do |listener|
377
+ listener.call(v) rescue nil
378
+ end
379
+ complete_listeners.each do |listener|
380
+ listener.call(self) rescue nil
381
+ end
382
+ nil
383
+ end
384
+
385
+ def fail(error)
386
+ failure_listeners = nil
387
+ complete_listeners = nil
388
+ @lock.synchronize do
389
+ raise FutureError, 'Future already completed' if @failed || @resolved
390
+ @failed = true
391
+ @error = error
392
+ failure_listeners = @failure_listeners
393
+ complete_listeners = @complete_listeners
394
+ @value_listeners = nil
395
+ @failure_listeners = nil
396
+ @complete_listeners = nil
397
+ end
398
+ failure_listeners.each do |listener|
399
+ listener.call(error) rescue nil
400
+ end
401
+ complete_listeners.each do |listener|
402
+ listener.call(self) rescue nil
403
+ end
404
+ nil
405
+ end
406
+ end
407
+
408
+ # @private
409
+ class CombinedFuture < CompletableFuture
410
+ def initialize(futures)
318
411
  super()
319
- values = [nil] * futures.size
320
- completed = [false] * futures.size
412
+ values = Array.new(futures.size)
413
+ remaining = futures.size
321
414
  futures.each_with_index do |f, i|
322
- f.on_complete do |v|
323
- all_done = false
324
- @state_lock.synchronize do
415
+ f.on_value do |v|
416
+ @lock.synchronize do
325
417
  values[i] = v
326
- completed[i] = true
327
- all_done = completed.all?
418
+ remaining -= 1
328
419
  end
329
- if all_done
330
- combined_complete!(values)
420
+ if remaining == 0
421
+ resolve(values)
331
422
  end
332
423
  end
333
424
  f.on_failure do |e|
334
425
  unless failed?
335
- combined_fail!(e)
426
+ fail(e)
336
427
  end
337
428
  end
338
429
  end
339
430
  end
431
+ end
340
432
 
341
- alias_method :combined_complete!, :complete!
342
- private :combined_complete!
343
-
344
- alias_method :combined_fail!, :fail!
345
- private :combined_fail!
433
+ # @private
434
+ class FirstFuture < CompletableFuture
435
+ def initialize(futures)
436
+ super()
437
+ futures.each do |f|
438
+ f.on_value do |value|
439
+ resolve(value) unless completed?
440
+ end
441
+ f.on_failure do |e|
442
+ fail(e) if futures.all?(&:failed?)
443
+ end
444
+ end
445
+ end
446
+ end
346
447
 
347
- def complete!(v=nil)
348
- raise FutureError, 'Cannot complete a combined future'
448
+ # @private
449
+ class ResolvedFuture < Future
450
+ def initialize(value=nil)
451
+ super()
452
+ @value = value
453
+ @resolved = true
349
454
  end
455
+ end
350
456
 
351
- def fail!(e)
352
- raise FutureError, 'Cannot fail a combined future'
457
+ # @private
458
+ class FailedFuture < Future
459
+ def initialize(error)
460
+ super()
461
+ @error = error
462
+ @failed = true
353
463
  end
354
464
  end
355
465
  end