Skip to content

(fix) cqlengine: handle missing table metadata after schema change in sync_table - #11

Open
mykaul wants to merge 174 commits into
masterfrom
fix/sync-table-metadata-keyerror
Open

mykaul wants to merge 174 commits into
masterfrom
fix/sync-table-metadata-keyerror

Conversation

@mykaul

@mykaul mykaul commented Mar 13, 2026

Copy link
Copy Markdown
Owner

Summary

  • Wrap table metadata lookups in _sync_table() and _get_table_metadata() with try/except KeyError, forcing a targeted cluster.refresh_table_metadata() on miss and retrying once
  • Raises a descriptive CQLEngineException (instead of a bare KeyError) if the table is still not present after the forced refresh
  • Follows the same defensive pattern already used in _sync_type() (which calls cluster.refresh_user_type_metadata() after CREATE TYPE)

Root cause

After CREATE TABLE or ALTER TABLE, the local metadata cache may not yet contain the new table if schema agreement timed out or the automatic metadata refresh was skipped. Both _sync_table() (line 273) and _get_table_metadata() (line 435) unconditionally accessed cluster.metadata.keyspaces[ks].tables[table], which raised KeyError in this race condition.

This manifested as a flaky CI failure in TestContainsOperator.setUpClass → sync_table(IndexedTestModel).

Tests

  • Added unit tests (tests/unit/cqlengine/test_management.py) for _get_table_metadata verifying: immediate hit (no refresh), successful retry after refresh, and failure after refresh
  • All existing unit tests pass

@mykaul
mykaul force-pushed the fix/sync-table-metadata-keyerror branch 2 times, most recently from 6f1be93 to d5c3f18 Compare March 14, 2026 09:18
sylwiaszunejko and others added 25 commits March 17, 2026 23:47
Introduce the data layer for Private Link client routes support:

- ClientRoutesChangeType enum for CLIENT_ROUTES_CHANGE event types
- ClientRouteProxy dataclass and ClientRoutesConfig for user-facing
  configuration
- _Route frozen dataclass for immutable route records
- _RouteStore for thread-safe route storage with atomic update/merge
  and preferred route selection that avoids unnecessary connection_id
  migration when multiple routes exist for the same host
Add _ClientRoutesHandler which manages the full lifecycle of dynamic
address translation via system.client_routes:

- initialize(): loads all routes at startup and on control connection
  reconnect
- handle_client_routes_change(): processes CLIENT_ROUTES_CHANGE events
  with targeted merge or full refresh depending on event data
- _query_all_routes_for_connections(): complete refresh query using
  connection_id IN (...)
- _query_routes_for_change_event(): targeted query grouping by
  connection_id with host_id IN (...) per group
- _execute_routes_query(): common query execution and result parsing
  with proxy address override support
- resolve_host(): host_id to (address, port) resolution with DNS lookup
- ClientRoutesEndPointFactory: creates endpoints from system.peers rows
  by extracting host_id, deferring address translation and DNS resolution
  until connection time
- ClientRoutesEndPoint: endpoint that resolves via _ClientRoutesHandler
  on each connection attempt, ensuring immediate reaction to route changes
  and CLIENT_ROUTES_CHANGE events
Cluster:
- Add client_routes_config parameter with mutual exclusivity check
  against endpoint_factory
- Create _ClientRoutesHandler and ClientRoutesEndPointFactory when
  client_routes_config is provided

ControlConnection:
- Register CLIENT_ROUTES_CHANGE event watcher when handler is present
- Forward events to handler via _handle_client_routes_change
- Trigger full route re-read on control connection reconnection
Cover ClientRouteEntry/ClientRoutesConfig validation, _RouteStore
get/merge operations, _ClientRoutesHandler initialization,
ClientRoutesEndPoint resolution with and without route mappings,
and SSL check_hostname rejection with client_routes_config.
Add comprehensive integration tests covering:
- TCP proxy and NLB emulator infrastructure for simulating
  private link connectivity
- query_routes filtering with different connection/host ID combinations
- Full private-link connectivity verifying all driver connections
  go exclusively through the NLB proxy
