Skip to content

Commit b3ad758

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)
1 parent 1b2ce1b commit b3ad758

2 files changed

Lines changed: 133 additions & 10 deletions

File tree

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

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -288,18 +288,26 @@ public void lockTablesWithIntensiveDbLock(Long dbId, List<Long> tableList, LockT
288288
Preconditions.checkState(lockType.equals(LockType.READ) || lockType.equals(LockType.WRITE));
289289
List<Long> tableListClone = new ArrayList<>(tableList);
290290
if (Config.lock_manager_enabled) {
291+
LockType intentionType = (lockType == LockType.WRITE)
292+
? LockType.INTENTION_EXCLUSIVE
293+
: LockType.INTENTION_SHARED;
294+
boolean dbLockHeld = false;
295+
List<Long> ridLockedList = new ArrayList<>();
291296
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-
}
297+
this.lock(dbId, intentionType, 0);
298+
dbLockHeld = true;
297299

298300
Collections.sort(tableListClone);
299301
for (Long rid : tableListClone) {
300302
this.lock(rid, lockType, 0);
303+
ridLockedList.add(rid);
301304
}
302305
} catch (LockException e) {
306+
// Roll back any locks already acquired so that a partial failure
307+
// (e.g. deadlock victim selection on the second / Nth lock call)
308+
// does not leave a stranded DB intention or table lock that
309+
// would block subsequent DB-WRITE operations indefinitely.
310+
rollbackPartialIntensiveLock(dbId, intentionType, dbLockHeld, ridLockedList, lockType);
303311
throw ErrorReportException.report(ErrorCode.ERR_LOCK_ERROR, e.getMessage());
304312
}
305313
} else {
@@ -308,6 +316,30 @@ public void lockTablesWithIntensiveDbLock(Long dbId, List<Long> tableList, LockT
308316
}
309317
}
310318

319+
/**
320+
* Best-effort rollback of a partial intensive-lock acquisition. Releases each
321+
* already-acquired lock and swallows any errors raised by individual release
322+
* calls so that the original LockException (the actual cause of the rollback)
323+
* surfaces to the caller instead of being masked by a release-side error.
324+
*/
325+
private void rollbackPartialIntensiveLock(Long dbId, LockType intentionType,
326+
boolean dbLockHeld, List<Long> ridLockedList, LockType tableLockType) {
327+
for (Long rid : ridLockedList) {
328+
try {
329+
this.release(rid, tableLockType);
330+
} catch (Exception releaseEx) {
331+
LOG.warn("Failed to release table lock {} during partial-acquire rollback", rid, releaseEx);
332+
}
333+
}
334+
if (dbLockHeld) {
335+
try {
336+
this.release(dbId, intentionType);
337+
} catch (Exception releaseEx) {
338+
LOG.warn("Failed to release DB intention lock {} during partial-acquire rollback", dbId, releaseEx);
339+
}
340+
}
341+
}
342+
311343
/**
312344
* No need to release lock explicitly, it will be released automatically when the locker failed.
313345
*/
@@ -389,14 +421,19 @@ public void unLockTableWithIntensiveDbLock(Long dbId, Long tableId, LockType loc
389421
public void lockTableWithIntensiveDbLock(Long dbId, Long tableId, LockType lockType) {
390422
Preconditions.checkState(lockType.equals(LockType.READ) || lockType.equals(LockType.WRITE));
391423
if (Config.lock_manager_enabled) {
424+
LockType intentionType = (lockType == LockType.WRITE)
425+
? LockType.INTENTION_EXCLUSIVE
426+
: LockType.INTENTION_SHARED;
427+
boolean dbLockHeld = false;
392428
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-
}
429+
this.lock(dbId, intentionType, 0);
430+
dbLockHeld = true;
398431
this.lock(tableId, lockType, 0);
399432
} catch (LockException e) {
433+
// Roll back the DB intention lock if step 1 succeeded but the
434+
// table-lock acquisition failed (e.g. deadlock victim, interrupt).
435+
// Otherwise the caller would leak an IS/IX lock on the DB.
436+
rollbackPartialIntensiveLock(dbId, intentionType, dbLockHeld, Collections.emptyList(), lockType);
400437
throw ErrorReportException.report(ErrorCode.ERR_LOCK_ERROR, e.getMessage());
401438
}
402439
} 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.jupiter.api.AfterEach;
@@ -224,6 +227,89 @@ public void testTryLockTablesWithIntensiveDbLock2() throws LockException {
224227
Assertions.assertTrue(lockManager.isOwner(rid2, locker, LockType.WRITE));
225228
}
226229

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

0 commit comments

Comments
 (0)