Skip to content

Commit 4cc1326

Browse files
lokioreclaude
andcommitted
PHOENIX-7905: address review feedback (alterIndex correctness, addColumn freshTable, tenantId-null lock contract, default interface methods, ops log demote, new tests)
- alterIndex: hoist transform-lock acquire + under-lock re-check to BEFORE the index-state UPSERT (was AFTER), so a contending caller cannot leave a half-committed BUILDING state behind on lock-acquire failure. Thread the post-re-check freshTable into evaluateStmtProperties, incrementTableSeqNum, updateIndexState, and addTransform. - addColumn: hoist freshTable to acquiredTransformLock scope and thread it into incrementTableSeqNum + addTransform so transform-decision sites see the post-re-check view rather than the stale snapshot (parallel to alterIndex fix). - ConnectionQueryServices: convert acquireTransformLock / releaseTransformLock to default methods so older binary-compatible delegates keep compiling; Javadoc clarifies tenantId is accepted for API symmetry but is NOT part of the lock rowkey, and that callers MUST release in a finally block. - ConnectionQueryServicesImpl: pass null for tenantId when constructing the SYSTEM.MUTEX rowkey to make tenant-scope independence explicit; mark TRANSFORM_LOCK_MARKER @VisibleForTesting; demote contended-mutex log line from ERROR to INFO (lock contention is expected steady-state, not an operator-actionable event). - TransformLockIT: rename testReleaseIsIdempotent to testReleaseDoesNotThrow to match the actual contract; assert schema/table render in the transform-in-progress exception message; add testGlobalTransformBlocksTenantTransform, testTenantBlocksGlobalAndOtherTenants, testNonTransformAlterIndexDoesNotAcquireLock, testConcurrentAlterIndexSerializesViaTransformLock. Generated-by: Claude Code (Opus 4.7) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent e7ef87b commit 4cc1326

4 files changed

Lines changed: 221 additions & 49 deletions

File tree

