fix(plugin-cassandra): Drop stale tables if table creation process fails#27100
Merged
pdabre12 merged 3 commits intoprestodb:masterfrom Feb 12, 2026
Merged
fix(plugin-cassandra): Drop stale tables if table creation process fails#27100pdabre12 merged 3 commits intoprestodb:masterfrom
pdabre12 merged 3 commits intoprestodb:masterfrom
Conversation
Contributor
Reviewer's GuideRefactors the Cassandra connector to use per-transaction CassandraMetadata instances tied to UUID-based transaction handles, adds commit/rollback semantics that execute a stored rollback action to drop newly created tables when table creation fails, wires the new lifecycle through the connector modules, and updates tests to use transactional metadata access and verify rollback behavior. Sequence diagram for Cassandra table creation rollback on transaction abortsequenceDiagram
actor PrestoEngine
participant CassandraConnector
participant CassandraMetadata
participant CassandraSession
participant Cassandra
PrestoEngine->>CassandraConnector: beginTransaction(isolationLevel, readOnly)
CassandraConnector->>CassandraConnector: checkConnectorSupports(READ_UNCOMMITTED, isolationLevel)
CassandraConnector->>CassandraTransactionHandle: new CassandraTransactionHandle()
CassandraConnector->>CassandraMetadata: new CassandraMetadata(connectorId, cassandraSession, partitionManager, extraColumnMetadataCodec, config)
CassandraConnector->>CassandraConnector: transactions.put(transaction, metadata)
CassandraConnector-->>PrestoEngine: transactionHandle
PrestoEngine->>CassandraConnector: getMetadata(transactionHandle)
CassandraConnector->>CassandraConnector: metadata = transactions.get(transactionHandle)
CassandraConnector-->>PrestoEngine: CassandraMetadata
PrestoEngine->>CassandraMetadata: createTable(session, tableMetadata)
CassandraMetadata->>CassandraSession: execute(CREATE TABLE ...)
CassandraSession->>Cassandra: CREATE TABLE
Cassandra-->>CassandraSession: success
CassandraSession-->>CassandraMetadata: ok
CassandraMetadata->>CassandraMetadata: setRollback(schemaName, tableName)
CassandraMetadata-->>PrestoEngine: CassandraOutputTableHandle
PrestoEngine->>PrestoEngine: failure before finishCreateTable
PrestoEngine->>CassandraConnector: rollback(transactionHandle)
CassandraConnector->>CassandraConnector: metadata = transactions.remove(transactionHandle)
CassandraConnector->>CassandraMetadata: rollback()
CassandraMetadata->>CassandraMetadata: rollbackAction.getAndSet(null)
CassandraMetadata->>CassandraSession: execute(DROP TABLE schema.table)
CassandraSession->>Cassandra: DROP TABLE
Cassandra-->>CassandraSession: success
CassandraSession-->>CassandraMetadata: ok
CassandraMetadata-->>CassandraConnector: rollback completed
CassandraConnector-->>PrestoEngine: rollback completed
Class diagram for updated Cassandra transaction and metadata lifecycleclassDiagram
class CassandraTransactionHandle {
- UUID uuid
+ CassandraTransactionHandle()
+ CassandraTransactionHandle(UUID uuid)
+ UUID getUuid()
+ boolean equals(Object obj)
+ int hashCode()
+ String toString()
}
class CassandraConnector {
- CassandraConnectorId connectorId
- LifeCycleManager lifeCycleManager
- CassandraPartitionManager partitionManager
- CassandraClientConfig config
- CassandraSession cassandraSession
- CassandraSplitManager splitManager
- ConnectorRecordSetProvider recordSetProvider
- ConnectorPageSinkProvider pageSinkProvider
- List~PropertyMetadata~ sessionProperties
- JsonCodec~List~ExtraColumnMetadata~~ extraColumnMetadataCodec
- ConcurrentMap~ConnectorTransactionHandle, CassandraMetadata~ transactions
+ CassandraConnector(CassandraConnectorId connectorId, LifeCycleManager lifeCycleManager, CassandraSplitManager splitManager, CassandraRecordSetProvider recordSetProvider, CassandraPageSinkProvider pageSinkProvider, CassandraSessionProperties sessionProperties, CassandraSession cassandraSession, CassandraPartitionManager partitionManager, JsonCodec~List~ExtraColumnMetadata~~ extraColumnMetadataCodec, CassandraClientConfig config)
+ ConnectorTransactionHandle beginTransaction(IsolationLevel isolationLevel, boolean readOnly)
+ ConnectorCommitHandle commit(ConnectorTransactionHandle transaction)
+ void rollback(ConnectorTransactionHandle transaction)
+ ConnectorMetadata getMetadata(ConnectorTransactionHandle transaction)
+ boolean isSingleStatementWritesOnly()
}
class CassandraMetadata {
- CassandraConnectorId connectorId
- CassandraSession cassandraSession
- CassandraPartitionManager partitionManager
- CassandraClientConfig config
- JsonCodec~List~ExtraColumnMetadata~~ extraColumnMetadataCodec
- AtomicReference~Runnable~ rollbackAction
- boolean allowDropTable
+ CassandraMetadata(CassandraConnectorId connectorId, CassandraSession cassandraSession, CassandraPartitionManager partitionManager, JsonCodec~List~ExtraColumnMetadata~~ extraColumnMetadataCodec, CassandraClientConfig config)
+ CassandraOutputTableHandle createTable(ConnectorSession session, ConnectorTableMetadata tableMetadata)
+ Optional~ConnectorOutputMetadata~ finishCreateTable(ConnectorSession session, ConnectorOutputTableHandle tableHandle, Collection~Slice~ fragments, Collection~ComputedStatistics~ computedStatistics)
+ void rollback()
+ String normalizeIdentifier(ConnectorSession session, String identifier)
- void setRollback(String schemaName, String tableName)
- void clearRollback()
}
class CassandraSession {
+ void execute(String cql)
}
class CassandraPartitionManager
class CassandraClientConfig
class ExtraColumnMetadata
class LifeCycleManager
class CassandraSplitManager
class CassandraRecordSetProvider
class CassandraPageSinkProvider
class CassandraSessionProperties {
+ List~PropertyMetadata~ getSessionProperties()
}
CassandraConnector --> CassandraTransactionHandle : creates
CassandraConnector "1" --> "*" CassandraMetadata : per transaction
CassandraConnector --> CassandraSession
CassandraConnector --> CassandraPartitionManager
CassandraConnector --> CassandraClientConfig
CassandraConnector --> CassandraSplitManager
CassandraConnector --> CassandraRecordSetProvider
CassandraConnector --> CassandraPageSinkProvider
CassandraConnector --> CassandraSessionProperties
CassandraConnector --> ExtraColumnMetadata : uses
CassandraMetadata --> CassandraSession
CassandraMetadata --> CassandraPartitionManager
CassandraMetadata --> CassandraClientConfig
CassandraMetadata --> ExtraColumnMetadata : encoded by
CassandraTransactionHandle ..|> ConnectorTransactionHandle
CassandraConnector ..|> Connector
CassandraMetadata ..|> ConnectorMetadata
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
54fffa4 to
ce28def
Compare
Contributor
There was a problem hiding this comment.
Hey - I've found 3 issues, and left some high level feedback:
- In
CassandraMetadata.rollback(), throwingPERMISSION_DENIEDwheneverallowDropTableis false will make any rollback fail for that catalog, even when no rollback action was registered; consider only enforcing this when a rollback action is actually present, to avoid breaking read-only transactions or transactions that never created a table. - The single
AtomicReference<Runnable> rollbackActioncombined withcheckStateinsetRollbackmeans a transaction can only safely create one table; if multiple table-creation operations per transaction are expected, this should be changed to track rollback actions per table or explicitly guarded/validated at a higher level. - The
transactionsmap inCassandraConnectorrelies on the engine to always callcommit/rollback; if that contract might not hold under failures, consider adding a defensive cleanup path (e.g., on connector shutdown) or logging to help detect and debug leaked transaction entries.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `CassandraMetadata.rollback()`, throwing `PERMISSION_DENIED` whenever `allowDropTable` is false will make *any* rollback fail for that catalog, even when no rollback action was registered; consider only enforcing this when a rollback action is actually present, to avoid breaking read-only transactions or transactions that never created a table.
- The single `AtomicReference<Runnable> rollbackAction` combined with `checkState` in `setRollback` means a transaction can only safely create one table; if multiple table-creation operations per transaction are expected, this should be changed to track rollback actions per table or explicitly guarded/validated at a higher level.
- The `transactions` map in `CassandraConnector` relies on the engine to always call `commit`/`rollback`; if that contract might not hold under failures, consider adding a defensive cleanup path (e.g., on connector shutdown) or logging to help detect and debug leaked transaction entries.
## Individual Comments
### Comment 1
<location> `presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraMetadata.java:374-380` </location>
<code_context>
return caseSensitiveNameMatchingEnabled ? identifier : identifier.toLowerCase(ROOT);
}
+
+ public void rollback()
+ {
+ if (!allowDropTable) {
+ throw new PrestoException(
+ PERMISSION_DENIED, "Table creation was aborted and requires rollback, but cleanup failed because DROP TABLE is disabled in this Cassandra catalog.");
+ }
+ Optional.ofNullable(rollbackAction.getAndSet(null)).ifPresent(Runnable::run);
+ }
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Rollback should only fail with PERMISSION_DENIED when there is an actual rollback action to run
Currently, `rollback()` throws `PrestoException(PERMISSION_DENIED, ...)` whenever `allowDropTable` is false, even if `rollbackAction` is `null`. That means transactions that never created a table will still fail rollback when `allowDropTable` is disabled.
You can avoid this by only enforcing the permission when there’s an actual action to run, e.g.:
```java
public void rollback()
{
Runnable action = rollbackAction.getAndSet(null);
if (action == null) {
return; // nothing to roll back
}
if (!allowDropTable) {
throw new PrestoException(
PERMISSION_DENIED,
"Table creation was aborted and requires rollback, but cleanup failed because DROP TABLE is disabled in this Cassandra catalog.");
}
action.run();
}
```
</issue_to_address>
### Comment 2
<location> `presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraConnector.java:143` </location>
<code_context>
+ concurrentCreateTable = new SchemaTableName(database, "concurrent_create_table");
}
@Test
</code_context>
<issue_to_address>
**suggestion (testing):** Strengthen `testRollbackTables` by asserting the table exists before rollback to prove cleanup actually happens
Currently `testRollbackTables` only asserts that `rollbackTable` is absent after rollback, which would still pass if the table creation silently failed. To verify that rollback actually cleans up an existing table, add an assertion that the table exists immediately after `beginCreateTable` and before simulating the failure, then keep the final `assertFalse` after `connector.rollback(transactionHandle)` so the test confirms a transition from present to absent.
</issue_to_address>
### Comment 3
<location> `presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraConnector.java:105` </location>
<code_context>
protected SchemaTableName tableUnpartitioned;
protected SchemaTableName invalidTable;
+ protected SchemaTableName rollbackTable;
+ protected SchemaTableName concurrentCreateTable;
private CassandraServer server;
- private ConnectorMetadata metadata;
</code_context>
<issue_to_address>
**suggestion (testing):** Either exercise `concurrentCreateTable` in a test or remove it; consider testing multiple `beginCreateTable` calls in the same transaction
`concurrentCreateTable` is added but never used in tests. Given the new `AtomicReference<Runnable> rollbackAction` and the `checkState` in `setRollback`, this would be a good candidate to verify behavior when `beginCreateTable` is invoked multiple times in the same transaction (e.g., second call throws `IllegalStateException` because a rollback is already set). If you don’t intend to test that scenario, consider removing `concurrentCreateTable` to keep the fixture focused.
Suggested implementation:
```java
protected SchemaTableName invalidTable;
protected SchemaTableName rollbackTable;
private CassandraServer server;
```
Search the remainder of `TestCassandraConnector.java` for any usages or initializations of `concurrentCreateTable` (for example, in `@BeforeClass setup()` or any test methods) and remove those lines as well, since the field is no longer defined.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
presto-cassandra/src/main/java/com/facebook/presto/cassandra/CassandraMetadata.java
Outdated
Show resolved
Hide resolved
presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraConnector.java
Show resolved
Hide resolved
presto-cassandra/src/test/java/com/facebook/presto/cassandra/TestCassandraConnector.java
Outdated
Show resolved
Hide resolved
tdcmeehan
previously approved these changes
Feb 11, 2026
tdcmeehan
approved these changes
Feb 12, 2026
This was referenced Mar 31, 2026
15 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Dropping stale tables left behind in Cassandra connector if table creation process fails.
Motivation and Context
When a CREATE TABLE or CTAS operation fails during execution, Cassandra connector may leave behind a partially created table.
This results in:
Impact
No user impact
Test Plan
Unit test, CI
Contributor checklist
Release Notes
Please follow release notes guidelines and fill in the release notes below.
Summary by Sourcery
Add per-transaction Cassandra metadata with rollback support to clean up tables created during failed or aborted table-creation operations.
Bug Fixes:
Enhancements:
Summary by Sourcery
Ensure Cassandra connector tracks per-transaction metadata and cleans up partially created tables on rollback to avoid stale tables after failed table creation.
Bug Fixes:
Enhancements:
Tests: