Skip to content

Commit a3d6309

Browse files
committed
RUBY-257 - prepared-statement cache should not be host-scoped.
1 parent 810cf80 commit a3d6309

File tree

4 files changed

+52
-72
lines changed

4 files changed

+52
-72
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ Features:
55
* Update Table metadata to include trigger-collection and view-collection metadata.
66
* Added execution profiles to encapsulate a group of request execution options.
77
* Added support for v5 beta protocol.
8+
* Make prepared statement cache not be scoped by host and optimistically execute prepared statements on hosts where
9+
we are not sure the statement is already prepared. The motivation is that in the steady state, all nodes have
10+
prepared statements already, so there is no need to prepare statements before executing them. If the guess is wrong,
11+
the client will prepare and execute at that point.
812

913
Bug Fixes:
1014
* [RUBY-255](https://datastax-oss.atlassian.net/browse/RUBY-255) ControlConnection.peer_ip ignores peers that are missing critical information in system.peers.

lib/cassandra/cluster/client.rb

Lines changed: 16 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def initialize(logger,
4747
@futures = futures_factory
4848
@connections = ::Hash.new
4949
@prepared_statements = ::Hash.new
50-
@preparing_statements = ::Hash.new
50+
@preparing_statements = ::Hash.new {|hash, host| hash[host] = {}}
5151
@pending_connections = ::Hash.new
5252
@keyspace = nil
5353
@state = :idle
@@ -83,7 +83,6 @@ def connect
8383

8484
connecting_hosts[host] = pool_size
8585
@pending_connections[host] = 0
86-
@prepared_statements[host] = {}
8786
@preparing_statements[host] = {}
8887
@connections[host] = ConnectionPool.new
8988
end
@@ -182,7 +181,6 @@ def host_up(host)
182181
end
183182

184183
@pending_connections[host] ||= 0
185-
@prepared_statements[host] = {}
186184
@preparing_statements[host] = {}
187185
@connections[host] = ConnectionPool.new
188186
end
@@ -197,7 +195,6 @@ def host_down(host)
197195
return Ione::Future.resolved unless @connections.key?(host)
198196

199197
@pending_connections.delete(host) unless @pending_connections[host] > 0
200-
@prepared_statements.delete(host)
201198
@preparing_statements.delete(host)
202199
pool = @connections.delete(host)
203200
end
@@ -632,15 +629,13 @@ def prepare_and_send_request_by_plan(host,
632629
errors,
633630
hosts)
634631
cql = statement.cql
635-
id = nil
636-
host_is_up = true
637-
synchronize do
638-
if @prepared_statements[host].nil?
639-
host_is_up = false
640-
else
641-
id = @prepared_statements[host][cql]
642-
end
643-
end
632+
633+
# Get the prepared statement id for this statement from our cache if possible. We are optimistic
634+
# that the statement has previously been prepared on all hosts, so the id will be valid. However, if
635+
# we're in the midst of preparing the statement on the given host, we know that executing with the id
636+
# will fail. So, act like we don't have the prepared-statement id in that case.
637+
638+
id = synchronize { @preparing_statements[host][cql] ? nil : @prepared_statements[cql] }
644639

645640
if id
646641
request.id = id
@@ -655,19 +650,6 @@ def prepare_and_send_request_by_plan(host,
655650
timeout,
656651
errors,
657652
hosts)
658-
elsif !host_is_up
659-
# We've hit a race condition where the plan says we can query this host, but the host has gone
660-
# down in the mean time. Just execute the plan again on the next host.
661-
@logger.debug("#{host} is down; executing plan on next host")
662-
execute_by_plan(promise,
663-
keyspace,
664-
statement,
665-
options,
666-
request,
667-
plan,
668-
timeout,
669-
errors,
670-
hosts)
671653
else
672654
prepare = prepare_statement(host, connection, cql, timeout)
673655
prepare.on_complete do |_|
@@ -825,29 +807,15 @@ def batch_and_send_request_by_plan(host,
825807
cql = statement.cql
826808

827809
if statement.is_a?(Statements::Bound)
828-
host_is_up = true
829-
id = nil
830-
synchronize do
831-
if @prepared_statements[host].nil?
832-
host_is_up = false
833-
else
834-
id = @prepared_statements[host][cql]
835-
end
836-
end
810+
# Get the prepared statement id for this statement from our cache if possible. We are optimistic
811+
# that the statement has previously been prepared on all hosts, so the id will be valid. However, if
812+
# we're in the midst of preparing the statement on the given host, we know that executing with the id
813+
# will fail. So, act like we don't have the prepared-statement id in that case.
814+
815+
id = synchronize { @preparing_statements[host][cql] ? nil : @prepared_statements[cql] }
837816

838817
if id
839818
request.add_prepared(id, statement.params, statement.params_types)
840-
elsif !host_is_up
841-
@logger.debug("#{host} is down; executing on next host in plan")
842-
return batch_by_plan(promise,
843-
keyspace,
844-
batch_statement,
845-
options,
846-
request,
847-
plan,
848-
timeout,
849-
errors,
850-
hosts)
851819
else
852820
unprepared[cql] << statement
853821
end
@@ -1097,7 +1065,6 @@ def handle_response(response_future,
10971065

10981066
synchronize do
10991067
@preparing_statements[host].delete(cql)
1100-
@prepared_statements[host].delete(cql)
11011068
end
11021069

11031070
prepare = prepare_statement(host, connection, cql, timeout)
@@ -1198,7 +1165,7 @@ def handle_response(response_future,
11981165
when Protocol::PreparedResultResponse
11991166
cql = request.cql
12001167
synchronize do
1201-
@prepared_statements[host][cql] = r.id
1168+
@prepared_statements[cql] = r.id
12021169
@preparing_statements[host].delete(cql)
12031170
end
12041171

@@ -1531,7 +1498,7 @@ def prepare_statement(host, connection, cql, timeout)
15311498
when Protocol::PreparedResultResponse
15321499
id = r.id
15331500
synchronize do
1534-
@prepared_statements[host][cql] = id
1501+
@prepared_statements[cql] = id
15351502
@preparing_statements[host].delete(cql)
15361503
end
15371504
id

spec/cassandra/cluster/client_spec.rb

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -695,7 +695,7 @@ class Cluster
695695
expect(sent).to be_truthy
696696
end
697697

698-
it 're-prepares a statement on new connection' do
698+
it 'does not re-prepare a statement on new connection if the new host already has the statement prepared' do
699699
count = 0
700700
io_reactor.on_connection do |connection|
701701
connection.handle_request do |request|
@@ -719,7 +719,12 @@ class Cluster
719719
cluster_registry.hosts.delete(cluster_registry.hosts.first)
720720

721721
client.execute(statement.bind, execution_options).get
722-
expect(count).to eq(2)
722+
723+
# Expected sequence of events:
724+
# 1. prepare on host1
725+
# 2. execute on host2. Since execute succeeds, the implication is that host2 already had the statement
726+
# prepared.
727+
expect(count).to eq(1)
723728
end
724729

725730
it 're-prepares a statement on unprepared error' do
@@ -752,7 +757,14 @@ class Cluster
752757
cluster_registry.hosts.delete(statement.execution_info.hosts.first)
753758

754759
client.execute(statement.bind, execution_options).get
755-
expect(count).to eq(3)
760+
761+
762+
# Expected sequence of events:
763+
# 1. prepare on host1
764+
# 2. execute on host2, yielding unprepared error.
765+
# 3. prepare on host2
766+
# 4. execute on host2.
767+
expect(count).to eq(2)
756768
expect(error).to be(false)
757769
end
758770

@@ -937,7 +949,7 @@ class Cluster
937949
expect(sent).to be_truthy
938950
end
939951

940-
it 'automatically re-prepares statements' do
952+
it 'does not automatically re-prepare statements' do
941953
sent = false
942954
count = 0
943955
batch = Statements::Batch::Logged.new(driver.execution_options)
@@ -983,7 +995,11 @@ class Cluster
983995

984996
client.batch(batch, execution_options.override(consistency: :one)).get
985997
expect(sent).to be_truthy
986-
expect(count).to eq(2)
998+
999+
# Expected sequence of events:
1000+
# 1. prepare on host1
1001+
# 2. run batch on host2 with prepared-statement id's. This succeeds, so no more work to do.
1002+
expect(count).to eq(1)
9871003
end
9881004

9891005
it 'follows the plan on failure' do

spec/regressions/RUBY-189_spec.rb

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -56,19 +56,17 @@ class Cluster
5656
let(:request) { double('request') }
5757
let(:batch_statement) { double('batch_statement') }
5858
let(:bound_statement) { double('bound_statement') }
59+
let(:response) { double('response future')}
60+
let(:prepared_id) { 1234 }
5961

6062
it 'RUBY-189 - handles node down after prepare' do
6163
expect(promise).to_not receive(:break)
6264
expect(statement).to receive(:cql).and_return('select * from foo')
63-
expect(client).to receive(:execute_by_plan).with(promise,
64-
'keyspace',
65-
statement,
66-
options,
67-
'request',
68-
plan,
69-
12,
70-
errors,
71-
hosts)
65+
66+
# We expect the down node to receive a request attempt.
67+
expect(connection).to receive(:send_request).and_return(response)
68+
expect(response).to receive(:map).and_return(response)
69+
expect(response).to receive(:on_complete)
7270
client.send(:prepare_and_send_request_by_plan,
7371
'down_host',
7472
connection,
@@ -85,19 +83,14 @@ class Cluster
8583

8684
it 'RUBY-189 - handles node down after prepare in batch' do
8785
expect(request).to receive(:clear)
86+
expect(connection).to receive(:send_request).and_return(response)
87+
expect(response).to receive(:map).and_return(response)
88+
expect(response).to receive(:on_complete)
89+
8890
expect(bound_statement).to receive(:is_a?).and_return(true)
8991
expect(bound_statement).to receive(:cql).and_return('select * from foo')
9092
expect(batch_statement).to receive(:statements).and_return([bound_statement])
9193
expect(promise).to_not receive(:break)
92-
expect(client).to receive(:batch_by_plan).with(promise,
93-
'keyspace',
94-
batch_statement,
95-
options,
96-
request,
97-
plan,
98-
12,
99-
errors,
100-
hosts)
10194
client.send(:batch_and_send_request_by_plan,
10295
'down_host',
10396
connection,

0 commit comments

Comments
 (0)