From 471cd0bc1a3d525db94c039bd5dffb923e3db8f4 Mon Sep 17 00:00:00 2001 From: Ravi Nadahar Date: Sat, 27 Sep 2025 21:33:16 +0200 Subject: [PATCH 1/3] Use a threaded consumer for Process output consumption Signed-off-by: Ravi Nadahar --- .../network/internal/utils/NetworkUtils.java | 81 ++++++++++--- .../internal/utils/OutputConsumptionUtil.java | 110 ++++++++++++++++++ 2 files changed, 172 insertions(+), 19 deletions(-) create mode 100644 bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/utils/OutputConsumptionUtil.java diff --git a/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/utils/NetworkUtils.java b/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/utils/NetworkUtils.java index f2c5b5a0b0a93..8e67a88d9b279 100644 --- a/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/utils/NetworkUtils.java +++ b/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/utils/NetworkUtils.java @@ -12,9 +12,7 @@ */ package org.openhab.binding.network.internal.utils; -import java.io.BufferedReader; import java.io.IOException; -import java.io.InputStreamReader; import java.net.ConnectException; import java.net.DatagramPacket; import java.net.DatagramSocket; @@ -39,6 +37,10 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -380,12 +382,35 @@ public enum IpPingMethodEnum { } // Consume the output while the process runs - List output = new ArrayList<>(); - try (InputStreamReader isr = new InputStreamReader(proc.getInputStream(), StandardCharsets.UTF_8); - BufferedReader br = new BufferedReader(isr)) { - String line; - while ((line = br.readLine()) != null) { - output.add(line); + FutureTask> consumer = OutputConsumptionUtil.consumeText(proc.getInputStream(), + StandardCharsets.UTF_8); + + if (!proc.waitFor(timeout.toMillis() * 10L, TimeUnit.MILLISECONDS)) { + logger.warn("Timed out while waiting for the ping process to execute"); + proc.destroy(); + return new PingResult(false, Duration.between(execStartTime, Instant.now())); + } + int result = proc.exitValue(); + Instant execStopTime = Instant.now(); + List output; + try { + if (Thread.currentThread().isInterrupted()) { + consumer.cancel(true); + output = List.of(); + } + output = consumer.get(5, TimeUnit.SECONDS); + } catch (ExecutionException e) { + output = List.of(); + logger.warn("An exception occurred while consuming ping process output: {}", e.getMessage()); + logger.trace("", e); + } catch (TimeoutException e) { + // This should never happen, since the output should be available as soon as the process has finished + output = List.of(); + logger.warn("Timed out while retrieving ping process output"); + logger.trace("", e); + } + if (logger.isTraceEnabled()) { + for (String line : output) { logger.trace("Network [ping output]: '{}'", line); } } @@ -395,9 +420,6 @@ public enum IpPingMethodEnum { // not ready. // Exception: return code is also 0 in Windows for all requests on the local subnet. // see https://superuser.com/questions/403905/ping-from-windows-7-get-no-reply-but-sets-errorlevel-to-0 - - int result = proc.waitFor(); - Instant execStopTime = Instant.now(); if (result != 0) { return new PingResult(false, Duration.between(execStartTime, execStopTime)); } @@ -474,20 +496,41 @@ public enum ArpPingUtilEnum { } // Consume the output while the process runs - List output = new ArrayList<>(); - try (InputStreamReader isr = new InputStreamReader(proc.getInputStream(), StandardCharsets.UTF_8); - BufferedReader br = new BufferedReader(isr)) { - String line; - while ((line = br.readLine()) != null) { - output.add(line); + FutureTask> consumer = OutputConsumptionUtil.consumeText(proc.getInputStream(), + StandardCharsets.UTF_8); + + if (!proc.waitFor(timeout.toMillis() * 10L, TimeUnit.MILLISECONDS)) { + logger.warn("Timed out while waiting for the arping process to execute"); + proc.destroy(); + return new PingResult(false, Duration.between(execStartTime, Instant.now())); + } + int result = proc.exitValue(); + Instant execStopTime = Instant.now(); + List output; + try { + if (Thread.currentThread().isInterrupted()) { + consumer.cancel(true); + output = List.of(); + } + output = consumer.get(5, TimeUnit.SECONDS); + } catch (ExecutionException e) { + output = List.of(); + logger.warn("An exception occurred while consuming arping process output: {}", e.getMessage()); + logger.trace("", e); + } catch (TimeoutException e) { + // This should never happen, since the output should be available as soon as the process has finished + output = List.of(); + logger.warn("Timed out while retrieving arping process output"); + logger.trace("", e); + } + if (logger.isTraceEnabled()) { + for (String line : output) { logger.trace("Network [arping output]: '{}'", line); } } // The return code is 0 for a successful ping. 1 if device didn't respond and 2 if there is another error like // network interface not ready. - int result = proc.waitFor(); - Instant execStopTime = Instant.now(); if (result != 0) { return new PingResult(false, Duration.between(execStartTime, execStopTime)); } diff --git a/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/utils/OutputConsumptionUtil.java b/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/utils/OutputConsumptionUtil.java new file mode 100644 index 0000000000000..1598e60569e46 --- /dev/null +++ b/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/utils/OutputConsumptionUtil.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.network.internal.utils; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.FutureTask; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * A utility class for spawning threads that consume all bytes from the specified {@link InputStream} and + * return the results as {@link FutureTask}s. + * + * @author Ravi Nadahar - Initial contribution + */ +@NonNullByDefault +public class OutputConsumptionUtil { + + private OutputConsumptionUtil() { + // Not to be instantiated + } + + /** + * Consumes the specified {@link InputStream} by spawning a new thread and converts in into text using UTF-8. The + * result is returned as a {@link FutureTask}. + * + * @param inputStream the {@link InputStream} to consume. + * @return The {@link FutureTask} where the task can be cancelled and the results retrieved. + */ + public static FutureTask> consumeText(InputStream inputStream) { + return consumeText(inputStream, null, null); + } + + /** + * Consumes the specified {@link InputStream} by spawning a new thread and converts in into text using the + * specified {@link Charset}. The result is returned as a {@link FutureTask}. + * + * @param inputStream the {@link InputStream} to consume. + * @param charset the {@link Charset} to use for byte to character conversion. + * will be generated. + * @return The {@link FutureTask} where the task can be cancelled and the results retrieved. + */ + public static FutureTask> consumeText(InputStream inputStream, @Nullable Charset charset) { + return consumeText(inputStream, charset, null); + } + + /** + * Consumes the specified {@link InputStream} by spawning a new thread and converts in into text using UTF-8. The + * result is returned as a {@link FutureTask}. + * + * @param inputStream the {@link InputStream} to consume. + * @param threadName the name of the worker thread that will do the consumption. If {@code null}, a thread name + * will be generated. + * @return The {@link FutureTask} where the task can be cancelled and the results retrieved. + */ + public static FutureTask> consumeText(InputStream inputStream, @Nullable String threadName) { + return consumeText(inputStream, null, threadName); + } + + /** + * Consumes the specified {@link InputStream} by spawning a new thread and converts in into text using the + * specified {@link Charset}. The result is returned as a {@link FutureTask}. + * + * @param inputStream the {@link InputStream} to consume. + * @param charset the {@link Charset} to use for byte to character conversion. + * @param threadName the name of the worker thread that will do the consumption. If {@code null}, a thread name + * will be generated. + * @return The {@link FutureTask} where the task can be cancelled and the results retrieved. + */ + public static FutureTask> consumeText(final InputStream inputStream, @Nullable Charset charset, + @Nullable String threadName) { + FutureTask> result = new FutureTask>(() -> { + List output = new ArrayList<>(); + try (InputStreamReader isr = new InputStreamReader(inputStream, + charset == null ? StandardCharsets.UTF_8 : charset); BufferedReader br = new BufferedReader(isr)) { + String line; + while ((line = br.readLine()) != null) { + output.add(line); + } + } + return output; + }); + + Thread runner; + if (threadName == null || threadName.isBlank()) { + runner = new Thread(result, Thread.currentThread().getName() + "-consumer"); + } else { + runner = new Thread(result, threadName); + } + runner.start(); + return result; + } +} From 570bbca30eec15c9357f62d5f4cc35659f3df1bf Mon Sep 17 00:00:00 2001 From: Ravi Nadahar Date: Sun, 28 Sep 2025 20:14:12 +0200 Subject: [PATCH 2/3] Use dedicated thread pool to resolve Futures This avoids thread starvation leading to Futures never resolving Signed-off-by: Ravi Nadahar --- .../internal/NetworkHandlerFactory.java | 30 ++++++++++- .../discovery/NetworkDiscoveryService.java | 50 +++++++++++++++---- .../internal/handler/NetworkHandler.java | 13 +++-- .../internal/handler/NetworkHandlerTest.java | 16 +++--- 4 files changed, 83 insertions(+), 26 deletions(-) diff --git a/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/NetworkHandlerFactory.java b/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/NetworkHandlerFactory.java index 36f123e98e39c..0c28d48a2a7d8 100644 --- a/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/NetworkHandlerFactory.java +++ b/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/NetworkHandlerFactory.java @@ -15,12 +15,17 @@ import static org.openhab.binding.network.internal.NetworkBindingConstants.*; import java.util.Map; +import java.util.concurrent.ExecutorService; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.network.internal.handler.NetworkHandler; import org.openhab.binding.network.internal.handler.SpeedTestHandler; +import org.openhab.core.common.NamedThreadFactory; import org.openhab.core.common.ThreadPoolManager; import org.openhab.core.config.core.Configuration; import org.openhab.core.thing.Thing; @@ -47,9 +52,11 @@ public class NetworkHandlerFactory extends BaseThingHandlerFactory { final NetworkBindingConfiguration configuration = new NetworkBindingConfiguration(); private static final String NETWORK_HANDLER_THREADPOOL_NAME = "networkBinding"; + private static final String NETWORK_RESOLVER_THREADPOOL_NAME = "binding-network-resolver"; private final Logger logger = LoggerFactory.getLogger(NetworkHandlerFactory.class); private final ScheduledExecutorService executor = ThreadPoolManager .getScheduledPool(NETWORK_HANDLER_THREADPOOL_NAME); + private volatile @Nullable ExecutorService resolver; @Override public boolean supportsThingType(ThingTypeUID thingTypeUID) { @@ -61,12 +68,24 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { protected void activate(ComponentContext componentContext, Map config) { super.activate(componentContext); modified(config); + ExecutorService resolver = this.resolver; + if (resolver != null) { + // This should not happen + resolver.shutdownNow(); + } + this.resolver = new ThreadPoolExecutor(1, Integer.MAX_VALUE, 20L, TimeUnit.SECONDS, + new SynchronousQueue(), new NamedThreadFactory(NETWORK_RESOLVER_THREADPOOL_NAME)); } @Override @Deactivate protected void deactivate(ComponentContext componentContext) { super.deactivate(componentContext); + ExecutorService resolver = this.resolver; + if (resolver != null) { + resolver.shutdownNow(); + this.resolver = null; + } } @Modified @@ -80,12 +99,19 @@ protected void modified(Map config) { @Override protected @Nullable ThingHandler createHandler(Thing thing) { + ExecutorService resolver = this.resolver; + if (resolver == null) { + // This should be impossible + logger.error("Failed to create handler for Thing \"{}\" - handler factory hasn't been activated", + thing.getUID()); + return null; + } ThingTypeUID thingTypeUID = thing.getThingTypeUID(); if (thingTypeUID.equals(PING_DEVICE) || thingTypeUID.equals(BACKWARDS_COMPATIBLE_DEVICE)) { - return new NetworkHandler(thing, executor, false, configuration); + return new NetworkHandler(thing, executor, resolver, false, configuration); } else if (thingTypeUID.equals(SERVICE_DEVICE)) { - return new NetworkHandler(thing, executor, true, configuration); + return new NetworkHandler(thing, executor, resolver, true, configuration); } else if (thingTypeUID.equals(SPEEDTEST_DEVICE)) { return new SpeedTestHandler(thing); } diff --git a/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/discovery/NetworkDiscoveryService.java b/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/discovery/NetworkDiscoveryService.java index 3edf0eac729bf..964a0c3cf8e35 100644 --- a/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/discovery/NetworkDiscoveryService.java +++ b/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/discovery/NetworkDiscoveryService.java @@ -80,6 +80,9 @@ public class NetworkDiscoveryService extends AbstractDiscoveryService implements /* All access must be guarded by "this" */ private @Nullable ExecutorService executorService; + + /* All access must be guarded by "this" */ + private @Nullable ExecutorService resolver; private final NetworkUtils networkUtils = new NetworkUtils(); private final ConfigurationAdmin admin; @@ -100,6 +103,10 @@ protected void deactivate() { executorService.shutdownNow(); executorService = null; } + if (resolver != null) { + resolver.shutdownNow(); + resolver = null; + } } super.deactivate(); } @@ -140,6 +147,14 @@ private ExecutorService createDiscoveryExecutor(@Nullable NetworkBindingConfigur } } + private ExecutorService createDiscoveryResolver() { + AtomicInteger count = new AtomicInteger(1); + return Executors.newCachedThreadPool(r -> { + Thread t = new Thread(r, "OH-binding-network-discoveryResolver-" + count.getAndIncrement()); + return t; + }); + } + /** * Starts the DiscoveryThread for each IP on each interface on the network */ @@ -147,13 +162,18 @@ private ExecutorService createDiscoveryExecutor(@Nullable NetworkBindingConfigur protected void startScan() { NetworkBindingConfiguration configuration = getConfig(); final ExecutorService service; + final ExecutorService resolver; synchronized (this) { if (executorService == null) { executorService = createDiscoveryExecutor(configuration); } service = executorService; + if (this.resolver == null) { + this.resolver = createDiscoveryResolver(); + } + resolver = this.resolver; } - if (service == null) { + if (service == null || resolver == null) { return; } @@ -178,7 +198,7 @@ protected void startScan() { final int targetCount = networkIPs.size(); for (String ip : networkIPs) { - final PresenceDetection pd = new PresenceDetection(this, Duration.ofSeconds(2), service); + final PresenceDetection pd = new PresenceDetection(this, Duration.ofSeconds(2), resolver); pd.setHostname(ip); pd.setIOSDevice(true); pd.setUseDhcpSniffing(false); @@ -211,26 +231,34 @@ protected void startScan() { }); } + @SuppressWarnings("sync-override") @Override protected void stopScan() { final ExecutorService service; + final ExecutorService resolver; synchronized (this) { super.stopScan(); service = executorService; - if (service == null) { - return; - } executorService = null; + resolver = this.resolver; + this.resolver = null; } logger.debug("Stopping Network Device Discovery"); - service.shutdownNow(); // Initiate shutdown - try { - if (!service.awaitTermination(PING_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS)) { - logger.warn("Network discovery scan failed to stop within the timeout of {}", PING_TIMEOUT); + if (service != null) { + service.shutdownNow(); // Initiate shutdown + } + if (resolver != null) { + resolver.shutdown(); // Initiate shutdown, but let it complete queued tasks + } + if (service != null) { + try { + if (!service.awaitTermination(PING_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS)) { + logger.warn("Network discovery scan failed to stop within the timeout of {}", PING_TIMEOUT); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); } } diff --git a/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/handler/NetworkHandler.java b/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/handler/NetworkHandler.java index 39d8a1e009e19..721d83b41419f 100644 --- a/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/handler/NetworkHandler.java +++ b/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/handler/NetworkHandler.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ExecutorService; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -84,14 +85,16 @@ public class NetworkHandler extends BaseThingHandler // Retry counter. Will be reset as soon as a device presence detection succeed. private volatile int retryCounter = 0; private final ScheduledExecutorService executor; + private final ExecutorService resolver; /** * Creates a new instance using the specified parameters. */ - public NetworkHandler(Thing thing, ScheduledExecutorService executor, boolean isTCPServiceDevice, - NetworkBindingConfiguration configuration) { + public NetworkHandler(Thing thing, ScheduledExecutorService executor, ExecutorService resolver, + boolean isTCPServiceDevice, NetworkBindingConfiguration configuration) { super(thing); this.executor = executor; + this.resolver = resolver; this.isTCPServiceDevice = isTCPServiceDevice; this.configuration = configuration; this.configuration.addNetworkBindingConfigurationListener(this); @@ -225,8 +228,8 @@ void initialize(PresenceDetection presenceDetection) { wakeOnLanPacketSender = new WakeOnLanPacketSender(config.macAddress, config.hostname, config.port, config.networkInterfaceNames); if (config.refreshInterval > 0) { - refreshJob = executor.scheduleWithFixedDelay(presenceDetection::refresh, 0, config.refreshInterval, - TimeUnit.MILLISECONDS); + refreshJob = executor.scheduleWithFixedDelay(presenceDetection::refresh, (long) (Math.random() * 5000), + config.refreshInterval, TimeUnit.MILLISECONDS); } } @@ -256,7 +259,7 @@ public void initialize() { updateStatus(ThingStatus.UNKNOWN); executor.submit(() -> { initialize(new PresenceDetection(this, Duration.ofMillis(configuration.cacheDeviceStateTimeInMS.intValue()), - executor)); + resolver)); }); } diff --git a/bundles/org.openhab.binding.network/src/test/java/org/openhab/binding/network/internal/handler/NetworkHandlerTest.java b/bundles/org.openhab.binding.network/src/test/java/org/openhab/binding/network/internal/handler/NetworkHandlerTest.java index 5a39777d0978d..6761bd5837df7 100644 --- a/bundles/org.openhab.binding.network/src/test/java/org/openhab/binding/network/internal/handler/NetworkHandlerTest.java +++ b/bundles/org.openhab.binding.network/src/test/java/org/openhab/binding/network/internal/handler/NetworkHandlerTest.java @@ -21,6 +21,7 @@ import java.time.Duration; import java.time.Instant; +import java.util.concurrent.ExecutorService; import java.util.concurrent.ScheduledExecutorService; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -61,6 +62,7 @@ public class NetworkHandlerTest extends JavaTest { private @Mock @NonNullByDefault({}) ThingHandlerCallback callback; private @Mock @NonNullByDefault({}) ScheduledExecutorService scheduledExecutorService; + private @Mock @NonNullByDefault({}) ExecutorService resolver; private @Mock @NonNullByDefault({}) Thing thing; @BeforeEach @@ -71,7 +73,7 @@ public void setUp() { @Test public void checkAllConfigurations() { NetworkBindingConfiguration config = new NetworkBindingConfiguration(); - NetworkHandler handler = spy(new NetworkHandler(thing, scheduledExecutorService, true, config)); + NetworkHandler handler = spy(new NetworkHandler(thing, scheduledExecutorService, resolver, true, config)); handler.setCallback(callback); // Provide all possible configuration when(thing.getConfiguration()).thenAnswer(a -> { @@ -82,8 +84,7 @@ public void checkAllConfigurations() { conf.put(NetworkBindingConstants.PARAMETER_TIMEOUT, 1234); return conf; }); - PresenceDetection presenceDetection = spy( - new PresenceDetection(handler, Duration.ofSeconds(2), scheduledExecutorService)); + PresenceDetection presenceDetection = spy(new PresenceDetection(handler, Duration.ofSeconds(2), resolver)); handler.initialize(presenceDetection); assertThat(handler.retries, is(10)); @@ -95,7 +96,7 @@ public void checkAllConfigurations() { @Test public void tcpDeviceInitTests() { NetworkBindingConfiguration config = new NetworkBindingConfiguration(); - NetworkHandler handler = spy(new NetworkHandler(thing, scheduledExecutorService, true, config)); + NetworkHandler handler = spy(new NetworkHandler(thing, scheduledExecutorService, resolver, true, config)); assertThat(handler.isTCPServiceDevice(), is(true)); handler.setCallback(callback); // Port is missing, should make the device OFFLINE @@ -104,7 +105,7 @@ public void tcpDeviceInitTests() { conf.put(NetworkBindingConstants.PARAMETER_HOSTNAME, "127.0.0.1"); return conf; }); - handler.initialize(new PresenceDetection(handler, Duration.ofSeconds(2), scheduledExecutorService)); + handler.initialize(new PresenceDetection(handler, Duration.ofSeconds(2), resolver)); // Check that we are offline ArgumentCaptor statusInfoCaptor = ArgumentCaptor.forClass(ThingStatusInfo.class); verify(callback).statusUpdated(eq(thing), statusInfoCaptor.capture()); @@ -115,7 +116,7 @@ public void tcpDeviceInitTests() { @Test public void pingDeviceInitTests() { NetworkBindingConfiguration config = new NetworkBindingConfiguration(); - NetworkHandler handler = spy(new NetworkHandler(thing, scheduledExecutorService, false, config)); + NetworkHandler handler = spy(new NetworkHandler(thing, scheduledExecutorService, resolver, false, config)); handler.setCallback(callback); // Provide minimal configuration when(thing.getConfiguration()).thenAnswer(a -> { @@ -124,8 +125,7 @@ public void pingDeviceInitTests() { conf.put(NetworkBindingConstants.PARAMETER_REFRESH_INTERVAL, 0); // disable auto refresh return conf; }); - PresenceDetection presenceDetection = spy( - new PresenceDetection(handler, Duration.ofSeconds(2), scheduledExecutorService)); + PresenceDetection presenceDetection = spy(new PresenceDetection(handler, Duration.ofSeconds(2), resolver)); doReturn(Instant.now()).when(presenceDetection).getLastSeen(); handler.initialize(presenceDetection); From 58fdbf6fffbc5f75b89a0ba8ba60252ff6eb387f Mon Sep 17 00:00:00 2001 From: lsiepel Date: Fri, 3 Oct 2025 17:08:49 +0200 Subject: [PATCH 3/3] Update bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/discovery/NetworkDiscoveryService.java Signed-off-by: lsiepel --- .../network/internal/discovery/NetworkDiscoveryService.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/discovery/NetworkDiscoveryService.java b/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/discovery/NetworkDiscoveryService.java index 964a0c3cf8e35..25c25d140e088 100644 --- a/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/discovery/NetworkDiscoveryService.java +++ b/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/discovery/NetworkDiscoveryService.java @@ -150,8 +150,7 @@ private ExecutorService createDiscoveryExecutor(@Nullable NetworkBindingConfigur private ExecutorService createDiscoveryResolver() { AtomicInteger count = new AtomicInteger(1); return Executors.newCachedThreadPool(r -> { - Thread t = new Thread(r, "OH-binding-network-discoveryResolver-" + count.getAndIncrement()); - return t; + return new Thread(r, "OH-binding-network-discoveryResolver-" + count.getAndIncrement()); }); }