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