Skip to content

Commit 70dc4e2

Browse files
authored
Re-introduce Caching (#2393)
* Implemented caching for cohort def list, concept set list, permissions, and data sources. Implementation uses ehCache 3.9. Changed editor config to tabs. Made cache endpoints auth restricted.
1 parent 67f8816 commit 70dc4e2

23 files changed

+655
-96
lines changed

.editorconfig

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
root = true
55

66
[*]
7-
indent_style = space
7+
indent_style = tab
88
indent_size = 2
99
end_of_line = crlf
1010
charset = utf-8

pom.xml

+13
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,10 @@
236236
<logging.level.org.hibernate>info</logging.level.org.hibernate>
237237
<logging.level.org.apache.shiro>warn</logging.level.org.apache.shiro>
238238

239+
<!-- Spring Cache properties -->
240+
<spring.cache.type>jcache</spring.cache.type>
241+
242+
<!-- Spring Batch -->
239243
<spring.batch.taskExecutor.corePoolSize>10</spring.batch.taskExecutor.corePoolSize>
240244
<spring.batch.taskExecutor.maxPoolSize>20</spring.batch.taskExecutor.maxPoolSize>
241245
<spring.batch.taskExecutor.queueCapacity>2147483647</spring.batch.taskExecutor.queueCapacity>
@@ -272,6 +276,7 @@
272276
<cache.jobs.count>3</cache.jobs.count>
273277
<!-- Achilles cache -->
274278
<cache.achilles.usePersonCount>true</cache.achilles.usePersonCount>
279+
<cache.webapi.enabled>true</cache.webapi.enabled>
275280

276281
<!-- Build info -->
277282
<buildinfo.atlas.milestone.id>47</buildinfo.atlas.milestone.id>
@@ -744,6 +749,10 @@
744749
<scope>provided</scope>
745750
</dependency>
746751
<dependency>
752+
<groupId>javax.cache</groupId>
753+
<artifactId>cache-api</artifactId>
754+
<version>1.1.1</version>
755+
</dependency> <dependency>
747756
<groupId>org.ohdsi.sql</groupId>
748757
<artifactId>SqlRender</artifactId>
749758
<version>${SqlRender.version}</version>
@@ -1202,6 +1211,10 @@
12021211
<version>1.1.7</version>
12031212
</dependency>
12041213
<dependency>
1214+
<groupId>org.ehcache</groupId>
1215+
<artifactId>ehcache</artifactId>
1216+
<version>3.9.11</version>
1217+
</dependency> <dependency>
12051218
<groupId>com.opentable.components</groupId>
12061219
<artifactId>otj-pg-embedded</artifactId>
12071220
<version>0.13.1</version>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package org.ohdsi.webapi;
2+
3+
import org.springframework.cache.annotation.EnableCaching;
4+
import org.springframework.context.annotation.Configuration;
5+
6+
@Configuration
7+
@EnableCaching
8+
public class CacheConfig {
9+
10+
}

src/main/java/org/ohdsi/webapi/JerseyConfig.java

+10-8
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737

3838
import javax.inject.Singleton;
3939
import javax.ws.rs.ext.RuntimeDelegate;
40+
import org.ohdsi.webapi.cache.CacheService;
4041

4142
/**
4243
*
@@ -59,31 +60,32 @@ public JerseyConfig() {
5960
public void afterPropertiesSet() throws Exception {
6061
packages(this.rootPackage);
6162
register(ActivityService.class);
63+
register(CacheService.class);
64+
register(CcController.class);
6265
register(CDMResultsService.class);
6366
register(CohortAnalysisService.class);
6467
register(CohortDefinitionService.class);
6568
register(CohortResultsService.class);
6669
register(CohortService.class);
6770
register(ConceptSetService.class);
71+
register(DDLService.class);
6872
register(EvidenceService.class);
6973
register(FeasibilityService.class);
74+
register(FeatureExtractionService.class);
7075
register(InfoService.class);
7176
register(IRAnalysisResource.class);
7277
register(JobService.class);
78+
register(MultiPartFeature.class);
79+
register(PermissionController.class);
7380
register(PersonService.class);
81+
register(ScriptExecutionController.class);
82+
register(ScriptExecutionCallbackController.class);
7483
register(SourceController.class);
7584
register(SqlRenderService.class);
76-
register(DDLService.class);
85+
register(SSOController.class);
7786
register(TherapyPathResultsService.class);
7887
register(UserService.class);
7988
register(VocabularyService.class);
80-
register(ScriptExecutionController.class);
81-
register(ScriptExecutionCallbackController.class);
82-
register(MultiPartFeature.class);
83-
register(FeatureExtractionService.class);
84-
register(CcController.class);
85-
register(SSOController.class);
86-
register(PermissionController.class);
8789
register(new AbstractBinder() {
8890
@Override
8991
protected void configure() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2019 cknoll1.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.ohdsi.webapi.cache;
17+
18+
import java.io.Serializable;
19+
import java.util.Optional;
20+
import javax.cache.management.CacheStatisticsMXBean;
21+
22+
/**
23+
*
24+
* @author cknoll1
25+
*/
26+
public class CacheInfo implements Serializable{
27+
public String cacheName;
28+
public Long entries;
29+
public CacheStatisticsMXBean cacheStatistics;
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright 2019 cknoll1.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.ohdsi.webapi.cache;
17+
18+
import java.util.ArrayList;
19+
import java.util.List;
20+
import java.util.stream.StreamSupport;
21+
import javax.cache.Cache;
22+
import javax.cache.CacheManager;
23+
import javax.ws.rs.GET;
24+
import javax.ws.rs.Path;
25+
import javax.ws.rs.Produces;
26+
import javax.ws.rs.core.MediaType;
27+
import org.ohdsi.webapi.util.CacheHelper;
28+
import org.springframework.beans.factory.annotation.Autowired;
29+
import org.springframework.stereotype.Component;
30+
31+
/**
32+
*
33+
* @author cknoll1
34+
*/
35+
@Path("/cache")
36+
@Component
37+
public class CacheService {
38+
39+
public static class ClearCacheResult {
40+
41+
public List<CacheInfo> clearedCaches;
42+
43+
private ClearCacheResult() {
44+
this.clearedCaches = new ArrayList<>();
45+
}
46+
}
47+
48+
private CacheManager cacheManager;
49+
50+
@Autowired(required = false)
51+
public CacheService(CacheManager cacheManager) {
52+
53+
this.cacheManager = cacheManager;
54+
}
55+
56+
public CacheService() {
57+
}
58+
59+
60+
@GET
61+
@Path("/")
62+
@Produces(MediaType.APPLICATION_JSON)
63+
public List<CacheInfo> getCacheInfoList() {
64+
List<CacheInfo> caches = new ArrayList<>();
65+
66+
if (cacheManager == null) return caches; //caching is disabled
67+
68+
for (String cacheName : cacheManager.getCacheNames()) {
69+
Cache cache = cacheManager.getCache(cacheName);
70+
CacheInfo info = new CacheInfo();
71+
info.cacheName = cacheName;
72+
info.entries = StreamSupport.stream(cache.spliterator(), false).count();
73+
info.cacheStatistics = CacheHelper.getCacheStats(cacheManager , cacheName);
74+
caches.add(info);
75+
}
76+
return caches;
77+
}
78+
@GET
79+
@Path("/clear")
80+
@Produces(MediaType.APPLICATION_JSON)
81+
public ClearCacheResult clearAll() {
82+
ClearCacheResult result = new ClearCacheResult();
83+
84+
for (String cacheName : cacheManager.getCacheNames()) {
85+
Cache cache = cacheManager.getCache(cacheName);
86+
CacheInfo info = new CacheInfo();
87+
info.cacheName = cacheName;
88+
info.entries = StreamSupport.stream(cache.spliterator(), false).count();
89+
result.clearedCaches.add(info);
90+
cache.clear();
91+
}
92+
return result;
93+
}
94+
}

src/main/java/org/ohdsi/webapi/security/PermissionService.java

+16
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ public class PermissionService {
6262
@Value("#{!'${security.provider}'.equals('DisabledSecurity')}")
6363
private boolean securityEnabled;
6464

65+
@Value("${security.defaultGlobalReadPermissions}")
66+
private boolean defaultGlobalReadPermissions;
67+
6568
private final EntityGraph PERMISSION_ENTITY_GRAPH = EntityGraphUtils.fromAttributePaths("rolePermissions", "rolePermissions.role");
6669

6770
public PermissionService(
@@ -227,4 +230,17 @@ public void fillReadAccess(CommonEntity entity, CommonEntityDTO entityDTO) {
227230
public boolean isSecurityEnabled() {
228231
return this.securityEnabled;
229232
}
233+
234+
// Use this key for cache (asset lists) that may be associated to a user or shared across users.
235+
public String getAssetListCacheKey() {
236+
if (this.isSecurityEnabled() && !defaultGlobalReadPermissions)
237+
return permissionManager.getSubjectName();
238+
else
239+
return "ALL_USERS";
240+
}
241+
242+
// use this cache key when the cache is associated to a user
243+
public String getSubjectCacheKey() {
244+
return this.isSecurityEnabled() ? permissionManager.getSubjectName() : "ALL_USERS";
245+
}
230246
}

src/main/java/org/ohdsi/webapi/service/CohortDefinitionService.java

+31-1
Original file line numberDiff line numberDiff line change
@@ -131,13 +131,18 @@
131131
import java.util.Optional;
132132
import java.util.Set;
133133
import java.util.stream.Collectors;
134+
import javax.cache.CacheManager;
135+
import javax.cache.configuration.MutableConfiguration;
134136
import javax.ws.rs.core.Response.ResponseBuilder;
135137

136138
import static org.ohdsi.webapi.Constants.Params.COHORT_DEFINITION_ID;
137139
import static org.ohdsi.webapi.Constants.Params.JOB_NAME;
138140
import static org.ohdsi.webapi.Constants.Params.SOURCE_ID;
139141
import org.ohdsi.webapi.source.SourceService;
140142
import static org.ohdsi.webapi.util.SecurityUtils.whitelist;
143+
import org.springframework.boot.autoconfigure.cache.JCacheManagerCustomizer;
144+
import org.springframework.cache.annotation.CacheEvict;
145+
import org.springframework.cache.annotation.Cacheable;
141146

142147
/**
143148
* Provides REST services for working with cohort definitions.
@@ -149,6 +154,24 @@
149154
@Component
150155
public class CohortDefinitionService extends AbstractDaoService implements HasTags<Integer> {
151156

157+
//create cache
158+
@Component
159+
public static class CachingSetup implements JCacheManagerCustomizer {
160+
161+
public static final String COHORT_DEFINITION_LIST_CACHE = "cohortDefinitionList";
162+
163+
@Override
164+
public void customize(CacheManager cacheManager) {
165+
// Evict when a cohort definition is created or updated, or permissions, or tags
166+
if (!CacheHelper.getCacheNames(cacheManager).contains(COHORT_DEFINITION_LIST_CACHE)) {
167+
cacheManager.createCache(COHORT_DEFINITION_LIST_CACHE, new MutableConfiguration<String, List<CohortMetadataDTO>>()
168+
.setTypes(String.class, (Class<List<CohortMetadataDTO>>) (Class<?>) List.class)
169+
.setStoreByValue(false)
170+
.setStatisticsEnabled(true));
171+
}
172+
}
173+
}
174+
152175
private static final CohortExpressionQueryBuilder queryBuilder = new CohortExpressionQueryBuilder();
153176

154177
@Autowired
@@ -205,7 +228,7 @@ public class CohortDefinitionService extends AbstractDaoService implements HasTa
205228
@Autowired
206229
private VersionService<CohortVersion> versionService;
207230

208-
@Value("${security.defaultGlobalReadPermissions}")
231+
@Value("${security.defaultGlobalReadPermissions}")
209232
private boolean defaultGlobalReadPermissions;
210233

211234
private final MarkdownRender markdownPF = new MarkdownRender();
@@ -408,6 +431,7 @@ public GenerateSqlResult generateSql(GenerateSqlRequest request) {
408431
@Path("/")
409432
@Produces(MediaType.APPLICATION_JSON)
410433
@Transactional
434+
@Cacheable(cacheNames = CachingSetup.COHORT_DEFINITION_LIST_CACHE, key = "@permissionService.getSubjectCacheKey()")
411435
public List<CohortMetadataDTO> getCohortDefinitionList() {
412436
List<CohortDefinition> definitions = cohortDefinitionRepository.list();
413437
return definitions.stream()
@@ -436,6 +460,7 @@ public List<CohortMetadataDTO> getCohortDefinitionList() {
436460
@Transactional
437461
@Produces(MediaType.APPLICATION_JSON)
438462
@Consumes(MediaType.APPLICATION_JSON)
463+
@CacheEvict(cacheNames = CachingSetup.COHORT_DEFINITION_LIST_CACHE, allEntries = true)
439464
public CohortDTO createCohortDefinition(CohortDTO dto) {
440465

441466
Date currentTime = Calendar.getInstance().getTime();
@@ -538,6 +563,7 @@ public int getCountCDefWithSameName(@PathParam("id") @DefaultValue("0") final in
538563
@Produces(MediaType.APPLICATION_JSON)
539564
@Consumes(MediaType.APPLICATION_JSON)
540565
@Transactional
566+
@CacheEvict(cacheNames = CachingSetup.COHORT_DEFINITION_LIST_CACHE, allEntries = true)
541567
public CohortDTO saveCohortDefinition(@PathParam("id") final int id, CohortDTO def) {
542568
Date currentTime = Calendar.getInstance().getTime();
543569

@@ -670,6 +696,7 @@ public List<CohortGenerationInfoDTO> getInfo(@PathParam("id") final int id) {
670696
@Produces(MediaType.APPLICATION_JSON)
671697
@Path("/{id}/copy")
672698
@Transactional
699+
@CacheEvict(cacheNames = CachingSetup.COHORT_DEFINITION_LIST_CACHE, allEntries = true)
673700
public CohortDTO copy(@PathParam("id") final int id) {
674701
CohortDTO sourceDef = getCohortDefinition(id);
675702
sourceDef.setId(null); // clear the ID
@@ -954,6 +981,7 @@ private Response printFrindly(String markdown, String format) {
954981
@POST
955982
@Produces(MediaType.APPLICATION_JSON)
956983
@Path("/{id}/tag/")
984+
@CacheEvict(cacheNames = CachingSetup.COHORT_DEFINITION_LIST_CACHE, allEntries = true)
957985
@Transactional
958986
public void assignTag(@PathParam("id") final Integer id, final int tagId) {
959987
CohortDefinition entity = cohortDefinitionRepository.findOne(id);
@@ -971,6 +999,7 @@ public void assignTag(@PathParam("id") final Integer id, final int tagId) {
971999
@DELETE
9721000
@Produces(MediaType.APPLICATION_JSON)
9731001
@Path("/{id}/tag/{tagId}")
1002+
@CacheEvict(cacheNames = CachingSetup.COHORT_DEFINITION_LIST_CACHE, allEntries = true)
9741003
@Transactional
9751004
public void unassignTag(@PathParam("id") final Integer id, @PathParam("tagId") final int tagId) {
9761005
CohortDefinition entity = cohortDefinitionRepository.findOne(id);
@@ -1106,6 +1135,7 @@ public void deleteVersion(@PathParam("id") final int id, @PathParam("version") f
11061135
@Produces(MediaType.APPLICATION_JSON)
11071136
@Path("/{id}/version/{version}/createAsset")
11081137
@Transactional
1138+
@CacheEvict(cacheNames = CachingSetup.COHORT_DEFINITION_LIST_CACHE, allEntries = true)
11091139
public CohortDTO copyAssetFromVersion(@PathParam("id") final int id, @PathParam("version") final int version) {
11101140
checkVersion(id, version, false);
11111141
CohortVersion cohortVersion = versionService.getById(VersionType.COHORT, id, version);

0 commit comments

Comments
 (0)