Skip to content

Commit b77c078

Browse files
authored
fix: prevent deadlock when accessing FeatureFlags (#17645)
Synchronizing over FeatureFlags class in FeatureFlags.get could lead to a deadlock when the call comes from a supplier provided to VaadinContext.getAttribute() and concurrently from another request. For example, in the linked issue: - thread A invokes IndexHtmlRequestHandler.getCachedIndexHtmlDocument(), that locks the ServletContext through VaadinContext.getAttribute(...) providing a supplier that calls FeatureFlags.get() - thread B concurrently executes IndexHtmlRequestHandler.getIndexHtmlDocument() that also calls FeatureFlags.get() that locks on FeaturFlags class When B invokes FeatureFlags.get() -> VaadinContext.getAttribute() it has to wait because ServletContext is already locked A, but A is calling FeatureFlags.get() and it is waiting to lock on FeatureFlags class. This change removes the synchronization on FeatureFlags class and relies on VaadinContext.getAttribute(Class, Supplier) internal synchronization to prevent FeatureFlag two be instanciated more than once. It also adds test to ensure there are no regressions for #13962. Fixes #17637
1 parent 7dc87e4 commit b77c078

File tree

5 files changed

+127
-20
lines changed

5 files changed

+127
-20
lines changed

flow-dnd/src/test/java/com/vaadin/flow/component/dnd/AbstractDnDUnitTest.java

+5
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818

1919
import java.util.Collections;
2020
import java.util.Properties;
21+
import java.util.function.Supplier;
2122

2223
import org.junit.Assert;
2324
import org.junit.Before;
2425
import org.junit.Test;
26+
import org.mockito.ArgumentMatchers;
2527
import org.mockito.Mockito;
2628

2729
import com.vaadin.flow.component.Component;
@@ -52,6 +54,9 @@ public void setup() {
5254

5355
Lookup lookup = Mockito.mock(Lookup.class);
5456
Mockito.when(context.getAttribute(Lookup.class)).thenReturn(lookup);
57+
Mockito.when(context.getAttribute(ArgumentMatchers.any(Class.class),
58+
ArgumentMatchers.any(Supplier.class)))
59+
.then(i -> i.getArgument(1, Supplier.class).get());
5560

5661
DefaultDeploymentConfiguration configuration = new DefaultDeploymentConfiguration(
5762
appConfig, VaadinServlet.class, new Properties());

flow-server/src/main/java/com/vaadin/experimental/FeatureFlags.java

+13-19
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,15 @@
2828
import java.util.Optional;
2929
import java.util.Properties;
3030

31+
import org.apache.commons.io.FileUtils;
32+
import org.slf4j.Logger;
33+
import org.slf4j.LoggerFactory;
34+
3135
import com.vaadin.flow.di.Lookup;
3236
import com.vaadin.flow.di.ResourceProvider;
3337
import com.vaadin.flow.server.VaadinContext;
3438
import com.vaadin.flow.server.startup.ApplicationConfiguration;
3539

36-
import org.apache.commons.io.FileUtils;
37-
import org.slf4j.Logger;
38-
import org.slf4j.LoggerFactory;
39-
4040
/**
4141
* Tracks available feature flags and their status.
4242
*
@@ -132,21 +132,15 @@ public FeatureFlags getFeatureFlags() {
132132
public static FeatureFlags get(final VaadinContext context) {
133133
assert context != null;
134134

135-
FeatureFlagsWrapper attribute;
136-
synchronized (FeatureFlags.class) {
137-
attribute = context.getAttribute(FeatureFlagsWrapper.class);
138-
139-
if (attribute == null) {
140-
final FeatureFlags featureFlags = new FeatureFlags(
141-
context.getAttribute(Lookup.class));
142-
featureFlags.configuration = ApplicationConfiguration
143-
.get(context);
144-
featureFlags.loadProperties();
145-
attribute = new FeatureFlagsWrapper(featureFlags);
146-
context.setAttribute(attribute);
147-
}
148-
}
149-
135+
FeatureFlagsWrapper attribute = context
136+
.getAttribute(FeatureFlagsWrapper.class, () -> {
137+
final FeatureFlags featureFlags = new FeatureFlags(
138+
context.getAttribute(Lookup.class));
139+
featureFlags.configuration = ApplicationConfiguration
140+
.get(context);
141+
featureFlags.loadProperties();
142+
return new FeatureFlagsWrapper(featureFlags);
143+
});
150144
return attribute.getFeatureFlags();
151145
}
152146

flow-server/src/test/java/com/vaadin/experimental/FeatureFlagsTest.java

+99-1
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,25 @@
1515
*/
1616
package com.vaadin.experimental;
1717

18-
import net.jcip.annotations.NotThreadSafe;
1918
import java.io.File;
2019
import java.io.IOException;
2120
import java.nio.charset.StandardCharsets;
2221
import java.nio.file.Files;
22+
import java.util.concurrent.CompletableFuture;
23+
import java.util.concurrent.CountDownLatch;
24+
import java.util.concurrent.TimeUnit;
25+
import java.util.function.BiConsumer;
26+
import java.util.function.Supplier;
2327

28+
import net.jcip.annotations.NotThreadSafe;
2429
import org.apache.commons.io.FileUtils;
2530
import org.junit.Assert;
2631
import org.junit.Before;
2732
import org.junit.Rule;
2833
import org.junit.Test;
2934
import org.junit.rules.TemporaryFolder;
3035
import org.mockito.Mockito;
36+
import org.slf4j.LoggerFactory;
3137

3238
import com.vaadin.flow.di.Lookup;
3339
import com.vaadin.flow.di.ResourceProvider;
@@ -36,6 +42,7 @@
3642
import com.vaadin.flow.server.VaadinContext;
3743
import com.vaadin.flow.server.VaadinService;
3844
import com.vaadin.flow.server.startup.ApplicationConfiguration;
45+
import com.vaadin.flow.server.startup.ApplicationRouteRegistry;
3946

4047
import static com.vaadin.experimental.FeatureFlags.PROPERTIES_FILENAME;
4148

@@ -308,6 +315,96 @@ public void noFeatureFlagFile_noSystemPropertyProvided_allFeatureDisabled()
308315
}
309316
}
310317

