Skip to content

Commit 4b34bd9

Browse files
committed
TRUNK-6694 & TRUNK-6695: Avoid synchronization in service fetching
1 parent 375abbe commit 4b34bd9

2 files changed

Lines changed: 104 additions & 13 deletions

File tree

api/src/main/java/org/openmrs/api/context/ServiceContext.java

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import java.util.Map;
1818
import java.util.Map.Entry;
1919
import java.util.Set;
20+
import java.util.concurrent.ConcurrentHashMap;
2021

2122
import org.aopalliance.aop.Advice;
2223
import org.openmrs.api.APIException;
@@ -82,7 +83,7 @@ public class ServiceContext implements ApplicationContextAware {
8283

8384
private ApplicationContext applicationContext;
8485

85-
private static boolean refreshingContext = false;
86+
private static volatile boolean refreshingContext = false;
8687

8788
private static final Object refreshingContextLock = new Object();
8889

@@ -93,7 +94,7 @@ public class ServiceContext implements ApplicationContextAware {
9394
private boolean useSystemClassLoader = false;
9495

9596
// Cached service objects
96-
Map<Class, Object> services = new HashMap<>();
97+
Map<Class, Object> services = new ConcurrentHashMap<>();
9798

9899
// Advisors added to services by this service
99100
Map<Class, Set<Advisor>> addedAdvisors = new HashMap<>();
@@ -680,20 +681,23 @@ public <T> T getService(Class<? extends T> cls) {
680681
log.trace("Getting service: " + cls);
681682
}
682683

683-
// if the context is refreshing, wait until it is
684-
// done -- otherwise a null service might be returned
685-
synchronized (refreshingContextLock) {
686-
try {
687-
while (refreshingContext) {
688-
log.debug("Waiting to get service: {} while the context is being refreshed", cls);
684+
// Only synchronize if we're refreshing the context; during context refreshes, we need
685+
// to wait for services to be available. Otherwise we can take the fast-path.
686+
if (refreshingContext) {
687+
synchronized (refreshingContextLock) {
688+
try {
689+
while (refreshingContext) {
690+
log.debug("Waiting to get service: {} while the context is being refreshed", cls);
689691

690-
refreshingContextLock.wait();
692+
refreshingContextLock.wait();
691693

692-
log.debug("Finished waiting to get service {} while the context was being refreshed", cls);
693-
}
694+
log.debug("Finished waiting to get service {} while the context was being refreshed", cls);
695+
}
694696

695-
} catch (InterruptedException e) {
696-
log.warn("Refresh lock was interrupted", e);
697+
} catch (InterruptedException e) {
698+
log.warn("Refresh lock was interrupted", e);
699+
Thread.currentThread().interrupt();
700+
}
697701
}
698702
}
699703

api/src/test/java/org/openmrs/api/context/ServiceContextTest.java

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,20 @@
1111

1212
import java.util.ArrayList;
1313
import java.util.List;
14+
import java.util.concurrent.CountDownLatch;
15+
import java.util.concurrent.TimeUnit;
16+
import java.util.concurrent.atomic.AtomicReference;
1417

1518
import org.junit.jupiter.api.AfterEach;
1619
import org.junit.jupiter.api.BeforeEach;
1720
import org.junit.jupiter.api.Test;
1821
import org.openmrs.api.APIException;
22+
import org.openmrs.api.PatientService;
1923
import org.openmrs.test.jupiter.BaseContextSensitiveTest;
2024
import org.openmrs.util.DatabaseUpdateException;
2125
import org.openmrs.util.InputRequiredException;
2226

27+
import static org.junit.jupiter.api.Assertions.assertFalse;
2328
import static org.junit.jupiter.api.Assertions.assertNotNull;
2429
import static org.junit.jupiter.api.Assertions.assertNull;
2530
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -102,4 +107,86 @@ public void getModuleOpenmrsServices_shouldRaiseApiExceptionWithNullClassInstanc
102107
verify(spiedServiceContext, never()).getMessageService();
103108
verify(spiedServiceContext, never()).getMessageSourceService();
104109
}
110+
111+
/**
112+
* @see ServiceContext#getService(Class)
113+
*/
114+
@Test
115+
public void getService_shouldReturnServiceOnTheFastPathWhenNoRefreshIsInProgress() {
116+
assertFalse(serviceContext.isRefreshingContext());
117+
118+
assertNotNull(serviceContext.getService(PatientService.class));
119+
}
120+
121+
/**
122+
* @see ServiceContext#getService(Class)
123+
*/
124+
@Test
125+
public void getService_shouldBlockUntilAnInProgressRefreshCompletes() throws Exception {
126+
AtomicReference<Object> retrievedService = new AtomicReference<>();
127+
AtomicReference<Throwable> thrown = new AtomicReference<>();
128+
CountDownLatch lookupStarted = new CountDownLatch(1);
129+
130+
serviceContext.startRefreshingContext();
131+
132+
Thread lookup = new Thread(() -> {
133+
lookupStarted.countDown();
134+
try {
135+
retrievedService.set(serviceContext.getService(PatientService.class));
136+
} catch (Throwable t) {
137+
thrown.set(t);
138+
}
139+
});
140+
lookup.start();
141+
142+
try {
143+
// wait for the lookup thread to reach getService, then give it time to block
144+
assertTrue(lookupStarted.await(5, TimeUnit.SECONDS));
145+
Thread.sleep(500);
146+
147+
// the lookup must not have returned while the refresh is in progress
148+
assertNull(retrievedService.get());
149+
assertTrue(lookup.isAlive());
150+
} finally {
151+
serviceContext.doneRefreshingContext();
152+
}
153+
154+
// once the refresh finishes, the blocked lookup should complete
155+
lookup.join(5000);
156+
assertFalse(lookup.isAlive());
157+
assertNull(thrown.get());
158+
assertNotNull(retrievedService.get());
159+
}
160+
161+
/**
162+
* @see ServiceContext#setService(Class, Object)
163+
*/
164+
@Test
165+
public void setService_shouldMakeRegistrationsVisibleToOtherThreads() throws Exception {
166+
serviceContext.setService(RegistryVisibilityService.class, new RegistryVisibilityServiceImpl());
167+
168+
AtomicReference<Object> retrievedService = new AtomicReference<>();
169+
AtomicReference<Throwable> thrown = new AtomicReference<>();
170+
171+
Thread reader = new Thread(() -> {
172+
try {
173+
retrievedService.set(serviceContext.getService(RegistryVisibilityService.class));
174+
} catch (Throwable t) {
175+
thrown.set(t);
176+
}
177+
});
178+
reader.start();
179+
reader.join(5000);
180+
181+
assertNull(thrown.get());
182+
assertNotNull(retrievedService.get());
183+
assertTrue(retrievedService.get() instanceof RegistryVisibilityService);
184+
}
185+
186+
/**
187+
* Simple service interface used to verify that registrations are safely published across threads.
188+
*/
189+
public interface RegistryVisibilityService {}
190+
191+
public static class RegistryVisibilityServiceImpl implements RegistryVisibilityService {}
105192
}

0 commit comments

Comments
 (0)