Skip to content

Commit 88c39cf

Browse files
authored
fix: Added handling of ClosedWatchServiceException in ConfigWatcher (#22030)
Signed-off-by: Alex Kehayov <[email protected]>
1 parent 15dae91 commit 88c39cf

File tree

2 files changed

+22
-40
lines changed

2 files changed

+22
-40
lines changed

hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/streaming/BlockNodeConnectionManager.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.net.InetAddress;
2323
import java.net.URI;
2424
import java.net.URL;
25+
import java.nio.file.ClosedWatchServiceException;
2526
import java.nio.file.Files;
2627
import java.nio.file.Path;
2728
import java.nio.file.Paths;
@@ -599,7 +600,7 @@ private void startConfigWatcher() {
599600
}
600601
}
601602
}
602-
} catch (final InterruptedException e) {
603+
} catch (final InterruptedException | ClosedWatchServiceException e) {
603604
break;
604605
} catch (Exception e) {
605606
logger.info("Exception in config watcher loop.", e);

hedera-node/hedera-app/src/test/java/com/hedera/node/app/blocks/impl/streaming/BlockNodeConnectionManagerTest.java

Lines changed: 20 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
import java.nio.file.Files;
4040
import java.nio.file.Path;
4141
import java.nio.file.StandardOpenOption;
42-
import java.nio.file.WatchService;
4342
import java.time.Duration;
4443
import java.time.Instant;
4544
import java.util.ArrayList;
@@ -1502,34 +1501,28 @@ void testConfigWatcher_handlesExceptionInRefresh() throws Exception {
15021501
}
15031502

15041503
@Test
1505-
void testConfigWatcher_handlesInterruptedException() throws Exception {
1506-
final Path file = tempDir.resolve("block-nodes.json");
1507-
final List<BlockNodeConfig> configs = new ArrayList<>();
1508-
configs.add(newBlockNodeConfig(PBJ_UNIT_TEST_HOST, 8080, 1));
1509-
final BlockNodeConnectionInfo connectionInfo = new BlockNodeConnectionInfo(configs);
1510-
final String valid = BlockNodeConnectionInfo.JSON.toJSON(connectionInfo);
1511-
Files.writeString(
1512-
file, valid, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
1504+
void testStartConfigWatcher_handlesIOException() throws Exception {
1505+
// Create a file instead of directory to trigger IOException when trying to watch it
1506+
final Path fileNotDir = tempDir.resolve("not-a-directory.txt");
1507+
Files.writeString(fileNotDir, "test", StandardOpenOption.CREATE);
15131508

1514-
connectionManager.start();
1515-
awaitCondition(() -> !availableNodes().isEmpty(), 2_000);
1509+
final var configProvider = createConfigProvider(createDefaultConfigProvider()
1510+
.withValue(
1511+
"blockNode.blockNodeConnectionFileDir",
1512+
fileNotDir.toAbsolutePath().toString()));
15161513

1517-
// Shutdown will interrupt the watcher thread
1518-
connectionManager.shutdown();
1514+
// This should trigger IOException when trying to create WatchService on a file
1515+
final var manager = new BlockNodeConnectionManager(configProvider, bufferService, metrics);
1516+
manager.start();
15191517

1520-
// Verify the watcher thread is no longer running
1521-
@SuppressWarnings("unchecked")
1522-
final AtomicReference<Thread> threadRef =
1523-
(AtomicReference<Thread>) configWatcherThreadRef.get(connectionManager);
1524-
final Thread watcherThread = threadRef.get();
1525-
if (watcherThread != null) {
1526-
watcherThread.join(1000);
1527-
assertThat(watcherThread.isAlive()).isFalse();
1528-
}
1518+
// Manager should start successfully even though config watcher failed
1519+
Thread.sleep(500);
1520+
1521+
manager.shutdown();
15291522
}
15301523

15311524
@Test
1532-
void testConfigWatcher_generalExceptionThenInterrupt_exitsCleanly() throws Exception {
1525+
void testConfigWatcher_handlesInterruptedException() throws Exception {
15331526
final Path file = tempDir.resolve("block-nodes.json");
15341527
final List<BlockNodeConfig> configs = new ArrayList<>();
15351528
configs.add(newBlockNodeConfig(PBJ_UNIT_TEST_HOST, 8080, 1));
@@ -1541,30 +1534,18 @@ void testConfigWatcher_generalExceptionThenInterrupt_exitsCleanly() throws Excep
15411534
connectionManager.start();
15421535
awaitCondition(() -> !availableNodes().isEmpty(), 2_000);
15431536

1544-
// Force the watch service to throw from take() by closing it,
1545-
// which will cause a ClosedWatchServiceException wrapped as a general Exception path
1546-
@SuppressWarnings("unchecked")
1547-
final AtomicReference<WatchService> wsRef =
1548-
(AtomicReference<WatchService>) configWatchServiceHandle.get(connectionManager);
1549-
final WatchService ws = wsRef.get();
1550-
assertThat(ws).isNotNull();
1551-
ws.close();
1552-
1553-
// Give time for watcher loop to hit the catch(Exception) and evaluate interrupted branch (should be false)
1554-
Thread.sleep(500);
1537+
// Shutdown will interrupt the watcher thread
1538+
connectionManager.shutdown();
15551539

1556-
// Now interrupt the watcher thread to exercise the interrupted branch inside the exception handler
1540+
// Verify the watcher thread is no longer running
15571541
@SuppressWarnings("unchecked")
15581542
final AtomicReference<Thread> threadRef =
15591543
(AtomicReference<Thread>) configWatcherThreadRef.get(connectionManager);
15601544
final Thread watcherThread = threadRef.get();
15611545
if (watcherThread != null) {
1562-
watcherThread.interrupt();
15631546
watcherThread.join(1000);
1547+
assertThat(watcherThread.isAlive()).isFalse();
15641548
}
1565-
1566-
// Ensure we can shutdown cleanly
1567-
connectionManager.shutdown();
15681549
}
15691550

15701551
@Test

0 commit comments

Comments
 (0)