|
21 | 21 |
|
22 | 22 | package org.apache.bookkeeper.bookie; |
23 | 23 |
|
| 24 | +import static org.junit.Assert.assertEquals; |
24 | 25 | import static org.junit.Assert.assertFalse; |
25 | 26 | import static org.junit.Assert.assertTrue; |
26 | 27 | import static org.junit.Assert.fail; |
27 | 28 |
|
28 | 29 | import java.io.File; |
29 | 30 | import java.util.Collections; |
| 31 | +import java.util.List; |
| 32 | +import java.util.Map; |
| 33 | +import java.util.Objects; |
| 34 | +import java.util.concurrent.ConcurrentHashMap; |
30 | 35 | import java.util.concurrent.CountDownLatch; |
31 | 36 | import java.util.concurrent.TimeUnit; |
32 | 37 | import java.util.concurrent.atomic.AtomicBoolean; |
33 | 38 | import org.apache.bookkeeper.bookie.LedgerDirsManager.LedgerDirsListener; |
34 | 39 | import org.apache.bookkeeper.bookie.LedgerDirsManager.NoWritableLedgerDirException; |
| 40 | +import org.apache.bookkeeper.bookie.storage.ldb.DbLedgerStorage; |
35 | 41 | import org.apache.bookkeeper.client.BookKeeper.DigestType; |
36 | 42 | import org.apache.bookkeeper.client.LedgerHandle; |
37 | 43 | import org.apache.bookkeeper.common.testing.annotations.FlakyTest; |
38 | 44 | import org.apache.bookkeeper.conf.ServerConfiguration; |
| 45 | +import org.apache.bookkeeper.conf.TestBKConfiguration; |
39 | 46 | import org.apache.bookkeeper.proto.BookieServer; |
40 | 47 | import org.apache.bookkeeper.test.BookKeeperClusterTestCase; |
41 | 48 | import org.apache.bookkeeper.util.DiskChecker; |
@@ -250,4 +257,132 @@ public void diskFull(File disk) { |
250 | 257 | Thread.sleep(500); |
251 | 258 | assertFalse("Bookie should be transitioned to ReadWrite", bookie.isReadOnly()); |
252 | 259 | } |
| 260 | + |
| 261 | + @org.junit.Test |
| 262 | + public void testStopGCOnCorrespondingDiskWhenDiskFull() throws Exception { |
| 263 | + // 1. Create test directories |
| 264 | + File ledgerDir1 = tmpDirs.createNew("ledger", "test1"); |
| 265 | + File ledgerDir2 = tmpDirs.createNew("ledger", "test2"); |
| 266 | + |
| 267 | + // 2. Configure Bookie |
| 268 | + ServerConfiguration conf = TestBKConfiguration.newServerConfiguration(); |
| 269 | + conf.setGcWaitTime(1000); |
| 270 | + conf.setLedgerDirNames(new String[] { ledgerDir1.getPath(), ledgerDir2.getPath() }); |
| 271 | + conf.setDiskCheckInterval(100); // Shorten disk check interval |
| 272 | + conf.setLedgerStorageClass(DbLedgerStorage.class.getName()); |
| 273 | + |
| 274 | + // 3. Start Bookie and obtain internal components |
| 275 | + Bookie bookie = new TestBookieImpl(conf); |
| 276 | + BookieImpl bookieImpl = (BookieImpl) bookie; |
| 277 | + LedgerDirsManager ledgerDirsManager = bookieImpl.getLedgerDirsManager(); |
| 278 | + |
| 279 | + // 4. Create custom disk checker (key step) |
| 280 | + GCTestDiskChecker diskChecker = new GCTestDiskChecker( |
| 281 | + conf.getDiskUsageThreshold(), |
| 282 | + conf.getDiskUsageWarnThreshold() |
| 283 | + ); |
| 284 | + // Set directory status: dir1 full (100%), dir2 normal (50%) |
| 285 | + File[] currentDirectories = BookieImpl.getCurrentDirectories(new File[] { ledgerDir1, ledgerDir2 }); |
| 286 | + diskChecker.setUsageMap(currentDirectories[0], 1.0f); // 100% usage |
| 287 | + diskChecker.setUsageMap(currentDirectories[1], 0.5f); // 50% usage |
| 288 | + |
| 289 | + // 5. Replace Bookie's disk checker |
| 290 | + bookieImpl.dirsMonitor.shutdown(); // Stop default monitor |
| 291 | + bookieImpl.dirsMonitor = new LedgerDirsMonitor( |
| 292 | + conf, |
| 293 | + diskChecker, |
| 294 | + Collections.singletonList(ledgerDirsManager) |
| 295 | + ); |
| 296 | + bookieImpl.dirsMonitor.start(); |
| 297 | + |
| 298 | + // 6. Add disk state listener |
| 299 | + CountDownLatch dir1Full = new CountDownLatch(1); |
| 300 | + CountDownLatch dir1Writable = new CountDownLatch(1); |
| 301 | + |
| 302 | + ledgerDirsManager.addLedgerDirsListener(new LedgerDirsListener() { |
| 303 | + @Override |
| 304 | + public void diskFull(File disk) { |
| 305 | + if (disk.equals(currentDirectories[0])) { |
| 306 | + dir1Full.countDown(); |
| 307 | + } |
| 308 | + } |
| 309 | + |
| 310 | + @Override |
| 311 | + public void diskWritable(File disk) { |
| 312 | + if (disk.equals(currentDirectories[0])) { |
| 313 | + dir1Writable.countDown(); |
| 314 | + } |
| 315 | + } |
| 316 | + }); |
| 317 | + |
| 318 | + // 7. Wait for state update (ensure event is triggered) |
| 319 | + assertTrue("dir1 did not trigger full state", dir1Full.await(30, TimeUnit.SECONDS)); |
| 320 | + |
| 321 | + // 8. Verify directory status |
| 322 | + List<File> fullDirs = ledgerDirsManager.getFullFilledLedgerDirs(); |
| 323 | + List<File> writableDirs = ledgerDirsManager.getWritableLedgerDirs(); |
| 324 | + |
| 325 | + assertTrue("dir1 should be marked as full", fullDirs.contains(currentDirectories[0])); |
| 326 | + assertTrue("dir2 should remain writable", writableDirs.contains(currentDirectories[1])); |
| 327 | + assertEquals("Only 1 writable directory should remain", 1, writableDirs.size()); |
| 328 | + |
| 329 | + // 9. Verify GC status |
| 330 | + ((DbLedgerStorage) bookieImpl.ledgerStorage).getLedgerStorageList().forEach(storage -> { |
| 331 | + if (Objects.equals(storage.getLedgerBaseDir(), currentDirectories[0].getPath())) { |
| 332 | + assertTrue("dir1 should suspend minor GC", storage.isMinorGcSuspended()); |
| 333 | + assertTrue("dir1 should suspend major GC", storage.isMajorGcSuspended()); |
| 334 | + } else { |
| 335 | + assertFalse("dir2 should not suspend minor GC", storage.isMinorGcSuspended()); |
| 336 | + assertFalse("dir2 should not suspend major GC", storage.isMajorGcSuspended()); |
| 337 | + } |
| 338 | + }); |
| 339 | + |
| 340 | + // 10. Restore dir1 status |
| 341 | + diskChecker.setUsageMap(currentDirectories[0], 0.5f); // 50% usage |
| 342 | + assertTrue("dir1 did not become writable again", dir1Writable.await(3, TimeUnit.SECONDS)); |
| 343 | + |
| 344 | + // 11. Verify GC status after recovery |
| 345 | + ((DbLedgerStorage) bookieImpl.ledgerStorage).getLedgerStorageList().forEach(storage -> { |
| 346 | + if (Objects.equals(storage.getLedgerBaseDir(), currentDirectories[0].getPath())) { |
| 347 | + assertFalse("dir1 should not suspend minor GC", storage.isMinorGcSuspended()); |
| 348 | + assertFalse("dir1 should not suspend major GC", storage.isMajorGcSuspended()); |
| 349 | + } else { |
| 350 | + assertFalse("dir2 should not suspend minor GC", storage.isMinorGcSuspended()); |
| 351 | + assertFalse("dir2 should not suspend major GC", storage.isMajorGcSuspended()); |
| 352 | + } |
| 353 | + }); |
| 354 | + |
| 355 | + // 12. Cleanup |
| 356 | + bookie.shutdown(); |
| 357 | + } |
| 358 | + |
| 359 | + // Custom disk checker (simulate different usage for directories) |
| 360 | + static class GCTestDiskChecker extends DiskChecker { |
| 361 | + private final Map<File, Float> usageMap = new ConcurrentHashMap<>(); |
| 362 | + |
| 363 | + public GCTestDiskChecker(float threshold, float warnThreshold) { |
| 364 | + super(threshold, warnThreshold); |
| 365 | + } |
| 366 | + |
| 367 | + // Set simulated usage for a directory |
| 368 | + public void setUsageMap(File dir, float usage) { |
| 369 | + usageMap.put(dir, usage); |
| 370 | + } |
| 371 | + |
| 372 | + @Override |
| 373 | + public float checkDir(File dir) throws DiskErrorException, DiskWarnThresholdException, DiskOutOfSpaceException { |
| 374 | + Float usage = usageMap.get(dir); |
| 375 | + if (usage == null) { |
| 376 | + return super.checkDir(dir); // Default behavior |
| 377 | + } |
| 378 | + // Throw exception based on preset usage rate |
| 379 | + if (usage >= 1.0) { |
| 380 | + throw new DiskOutOfSpaceException("Simulated disk full", usage); |
| 381 | + } else if (usage >= 0.9) { |
| 382 | + throw new DiskWarnThresholdException("Simulated disk warning", usage); |
| 383 | + } |
| 384 | + return usage; |
| 385 | + } |
| 386 | + } |
| 387 | + |
253 | 388 | } |
0 commit comments