phoenix-core-client/src/main/java/org/apache/phoenix/query/ConnectionQueryServices.java

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -303,24 +303,37 @@ public void deleteMutexCell(String tenantId, String schemaName, String tableName
303303
* physical table) with concurrent transform lifecycle operations. Non-transform-triggering ALTERs
304304
* (e.g., SET TTL, ADD COLUMN, SET IMMUTABLE_ROWS) do not contend on this lock.
305305
* <p>
306+
* Lock scope: keyed on {@code (schemaName, tableName)} only — the {@code tenantId} arg is
307+
* accepted for API symmetry but does NOT participate in the lock rowkey. A transform-triggering
308+
* change on a (schema, table) blocks any new transform attempt on the same table regardless of
309+
* tenant, and vice versa.
310+
* <p>
306311
* Implemented on top of SYSTEM.MUTEX, so the lock auto-expires after the column-family TTL
307312
* ({@link org.apache.phoenix.jdbc.PhoenixDatabaseMetaData#TTL_FOR_MUTEX} = 15 min) if the holder
308313
* dies without releasing.
314+
* <p>
315+
* Callers MUST invoke {@link #releaseTransformLock} from a {@code finally} block on every path
316+
* where this method returned {@code true}.
309317
* @return true if the caller acquired the lock; false if it is currently held by another caller
310318
* <p>
311319
* Caller is responsible for completing the operation within the column-family TTL; this
312320
* is a coarse advisory lock with no fencing token. Holders that pause past the TTL may
313321
* both observe lock-acquired simultaneously.
314322
*/
315-
boolean acquireTransformLock(String tenantId, String schemaName, String tableName)
316-
throws SQLException;
323+
default boolean acquireTransformLock(String tenantId, String schemaName, String tableName)
324+
throws SQLException {
325+
return true;
326+
}
317327

318328
/**
319-
* Release the transform lock on the given logical table. Idempotent — no-op if the lock is not
320-
* held (already released, expired via TTL, or never acquired by this caller).
329+
* Release the transform lock on the given logical table. Caller MUST call this from a
330+
* {@code finally} block whenever {@link #acquireTransformLock} returned {@code true}. The release
331+
* deletes the lock cell unconditionally; callers MUST NOT call release after the column-family
332+
* TTL expiry, otherwise a different caller's lock cell may be deleted.
321333
*/
322-
void releaseTransformLock(String tenantId, String schemaName, String tableName)
323-
throws SQLException;
334+
default void releaseTransformLock(String tenantId, String schemaName, String tableName)
335+
throws SQLException {
336+
}
324337

325338
/**
326339
* Truncate a phoenix table

phoenix-core-client/src/main/java/org/apache/phoenix/query/ConnectionQueryServicesImpl.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,8 @@ public ConnectionInfo getConnectionInfo() {
423423
private final boolean shouldThrottleNumConnections;
424424
public static final byte[] MUTEX_LOCKED = "MUTEX_LOCKED".getBytes(StandardCharsets.UTF_8);
425425
// Disambiguate the transform-lock row in SYSTEM.MUTEX from other lock purposes
426-
public static final String TRANSFORM_LOCK_MARKER = "TRANSFORM_LOCK";
426+
@VisibleForTesting
427+
static final String TRANSFORM_LOCK_MARKER = "TRANSFORM_LOCK";
427428
private ServerSideRPCControllerFactory serverSideRPCControllerFactory;
428429
private boolean localIndexUpgradeRequired;
429430

@@ -5614,7 +5615,7 @@ public boolean writeMutexCell(String tenantId, String schemaName, String tableNa
56145615
String msg = " tenantId : " + tenantId + " schemaName : " + schemaName + " tableName : "
56155616
+ tableName + " columnName : " + columnName + " familyName : " + familyName;
56165617
if (!checkAndPut) {
5617-
LOGGER.error(processName + " failed to acquire mutex for " + msg);
5618+
LOGGER.info(processName + " mutex acquire contended for " + msg);
56185619
} else {
56195620
LOGGER.debug(processName + " acquired mutex for " + msg);
56205621
}
@@ -5659,13 +5660,16 @@ public void deleteMutexCell(String tenantId, String schemaName, String tableName
56595660
@Override
56605661
public boolean acquireTransformLock(String tenantId, String schemaName, String tableName)
56615662
throws SQLException {
5662-
return writeMutexCell(tenantId, schemaName, tableName, TRANSFORM_LOCK_MARKER, null);
5663+
// Lock is keyed on (schemaName, tableName) only — tenantId is intentionally not part of the
5664+
// SYSTEM.MUTEX rowkey so that a transform on a (schema, table) blocks any concurrent
5665+
// transform on the same table regardless of tenant scope.
5666+
return writeMutexCell(null, schemaName, tableName, TRANSFORM_LOCK_MARKER, null);
56635667
}
56645668

56655669
@Override
56665670
public void releaseTransformLock(String tenantId, String schemaName, String tableName)
56675671
throws SQLException {
5668-
deleteMutexCell(tenantId, schemaName, tableName, TRANSFORM_LOCK_MARKER, null);
5672+
deleteMutexCell(null, schemaName, tableName, TRANSFORM_LOCK_MARKER, null);
56695673
}
56705674

56715675
@VisibleForTesting

phoenix-core-client/src/main/java/org/apache/phoenix/schema/MetaDataClient.java

Lines changed: 51 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4818,6 +4818,10 @@ public MutationState addColumn(PTable table, List<ColumnDef> origColumnDefs,
48184818
Set<String> acquiredColumnMutexSet = Sets.newHashSetWithExpectedSize(3);
48194819
boolean acquiredBaseTableMutex = false;
48204820
boolean acquiredTransformLock = false;
4821+
// Tracks the post-re-check fresh table view; used as the source-of-truth for transform-decision
4822+
// sites (incrementTableSeqNum + addTransform). Defaults to {@code table} when no transform is
4823+
// needed or when the lock was already held by a parent frame (no second re-check happens).
4824+
PTable freshTable = table;
48214825
try {
48224826
connection.setAutoCommit(false);
48234827
List<ColumnDef> columnDefs;
@@ -4959,8 +4963,9 @@ public MutationState addColumn(PTable table, List<ColumnDef> origColumnDefs,
49594963
// Re-check under lock with fresh metadata: a concurrent transform-triggering ALTER
49604964
// may have committed between our first checkIsTransformNeeded and the lock acquire.
49614965
// If so, the transform we observed is stale — drop through and treat as no-op.
4962-
PTable freshTable =
4963-
connection.getTableNoCache(SchemaUtil.getTableName(schemaName, tableName));
4966+
// Capture freshTable so downstream transform-decision sites (incrementTableSeqNum,
4967+
// addTransform) see the post-re-check view rather than the stale {@code table}.
4968+
freshTable = connection.getTableNoCache(SchemaUtil.getTableName(schemaName, tableName));
49644969
isTransformNeeded = TransformClient.checkIsTransformNeeded(metaProperties, schemaName,
49654970
freshTable, tableName, null, tenantIdToUse, connection);
49664971
}
@@ -5201,7 +5206,7 @@ public MutationState addColumn(PTable table, List<ColumnDef> origColumnDefs,
52015206
long seqNum = 0;
52025207
if (changingPhoenixTableProperty || columnDefs.size() > 0) {
52035208
seqNum =
5204-
incrementTableSeqNum(table, tableType, columnDefs.size(), metaPropertiesEvaluated);
5209+
incrementTableSeqNum(freshTable, tableType, columnDefs.size(), metaPropertiesEvaluated);
52055210

52065211
tableMetaData
52075212
.addAll(connection.getMutationState().toMutations(timeStamp).next().getSecond());
@@ -5211,8 +5216,8 @@ public MutationState addColumn(PTable table, List<ColumnDef> origColumnDefs,
52115216
PTable transformingNewTable = null;
52125217
if (isTransformNeeded) {
52135218
try {
5214-
transformingNewTable = TransformClient.addTransform(connection, tenantIdToUse, table,
5215-
metaProperties, seqNum, PTable.TransformType.METADATA_TRANSFORM);
5219+
transformingNewTable = TransformClient.addTransform(connection, tenantIdToUse,
5220+
freshTable, metaProperties, seqNum, PTable.TransformType.METADATA_TRANSFORM);
52165221
} catch (SQLException ex) {
52175222
connection.rollback();
52185223
throw ex;
@@ -6001,9 +6006,6 @@ public MutationState alterIndex(AlterIndexStatement statement) throws SQLExcepti
60016006

60026007
boolean isTransformNeeded = TransformClient.checkIsTransformNeeded(metaProperties, schemaName,
60036008
table, indexName, dataTableName, tenantId, connection);
6004-
MetaPropertiesEvaluated metaPropertiesEvaluated = new MetaPropertiesEvaluated();
6005-
boolean changingPhoenixTableProperty = evaluateStmtProperties(metaProperties,
6006-
metaPropertiesEvaluated, table, schemaName, tableName, new MutableBoolean(false));
60076009

60086010
PIndexState newIndexState = statement.getIndexState();
60096011
IndexConsistency newIndexConsistency = statement.getIndexConsistency();
@@ -6050,6 +6052,37 @@ public MutationState alterIndex(AlterIndexStatement statement) throws SQLExcepti
60506052
connection.setAutoCommit(false);
60516053
// Confirm index table is valid and up-to-date
60526054
TableRef indexRef = FromCompiler.getResolver(statement, connection).getTables().get(0);
6055+
6056+
// If the statement is a transform-triggering ALTER INDEX, acquire the transform lock and
6057+
// re-check freshness under the lock BEFORE we commit any index-state UPSERT. Otherwise a
6058+
// contending caller would see (or leave behind) a half-committed BUILDING state if the
6059+
// lock acquire failed below.
6060+
PTable freshTable = table;
6061+
boolean stillTransformNeeded = false;
6062+
if (isTransformNeeded) {
6063+
if (indexRef.getTable().getViewIndexId() != null) {
6064+
throw new SQLExceptionInfo.Builder(SQLExceptionCode.CANNOT_TRANSFORM_LOCAL_OR_VIEW_INDEX)
6065+
.setSchemaName(schemaName).setTableName(indexName).build().buildException();
6066+
}
6067+
acquiredTransformLock =
6068+
connection.getQueryServices().acquireTransformLock(tenantId, schemaName, tableName);
6069+
if (!acquiredTransformLock) {
6070+
throw new SQLExceptionInfo.Builder(
6071+
SQLExceptionCode.CANNOT_MODIFY_TABLE_WITH_TRANSFORM_IN_PROGRESS)
6072+
.setSchemaName(schemaName).setTableName(tableName).build().buildException();
6073+
}
6074+
// Re-check under lock with fresh metadata: a concurrent transform-triggering ALTER may
6075+
// have committed between our first checkIsTransformNeeded and the lock acquire. If the
6076+
// observed transform is no longer needed, drop through and treat as no-op.
6077+
freshTable = connection.getTableNoCache(SchemaUtil.getTableName(schemaName, tableName));
6078+
stillTransformNeeded = TransformClient.checkIsTransformNeeded(metaProperties, schemaName,
6079+
freshTable, indexName, dataTableName, tenantId, connection);
6080+
}
6081+
6082+
MetaPropertiesEvaluated metaPropertiesEvaluated = new MetaPropertiesEvaluated();
6083+
boolean changingPhoenixTableProperty = evaluateStmtProperties(metaProperties,
6084+
metaPropertiesEvaluated, freshTable, schemaName, tableName, new MutableBoolean(false));
6085+
60536086
try (PreparedStatement tableUpsert = connection.prepareStatement(
60546087
newIndexState == PIndexState.ACTIVE ? UPDATE_INDEX_STATE_TO_ACTIVE : UPDATE_INDEX_STATE)) {
60556088
tableUpsert.setString(1,
@@ -6069,14 +6102,15 @@ public MutationState alterIndex(AlterIndexStatement statement) throws SQLExcepti
60696102
connection.rollback();
60706103

60716104
if (changingPhoenixTableProperty) {
6072-
seqNum = incrementTableSeqNum(table, statement.getTableType(), 0, metaPropertiesEvaluated);
6105+
seqNum =
6106+
incrementTableSeqNum(freshTable, statement.getTableType(), 0, metaPropertiesEvaluated);
60736107
tableMetadata
60746108
.addAll(connection.getMutationState().toMutations(timeStamp).next().getSecond());
60756109
connection.rollback();
60766110
}
60776111

60786112
MetaDataMutationResult result = connection.getQueryServices().updateIndexState(tableMetadata,
6079-
dataTableName, properties, table);
6113+
dataTableName, properties, freshTable);
60806114

60816115
try {
60826116
MutationCode code = result.getMutationCode();
@@ -6090,34 +6124,13 @@ public MutationState alterIndex(AlterIndexStatement statement) throws SQLExcepti
60906124
.setSchemaName(schemaName).setTableName(indexName).build().buildException();
60916125
}
60926126

6093-
if (isTransformNeeded) {
6094-
if (indexRef.getTable().getViewIndexId() != null) {
6095-
throw new SQLExceptionInfo.Builder(
6096-
SQLExceptionCode.CANNOT_TRANSFORM_LOCAL_OR_VIEW_INDEX).setSchemaName(schemaName)
6097-
.setTableName(indexName).build().buildException();
6098-
}
6099-
acquiredTransformLock =
6100-
connection.getQueryServices().acquireTransformLock(tenantId, schemaName, tableName);
6101-
if (!acquiredTransformLock) {
6102-
throw new SQLExceptionInfo.Builder(
6103-
SQLExceptionCode.CANNOT_MODIFY_TABLE_WITH_TRANSFORM_IN_PROGRESS)
6104-
.setSchemaName(schemaName).setTableName(tableName).build().buildException();
6105-
}
6106-
// Re-check under lock with fresh metadata: a concurrent transform-triggering ALTER may
6107-
// have committed between our first checkIsTransformNeeded and the lock acquire. If the
6108-
// observed transform is no longer needed, drop through and treat as no-op.
6109-
PTable freshTable =
6110-
connection.getTableNoCache(SchemaUtil.getTableName(schemaName, tableName));
6111-
boolean stillTransformNeeded = TransformClient.checkIsTransformNeeded(metaProperties,
6112-
schemaName, freshTable, indexName, dataTableName, tenantId, connection);
6113-
if (stillTransformNeeded) {
6114-
try {
6115-
TransformClient.addTransform(connection, tenantId, table, metaProperties, seqNum,
6116-
PTable.TransformType.METADATA_TRANSFORM);
6117-
} catch (SQLException ex) {
6118-
connection.rollback();
6119-
throw ex;
6120-
}
6127+
if (isTransformNeeded && stillTransformNeeded) {
6128+
try {
6129+
TransformClient.addTransform(connection, tenantId, freshTable, metaProperties, seqNum,
6130+
PTable.TransformType.METADATA_TRANSFORM);
6131+
} catch (SQLException ex) {
6132+
connection.rollback();
6133+
throw ex;
61216134
}
61226135
}
61236136

0 commit comments

Comments
 (0)