Skip to content

Commit f6cbf8d

Browse files
committed
prepared-metadata: harden cached decoder state
1 parent b8b714c commit f6cbf8d

10 files changed

Lines changed: 1779 additions & 119 deletions

CHANGELOG.rst

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,26 @@ Features
77
statements skip re-sending result metadata on EXECUTE, and the driver automatically
88
refreshes cached metadata when the server detects a schema change (DRIVER-153)
99

10+
Bug Fixes
11+
---------
12+
* Harden prepared-result metadata caching against concurrent schema/client decoder
13+
changes. Cached metadata, ids, and decoder provenance are published atomically;
14+
UDT descriptors are isolated by registered Python class; UDT registration
15+
invalidates cached ids before publishing mapping changes; and futures refresh
16+
their request snapshot before fetching another page or after re-prepare.
17+
* Restrict ``skip_meta`` to immutable snapshots of built-in protocol handlers.
18+
Modifying their public decoder configuration disables the optimization until
19+
it is restored, while custom handlers continue to receive full result metadata.
20+
1021
Others
1122
------
1223
* ``PreparedStatement.result_metadata`` and ``PreparedStatement.result_metadata_id`` are
1324
now read-only. They are replaced together by
1425
``PreparedStatement.update_result_metadata()``, so a request can never observe a metadata
1526
id paired with result metadata from a different schema version. Code that assigned either
16-
attribute directly must call ``update_result_metadata()`` instead.
27+
attribute directly must call ``update_result_metadata()`` instead. The metadata getter
28+
keeps its historical list shape but returns a defensive copy, so mutating it cannot
29+
change the internal metadata/id pair.
1730
* Message serialization now receives the connection's negotiated ``ProtocolFeatures``:
1831
``Connection.send_msg`` passes ``protocol_features`` to the encoder, and
1932
``_ProtocolHandler.encode_message`` forwards it to each message's ``send_body``.
@@ -24,8 +37,10 @@ Others
2437
forward it. There is deliberately no compatibility fallback: protocol extensions
2538
are negotiated per connection at STARTUP, so an encoder unaware of
2639
``protocol_features`` could silently omit fields a negotiated extension requires.
27-
This release emits no new bytes on the wire; the parameter is groundwork for
28-
upcoming protocol extensions (``SCYLLA_USE_METADATA_ID``, ``TABLETS_ROUTING_V2``).
40+
When ``SCYLLA_USE_METADATA_ID`` is negotiated, EXECUTE messages use this plumbing
41+
to include the extension's metadata id and may request that result metadata be
42+
omitted. Connections that do not negotiate the extension retain their existing
43+
frame layout.
2944

3045
3.29.11
3146
=======

cassandra/cluster.py

Lines changed: 429 additions & 35 deletions
Large diffs are not rendered by default.

cassandra/cqltypes.py

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import time
4242
import struct
4343
import sys
44+
from threading import Lock
4445
from uuid import UUID
4546

4647
from cassandra.marshal import (int8_pack, int8_unpack, int16_pack, int16_unpack,
@@ -977,30 +978,49 @@ class UserType(TupleType):
977978
typename = "org.apache.cassandra.db.marshal.UserType"
978979

979980
_cache = {}
981+
_cache_lock = Lock()
980982
_module = sys.modules[__name__]
981983

982984
@classmethod
983-
def make_udt_class(cls, keyspace, udt_name, field_names, field_types):
985+
def make_udt_class(cls, keyspace, udt_name, field_names, field_types,
986+
mapped_class=None):
984987
assert len(field_names) == len(field_types)
985988

986-
instance = cls._cache.get((keyspace, udt_name))
987-
if not instance or instance.fieldnames != field_names or instance.subtypes != field_types:
988-
instance = type(udt_name, (cls,), {'subtypes': field_types,
989-
'cassname': cls.cassname,
990-
'typename': udt_name,
991-
'fieldnames': field_names,
992-
'keyspace': keyspace,
993-
'mapped_class': None,
994-
'tuple_type': cls._make_registered_udt_namedtuple(keyspace, udt_name, field_names)})
995-
cls._cache[(keyspace, udt_name)] = instance
989+
# A UDT type descriptor can outlive the response that created it (for
990+
# example, in a PreparedStatement's cached result metadata). Keep a
991+
# distinct descriptor for each registered Python mapping so parsing
992+
# metadata for another Cluster cannot mutate an existing descriptor.
993+
#
994+
# Use identity rather than the class itself in the key. Registered
995+
# classes normally are hashable, but a custom metaclass may opt out of
996+
# hashing. The generated descriptor holds a strong reference to
997+
# mapped_class, so its id cannot be reused while this cache entry exists.
998+
cache_key = (keyspace, udt_name, id(mapped_class))
999+
with cls._cache_lock:
1000+
instance = cls._cache.get(cache_key)
1001+
if not instance or instance.fieldnames != field_names or instance.subtypes != field_types:
1002+
instance = type(udt_name, (cls,), {'subtypes': field_types,
1003+
'cassname': cls.cassname,
1004+
'typename': udt_name,
1005+
'fieldnames': field_names,
1006+
'keyspace': keyspace,
1007+
'mapped_class': mapped_class,
1008+
'tuple_type': cls._make_registered_udt_namedtuple(keyspace, udt_name, field_names)})
1009+
cls._cache[cache_key] = instance
9961010
return instance
9971011

9981012
@classmethod
9991013
def evict_udt_class(cls, keyspace, udt_name):
1000-
try:
1001-
del cls._cache[(keyspace, udt_name)]
1002-
except KeyError:
1003-
pass
1014+
# Registration changes must evict mapped and unmapped variants alike.
1015+
# Slicing also recognizes two-element keys left by older driver code in
1016+
# a long-running process that reloads this module.
1017+
with cls._cache_lock:
1018+
cache_keys = [
1019+
cache_key for cache_key in cls._cache
1020+
if cache_key[:2] == (keyspace, udt_name)
1021+
]
1022+
for cache_key in cache_keys:
1023+
del cls._cache[cache_key]
10041024

10051025
@classmethod
10061026
def apply_parameters(cls, subtypes, names):

cassandra/protocol.py

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -891,9 +891,9 @@ def read_type(cls, f, user_type_map):
891891
num_fields = read_short(f)
892892
names, types = zip(*((read_string(f), cls.read_type(f, user_type_map))
893893
for _ in range(num_fields)))
894-
specialized_type = typeclass.make_udt_class(ks, udt_name, names, types)
895-
specialized_type.mapped_class = user_type_map.get(ks, {}).get(udt_name)
896-
typeclass = specialized_type
894+
mapped_class = user_type_map.get(ks, {}).get(udt_name)
895+
typeclass = typeclass.make_udt_class(
896+
ks, udt_name, names, types, mapped_class=mapped_class)
897897
elif typeclass == CUSTOM_TYPE:
898898
classname = read_string(f)
899899
typeclass = lookup_casstype(classname)
@@ -1100,6 +1100,76 @@ def send_body(self, f, protocol_version, protocol_features=None):
11001100
"Continuous paging backpressure is not supported.")
11011101

