Skip to content

Commit 58572b3

Browse files
author
Ravi Nadahar
committed
Fix WatchServiceImpl service registration
The class combined annotated OSGi DS registration and programmatic registration. This clashes with the internal logic of DS, and causes visibility issues between services that are registered by different threads. In the chaos of OH startup, which can lead to intermittent failures and general unpredictability Signed-off-by: Ravi Nadahar <nadahar@rediffmail.com>
1 parent 81e9be0 commit 58572b3

3 files changed

Lines changed: 182 additions & 40 deletions

File tree

bundles/org.openhab.core/src/main/java/org/openhab/core/internal/service/WatchServiceFactoryImpl.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,14 @@ public void createWatchService(String name, Path basePath) {
6969
Dictionary<String, Object> map = new Hashtable<>();
7070

7171
map.put("name", name);
72+
map.put(WatchService.SERVICE_PROPERTY_NAME, name);
7273
map.put("path", basePath.toString());
7374
config.update(map);
7475
} else {
7576
Configuration config = configurations[0];
7677
Dictionary<String, Object> map = config.getProperties();
7778
map.put("name", name);
79+
map.put(WatchService.SERVICE_PROPERTY_NAME, name);
7880
map.put("path", basePath.toString());
7981
config.update(map);
8082
}

bundles/org.openhab.core/src/main/java/org/openhab/core/internal/service/WatchServiceImpl.java

Lines changed: 138 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -22,27 +22,37 @@
2222
import java.util.List;
2323
import java.util.Map;
2424
import java.util.Objects;
25-
import java.util.concurrent.CompletableFuture;
25+
import java.util.concurrent.CancellationException;
2626
import java.util.concurrent.ConcurrentHashMap;
2727
import java.util.concurrent.CopyOnWriteArrayList;
28+
import java.util.concurrent.ExecutionException;
2829
import java.util.concurrent.ExecutorService;
2930
import java.util.concurrent.Executors;
3031
import java.util.concurrent.ScheduledExecutorService;
3132
import java.util.concurrent.ScheduledFuture;
3233
import java.util.concurrent.TimeUnit;
34+
import java.util.concurrent.TimeoutException;
35+
import java.util.concurrent.atomic.AtomicReference;
3336
import java.util.function.Predicate;
3437

3538
import org.eclipse.jdt.annotation.NonNullByDefault;
3639
import org.eclipse.jdt.annotation.Nullable;
3740
import org.openhab.core.common.ThreadPoolManager;
3841
import org.openhab.core.service.WatchService;
3942
import org.osgi.framework.BundleContext;
43+
import org.osgi.framework.InvalidSyntaxException;
4044
import org.osgi.framework.ServiceRegistration;
45+
import org.osgi.service.cm.Configuration;
46+
import org.osgi.service.cm.ConfigurationAdmin;
47+
import org.osgi.service.cm.ConfigurationEvent;
48+
import org.osgi.service.cm.ConfigurationListener;
49+
import org.osgi.service.component.ComponentContext;
4150
import org.osgi.service.component.annotations.Activate;
4251
import org.osgi.service.component.annotations.Component;
4352
import org.osgi.service.component.annotations.ConfigurationPolicy;
4453
import org.osgi.service.component.annotations.Deactivate;
4554
import org.osgi.service.component.annotations.Modified;
55+
import org.osgi.service.component.annotations.Reference;
4656
import org.slf4j.Logger;
4757
import org.slf4j.LoggerFactory;
4858

