BeanDescriptionDefaultViewsRaceTest.java
package tools.jackson.databind.introspect;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.Test;
import com.fasterxml.jackson.annotation.JsonView;
import tools.jackson.databind.BeanDescription;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.ObjectMapperTestAccess;
import tools.jackson.databind.cfg.MapperConfig;
import tools.jackson.databind.testutil.DatabindTestUtil;
import static org.junit.jupiter.api.Assertions.*;
// [databind#6227] `findDefaultViews()` set its "resolved" flag BEFORE computing the
// value, so a second thread could observe the flag and return `null` views.
public class BeanDescriptionDefaultViewsRaceTest extends DatabindTestUtil
{
static class Views { static class Pub { } }
@JsonView(Views.Pub.class)
static class ViewedBean {
public String a = "a";
public String b = "b";
}
/**
* Introspector that blocks inside the very first {@code findViews()} call, holding
* open the window between the flag write and the value write.
*/
static class BlockingIntrospector extends JacksonAnnotationIntrospector
{
private static final long serialVersionUID = 1L;
final CountDownLatch inside = new CountDownLatch(1);
final CountDownLatch release = new CountDownLatch(1);
final Semaphore firstCall = new Semaphore(1);
@Override
public Class<?>[] findViews(MapperConfig<?> config, Annotated a) {
if (firstCall.tryAcquire()) { // only the first caller blocks
inside.countDown();
try {
release.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
return super.findViews(config, a);
}
}
@Test
public void testConcurrentFindDefaultViews() throws Exception
{
final BlockingIntrospector intr = new BlockingIntrospector();
ObjectMapper mapper = jsonMapperBuilder()
.annotationIntrospector(intr)
.build();
final BeanDescription desc = ObjectMapperTestAccess.beanDescriptionForSer(mapper, ViewedBean.class);
final AtomicReference<Class<?>[]> fromA = new AtomicReference<>();
final AtomicReference<Class<?>[]> fromB = new AtomicReference<>();
ExecutorService pool = Executors.newFixedThreadPool(2);
try {
Future<?> a = pool.submit(() -> fromA.set(desc.findDefaultViews()));
// wait until thread A is INSIDE findViews(), i.e. mid-resolution
assertTrue(intr.inside.await(10, TimeUnit.SECONDS), "thread A never reached findViews()");
Future<?> b = pool.submit(() -> fromB.set(desc.findDefaultViews()));
// give B a chance to read the half-resolved state, then let A finish
Thread.sleep(100L);
intr.release.countDown();
a.get(10, TimeUnit.SECONDS);
b.get(10, TimeUnit.SECONDS);
} finally {
pool.shutdownNow();
}
assertNotNull(fromA.get(), "views resolved by first thread should not be null");
assertNotNull(fromB.get(),
"second thread saw the 'resolved' flag before the value was assigned, and got null views");
assertArrayEquals(fromA.get(), fromB.get());
assertEquals(Views.Pub.class, fromB.get()[0]);
}
}