- Dynamic route updates via REST API with driver reconnection
  through new proxy ports
Recently scylladb started to rely on the options "--auth-superuser-name"
and "--auth-superuser-salted-password" to ensure that a
cassandra/cassandra user exists for tests - without those options
a default superuser no longer exists.
…ames

Skip the regex scanner and stack-based parser in parse_casstype_args()
when the type string has no parentheses. For simple types like
'AsciiType' or 'org.apache.cassandra.db.marshal.FloatType', go directly
to lookup_casstype_simple() which is just a prefix strip + dict lookup.

This avoids re.Scanner, re.split on ':' / '=>', int() try/except, and
list-of-lists stack manipulation for the common case of non-parameterized
types.

Signed-off-by: Yaniv Kaul <yaniv.kaul@scylladb.com>
The time.sleep(10) in setup_keyspace() is redundant because callers
already ensure the cluster is fully ready before calling it:
- use_cluster() calls start_cluster_wait_for_up() which uses
  wait_for_binary_proto=True + wait_other_notice=True, then
  wait_for_node_socket() per node
- External cluster path (wait=False) had no sleep anyway

Remove the wait parameter entirely and its associated sleep, saving 10s
per cluster startup.
Replace fixed sleeps with condition-based polling to speed up tests:

- simulacron/utils.py: replace 5s sleep with HTTP endpoint polling
  (max 15s timeout, typically <1s)
- test_authentication.py: replace 10s sleep with auth readiness poll
  that tries connecting with default credentials
- upgrade/__init__.py: replace 10s auth sleep with same polling pattern
- upgrade/test_upgrade.py: replace 3x 20s sleeps (60s total) with
  control connection readiness polling

Total potential saving: ~95s of unconditional waiting per test run.
Replace fixed sleeps with condition-based polling in four test files:

- test_shard_aware.py: replace 25s of sleeps (5+10+5+5) with
  wait_until_not_raised polling for reconnection after shard connection
  close and iptables blocking
- test_metrics.py: replace 15s of sleeps (5+5+5) with polling for
  cluster recovery and node-down detection
- test_tablets.py: replace 13s of sleeps (3+10) with polling for
  metadata refresh and decommission completion
- simulacron/test_connection.py: replace 20s of sleeps (10+10) with
  polling for quiescent pool state

Total potential saving: ~73s of unconditional waiting.
… for invalidation

The tablet tests were intermittently failing because:
1. get_query_trace() used the default 2s max_wait, which is too short
   under resource pressure (--smp 2). Increased to 10s.
2. test_tablets_invalidation_decommission_non_cc_node used a fixed
   time.sleep(2) hoping tablet metadata invalidation would complete.
   Replaced with wait_until polling for the tablet record to be purged
   (0.5s delay, 20 attempts = 10s budget).
- test_cluster.py: replace sleep(1) x10 iterations with
  connect(wait_for_all_pools=True) for deterministic pool readiness
- test_query.py: replace sleep(5) with wait_until polling for
  'Preparing all known prepared statements' log message
- test_connection.py: replace sleep(2) with wait_until polling for
  host_down listener notification
…superuser config

Use set_configuration_options() (the Python API behind `ccm updateconf`) to
set auth_superuser_name and auth_superuser_salted_password directly in the
YAML config instead of passing them via the SCYLLA_EXT_OPTS environment
variable.
…ema_agreement

min(self._timeout, total_timeout - elapsed) raises TypeError when
control_connection_timeout is set to None, which is explicitly
documented as a supported value (meaning no timeout). Guard the
min() call so that when self._timeout is None, we use only the
remaining schema agreement wait time.
Patch reactor.running to False in setUp() so that maybe_start() always
enters the branch that spawns the reactor thread. Without this, leaked
global reactor state from prior tests can leave reactor.running as True,
causing maybe_start() to skip thread creation and the reactor.run mock
to never be called — making the assertion in test_connection_initialization
fail intermittently.

