|
17 | 17 | import com.google.common.collect.Lists; |
18 | 18 | import com.starrocks.catalog.Database; |
19 | 19 | import com.starrocks.common.Config; |
| 20 | +import com.starrocks.common.ErrorReportException; |
20 | 21 | import com.starrocks.common.util.concurrent.QueryableReentrantReadWriteLock; |
21 | 22 | import com.starrocks.common.util.concurrent.lock.LockException; |
22 | 23 | import com.starrocks.common.util.concurrent.lock.LockManager; |
23 | 24 | import com.starrocks.common.util.concurrent.lock.LockParams; |
24 | 25 | import com.starrocks.common.util.concurrent.lock.LockType; |
25 | 26 | import com.starrocks.common.util.concurrent.lock.Locker; |
| 27 | +import com.starrocks.common.util.concurrent.lock.NotSupportLockException; |
26 | 28 | import com.starrocks.server.GlobalStateMgr; |
| 29 | +import mockit.Invocation; |
27 | 30 | import mockit.Mock; |
28 | 31 | import mockit.MockUp; |
29 | 32 | import org.junit.jupiter.api.AfterEach; |
@@ -224,6 +227,89 @@ public void testTryLockTablesWithIntensiveDbLock2() throws LockException { |
224 | 227 | Assertions.assertTrue(lockManager.isOwner(rid2, locker, LockType.WRITE)); |
225 | 228 | } |
226 | 229 |
|
| 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 | + |
227 | 313 | @Test |
228 | 314 | public void testTryLockTablesWithIntensiveDbLock3() throws LockException { |
229 | 315 | long rid = 1L; |
|
0 commit comments