@@ -73,21 +83,22 @@ public class WatchServiceImpl implements WatchService, DirectoryChangeListener {
7383
private final List<Listener> dirPathListeners = new CopyOnWriteArrayList<>();
7484
private final List<Listener> subDirPathListeners = new CopyOnWriteArrayList<>();
7585
private final Map<Path, FileHash> hashCache = new ConcurrentHashMap<>();
86+
private final ConfigurationAdmin configurationAdmin;
7687
private final ExecutorService executor;
7788
private final ScheduledExecutorService scheduler;
7889

7990
private final String name;
8091
private final BundleContext bundleContext;
81-
82-
private @Nullable Path basePath;
83-
private @Nullable DirectoryWatcher dirWatcher;
84-
private @Nullable ServiceRegistration<WatchService> reg;
92+
private volatile @Nullable Path basePath;
93+
volatile @Nullable DirectoryWatcher dirWatcher;
8594

8695
private final Map<Path, ScheduledFuture<?>> scheduledEvents = new HashMap<>();
8796
private final Map<Path, List<DirectoryChangeEvent>> scheduledEventKinds = new ConcurrentHashMap<>();
8897

8998
@Activate
90-
public WatchServiceImpl(WatchServiceConfiguration config, BundleContext bundleContext) throws IOException {
99+
public WatchServiceImpl(@Reference ConfigurationAdmin configurationAdmin, WatchServiceConfiguration config,
100+
BundleContext bundleContext, ComponentContext componentContext) throws IOException {
101+
this.configurationAdmin = configurationAdmin;
91102
this.bundleContext = bundleContext;
92103
if (config.name().isBlank()) {
93104
throw new IllegalArgumentException("service name must not be blank");
@@ -96,11 +107,11 @@ public WatchServiceImpl(WatchServiceConfiguration config, BundleContext bundleCo
96107
this.name = config.name();
97108
executor = Executors.newSingleThreadExecutor(r -> new Thread(r, name));
98109
scheduler = ThreadPoolManager.getScheduledPool("watchservice");
99-
modified(config);
110+
modified(config, componentContext);
100111
}
101112

102113
@Modified
103-
public void modified(WatchServiceConfiguration config) throws IOException {
114+
public void modified(WatchServiceConfiguration config, final ComponentContext componentContext) throws IOException {
104115
logger.trace("Trying to setup WatchService '{}' with path '{}'", config.name(), config.path());
105116

106117
Path basePath = Path.of(config.path()).toAbsolutePath();
@@ -109,23 +120,76 @@ public void modified(WatchServiceConfiguration config) throws IOException {
109120
return;
110121
}
111122

123+
final boolean cycle = this.basePath != null;
112124
this.basePath = basePath;
113125

114126
try {
115-
closeWatcherAndUnregister();
127+
closeWatcher();
116128

117129
if (!Files.exists(basePath)) {
118130
logger.info("Watch directory '{}' does not exist. Trying to create it.", basePath);
119131
Files.createDirectories(basePath);
120132
}
121133

122134
DirectoryWatcher newDirWatcher = DirectoryWatcher.builder().listener(this).path(basePath).build();
123-
CompletableFuture
124-
.runAsync(
125-
() -> newDirWatcher.watchAsync(executor)
126-
.thenRun(() -> logger.debug("WatchService '{}' has been shut down.", name)),
127-
ThreadPoolManager.getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON))
128-
.thenRun(this::registerWatchService);
135+
ThreadPoolManager.getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON).execute(() -> {
136+
if (cycle) {
137+
Object pid = componentContext.getProperties().get("service.pid");
138+
if (pid instanceof String pidString) {
139+
Configuration[] configs = null;
140+
try {
141+
configs = configurationAdmin.listConfigurations("(service.pid=" + pidString + ")");
142+
} catch (IOException | InvalidSyntaxException e) {
143+
logger.warn("WatchService '{}': Failed to acquire configuration, cannot restart service",
144+
name, e);
145+
}
146+
147+
if (configs != null && configs.length > 0) {
148+
final AtomicReference<@Nullable ServiceRegistration<ConfigurationListener>> registrationReference = new AtomicReference<>();
149+
150+
ConfigurationListener tempListener = new ConfigurationListener() {
151+
@Override
152+
public void configurationEvent(@Nullable ConfigurationEvent event) {
153+
if (event != null && event.getType() == ConfigurationEvent.CM_DELETED
154+
&& pidString.equals(event.getPid())) {
155+
logger.debug("WatchService '{}': Configuration deleted", name);
156+
157+
// Unregister the listener first, this is a one trick pony
158+
ServiceRegistration<ConfigurationListener> registration = registrationReference
159+
.get();
160+
if (registration != null) {
161+
try {
162+
registration.unregister();
163+
} catch (IllegalStateException e) {
164+
// Already unregistered
165+
}
166+
}
167+
168+
createConfiguration(basePath);
169+
}
170+
}
171+
};
172+
173+
ServiceRegistration<ConfigurationListener> registration = bundleContext
174+
.registerService(ConfigurationListener.class, tempListener, null);
175+
registrationReference.set(registration);
176+
try {
177+
configs[0].delete();
178+
} catch (IOException | RuntimeException e) {
179+
logger.warn("WatchService '{}': Failed to delete configuration, cannot restart service",
180+
name, e);
181+
try {
182+
registration.unregister();
183+
} catch (IllegalStateException e2) {
184+
// Already unregistered
185+
}
186+
}
187+
}
188+
}
189+
}
190+
newDirWatcher.watchAsync(executor)
191+
.thenRun(() -> logger.debug("WatchService '{}' has been shut down.", name));
192+
});
129193
this.dirWatcher = newDirWatcher;
130194
} catch (NoSuchFileException e) {
131195
// log message here, otherwise it'll be swallowed by the call to newInstance in the factory
@@ -143,37 +207,47 @@ public void modified(WatchServiceConfiguration config) throws IOException {
143207
@Deactivate
144208
public void deactivate() {
145209
try {
146-
closeWatcherAndUnregister();
147-
executor.shutdown();
210+
closeWatcher();
211+
executor.shutdownNow();
212+
try {
213+
executor.awaitTermination(1000L, TimeUnit.MILLISECONDS);
214+
} catch (InterruptedException e) {
215+
Thread.currentThread().interrupt();
216+
// We still want to cancel the scheduled events, so let it be re-caught after that
217+
}
218+
synchronized (scheduledEvents) {
219+
for (ScheduledFuture<?> future : scheduledEvents.values()) {
220+
if (!future.isDone()) {
221+
future.cancel(true);
222+
}
223+
}
224+
for (ScheduledFuture<?> future : scheduledEvents.values()) {
225+
if (!future.isDone()) {
226+
try {
227+
future.get(1000L, TimeUnit.MILLISECONDS);
228+
} catch (CancellationException e) {
229+
// This is what we want. move on
230+
} catch (InterruptedException e) {
231+
Thread.currentThread().interrupt();
232+
return;
233+
} catch (ExecutionException | TimeoutException e) {
234+
logger.debug("Failed to conclude scheduled event during deactivate: {}", e.getMessage(), e);
235+
}
236+
}
237+
}
238+
}
148239
} catch (IOException e) {
149240
logger.warn("Failed to shutdown WatchService '{}'", name, e);
150241
}
151242
}
152243

153-
private void registerWatchService() {
154-
Dictionary<String, Object> properties = new Hashtable<>();
155-
properties.put(WatchService.SERVICE_PROPERTY_NAME, name);
156-
this.reg = bundleContext.registerService(WatchService.class, this, properties);
157-
logger.debug("WatchService '{}' completed initialization and registered itself as service.", name);
158-
}
159-
160-
private void closeWatcherAndUnregister() throws IOException {
244+
private void closeWatcher() throws IOException {
161245
DirectoryWatcher localDirWatcher = this.dirWatcher;
162246
if (localDirWatcher != null) {
163247
localDirWatcher.close();
164248
this.dirWatcher = null;
165249
}
166250

167-
ServiceRegistration<?> localReg = this.reg;
168-
if (localReg != null) {
169-
try {
170-
localReg.unregister();
171-
} catch (IllegalStateException e) {
172-
logger.debug("WatchService '{}' was already unregistered.", name, e);
173-
}
174-
this.reg = null;
175-
}
176-
177251
hashCache.clear();
178252
}
179253

@@ -230,11 +304,39 @@ public void onEvent(@Nullable DirectoryChangeEvent directoryChangeEvent) throws
230304
future.cancel(true);
231305
}
232306
future = scheduler.schedule(() -> notifyListeners(path), PROCESSING_TIME, TimeUnit.MILLISECONDS);
233-
scheduledEventKinds.computeIfAbsent(path, k -> new CopyOnWriteArrayList<>()).add(directoryChangeEvent);
307+
Objects.requireNonNull(scheduledEventKinds.computeIfAbsent(path, k -> new CopyOnWriteArrayList<>()))
308+
.add(directoryChangeEvent);
234309
scheduledEvents.put(path, future);
235310
}
236311
}
237312

313+
private void createConfiguration(Path basePath) {
314+
logger.debug("WatchService '{}': Creating new configuration for path '{}'", name, basePath);
315+
try {
316+
String filter = "(&(name=" + name + ")" + "(service.factoryPid=" + WatchService.SERVICE_PID + "))";
317+
Configuration[] configurations = configurationAdmin.listConfigurations(filter);
318+
319+
if (configurations == null || configurations.length == 0) {
320+
Configuration c = configurationAdmin.createFactoryConfiguration(WatchService.SERVICE_PID, "?");
321+
Dictionary<String, Object> map = new Hashtable<>();
322+
323+
map.put("name", name);
324+
map.put(WatchService.SERVICE_PROPERTY_NAME, name);
325+
map.put("path", basePath.toString());
326+
c.update(map);
327+
} else {
328+
Configuration c = configurations[0];
329+
Dictionary<String, Object> map = c.getProperties();
330+
map.put("name", name);
331+
map.put(WatchService.SERVICE_PROPERTY_NAME, name);
332+
map.put("path", basePath.toString());
333+
c.update(map);
334+
}
335+
} catch (IOException | InvalidSyntaxException e) {
336+
logger.error("WatchService '{}': Failed to create configuration with path '{}'", name, basePath, e);
337+
}
338+
}
339+
238340
private void notifyListeners(Path path) {
239341
List<DirectoryChangeEvent> events = scheduledEventKinds.remove(path);
240342
if (events == null || events.isEmpty()) {

bundles/org.openhab.core/src/test/java/org/openhab/core/internal/service/WatchServiceImplTest.java

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import java.nio.file.Path;
2323
import java.util.List;
2424
import java.util.concurrent.CopyOnWriteArrayList;
25+
import java.util.concurrent.CountDownLatch;
26+
import java.util.concurrent.TimeUnit;
2527

2628
import org.eclipse.jdt.annotation.NonNullByDefault;
2729
import org.junit.jupiter.api.AfterEach;
@@ -37,6 +39,11 @@
3739
import org.openhab.core.service.WatchService;
3840
import org.openhab.core.service.WatchService.Kind;
3941
import org.osgi.framework.BundleContext;
42+
import org.osgi.service.cm.ConfigurationAdmin;
43+
import org.osgi.service.component.ComponentContext;
44+
45+
import io.methvin.watcher.DirectoryChangeListener;
46+
import io.methvin.watcher.DirectoryWatcher;
4047

4148
/**
4249
* The {@link WatchServiceImplTest} is a
@@ -52,21 +59,23 @@ public class WatchServiceImplTest extends JavaTest {
5259
private static final String TEST_FILE_NAME = "testFile";
5360

5461
public @Mock @NonNullByDefault({}) WatchServiceImpl.WatchServiceConfiguration configurationMock;
62+
public @Mock @NonNullByDefault({}) ComponentContext componentContextMock;
5563
public @Mock @NonNullByDefault({}) BundleContext bundleContextMock;
64+
public @Mock @NonNullByDefault({}) ConfigurationAdmin configurationAdminMock;
5665

5766
private @NonNullByDefault({}) WatchServiceImpl watchService;
5867
private @TempDir @NonNullByDefault({}) Path rootPath;
5968
private @NonNullByDefault({}) TestWatchEventListener listener;
6069

6170
@BeforeEach
62-
public void setup() throws IOException {
71+
public void setup() throws Exception {
6372
when(configurationMock.name()).thenReturn("unnamed");
6473
when(configurationMock.path()).thenReturn(rootPath.toString());
6574

66-
watchService = new WatchServiceImpl(configurationMock, bundleContextMock);
75+
watchService = new WatchServiceImpl(configurationAdminMock, configurationMock, bundleContextMock,
76+
componentContextMock);
6777
listener = new TestWatchEventListener();
68-
69-
verify(bundleContextMock, timeout(5000)).registerService(eq(WatchService.class), eq(watchService), any());
78+
waitForDirWatcher();
7079
}
7180

7281
@AfterEach
@@ -234,6 +243,35 @@ private void assertEvent(Path fullPath, Kind kind) throws InterruptedException {
234243
listener.events.clear();
235244
}
236245

246+
private void waitForDirWatcher() throws IOException, InterruptedException {
247+
while (!isDirWatcherReady()) {
248+
Thread.sleep(5L);
249+
}
250+
251+
final CountDownLatch latch = new CountDownLatch(1);
252+
WatchService.WatchEventListener warmUpListener = new WatchService.WatchEventListener() {
253+
254+
@Override
255+
public void processWatchEvent(Kind kind, Path fullPath) {
256+
latch.countDown();
257+
}
258+
};
259+
Path pingFile = rootPath.resolve("watcher-warmup.ping");
260+
watchService.registerListener(warmUpListener, pingFile, false);
261+
Files.createFile(pingFile);
262+
latch.await(1000L, TimeUnit.MILLISECONDS);
263+
watchService.unregisterListener(warmUpListener);
264+
}
265+
266+
private boolean isDirWatcherReady() {
267+
DirectoryWatcher watcher = watchService.dirWatcher;
268+
if (watcher == null) {
269+
return false;
270+
}
271+
DirectoryChangeListener listener = watcher.getListener();
272+
return listener != null && listener.isWatching();
273+
}
274+
237275
private static class TestWatchEventListener implements WatchService.WatchEventListener {
238276
List<Event> events = new CopyOnWriteArrayList<>();
239277

0 commit comments

Comments
 (0)