Observed in CI on PyPy 3.11 + macOS x86 (Rosetta 2), where timing
differences make the reactor state leak more likely.
The column kind filter at line 2744 used 'clustering_key' but
system_schema.columns uses 'clustering' as the kind value. This caused
clustering columns to not be excluded from the 'other columns' loop,
resulting in them being processed twice (once as clustering key, once
as regular column). The correct value 'clustering' was already used
6 lines above in the clustering key extraction loop.
ScyllaDB doesn't support triggers, so skip the triggers query when
connected to ScyllaDB. This is detected by checking if the connection
has shard awareness (using the existing _is_not_scylla() method).

Changes to both SchemaParserV3 and SchemaParserV4:
- Modified _query_all() to conditionally append triggers query only for non-ScyllaDB
- Modified _query_all() response unpacking to use array slicing for cleaner code
- Modified get_table() in V3 to conditionally query triggers

This eliminates unnecessary failed queries to system_schema.triggers on ScyllaDB.

Signed-off-by: Yaniv Kaul <yaniv.kaul@scylladb.com>
- Fix spelling: 'tring' → 'string' in docstring
- Remove extra 't' at end of comment
- Refactor complex list comprehension for clarity
- Use 'is None' instead of '== None' for None comparison

Co-authored-by: mykaul <4655593+mykaul@users.noreply.github.com>
Co-authored-by: mykaul <4655593+mykaul@users.noreply.github.com>
@mykaul
mykaul force-pushed the fix/sync-table-metadata-keyerror branch from 30a8fd7 to 59408e4 Compare April 2, 2026 16:56
avikivity and others added 2 commits April 7, 2026 01:05
The pool module emits a DEBUG log message when selecting a connection
for a query.

Emitting a log message for every query is too noisy.

Since Python logging lacks a TRACE level, just remove the log.
…e_async