318+
// https://github.com/vaadin/flow/issues/17637
319+
@Test
320+
public void get_concurrentAccess_servletContextLock_noDeadlock()
321+
throws Exception {
322+
BiConsumer<Void, Throwable> errorLogger = (unused, throwable) -> {
323+
if (throwable != null) {
324+
LoggerFactory.getLogger(FeatureFlagsTest.class)
325+
.error("Future failed", throwable);
326+
}
327+
};
328+
context = new MockVaadinContext() {
329+
@Override
330+
public <T> T getAttribute(Class<T> type) {
331+
doSleep();
332+
return super.getAttribute(type);
333+
}
334+
335+
@Override
336+
public <T> T getAttribute(Class<T> type,
337+
Supplier<T> defaultValueSupplier) {
338+
doSleep();
339+
return super.getAttribute(type, defaultValueSupplier);
340+
}
341+
342+
private void doSleep() {
343+
try {
344+
Thread.sleep(5);
345+
} catch (InterruptedException e) {
346+
throw new RuntimeException(e);
347+
}
348+
}
349+
};
350+
CountDownLatch latch = new CountDownLatch(2);
351+
CompletableFuture<Void> directTask = CompletableFuture.runAsync(() -> {
352+
FeatureFlags.get(context);
353+
latch.countDown();
354+
}).whenComplete(errorLogger);
355+
CompletableFuture<Void> supplierTask = CompletableFuture
356+
.runAsync(() -> {
357+
context.getAttribute(FeatureFlags.class, () -> {
358+
FeatureFlags out = FeatureFlags.get(context);
359+
latch.countDown();
360+
return out;
361+
});
362+
}).whenComplete(errorLogger);
363+
CompletableFuture.allOf(directTask, supplierTask);
364+
Assert.assertTrue("Futures not completed, potential deadlock",
365+
latch.await(1, TimeUnit.SECONDS));
366+
}
367+
368+
// https://github.com/vaadin/flow/issues/13962
369+
@Test
370+
public void get_concurrentAccess_vaadinContextLock_noDeadlock()
371+
throws Exception {
372+
BiConsumer<Void, Throwable> errorLogger = (unused, throwable) -> {
373+
if (throwable != null) {
374+
LoggerFactory.getLogger(FeatureFlagsTest.class)
375+
.error("Future failed", throwable);
376+
}
377+
};
378+
context = new MockVaadinContext();
379+
CountDownLatch latch = new CountDownLatch(2);
380+
CompletableFuture<Void> supplierTask = CompletableFuture
381+
.runAsync(() -> {
382+
// Simulation of ApplicationRouteRegistry.getInstance()
383+
// locking on VaadinContext
384+
synchronized (context) {
385+
ApplicationRouteRegistry attribute = context
386+
.getAttribute(ApplicationRouteRegistry.class);
387+
if (attribute == null) {
388+
attribute = Mockito
389+
.mock(ApplicationRouteRegistry.class);
390+
context.setAttribute(attribute);
391+
}
392+
}
393+
context.getAttribute(FeatureFlags.class, () -> {
394+
FeatureFlags out = FeatureFlags.get(context);
395+
latch.countDown();
396+
return out;
397+
});
398+
}).whenComplete(errorLogger);
399+
CompletableFuture<Void> directTask = CompletableFuture.runAsync(() -> {
400+
FeatureFlags.get(context);
401+
latch.countDown();
402+
}).whenComplete(errorLogger);
403+
CompletableFuture.allOf(directTask, supplierTask);
404+
Assert.assertTrue("Futures not completed, potential deadlock",
405+
latch.await(1, TimeUnit.SECONDS));
406+
}
407+
311408
private boolean hasUsageStatsEntry(String name) {
312409
return UsageStatistics.getEntries()
313410
.filter(entry -> entry.getName().equals(name)).findFirst()
@@ -333,4 +430,5 @@ private void mockResourcesLocation() {
333430
private void createFeatureFlagsFile(String data) throws IOException {
334431
FileUtils.write(featureFlagsFile, data, StandardCharsets.UTF_8);
335432
}
433+
336434
}

vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java

+4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.io.Serializable;
1919
import java.util.Collections;
2020
import java.util.Properties;
21+
import java.util.function.Supplier;
2122
import java.util.stream.Stream;
2223

2324
import org.junit.Assert;
@@ -158,6 +159,9 @@ private TestBeanStore createStore() {
158159
Mockito.when(appConfig.getPropertyNames())
159160
.thenReturn(Collections.emptyEnumeration());
160161
VaadinContext context = Mockito.mock(VaadinContext.class);
162+
Mockito.when(
163+
context.getAttribute(Mockito.any(Class.class), Mockito.any()))
164+
.then(i -> i.getArgument(1, Supplier.class).get());
161165
Mockito.when(context.getAttribute(
162166
Mockito.eq(ApplicationConfiguration.class), Mockito.any()))
163167
.thenReturn(appConfig);

vaadin-spring/src/test/java/com/vaadin/flow/spring/scopes/AbstractScopeTest.java

+6
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818
import java.util.Collections;
1919
import java.util.Properties;
2020
import java.util.concurrent.atomic.AtomicInteger;
21+
import java.util.function.Supplier;
2122

2223
import org.junit.After;
2324
import org.junit.Assert;
2425
import org.junit.Test;
26+
import org.mockito.ArgumentMatchers;
2527
import org.mockito.Mockito;
2628
import org.springframework.beans.factory.ObjectFactory;
2729
import org.springframework.beans.factory.config.Scope;
@@ -153,6 +155,10 @@ protected VaadinSession mockSession() {
153155
VaadinContext context = Mockito.mock(VaadinContext.class);
154156
Lookup lookup = Mockito.mock(Lookup.class);
155157
Mockito.when(context.getAttribute(Lookup.class)).thenReturn(lookup);
158+
Mockito.when(context.getAttribute(ArgumentMatchers.any(Class.class),
159+
ArgumentMatchers.any(Supplier.class)))
160+
.then(i -> i.getArgument(1, Supplier.class).get());
161+
156162
Mockito.when(appConfig.getContext()).thenReturn(context);
157163
DefaultDeploymentConfiguration config = new DefaultDeploymentConfiguration(
158164
appConfig, getClass(), initParameters);

0 commit comments

Comments
 (0)