cql-rb 1.1.0.pre3 → 1.1.0.pre6

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.
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