Escape double quotes in keyspace names when constructing USE statements
to prevent CQL injection. A keyspace name containing '"' would produce
malformed or injectable CQL (e.g., USE "foo"bar"). This is the Python
equivalent of the vulnerability fixed in the Go driver (gocql#783).

The fix escapes '"' as '""' per CQL quoted-identifier rules, matching
the existing escape_name() function in cassandra/metadata.py.
sylwiaszunejko and others added 24 commits August 20, 2026 19:35
A connection now takes a session id, reported by every connection since
correlating a cluster's connections is the whole point of it, and a
configuration reporter, consulted only on the control connection because
the configuration is identical across them.

Both go in after the application's own options, and the four keys the driver
owns are cleared from those first, so an ApplicationInfo can neither
misreport the driver nor break the correlation. CQL_VERSION needs no
clearing, since StartupMessage writes it after the options map.

The application info is one object shared by every connection of a cluster,
so warning on each would repeat hosts x shards + 1 times per connect() and
again on every pool replacement. The warning is emitted on the control
connection, established once and before the pools, and left at debug
elsewhere.
The id is scoped to the Cluster rather than the Session: the control
connection, the one that describes the configuration, belongs to the
Cluster, so a per-Session id would leave it uncorrelated with the
connections it describes. It is exposed read-only as Cluster.session_id, so
an application can match its own logs against the clients table instead of
correlating by address and port, and is unrelated to Session.session_id,
which identifies a Session to Insights and is never sent to the cluster.

driver_config_reporting_enabled is read when a connection is opened, like
compression and no_compact beside it, so changing it after construction
takes effect on later connections. The session id carries no configuration
and is deliberately unaffected by it.
Check against a live ScyllaDB that every connection of a cluster reports the
same session id, that distinct clusters report distinct ones, that the
configuration report appears exactly once and only on the control
connection, and that disabling reporting removes it while leaving the id.

The tests run on a single node so the clients table, which lists only the
connections made to the node serving the query, is complete. Each claim
waits for the rows it rests on: a connection is listed only once the server
has registered it, and an absence or a "reported exactly once" holds
trivially over rows that have not arrived yet.

Share the lookup as get_client_options, which also skips the rows the server
has not filled in, and catch only InvalidRequest, the answer an absent table
gives: a bare except reported a timeout on system.clients as a failure of
system_views.clients.
Add a Scylla-specific guide section on SESSION_ID and DRIVER_CONFIG, how to
read them back out of the clients table, and how they relate to the
application_info options an application supplies itself. Spell out the two
points that are easy to get wrong: the option is named after the convention
shared with the other ScyllaDB drivers, where a session is what this driver
calls a Cluster, so it is unrelated to Session.session_id; and
driver_config_reporting_enabled turns off the configuration report only,
never the session id.

Also list the two new Cluster attributes in the API reference, and record in
the changelog that the startup options describing the driver are no longer
the application's to set.
It carried its own copy of the system/system_views fallback and the null row
skip, with the same bare except that reported a timeout on system.clients as
a failure of system_views.clients.
…d_info.pyx

cassandra/c_shard_info.pyx computed the high 64 bits of a 64x32-bit product
by casting biased_token to `__uint128_t` and shifting right by 64:

    cdef int shardId = (<__uint128_t>biased_token * self.shards_count) >> 64;

`__uint128_t` is a GCC/Clang compiler-builtin extension type, not standard
C/C++ and not part of Cython's own type system. MSVC has no 128-bit integer
type at all, so it treats `__uint128_t` as an undeclared identifier and fails
with cascading syntax errors (C2065/C2146/C2059) in the generated
c_shard_info.c. This breaks Windows wheel builds for any change that
triggers a rebuild of this extension.

Replaced it with a portable multiply-high decomposition that splits the
64x32-bit multiplication into 32-bit halves, using only 64-bit arithmetic
(uint64_t), matching the existing pure-Python fallback already implemented
in cassandra/shard_info.py. This compiles identically on GCC, Clang, and
MSVC, and is numerically identical to the previous 128-bit computation
(verified against 2M+ random inputs, cross-checked against both the pure
-Python fallback and the actual compiled extension).

Found while investigating an unrelated Windows CI failure on PR scylladb#806; the
bug is pre-existing and independent of that PR's changes, so it's fixed
here as its own standalone commit rather than folded into that PR.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The extension picks its multiply-high implementation at compile time: native
`unsigned __int128` where the compiler has it, the portable 64-bit
decomposition otherwise. Only one of the two is ever compiled on a given
platform, so the portable path — the whole point of this fix — was exercised
only on the Windows job, whose extension build is optional.

Split the shim into `cassandra_shard_id_native` / `cassandra_shard_id_portable`,
export both plus a `HAVE_INT128` flag, and cross-check them against
`_ShardingInfo` in the parity test. The production path is unchanged;
`shard_id_from_token` still calls `cassandra_shard_id_from_token`, which is
`#define`d to whichever implementation the platform supports.

Also narrow the test's import guard so only a missing extension is tolerated:
a missing transitive dependency or a failed module init now propagates instead
of silently downgrading the parity test to a skip. And add the missing
docstring.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Neither install was ever used by anything we build.

Windows: nothing in the job ever used it. Nothing there compiles against
OpenSSL, so no headers were needed. And everything that does TLS at
runtime carries its own copy - Python brings its own libssl/libcrypto,
and the cryptography package has OpenSSL built in - so nothing ever
looks
for a system-wide install. The step just put a copy on the runner that
sat unused.

Linux: the same, in two halves. `openssl` was already in the manylinux
image, so asking for it changed nothing. `openssl-devel` (the headers)
did install something new, but nothing ever compiled against it - every
test dependency arrives as a ready-made wheel, and the only one that
needs OpenSSL, cryptography, has it built into that wheel.
The configuration report has to tell a datacenter the user chose from
one the driver inferred, and DCAwareRoundRobinPolicy cannot: on_up()
assigns the inferred datacenter to the same local_dc attribute the
constructor set, so from the first host coming up the two are
indistinguishable. Capture it at construction instead, where an empty
local_dc counts as inferred -- which is what makes on_up() infer.

local_dc becomes read-only for the same reason. An assignment afterwards
would be indistinguishable from that inference all over again, so there
would be nothing left to capture: the constructor sets it, and on_up()
fills it in when the constructor was given none. Code that assigned it
should pass local_dc to the constructor instead.

RackAwareRoundRobinPolicy needs no such flag: both values are mandatory
constructor arguments and are never reassigned.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Connection.max_request_id and Connection.orphaned_threshold are both
derived from max_in_flight in the class body -- which runs once. A
subclass that lowers max_in_flight inherits values derived from this
class's, not from its own.

For orphaned_threshold that leaves a max_in_flight of 256 paired with a
threshold of 24576. A connection can hold no more orphaned stream ids
than it has request ids, so the count never reaches the threshold and
orphan-based replacement never happens for such a subclass at all -- the
safety mechanism is there and silently dead. max_in_flight is documented
as the knob for lower-level integrations that want an upper bound
without reimplementing the connection, so it is meant to be lowered.

Both are now derived rather than fixed in the class body. A connection
derives them in __init__ from the limit in force when it is built, so
they follow max_in_flight however it was tuned -- a subclass setting it
in a class body, an assignment on the class, or a patch in a test. The
last two are how the integration tests covering the in-flight bound set
it, and a value derived once would leave the pool's `in_flight <
max_request_id` gate at the untuned bound, never tripping.

The two staticmethods are for the configuration report, which has to
describe both before any connection exists: it asks with the class's
current limit rather than restating the arithmetic. Three places did
restate it -- the report and two of its tests -- and halving the
expression in __init__ left every one of them agreeing with each other
and disagreeing with the driver, with the whole suite passing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The DRIVER_CONFIG report is a cross-driver contract: an operator reading
system.clients.client_options relies on the same document whichever
driver wrote the row. That contract is a JSON Schema maintained
upstream, vendored here byte for byte -- and pinned as such by a test --
so drift from the shared copy shows up as a diff rather than as a
divergence nobody notices.

Validating is worth the dependency because every group in the schema is
additionalProperties: false, so a key this driver invents or misspells
fails hard instead of being silently dropped by a consumer.

These tests cover the harness and the contract, not the reporter, whose
report is still only {"version":1}: connection, control-plane and query
are all required, so it becomes conformant once the last of those groups
lands. Landing the schema first means every commit in between is checked
against it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The configuration groups that follow need the cluster whose settings
they describe, and one of them needs to know whether the node is a
ScyllaDB one. This puts both in place without changing what is reported.

The cluster is held weakly: it owns the reporter and hands it to every
connection it opens, so a strong reference here would keep it alive for
as long as any connection holds a reporter. Finding it gone is a
shutdown race rather than a misconfiguration, so the option is left out
at debug level.

is_scylla is passed in rather than discovered, because the connection
already knows -- _handle_options_response parses SUPPORTED into
self.features before it builds these options -- and the predicate is the
one the driver itself keys ScyllaDB-only behaviour off, so the report
describes what the driver will do rather than only what it was
configured to do. It is required: the sole caller always knows, and a
default would let a wrong answer through quietly.

The connection tests move to a stub report, so that what they establish
-- which connections carry the report, and that an application cannot
supply its own -- does not break on every configuration group that lands
next.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
What the driver does with a single connection: connect timeout, request
capacity, shard-aware pooling, socket options, reconnection policy and
TLS hostname verification.

Three of the schema's optional groups are left out for want of anything
to put in them -- this driver has no socket read or write timeout, and
the heartbeat group is empty in this schema version, so
idle_heartbeat_interval has nowhere to go -- worth raising for v2.
orphaned is reported, since Connection.orphaned_threshold bounds them;
the schema leaves the group out for a client where nothing does. Its max
is one below that threshold and floored at zero: the count is tested
after the id is added, so a threshold of one or less tolerates no orphan
at all, and the schema's nonNegativeInteger has no room for what
subtracting would otherwise give.

Socket options are read from sockopts rather than off a live socket.
That would be the effective value the schema asks for, but only some of
this driver's six reactors expose a socket object -- asyncio holds a
transport -- so the report would change shape with the reactor in use.
The driver sets none of its own, so an option absent from sockopts is at
the operating system's default, which for a fresh TCP socket is off.

Cluster.sockopts is materialized at construction, because it now has two
readers: the sockets the cluster opens, and this report. A one-shot
iterable would leave whichever ran second with nothing at all.

The reconnection policy is described by driving its schedule rather than
by reading max_attempts, because the values that stop one are not one kind
of thing: zero and a negative stop the loop on its first test, and a nan
stops it because every comparison against one is false -- and reaches the
constructor unchallenged, since `nan < 0` is false too. float('inf')
passes all three of those tests and means the opposite. A schedule that
yields nothing is the schema's null arm; reporting an exponential policy
with max-attempts left out would say the opposite, since the schema reads
an absent max-attempts as unlimited.

The reported base-ms is min(base_delay, max_delay). _add_jitter clamps
every delay with `min(max(base_delay, delay), max_delay)`, so max_delay
wins when the two are the wrong way round -- which the constructor
rejects, but both stay writable afterwards. Taking the minimum reports the
delay the policy will actually start at, and keeps the schema's
requirement that max-ms be at least base-ms true by construction: it is a
cross-property invariant JSON Schema cannot express, so the producer is
the only thing that can hold it.

inf and nan reach every duration setting unchallenged -- nothing
validates one, and a nan passes even the constructors that reject a
negative, since every comparison against one is false -- and both then
raise out of int(). That cost the whole report rather than the one key
that could not be converted, so each duration now says what the driver
does with such a value: the optional fields leave the key out, which is
already how this report says no limit is imposed, and the required ones
raise with a message naming the kind of value rather than an opaque
OverflowError.

A reconnection delay is the null arm. Both schedulers compare a deadline
of time.time() + delay against the clock, so an infinite delay or a nan
is never due and nothing is ever reconnected -- the same thing the null
arm already says of a schedule that yields nothing. A negative infinity
is the opposite and is not one of these: its deadline is already past,
so the timer fires at once and it reports as the immediate delay it is.

Policies dispatch on their exact type: a subclass of a built-in is a
policy the driver knows nothing about, and describing it as its parent
would put the parent's parameters against behaviour it does not have. A
custom one is reported by name and nothing else, though the schema
permits its public attributes too -- whatever it holds, an auth provider
or a credential, would land in system.clients for anyone who can select
from it, and there is no telling which attributes are safe. That also
bounds the report, so no configuration can drive it past the size limit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The timeouts on the driver's own queries: the ones it runs to discover
the cluster rather than on behalf of the application.

The two system-query timeouts are different things, which is why the
schema has both. client-side-ms is how long the driver waits for a
reply. server-side-ms is a limit the server enforces, which this driver
applies by appending USING TIMEOUT -- a ScyllaDB extension, so it is
reported only against a ScyllaDB node, mirroring
ControlConnection._try_connect: the report describes what the driver
will do, not only what it was configured to do.

Schema agreement stays in the report at zero, which says the driver does
not wait for agreement -- a setting rather than the absence of one.

Its timeout-ms is also the one field here that a duration the report
cannot carry takes down with it. A wait of inf or nan has no describable
length and the schema requires the key, so there is no conformant
document to be had and the report is dropped -- with a message saying
which kind of value did it, where an OverflowError under the generic
warning would not. The other two are optional and are simply left out:
server-side-ms because the builder cannot carry one either, being a
timedelta, so no USING TIMEOUT clause is appended at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
What the driver does with a statement that overrides none of it: the
defaults it applies, how a failure is retried, which node it goes to,
and whether a slow one is raced.

Reported from the default execution profile. The schema has one query
group and this driver has as many profiles as the application defines,
so the one that describes the session is the one a statement gets when
it names none -- which covers legacy configuration too, since Cluster
folds a load_balancing_policy or default_retry_policy given to its
constructor into that same profile. Other profiles cannot be described
under this schema version; worth raising for v2.

The built-in retry policies all subclass RetryPolicy, so dispatch is on
the exact type: isinstance would report every one of them as the
standard policy. ExponentialBackoffRetryPolicy has no arm of its own,
but it retries what the standard policy retries and adds a growing
delay, which is what the schema's backoff describes.

The schema's built-in load balancing arm carries flags describing where
a request may go, so it is claimed only when the chain holds a
token-aware policy and every policy in it is one this driver can account
for. A transparent wrapper above the token-aware policy does not
disqualify the chain; a policy whose routing cannot be seen from here
does, however ordinary the policy wrapping it --
WhiteListRoundRobinPolicy and HostFilterPolicy each confine routing to a
subset the flags have nowhere to record. The datacenter preference is
reported either way, found wherever in the policy chain it is set: it
says where requests go, not which policy sends them, and a bare
DCAwareRoundRobinPolicy -- what the driver falls back to without the
murmur3 extension -- pins the client just as firmly as a token-aware one
wrapping it.

A load balancer of None is resolved the way ResponseFuture resolves it --
`load_balancer or _default_load_balancing_policy` -- so legacy mode with
none set reports the policy a request routes with rather than the None.
One that nothing resolves raises instead of being reported: a request
takes make_query_plan off None and raises too, policy is a required key so
there is no conformant document to be had, and reporting a custom policy
would tell an operator a user-supplied one is routing.

Both of the durations this group adds are left out when they are not
finite, which the schema lets it do: speculative-execution because a
delay that is never due starts no additional execution, and retry
backoff because a retry scheduled that far out is one the driver never
reaches -- backoff being optional for exactly the case where there is no
delay to describe.

Three of the defaults are not the profile's. Paging and client
timestamps are Session settings and no Session exists when the control
connection reports, so what is described is the default every Session
starts with. Idempotence has no configurable default at all, so it is
always false. A custom timestamp generator leaves client-timestamps out
entirely: it may return None for some requests, leaving the coordinator
to assign the timestamp after all, and there is no telling from here
which it will do.

With this group the report is conformant, which the tests now assert
against the vendored schema -- including one that gives a custom policy
a password and asserts it appears nowhere in the report.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The unit tests establish that the reporter builds the right document.
These establish that the document reaches the server intact and
describes the client that sent it -- which is all an operator reading
system.clients has.

The existing {"version":1} assertion becomes a schema validation:
pinning the document here would duplicate the unit tests and break on
every group added to it. The round-trip test sets every setting it
checks away from its default, so a report built from the wrong source,
or from defaults, fails rather than happening to match.

Two of these cannot be unit tests. The server-side timeout is reported
only against ScyllaDB, and a unit test can only assert that for a flag
it passes in itself; here the detection runs against the SUPPORTED
response of an actual node. The inferred datacenter is the other: it is
inferred from a host that has to exist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The guide said the report carried a version and that more keys would
follow. Now that they have, it describes the three groups, shows what a
default Cluster reports, and links the schema where it is maintained --
an operator reading a report may well not be reading this driver's.

Five things get called out, because each is a way to misread a report
rather than a detail of it: that only the default execution profile is
described, that a custom policy is named and never serialized, that an
absent key means "does not apply" rather than "off", that the datacenter
says whether it was configured or inferred, and that the query defaults
are a snapshot taken before any Session exists.

The example is the real output of a default Cluster, verified against it
rather than written by hand.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Keep a newly discovered local host on the Unix socket used by the control connection while retaining advertised network metadata and factory-based duplicate detection.

Resolve control hosts by stable identity across topology refreshes, schema checks, error attribution, and DOWN/REMOVE callbacks. Preserve direct reconnect fallback for Unix-backed and alternate routes when DOWN handling queues no callback.

Keep shard-aware pools on the Unix route without advertised TCP ports or source-port shard targeting, and make mixed Unix/network Host ordering deterministic.
…ixin

NonCallableMagicMock.__init__ (via MagicMixin) replaces __hash__ on the
type with a MagicMock object. That MagicMock is not thread-safe, so
concurrent hash() calls — e.g. `connection in self._trash` in pool.py
return_connection — can raise `TypeError: __hash__ method should return
an integer` under concurrent access on Windows.

The previous __hash__ override was dead code: MagicMixin.__init__ always
replaced it with a MagicMock before any test could call it.

Fix by restoring a plain function as the class-level __hash__ after
super().__init__ runs, so hash() always resolves to a real function
instead of a thread-unsafe MagicMock callable.

Fixes flaky test_successful_wait_for_connection on Windows CI.
cibuildwheel started building cp315 by default in 4.2.0, without needing
the "cpython-prerelease" enable set: it pins a 3.15 release candidate,
which upstream guarantees is ABI compatible with the final release. 3.2.1
knows nothing about 3.15, so no amount of configuration would have
produced those wheels.

4.x dropped everything below Python 3.9 and no longer builds PyPy unless
`enable` asks for it, which leaves four dead entries in our `skip` list
that 4.2.1 warns about: `cp38*` and `pp38*` name versions it no longer
knows, `pp39*` names a PyPy it does not enable, and `pp3*t-*` matches
nothing, as there are no free-threaded PyPy builds. All four are removed.
`cp39*` stays, since 3.9 is still a build target we opt out of, and so
does `cp3*t-*`, which is what keeps the free-threaded CPython wheels out
of the set.

Nothing else in the configuration needs to change: `build = ["cp3*"]`
already selects cp315, PyPy is already covered by `enable = ["pypy"]`,
and 32-bit Linux, which 4.0.0 stopped building by default, was never
built here anyway.

Comparing `cibuildwheel --print-build-identifiers` between 3.2.1 and
4.2.1 for linux, macos and windows confirms the only difference is the
added cp315 target; PyPy stays at pp311 and no new architecture or
free-threaded identifier appears. The aarch64 invocation, which overrides
CIBW_BUILD, was checked the same way, and neither emits warnings.
Declare the 3.15 classifier, run the integration tests on 3.15 and on
free-threaded 3.15t, and say so in the docs. With the cibuildwheel bump
in place this is all that publishing 3.15 wheels needs; no driver code
change was required.

The matrix rows mirror the 3.14 ones, including the asyncore exclusions:
asyncore was removed from the standard library in 3.12, so only 3.11
still runs that event loop manager. As for 3.14, free-threaded wheels
are not published - 3.15t is tested but `skip = ["cp3*t-*"]` keeps it
out of the wheel set.

Verified by running the unit suite on both 3.15 and 3.15t (953 passed,
112 skipped on each) and by checking that the Cython-generated
extensions compile under 3.15.
@mykaul
mykaul force-pushed the fix/sync-table-metadata-keyerror branch from db3ea26 to efa4dae Compare September 13, 2026 12:56
dkropachev and others added 4 commits September 14, 2026 07:50
… sync_table

After CREATE TABLE or ALTER TABLE, the local metadata cache may not yet
contain the new table if schema agreement timed out or the automatic
metadata refresh was skipped.  _sync_table() and _get_table_metadata()
unconditionally accessed cluster.metadata.keyspaces[ks].tables[table],
which raised KeyError in this case.

Wrap both lookups in try/except KeyError.  On miss, force a targeted
cluster.refresh_table_metadata() call and retry once.  If the table is
still not present after the forced refresh, raise a descriptive
CQLEngineException instead of a bare KeyError.

This follows the same defensive pattern already used in _sync_type(),
which calls cluster.refresh_user_type_metadata() after CREATE TYPE.

Add unit tests for _get_table_metadata verifying: immediate hit (no
refresh), successful retry after refresh, and failure after refresh.

Also wrap the refresh_table_metadata() call itself in try/except: it
can raise a DriverException (e.g. if schema agreement fails), which
was previously left to propagate uncaught.  Catch it and re-raise as a
contextual CQLEngineException, matching the exception path used for
the "still missing after refresh" case.  Chain both re-raises with
`from exc` so the original driver error/KeyError is preserved in the
traceback instead of being replaced by a bare raise.

Add a unit test verifying that a DriverException raised during the
refresh itself results in a CQLEngineException, not an unhandled
driver error.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Drop the detailed description from the test_management.py docstring,
keeping only the general module-level summary.
@mykaul
mykaul force-pushed the fix/sync-table-metadata-keyerror branch from efa4dae to 9b78589 Compare September 20, 2026 14:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.