Skip to content

Commit 68be118

Browse files
kevincaimergify[bot]
authored andcommitted
[BugFix] Locker: rollback partial intensive-lock acquisition (#72423)
Signed-off-by: Kevin Cai <kevin.cai@celerdata.com> (cherry picked from commit f32f70b) # Conflicts: # fe/fe-core/src/main/java/com/starrocks/common/util/concurrent/lock/Locker.java
1 parent 52de1a0 commit 68be118

2 files changed

Lines changed: 141 additions & 10 deletions

File tree

fe/fe-core/src/main/java/com/starrocks/common/util/concurrent/lock/Locker.java

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -287,19 +287,31 @@ private boolean checkExistenceInLock(Database database, LockType lockType) {
287287
public void lockTablesWithIntensiveDbLock(Long dbId, List<Long> tableList, LockType lockType) {
288288
Preconditions.checkState(lockType.equals(LockType.READ) || lockType.equals(LockType.WRITE));
289289
List<Long> tableListClone = new ArrayList<>(tableList);
290+
<<<<<<< HEAD
290291
if (Config.lock_manager_enabled && Config.lock_manager_enable_using_fine_granularity_lock) {
292+
=======
293+
if (Config.lock_manager_enabled) {
294+
LockType intentionType = (lockType == LockType.WRITE)
295+
? LockType.INTENTION_EXCLUSIVE
296+
: LockType.INTENTION_SHARED;
297+
boolean dbLockHeld = false;
298+
List<Long> ridLockedList = new ArrayList<>();
299+
>>>>>>> f32f70b5ca ([BugFix] Locker: rollback partial intensive-lock acquisition (#72423))
291300
try {
292-
if (lockType == LockType.WRITE) {
293-
this.lock(dbId, LockType.INTENTION_EXCLUSIVE, 0);
294-
} else {
295-
this.lock(dbId, LockType.INTENTION_SHARED, 0);
296-
}
301+
this.lock(dbId, intentionType, 0);
302+
dbLockHeld = true;
297303

298304
Collections.sort(tableListClone);
299305
for (Long rid : tableListClone) {
300306
this.lock(rid, lockType, 0);
307+
ridLockedList.add(rid);
301308
}
302309
} catch (LockException e) {
310+
// Roll back any locks already acquired so that a partial failure
311+
// (e.g. deadlock victim selection on the second / Nth lock call)
312+
// does not leave a stranded DB intention or table lock that
313+
// would block subsequent DB-WRITE operations indefinitely.
314+
rollbackPartialIntensiveLock(dbId, intentionType, dbLockHeld, ridLockedList, lockType);
303315
throw ErrorReportException.report(ErrorCode.ERR_LOCK_ERROR, e.getMessage());
304316
}
305317
} else {
@@ -308,6 +320,30 @@ public void lockTablesWithIntensiveDbLock(Long dbId, List<Long> tableList, LockT
308320
}
309321
}
310322

323+
/**
324+
* Best-effort rollback of a partial intensive-lock acquisition. Releases each
325+
* already-acquired lock and swallows any errors raised by individual release
326+
* calls so that the original LockException (the actual cause of the rollback)
327+
* surfaces to the caller instead of being masked by a release-side error.
328+
*/
329+
private void rollbackPartialIntensiveLock(Long dbId, LockType intentionType,
330+
boolean dbLockHeld, List<Long> ridLockedList, LockType tableLockType) {
331+
for (Long rid : ridLockedList) {
332+
try {
333+
this.release(rid, tableLockType);
334+
} catch (Exception releaseEx) {
335+
LOG.warn("Failed to release table lock {} during partial-acquire rollback", rid, releaseEx);
336+
}
337+
}
338+
if (dbLockHeld) {
339+
try {
340+
this.release(dbId, intentionType);
341+
} catch (Exception releaseEx) {
342+
LOG.warn("Failed to release DB intention lock {} during partial-acquire rollback", dbId, releaseEx);
343+
}
344+
}
345+
}
346+
311347
/**
312348
* No need to release lock explicitly, it will be released automatically when the locker failed.
313349
*/
@@ -388,15 +424,24 @@ public void unLockTableWithIntensiveDbLock(Long dbId, Long tableId, LockType loc
388424
*/
389425
public void lockTableWithIntensiveDbLock(Long dbId, Long tableId, LockType lockType) {
390426
Preconditions.checkState(lockType.equals(LockType.READ) || lockType.equals(LockType.WRITE));
427+
<<<<<<< HEAD
391428
if (Config.lock_manager_enabled && Config.lock_manager_enable_using_fine_granularity_lock) {
429+
=======
430+
if (Config.lock_manager_enabled) {
431+
LockType intentionType = (lockType == LockType.WRITE)
432+
? LockType.INTENTION_EXCLUSIVE
433+
: LockType.INTENTION_SHARED;
434+
boolean dbLockHeld = false;
435+
>>>>>>> f32f70b5ca ([BugFix] Locker: rollback partial intensive-lock acquisition (#72423))
392436
try {
393-
if (lockType == LockType.WRITE) {
394-
this.lock(dbId, LockType.INTENTION_EXCLUSIVE, 0);
395-
} else {
396-
this.lock(dbId, LockType.INTENTION_SHARED, 0);
397-
}
437+
this.lock(dbId, intentionType, 0);
438+
dbLockHeld = true;
398439
this.lock(tableId, lockType, 0);
399440
} catch (LockException e) {
441+
// Roll back the DB intention lock if step 1 succeeded but the
442+
// table-lock acquisition failed (e.g. deadlock victim, interrupt).
443+
// Otherwise the caller would leak an IS/IX lock on the DB.
444+
rollbackPartialIntensiveLock(dbId, intentionType, dbLockHeld, Collections.emptyList(), lockType);
400445
throw ErrorReportException.report(ErrorCode.ERR_LOCK_ERROR, e.getMessage());
401446
}
402447
} else {

fe/fe-core/src/test/java/com/starrocks/common/lock/TestLockInterface.java

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,16 @@
1717
import com.google.common.collect.Lists;
1818
import com.starrocks.catalog.Database;
1919
import com.starrocks.common.Config;
20+
import com.starrocks.common.ErrorReportException;
2021
import com.starrocks.common.util.concurrent.QueryableReentrantReadWriteLock;
2122
import com.starrocks.common.util.concurrent.lock.LockException;
2223
import com.starrocks.common.util.concurrent.lock.LockManager;
2324
import com.starrocks.common.util.concurrent.lock.LockParams;
2425
import com.starrocks.common.util.concurrent.lock.LockType;
2526
import com.starrocks.common.util.concurrent.lock.Locker;
27+
import com.starrocks.common.util.concurrent.lock.NotSupportLockException;
2628
import com.starrocks.server.GlobalStateMgr;
29+
import mockit.Invocation;
2730
import mockit.Mock;
2831
import mockit.MockUp;
2932
import org.junit.After;
@@ -226,6 +229,89 @@ public void testTryLockTablesWithIntensiveDbLock2() throws LockException {
226229
Assert.assertTrue(lockManager.isOwner(rid2, locker, LockType.WRITE));
227230
}
228231

232+
/**
233+
* Regression: lockTableWithIntensiveDbLock acquires a DB intention lock first
234+
* and a table lock second. If the table-lock acquisition throws (deadlock
235+
* victim, interrupt, etc.) the helper must release the already-held DB
236+
* intention lock; otherwise it leaks an IS / IX lock and blocks subsequent
237+
* DB-WRITE operations until the FE restarts.
238+
*/
239+
@Test
240+
public void testLockTableWithIntensiveDbLockRollsBackDbLockOnTableLockFailure() {
241+
long dbId = 100L;
242+
long tableId = 101L;
243+
Database database = new Database(dbId, "db_rollback_single");
244+
245+
// Force the second LockManager.lock call (the table-lock acquisition) to fail.
246+
// The first call (DB intention) is allowed to proceed normally so the rollback
247+
// path actually has something to release.
248+
final int[] callCount = {0};
249+
new MockUp<LockManager>() {
250+
@Mock
251+
public void lock(Invocation inv, long rid, Locker locker, LockType lockType, long timeout)
252+
throws LockException {
253+
callCount[0]++;
254+
if (callCount[0] == 2) {
255+
throw new NotSupportLockException("simulated table-lock failure");
256+
}
257+
inv.proceed();
258+
}
259+
};
260+
261+
Locker locker = new Locker();
262+
Assertions.assertThrows(ErrorReportException.class, () ->
263+
locker.lockTableWithIntensiveDbLock(database.getId(), tableId, LockType.WRITE));
264+
265+
LockManager lockManager = GlobalStateMgr.getCurrentState().getLockManager();
266+
Assertions.assertFalse(lockManager.isOwner(database.getId(), locker, LockType.INTENTION_EXCLUSIVE),
267+
"DB intention lock must be released after partial-acquire rollback");
268+
Assertions.assertFalse(lockManager.isOwner(tableId, locker, LockType.WRITE),
269+
"Table lock must not be held (acquisition failed)");
270+
}
271+
272+
/**
273+
* Regression: lockTablesWithIntensiveDbLock loops over multiple table ids.
274+
* If the Nth table-lock acquisition fails after earlier ones succeeded, the
275+
* helper must release the DB intention lock AND every already-acquired table
276+
* lock; otherwise it strands all of them.
277+
*/
278+
@Test
279+
public void testLockTablesWithIntensiveDbLockRollsBackAllAcquiredLocksOnNthTableLockFailure() {
280+
long dbId = 200L;
281+
long tableId1 = 201L;
282+
long tableId2 = 202L;
283+
Database database = new Database(dbId, "db_rollback_multi");
284+
285+
// Call sequence: 1 = DB IX, 2 = tableId1 (succeeds), 3 = tableId2 (fails).
286+
// The helper sorts the table list, and 201 < 202, so this ordering is
287+
// deterministic.
288+
final int[] callCount = {0};
289+
new MockUp<LockManager>() {
290+
@Mock
291+
public void lock(Invocation inv, long rid, Locker locker, LockType lockType, long timeout)
292+
throws LockException {
293+
callCount[0]++;
294+
if (callCount[0] == 3) {
295+
throw new NotSupportLockException("simulated table-lock failure");
296+
}
297+
inv.proceed();
298+
}
299+
};
300+
301+
Locker locker = new Locker();
302+
Assertions.assertThrows(ErrorReportException.class, () ->
303+
locker.lockTablesWithIntensiveDbLock(database.getId(),
304+
Lists.newArrayList(tableId1, tableId2), LockType.WRITE));
305+
306+
LockManager lockManager = GlobalStateMgr.getCurrentState().getLockManager();
307+
Assertions.assertFalse(lockManager.isOwner(database.getId(), locker, LockType.INTENTION_EXCLUSIVE),
308+
"DB intention lock must be released after partial-acquire rollback");
309+
Assertions.assertFalse(lockManager.isOwner(tableId1, locker, LockType.WRITE),
310+
"First table lock (acquired before failure) must be released by rollback");
311+
Assertions.assertFalse(lockManager.isOwner(tableId2, locker, LockType.WRITE),
312+
"Second table lock (acquisition failed) must not be held");
313+
}
314+
229315
@Test
230316
public void testTryLockTablesWithIntensiveDbLock3() throws LockException {
231317
long rid = 1L;

0 commit comments

Comments
 (0)