11021102

1103+
_PREPARED_METADATA_RESULT_ATTRIBUTES = (
1104+
'opcode',
1105+
'name',
1106+
'kind',
1107+
'results',
1108+
'paging_state',
1109+
'_FLAGS_GLOBAL_TABLES_SPEC',
1110+
'_HAS_MORE_PAGES_FLAG',
1111+
'_NO_METADATA_FLAG',
1112+
'_CONTINUOUS_PAGING_FLAG',
1113+
'_CONTINUOUS_PAGING_LAST_FLAG',
1114+
'_METADATA_ID_FLAG',
1115+
'column_names',
1116+
'column_types',
1117+
'parsed_rows',
1118+
'continuous_paging_seq',
1119+
'continuous_paging_last',
1120+
'new_keyspace',
1121+
'column_metadata',
1122+
'query_id',
1123+
'bind_metadata',
1124+
'pk_indexes',
1125+
'schema_change_event',
1126+
'is_lwt',
1127+
'tracing',
1128+
'custom_payload',
1129+
'warnings',
1130+
'__init__',
1131+
'recv',
1132+
'recv_body',
1133+
'recv_results_rows',
1134+
'recv_results_prepared',
1135+
'recv_results_metadata',
1136+
'recv_prepared_metadata',
1137+
'recv_results_schema_change',
1138+
'read_type',
1139+
'recv_row',
1140+
)
1141+
1142+
1143+
def _class_attribute(cls, name):
1144+
for base in cls.__mro__:
1145+
if name in vars(base):
1146+
return vars(base)[name]
1147+
return None
1148+
1149+
1150+
def _prepared_metadata_cache_config(handler, result_message_type):
1151+
"""
1152+
Capture the canonical driver-owned result-decoder configuration.
1153+
1154+
Cluster-side snapshots compare against this exact configuration. Installing
1155+
an application ResultMessage or mutating a decoding attribute therefore
1156+
makes the handler ineligible for skip-metadata instead of attempting to
1157+
freeze arbitrary application class state.
1158+
"""
1159+
return (
1160+
tuple(handler.message_types_by_opcode.items()),
1161+
result_message_type,
1162+
tuple(result_message_type.type_codes.items()),
1163+
tuple(getattr(result_message_type, 'code_to_type', {}).items()),
1164+
_class_attribute(handler, 'decode_message'),
1165+
tuple(
1166+
(name, _class_attribute(result_message_type, name))
1167+
for name in _PREPARED_METADATA_RESULT_ATTRIBUTES
1168+
),
1169+
handler.column_encryption_policy,
1170+
)
1171+
1172+
11031173
class _ProtocolHandler(object):
11041174
"""
11051175
_ProtocolHander handles encoding and decoding messages.
@@ -1121,6 +1191,11 @@ class _ProtocolHandler(object):
11211191
column_encryption_policy = None
11221192
"""Instance of :class:`cassandra.policies.ColumnEncryptionPolicy` in use by this handler"""
11231193

1194+
# Marks handlers whose result-decoder configuration the driver may copy
1195+
# into an immutable per-request snapshot. Subclasses do not inherit
1196+
# eligibility because their opcode and type maps are application-owned.
1197+
_prepared_metadata_cache_token = object()
1198+
11241199
@classmethod
11251200
def encode_message(cls, msg, stream_id, protocol_version, compressor, allow_beta_protocol_version,
11261201
protocol_features):
@@ -1245,6 +1320,10 @@ def decode_message(cls, protocol_version, protocol_features, user_type_map, stre
12451320
return msg
12461321

12471322

1323+
_ProtocolHandler._prepared_metadata_cache_config = \
1324+
_prepared_metadata_cache_config(_ProtocolHandler, ResultMessage)
1325+
1326+
12481327
def cython_protocol_handler(colparser):
12491328
"""
12501329
Given a column parser to deserialize ResultMessages, return a suitable
@@ -1284,7 +1363,11 @@ class CythonProtocolHandler(_ProtocolHandler):
12841363
message_types_by_opcode = my_opcodes
12851364

12861365
col_parser = colparser
1366+
_prepared_metadata_cache_token = object()
12871367

1368+
CythonProtocolHandler._prepared_metadata_cache_config = \
1369+
_prepared_metadata_cache_config(
1370+
CythonProtocolHandler, FastResultMessage)
12881371
return CythonProtocolHandler
12891372

12901373

0 commit comments

Comments
 (0)