Skip to content

sql: restore cursor state during automatic retries - #174676

Open
Alignyx wants to merge 1 commit into
cockroachdb:masterfrom
Alignyx:fix-173505-cursor-retry-state
Open

sql: restore cursor state during automatic retries#174676
Alignyx wants to merge 1 commit into
cockroachdb:masterfrom
Alignyx:fix-173505-cursor-retry-state

Conversation

@Alignyx

@Alignyx Alignyx commented Sep 4, 2026

Copy link
Copy Markdown

Fixes #173505.

Problem

SQL cursors are mutable session state, but their membership and iterator position were not part of either automatic-retry checkpoint:

  • a serializable transaction rewind restored the statement buffer, prepared-statement namespace, savepoints, session data, and advisory locks, but not sqlCursors;
  • a READ COMMITTED statement retry rolled back the KV savepoint and discarded buffered results, but left cursor state at the position reached by the failed attempt.

As a result, replaying FETCH or MOVE consumed rows twice, while replaying CLOSE failed because the first attempt had already removed and destroyed the cursor.

Solution

Snapshot cursor-map membership and the logical/persisted iterator position together with the existing rewindable state. Cursor resources are retained while a snapshot owns them, so a replayed CLOSE can be undone safely. Whole-transaction rewinds restore the transaction snapshot, and READ COMMITTED retries restore a statement-local snapshot after rolling back the KV savepoint.

Lazy cursors remain lazy at DECLARE. Immediately before a protected FETCH or MOVE, the cursor iterator is made seekable so its exact position can be restored. If that conversion fails before the cursor becomes rewindable, CockroachDB now returns the retry error instead of transparently replaying from corrupted cursor state.

Testing

Added regression coverage for:

  • a held cursor FETCH with no retry versus three transaction retries;
  • a second retry from a non-zero cursor position;
  • replaying CLOSE on a held cursor;
  • rebuilding an ordinary cursor declared before a savepoint;
  • a READ COMMITTED FETCH with no retry versus exactly three statement retries.

The full sharded //pkg/sql:sql_test target and the cursor-related local and local-read-committed logic tests pass.

Release note (bug fix): Automatic transaction and READ COMMITTED statement retries no longer advance or close SQL cursors more than once.

Snapshot cursor membership and iterator positions alongside other rewindable session state. Retain snapshotted cursor resources so replayed CLOSE operations can be undone, and make lazy cursors rewindable before retried FETCH or MOVE operations consume them.

Release note (bug fix): Automatic transaction and READ COMMITTED statement retries no longer advance or close SQL cursors more than once.
@blathers-crl

blathers-crl Bot commented Sep 4, 2026

Copy link
Copy Markdown

Thank you for contributing to CockroachDB. Please ensure you have followed the guidelines for creating a PR.

My owl senses detect your PR is good for review. Please keep an eye out for any test failures in CI.

I have added a few people who may be able to assist in reviewing:

🦉 Hoot! I am a Blathers, a bot for CockroachDB. My owner is dev-inf.

@blathers-crl blathers-crl Bot added O-community Originated from the community X-blathers-triaged blathers was able to find an owner labels Sep 4, 2026
@blathers-crl
blathers-crl Bot requested a review from DrewKimball September 4, 2026 08:09
@cockroachlabs-cla-agent

cockroachlabs-cla-agent Bot commented Sep 4, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@Alignyx

Alignyx commented Sep 4, 2026

Copy link
Copy Markdown
Author

Metamorphic validation and design notes

I validated the bug with paired executions in which the cursor query and query plan are identical; the only transformed input is the retry trigger (force_retry(0) versus force_retry(3)). Before this patch, the retry variants returned different rows:

Retry path Control Before the fix After the fix
Serializable transaction rewind, first held-cursor fetch [1, 2, 0] [7, 8, 0] [1, 2, 0]
Serializable transaction rewind, next fetch from the same cursor [3, 4, 0] advanced again [3, 4, 0]
READ COMMITTED statement retry [1, 2] [7, 8] [1, 2]
Serializable replay of CLOSE success cursor "held_close" does not exist success

The first execution-path divergence occurs after a retryable error. The executor truncates the failed attempt's buffered results and restores its transaction/KV checkpoint, but the SQL cursor map and iterator stay mutated. Each replay therefore begins from the state left by the previous failed attempt.

A smaller-looking fix that saves only curRow is insufficient: persisted cursors also hold a row-container iterator whose physical offset must be restored, and CLOSE changes map membership and destroys the backing resources. This patch therefore snapshots membership, logical position, iterator offset, and row-count state, while reference-counting resources owned by a retry snapshot.

Ordinary cursors add another constraint: DECLARE is intentionally lazy and the underlying plan iterator is not generally seekable. The transaction rewind point stays before such a declaration so a full replay rebuilds it. For statement-level retries, a snapshot-protected cursor is materialized immediately before FETCH/MOVE; this preserves lazy DECLARE behavior and cursor sensitivity up to the first operation while making the operation rewindable. If materialization itself fails before that invariant is established, the executor surfaces the retry error rather than returning silently incorrect rows.

Validation completed locally:

  • all 16 shards of //pkg/sql:sql_test;
  • focused TestInjectRetryErrors and cursor preparation coverage;
  • cursor logic tests under //pkg/sql/logictest/tests/local:local_test and //pkg/sql/logictest/tests/local-read-committed:local-read-committed_test;
  • repository formatting checks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

O-community Originated from the community X-blathers-triaged blathers was able to find an owner

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sql: automatic retries corrupt cursor state

1 participant