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.
- data/README.md +2 -2
- data/lib/cql/client.rb +9 -5
- data/lib/cql/client/asynchronous_client.rb +105 -192
- data/lib/cql/client/asynchronous_prepared_statement.rb +51 -9
- data/lib/cql/client/connection_helper.rb +155 -0
- data/lib/cql/client/connection_manager.rb +56 -0
- data/lib/cql/client/keyspace_changer.rb +27 -0
- data/lib/cql/client/null_logger.rb +21 -0
- data/lib/cql/client/request_runner.rb +5 -3
- data/lib/cql/client/synchronous_client.rb +5 -5
- data/lib/cql/client/synchronous_prepared_statement.rb +4 -8
- data/lib/cql/future.rb +320 -210
- data/lib/cql/io/connection.rb +5 -5
- data/lib/cql/io/io_reactor.rb +21 -23
- data/lib/cql/protocol/cql_protocol_handler.rb +69 -38
- data/lib/cql/protocol/encoding.rb +5 -1
- data/lib/cql/protocol/requests/register_request.rb +2 -0
- data/lib/cql/protocol/type_converter.rb +1 -0
- data/lib/cql/version.rb +1 -1
- data/spec/cql/client/asynchronous_client_spec.rb +368 -175
- data/spec/cql/client/asynchronous_prepared_statement_spec.rb +132 -22
- data/spec/cql/client/connection_helper_spec.rb +335 -0
- data/spec/cql/client/connection_manager_spec.rb +118 -0
- data/spec/cql/client/keyspace_changer_spec.rb +50 -0
- data/spec/cql/client/request_runner_spec.rb +12 -12
- data/spec/cql/client/synchronous_client_spec.rb +15 -15
- data/spec/cql/client/synchronous_prepared_statement_spec.rb +15 -11
- data/spec/cql/future_spec.rb +529 -301
- data/spec/cql/io/connection_spec.rb +12 -12
- data/spec/cql/io/io_reactor_spec.rb +61 -61
- data/spec/cql/protocol/cql_protocol_handler_spec.rb +26 -12
- data/spec/cql/protocol/encoding_spec.rb +5 -0
- data/spec/cql/protocol/type_converter_spec.rb +1 -1
- data/spec/cql/time_uuid_spec.rb +7 -7
- data/spec/integration/client_spec.rb +2 -2
- data/spec/integration/io_spec.rb +20 -20
- data/spec/integration/protocol_spec.rb +17 -17
- data/spec/integration/regression_spec.rb +6 -0
- data/spec/integration/uuid_spec.rb +4 -0
- data/spec/support/fake_io_reactor.rb +38 -8
- data/spec/support/fake_server.rb +3 -3
- 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
|
-
|
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.
|
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.
|
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).
|
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).
|
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).
|
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).
|
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.
|
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
|
41
|
-
|
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
|
data/lib/cql/future.rb
CHANGED
@@ -6,168 +6,128 @@ require 'thread'
|
|
6
6
|
module Cql
|
7
7
|
FutureError = Class.new(CqlError)
|
8
8
|
|
9
|
-
# A
|
9
|
+
# A promise of delivering a value some time in the future.
|
10
10
|
#
|
11
|
-
#
|
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
|
-
|
15
|
+
# @private
|
16
|
+
class Promise
|
17
|
+
attr_reader :future
|
18
|
+
|
14
19
|
def initialize
|
15
|
-
@
|
16
|
-
@failure_listeners = []
|
17
|
-
@value_barrier = Queue.new
|
18
|
-
@state_lock = Mutex.new
|
20
|
+
@future = CompletableFuture.new
|
19
21
|
end
|
20
22
|
|
21
|
-
#
|
22
|
-
# constituent futures complete, or fail when one or more of them fails.
|
23
|
+
# Fulfills the promise.
|
23
24
|
#
|
24
|
-
#
|
25
|
-
#
|
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 [
|
28
|
-
|
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
|
-
#
|
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
|
-
#
|
43
|
-
#
|
36
|
+
# This will fail this promise's future, and trigger all listeners of that
|
37
|
+
# future.
|
44
38
|
#
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
#
|
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
|
-
|
64
|
-
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
58
|
+
# @example
|
59
|
+
# promise.try { 3 + 4 }
|
60
|
+
# promise.future.value # => 7
|
79
61
|
#
|
80
|
-
# @
|
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
|
-
|
83
|
-
|
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
|
-
|
99
|
-
|
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
|
-
|
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
|
-
#
|
84
|
+
# The value of the combined future is an array of the values of the
|
85
|
+
# constituent futures.
|
105
86
|
#
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
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
|
117
|
-
#
|
118
|
-
#
|
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
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
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
|
-
#
|
112
|
+
# Creates a new pre-resolved future.
|
152
113
|
#
|
153
|
-
|
154
|
-
|
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
|
-
#
|
158
|
-
#
|
159
|
-
# @yieldparam [Error] error the error that failed the future
|
120
|
+
# Creates a new pre-failed future.
|
160
121
|
#
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
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
|
-
|
182
|
-
on_failure { |e|
|
183
|
-
|
184
|
-
|
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
|
-
|
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
|
-
|
208
|
-
on_failure { |e|
|
209
|
-
|
160
|
+
p = Promise.new
|
161
|
+
on_failure { |e| p.fail(e) }
|
162
|
+
on_value do |v|
|
210
163
|
begin
|
211
|
-
|
212
|
-
|
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
|
-
|
167
|
+
p.fail(e)
|
218
168
|
end
|
219
169
|
end
|
220
|
-
|
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
|
236
|
-
# future2.
|
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
|
-
|
192
|
+
p = Promise.new
|
244
193
|
on_failure do |e|
|
245
|
-
|
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
|
-
|
253
|
-
|
196
|
+
on_value do |v|
|
197
|
+
p.fulfill(v)
|
254
198
|
end
|
255
|
-
|
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
|
-
#
|
270
|
-
#
|
271
|
-
#
|
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
|
276
|
-
#
|
222
|
+
# @return [Cql::Future] a new future representing a value recovered from the
|
223
|
+
# error
|
277
224
|
def fallback(&block)
|
278
|
-
|
225
|
+
p = Promise.new
|
279
226
|
on_failure do |e|
|
280
227
|
begin
|
281
|
-
|
282
|
-
|
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
|
-
|
231
|
+
p.fail(e)
|
290
232
|
end
|
291
233
|
end
|
292
|
-
|
293
|
-
|
234
|
+
on_value do |v|
|
235
|
+
p.fulfill(v)
|
294
236
|
end
|
295
|
-
|
237
|
+
p.future
|
296
238
|
end
|
297
239
|
end
|
298
240
|
|
299
241
|
# @private
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
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
|
309
|
-
|
310
|
-
|
311
|
-
|
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
|
317
|
-
def
|
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 =
|
320
|
-
|
412
|
+
values = Array.new(futures.size)
|
413
|
+
remaining = futures.size
|
321
414
|
futures.each_with_index do |f, i|
|
322
|
-
f.
|
323
|
-
|
324
|
-
@state_lock.synchronize do
|
415
|
+
f.on_value do |v|
|
416
|
+
@lock.synchronize do
|
325
417
|
values[i] = v
|
326
|
-
|
327
|
-
all_done = completed.all?
|
418
|
+
remaining -= 1
|
328
419
|
end
|
329
|
-
if
|
330
|
-
|
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
|
-
|
426
|
+
fail(e)
|
336
427
|
end
|
337
428
|
end
|
338
429
|
end
|
339
430
|
end
|
431
|
+
end
|
340
432
|
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
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
|
-
|
348
|
-
|
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
|
-
|
352
|
-
|
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
|