Skip to content

perf(storage-cassandra): restore copy writes plain INSERTs, not the LWT path - #527

Open
LeeroyHannigan wants to merge 1 commit into
mainfrom
fix/404-cassandra-restore-copy
Open

LeeroyHannigan wants to merge 1 commit into
mainfrom
fix/404-cassandra-restore-copy

Conversation

@LeeroyHannigan

Copy link
Copy Markdown
Collaborator

Summary

RestoreTableFromBackup on Cassandra copied each backup item through the ordinary put path: a read of the empty target row, a lightweight transaction, and a logged batch for the index rows, eight items at a time (#404). The target is CREATING for the whole copy and the copy is its only writer, so none of that guards anything. A 60,000-item restore took 27.8 s on this host against 1.5 to 3.6 s on the other three backends.

The copy now writes each item as plain INSERTs with no read and no IF condition: the base row and one row per GSI and LSI the item carries keys for, built by the same function the put path uses so their shape and projection are identical, 64 items in flight with two payload buckets read ahead. Measured from the CREATING response to ACTIVE: 60,000 items with no index 27.8 s to 2.2 s; 20,000 items with one GSI and one LSI 33.9 s to 0.7 s. In both shapes the item count, every item (Scan) and every index row (Query on each index partition) compared equal between source and target.

Stamps on restored rows

Every later conditional write fences on the row's version, so restored rows carry one: the clock reading in microseconds, which is the value a later IF version = ? compares against on this tree and the value #424 moves new rows to. Neither the base row nor the index rows carry a client write timestamp: every later index mutation on this tree (put, update, delete, transaction commit, the GSI queue worker) is stamped by the coordinator, and a restored index row stamped from the host clock that ran the restore could outlive a later delete whose coordinator clock trailed that host (the Cassandra rule, reproduced with cqlsh: a cell stamped ahead of the coordinator survives an unstamped delete). Once index mutations carry version-derived timestamps (#424), the restored index rows take version as theirs; that is a one-argument follow-up noted in restore_copy.rs.

Direct tests pin the behaviour: after a restore, UpdateItem moving an item across GSI and LSI partitions, conditional PutItem and DeleteItem on restored values, attribute_not_exists on a restored key, and TransactWriteItems over restored items all behave as on put-path rows; the restored row shape (SELECT JSON * of base and index tables, minus version) equals the put path's; the index rows are coordinator-stamped.

Recovery

The copy runs in the task #384 detached from the request, with its heartbeat. Measured on the node: a target data table dropped from outside mid-copy was detected and the target removed from the catalog in 1.2 s, after which a second restore of the same backup and a CreateTable of the name both succeeded; a node restart mid-copy left the target CREATING for the sweep, which the next server start ran (the client session does not reconnect after a node restart on this tree, so the in-process sweep could not; that is #403 and #457's territory, noted there). The copy checks the target's ownership per item as well as per bucket, so a swept target stops taking writes within one item. Two restores at once complete normally; a 60,000-item restore under a sixteen-thread PutItem storm on another table took 10.3 s with zero writer errors and no node warnings. 64 writes in flight is far below one connection's 32,767 stream ids.

Tests

  • crates/storage-cassandra/tests/direct/restore_write.rs (2): restored rows take every later write; pk-only rows across two payload buckets.
  • crates/storage-cassandra/tests/direct/restore_write_controls.rs (4): coordinator-stamped index rows; a backup item with a mistyped GSI key fails the restore (the target is removed, never ACTIVE); a recorded item count that disagrees with the payload fails the restore; the restored row shape equals the put path's. Each fails under the mutant it was written for (host-clock stamp; validate_index_keys removed; count check removed; an extra column written).
  • crates/storage-cassandra/tests/direct/restore_timing.rs (2, ignored): the two measured shapes, run with --ignored.
  • Eight mutants over the copy: each fails at least one test.
  • Four existing tests that probe the CREATING window use 10,000 items so the copy outlasts their probes; the simultaneous-restore test holds a registration on the backup and asserts every concurrent restore is BackupInUse naming the held target, then that the next restore is admitted once the registration is released (10 of 10 runs); its sequential section is unchanged.
  • backup_engine:: direct tests 16, --lib 64; the backup and restore wire suite 36 passed on a Cassandra server; cargo fmt, clippy 1.97.0 with -D warnings, 1.88.0 check: clean. No dependency change. New files under 600 lines; data/index.rs 723 to 748, tests/direct/backup_engine.rs 2091 to 2155 (pre-existing files over 600).

Merge notes

Not in this PR

Pre-existing, noted or filed separately: the Cassandra client session does not reconnect after a node restart, so a restore that fails while the node is unreachable cannot abandon its target and the in-process sweep does not run until the server restarts (#403, #457); CreateBackup on Cassandra is bound to the request future (a dropped 200,000-item request left no backup record); the restore response includes the target's DDL (10.7 s on a node with 470 tables); TransactWriteItems Put over an existing item leaves its old index row when the put moves an index key (#424 fixes it); test_restore_sweep_worker_sweeps_at_startup_and_stops_on_shutdown has an 8 s budget that a loaded host can exceed.

Closes #404

…WT put path

RestoreTableFromBackup copied each backup item through put_item_impl:
a read of the (empty) target row, a lightweight transaction, and a
logged batch for the index rows, eight items at a time. The target is
CREATING for the whole copy and the copy is its only writer, so none of
that guards anything.

data/restore_copy.rs holds the copy's reads and writes. read_backup_bucket
returns one payload bucket's items. restore_writer resolves the target's
indexes once per copy (LOCAL_QUORUM), and write_restored_item writes an
item as plain INSERTs with no read and no IF condition: the base row
(pk, sort key column, item_data, version) and one row per GSI and LSI
the item carries keys for, issued concurrently at LOCAL_QUORUM. Index
rows come from index_row_insert, so their shape and projection are the
put path's. The index key type and emptiness validation is kept.

version is the clock reading in microseconds, the value a later
IF version = ? fence compares against. Neither the base row nor the
index rows carry a client timestamp: every later index mutation on this
tree (put, update, delete, transaction commit, the GSI queue worker) is
stamped by the coordinator, and a restored row stamped from the host
clock that ran the restore could outlive a later delete whose
coordinator clock trailed that host.

Nothing else is written: a restored table starts with streams disabled,
TTL settings are not restored, and every GSI row is written in the copy,
so there is no gsi_pending entry.

copy_backup_into_target reads payload buckets two ahead of the writes
and writes 64 items at a time across bucket boundaries, checking the
target's ownership per item as well as per bucket, so a swept target
stops taking writes within one item. The size accumulation and the item
count check are unchanged. The IndexWrites enum and put_restored_item
that the old copy needed are removed from put_get_item.rs.

Measured on one host against a single cassandra:4.1 node with a 4 GB
heap, from the CREATING response to ACTIVE: 60,000 items with no index
27.8 s before, 2.2 s after; 20,000 items with one GSI and one LSI 33.9 s
before, 0.7 s after. Item counts, every item and every index row
compared equal between source and target in both shapes.

Direct tests: restored rows take every later write (update, conditional
put and delete, transactions, across GSI and LSI rows); the index rows
are coordinator-stamped; a backup item with a mistyped index key fails
the restore; a recorded item count that disagrees with the payload fails
the restore; the restored row shape equals the put path's. Four existing
tests that probe the CREATING window use 10,000 items so the copy
outlasts their probes, and the simultaneous-restore test holds a
registration on the backup so exclusivity is asserted against a held
registration rather than a race.

Closes #404

This branch has not been deployed

No deployments
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.

storage-cassandra: restore copy writes every item through the LWT put path

1 participant