Skip to content

Commit 809b7ad

Browse files
Merge pull request #200 from datastax/RUBY-257
RUBY-257 - prepared-statement cache should not be host-scoped.
2 parents 2daf0c6 + 5a6fcb5 commit 809b7ad

File tree

7 files changed

+80
-83
lines changed

7 files changed

+80
-83
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: 30 additions & 55 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
@@ -1093,17 +1061,23 @@ def handle_response(response_future,
10931061
r.data_present,
10941062
retries)
10951063
when Protocol::UnpreparedErrorResponse
1096-
cql = statement.cql
1097-
1098-
synchronize do
1099-
@preparing_statements[host].delete(cql)
1100-
@prepared_statements[host].delete(cql)
1064+
cql = nil
1065+
if statement.is_a?(Cassandra::Statements::Batch)
1066+
# Find the prepared statement with the prepared-statement-id reported by the node.
1067+
unprepared_child = statement.statements.select do |s|
1068+
(s.is_a?(Cassandra::Statements::Prepared) || s.is_a?(Cassandra::Statements::Bound)) && s.id == r.id
1069+
end.first
1070+
cql = unprepared_child ? unprepared_child.cql : nil
1071+
else
1072+
# This is a normal statement, so we have everything we need.
1073+
cql = statement.cql
1074+
synchronize { @preparing_statements[host].delete(cql) }
11011075
end
11021076

11031077
prepare = prepare_statement(host, connection, cql, timeout)
11041078
prepare.on_complete do |_|
11051079
if prepare.resolved?
1106-
request.id = prepare.value
1080+
request.id = prepare.value unless request.is_a?(Cassandra::Protocol::BatchRequest)
11071081
do_send_request_by_plan(host,
11081082
connection,
11091083
promise,
@@ -1198,7 +1172,7 @@ def handle_response(response_future,
11981172
when Protocol::PreparedResultResponse
11991173
cql = request.cql
12001174
synchronize do
1201-
@prepared_statements[host][cql] = r.id
1175+
@prepared_statements[cql] = r.id
12021176
@preparing_statements[host].delete(cql)
12031177
end
12041178

@@ -1207,7 +1181,8 @@ def handle_response(response_future,
12071181
pk_idx ||= @schema.get_pk_idx(metadata)
12081182

12091183
promise.fulfill(
1210-
Statements::Prepared.new(r.custom_payload,
1184+
Statements::Prepared.new(r.id,
1185+
r.custom_payload,
12111186
r.warnings,
12121187
cql,
12131188
metadata,
@@ -1531,7 +1506,7 @@ def prepare_statement(host, connection, cql, timeout)
15311506
when Protocol::PreparedResultResponse
15321507
id = r.id
15331508
synchronize do
1534-
@prepared_statements[host][cql] = id
1509+
@prepared_statements[cql] = id
15351510
@preparing_statements[host].delete(cql)
15361511
end
15371512
id

lib/cassandra/statements/bound.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,19 @@ class Bound
2828
attr_reader :params
2929
# @private
3030
attr_reader :params_types, :result_metadata, :keyspace, :partition_key
31+
# @private prepared-statement id
32+
attr_reader :id
3133

3234
# @private
33-
def initialize(cql,
35+
def initialize(id,
36+
cql,
3437
params_types,
3538
result_metadata,
3639
params,
3740
keyspace = nil,
3841
partition_key = nil,
3942
idempotent = false)
43+
@id = id
4044
@cql = cql
4145
@params_types = params_types
4246
@result_metadata = result_metadata

lib/cassandra/statements/prepared.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,12 @@ class Prepared
2727
attr_reader :cql
2828
# @private
2929
attr_reader :result_metadata
30+
# @private prepared-statement id
31+
attr_reader :id
3032

3133
# @private
32-
def initialize(payload,
34+
def initialize(id,
35+
payload,
3336
warnings,
3437
cql,
3538
params_metadata,
@@ -44,6 +47,7 @@ def initialize(payload,
4447
retries,
4548
client,
4649
connection_options)
50+
@id = id
4751
@payload = payload
4852
@warnings = warnings
4953
@cql = cql
@@ -131,7 +135,8 @@ def bind(args = nil)
131135

132136
partition_key = create_partition_key(params)
133137

134-
Bound.new(@cql,
138+
Bound.new(@id,
139+
@cql,
135140
param_types,
136141
@result_metadata,
137142
params,

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/cassandra/session_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ module Cassandra
131131
let(:cql) { "INSERT INTO songs (id, title, album, artist, tags) VALUES (?, ?, ?, ?, ?)" }
132132
let(:result_metadata) { nil }
133133
let(:params_metadata) { Array.new(5) }
134-
let(:statement) { Statements::Prepared.new(nil, nil, cql, params_metadata, result_metadata, nil, nil, nil, nil, VOID_OPTIONS, nil, nil, nil, nil, nil) }
134+
let(:statement) { Statements::Prepared.new(nil, nil, nil, cql, params_metadata, result_metadata, nil, nil, nil, nil, VOID_OPTIONS, nil, nil, nil, nil, nil) }
135135

136136
it 'binds and executes result' do
137137
promise = double('promise')
@@ -151,7 +151,7 @@ module Cassandra
151151
let(:result_metadata) { nil }
152152
let(:params_metadata) { Array.new(5) }
153153
let(:params) { [1,2,3,4,5] }
154-
let(:statement) { Statements::Bound.new(cql, params_metadata, result_metadata, params) }
154+
let(:statement) { Statements::Bound.new(nil, cql, params_metadata, result_metadata, params) }
155155

156156
it 'executes statement' do
157157
promise = double('promise')

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)