Skip to content

Commit

Permalink
chore: Update EhCache (#3098)
Browse files Browse the repository at this point in the history
Signed-off-by: Pavel Jareš <[email protected]>
  • Loading branch information
pj892031 authored Sep 25, 2023
1 parent 70acfa8 commit 720e14d
Show file tree
Hide file tree
Showing 10 changed files with 194 additions and 131 deletions.
1 change: 1 addition & 0 deletions common-service-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ dependencies {
exclude group: "org.springframework", module: "spring-test"
}
testImplementation libraries.spring_test
testImplementation libraries.spring_context_support
testImplementation libraries.json_smart
testImplementation(libraries.eh_cache)
testImplementation(libraries.jackson_core)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@

package org.zowe.apiml.util;

import net.sf.ehcache.Element;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.zowe.apiml.cache.CompositeKey;

import java.util.List;
import java.util.Set;
import java.util.Spliterator;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

/**
* This utils offer base operation with cache, which can be shared to multiple codes.
Expand Down Expand Up @@ -61,18 +63,15 @@ public void evictSubset(CacheManager cacheManager, String cacheName, Predicate<C
final Cache cache = cacheManager.getCache(cacheName);
if (cache == null) throw new IllegalArgumentException("Unknown cache " + cacheName);
final Object nativeCache = cache.getNativeCache();
if (nativeCache instanceof net.sf.ehcache.Cache) {
final net.sf.ehcache.Cache ehCache = (net.sf.ehcache.Cache) nativeCache;

for (final Object key : ehCache.getKeys()) {
if (key instanceof CompositeKey) {
// if entry is compositeKey and first param is different, skip it (be sure this is not to evict)
final CompositeKey compositeKey = ((CompositeKey) key);
if (!keyPredicate.test(compositeKey)) continue;
}
// if key is not composite key (unknown for evict) or has same serviceId, evict record
ehCache.remove(key);
}
if (nativeCache instanceof javax.cache.Cache) {
Spliterator<javax.cache.Cache.Entry<Object, Object>> spliterator = ((javax.cache.Cache<Object, Object>) nativeCache).spliterator();
Set<Object> keysToRemove = StreamSupport.stream(spliterator, true)
// if entry is compositeKey and first param is the same filter it to be removed or
// if key is not composite key (unknown for evict) evict record (as failover)
.filter(e -> !(e.getKey() instanceof CompositeKey) || keyPredicate.test((CompositeKey) e.getKey()))
.map(javax.cache.Cache.Entry::getKey)
.collect(Collectors.toSet());
((javax.cache.Cache<Object, Object>) nativeCache).removeAll(keysToRemove);
} else {
// in case of using different cache manager, evict all records for sure
cache.clear();
Expand All @@ -93,14 +92,9 @@ public <T> List<T> getAllRecords(CacheManager cacheManager, String cacheName) {
if (cache == null) throw new IllegalArgumentException("Unknown cache " + cacheName);

final Object nativeCache = cache.getNativeCache();
if (nativeCache instanceof net.sf.ehcache.Cache) {
final net.sf.ehcache.Cache ehCache = (net.sf.ehcache.Cache) nativeCache;

return (List<T>) ehCache.getAll(ehCache.getKeys())
.values()
.stream()
.map(Element::getObjectValue)
.collect(Collectors.toList());
if (nativeCache instanceof javax.cache.Cache) {
Spliterator<javax.cache.Cache.Entry<Object, T>> spliterator = ((javax.cache.Cache<Object, T>) nativeCache).spliterator();
return StreamSupport.stream(spliterator, true).map(javax.cache.Cache.Entry::getValue).collect(Collectors.toList());
} else {
throw new IllegalArgumentException("Unsupported type of cache : " + nativeCache.getClass());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,16 @@

package org.zowe.apiml.util;

import net.sf.ehcache.Element;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.stubbing.Answer;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.zowe.apiml.cache.CompositeKey;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.*;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

class CacheUtilsTest {

Expand All @@ -49,6 +31,25 @@ void setUp() {
underTest = new CacheUtils();
}

private javax.cache.Cache.Entry<Object, Object> createEntry(Object key, Object value) {
return new javax.cache.Cache.Entry<Object, Object>() {
@Override
public Object getKey() {
return key;
}

@Override
public Object getValue() {
return value;
}

@Override
public <T> T unwrap(Class<T> clazz) {
return (T) value;
}
};
}

@Test
void testEvictSubset() {
CacheManager cacheManager = mock(CacheManager.class);
Expand All @@ -60,16 +61,22 @@ void testEvictSubset() {

Cache cache2 = mock(Cache.class);
when(cacheManager.getCache("cache2")).thenReturn(cache2);
net.sf.ehcache.Cache ehCache2 = mock(net.sf.ehcache.Cache.class);
javax.cache.Cache ehCache2 = mock(javax.cache.Cache.class);

when(cache2.getNativeCache()).thenReturn(ehCache2);
List<Object> keys = Arrays.asList(
"abc", // not composite key
"abc",
new CompositeKey("test", 5),
new CompositeKey("next", 10),
new CompositeKey("next", 15)
);
when(ehCache2.getKeys()).thenReturn(keys);
List<javax.cache.Cache.Entry<Object, Object>> values = Arrays.asList(
createEntry(keys.get(0), "A"),
createEntry(keys.get(1), "B"),
createEntry(keys.get(2), "C"),
createEntry(keys.get(3), "D")
);
when(ehCache2.spliterator()).thenAnswer(invocation -> values.spliterator());

try {
underTest.evictSubset(cacheManager, "missing", x -> true);
Expand All @@ -79,34 +86,20 @@ void testEvictSubset() {
assertTrue(e.getMessage().contains("missing"));
}

// not EhCache - clean all, dont use keyPredicate
// not EhCache - clean all, do not use keyPredicate
verify(cache1, never()).clear();
underTest.evictSubset(cacheManager, "cache1", x -> false);
verify(cache1, times(1)).clear();

final Answer<Boolean> answer = invocation -> {
removeCounter++;
return true;
};

doAnswer(answer).when(ehCache2).remove(any(Serializable.class));
doAnswer(answer).when(ehCache2).remove((Object) any());

assertEquals(0, removeCounter);
// in all cases remove entries without CompositeKey
underTest.evictSubset(cacheManager, "cache2", x -> false);
assertEquals(1, removeCounter);
verify(ehCache2, times(1)).remove(keys.get(0));
verify(ehCache2, times(1)).removeAll(Collections.singleton(keys.get(0)));

underTest.evictSubset(cacheManager, "cache2", x -> x.equals(0, "test"));
assertEquals(3, removeCounter);
verify(ehCache2, times(2)).remove(keys.get(0));
verify(ehCache2, times(1)).remove(keys.get(1));
verify(ehCache2, times(1)).removeAll(new HashSet(Arrays.asList(keys.get(0), keys.get(1))));

underTest.evictSubset(cacheManager, "cache2", x -> (Integer) x.get(1) > 10);
assertEquals(5, removeCounter);
verify(ehCache2, times(3)).remove(keys.get(0));
verify(ehCache2, times(1)).remove(keys.get(3));
verify(ehCache2, times(1)).removeAll(new HashSet(Arrays.asList(keys.get(0), keys.get(3))));
}

@Test
Expand All @@ -132,30 +125,21 @@ void givenUnsupportedCacheManager_whenGetAllRecords_thenThrowsException() {
assertTrue(iae.getMessage().startsWith("Unsupported type of cache : "));
}

private Map<Object, Element> convert(Map<Integer, String> in) {
Map<Object, Element> out = new HashMap<>();
for (Map.Entry<Integer, String> entry : in.entrySet()) {
out.put(entry.getKey(), new Element(entry.getKey(), entry.getValue()));
}
return out;
}

@Test
void givenValidCacheManager_whenGetAllRecords_thenReadAllStoredRecords() {
CacheManager cacheManager = mock(CacheManager.class);
Cache cache = mock(Cache.class);
net.sf.ehcache.Cache ehCache = mock(net.sf.ehcache.Cache.class);
javax.cache.Cache ehCache = mock(javax.cache.Cache.class);

Map<Integer, String> entries = new HashMap<>();
entries.put(1, "a");
entries.put(2, "b");
entries.put(3, "c");
List<Object> keys = new ArrayList<>(entries.keySet());
List entries = Arrays.asList(
createEntry(1, "a"),
createEntry(2, "b"),
createEntry(3, "c")
);

when(cacheManager.getCache("knownCacheName")).thenReturn(cache);
when(cache.getNativeCache()).thenReturn(ehCache);
when(ehCache.getKeys()).thenReturn(keys);
when(ehCache.getAll(keys)).thenReturn(convert(entries));
when(ehCache.spliterator()).thenAnswer(invocation -> entries.spliterator());

Collection<String> values = underTest.getAllRecords(cacheManager, "knownCacheName");
assertNotNull(values);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.ehcache.EhCacheCacheManager;
import org.springframework.cache.ehcache.EhCacheManagerFactoryBean;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.cache.jcache.JCacheCacheManager;
import org.springframework.cache.jcache.JCacheManagerFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
Expand All @@ -23,6 +23,7 @@
import org.zowe.apiml.util.CacheUtils;

import javax.annotation.PostConstruct;
import java.io.IOException;

/**
* Spring configuration to use EhCache. This context is using from application and also from tests.
Expand All @@ -48,18 +49,17 @@ public void afterPropertiesSet() {
}

@Bean
public CacheManager cacheManager() {
net.sf.ehcache.CacheManager cache = ehCacheCacheManager().getObject();
assert cache != null;
return new EhCacheCacheManager(cache);
public JCacheManagerFactoryBean cacheManagerFactoryBean() throws IOException {
JCacheManagerFactoryBean jCacheManagerFactoryBean = new JCacheManagerFactoryBean();
jCacheManagerFactoryBean.setCacheManagerUri(new ClassPathResource("ehcache.xml").getURI());
return jCacheManagerFactoryBean;
}

@Bean
public EhCacheManagerFactoryBean ehCacheCacheManager() {
EhCacheManagerFactoryBean cmfb = new EhCacheManagerFactoryBean();
cmfb.setConfigLocation(new ClassPathResource("ehcache.xml"));
cmfb.setShared(true);
return cmfb;
public CacheManager cacheManager() throws IOException {
final JCacheCacheManager jCacheCacheManager = new JCacheCacheManager();
jCacheCacheManager.setCacheManager(cacheManagerFactoryBean().getObject());
return jCacheCacheManager;
}

@Bean(CacheConfig.COMPOSITE_KEY_GENERATOR)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,29 +15,31 @@
import com.netflix.discovery.shared.Application;
import com.netflix.loadbalancer.reactive.ExecutionListener;
import com.netflix.zuul.context.RequestContext;
import java.util.Optional;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang.StringUtils;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.context.ApplicationContext;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Service;
import org.zowe.apiml.auth.Authentication;
import org.zowe.apiml.eurekaservice.client.util.EurekaMetadataParser;
import org.zowe.apiml.gateway.cache.RetryIfExpired;
import org.zowe.apiml.gateway.config.CacheConfig;
import org.zowe.apiml.gateway.security.service.schema.IAuthenticationScheme;
import org.zowe.apiml.gateway.security.service.schema.AuthenticationCommand;
import org.zowe.apiml.gateway.security.service.schema.AuthenticationSchemeFactory;
import org.zowe.apiml.gateway.security.service.schema.IAuthenticationScheme;
import org.zowe.apiml.gateway.security.service.schema.ServiceAuthenticationService;
import org.zowe.apiml.gateway.security.service.schema.source.AuthSchemeException;
import org.zowe.apiml.gateway.security.service.schema.source.AuthSource;
import org.zowe.apiml.gateway.security.service.schema.source.AuthSourceService;
import org.zowe.apiml.util.CacheUtils;

import javax.annotation.PostConstruct;
import java.util.List;
import java.util.Optional;

/**
* This bean is responsible for "translating" security to specific service. It decorate request with security data for
Expand All @@ -60,24 +62,34 @@
* also in loadbalancer
*/
@Service
@AllArgsConstructor
@RequiredArgsConstructor
public class ServiceAuthenticationServiceImpl implements ServiceAuthenticationService {

public static final String AUTHENTICATION_COMMAND_KEY = "zoweAuthenticationCommand";

private static final String CACHE_BY_SERVICE_ID = "serviceAuthenticationByServiceId";
private static final String CACHE_BY_AUTHENTICATION = "serviceAuthenticationByAuthentication";
static final String CACHE_BY_AUTHENTICATION = "serviceAuthenticationByAuthentication";

private final LoadBalancerAuthenticationCommand loadBalancerCommand = new LoadBalancerAuthenticationCommand();
private final LoadBalancerAuthentication loadBalancerAuthentication = new LoadBalancerAuthentication();

private final ApplicationContext applicationContext;
private final EurekaClient discoveryClient;
private final EurekaMetadataParser eurekaMetadataParser;
private final AuthenticationSchemeFactory authenticationSchemeFactory;
private final AuthSourceService authSourceService;
private final CacheManager cacheManager;
private final CacheUtils cacheUtils;

// to force calling inside methods with aspects - ie. ehCache aspect
private ServiceAuthenticationService meAsProxy;

@PostConstruct
public void afterPropertiesSet() {
meAsProxy = applicationContext.getBean(ServiceAuthenticationService.class);
}


/**
* Marker type of Authentication, the sole purpose of it is to highlight the fact that
* authentication cannot be determined before load balancer
Expand Down Expand Up @@ -113,8 +125,9 @@ public Authentication getAuthentication(String serviceId) {
}

@Override
@RetryIfExpired
@CacheEvict(value = CACHE_BY_AUTHENTICATION, condition = "#result != null && #result.isExpired()")
@Cacheable(CACHE_BY_AUTHENTICATION)
@Cacheable(value = CACHE_BY_AUTHENTICATION, unless = "#result == null")
public AuthenticationCommand getAuthenticationCommand(Authentication authentication, AuthSource authSource) {
final IAuthenticationScheme scheme = authenticationSchemeFactory.getSchema(authentication.getScheme());
return scheme.createCommand(authentication, authSource);
Expand All @@ -127,14 +140,14 @@ public AuthenticationCommand getAuthenticationCommand(Authentication authenticat
condition = "#result != null && #result.isExpired()",
keyGenerator = CacheConfig.COMPOSITE_KEY_GENERATOR
)
@Cacheable(value = CACHE_BY_SERVICE_ID, keyGenerator = CacheConfig.COMPOSITE_KEY_GENERATOR)
@Cacheable(value = CACHE_BY_SERVICE_ID, keyGenerator = CacheConfig.COMPOSITE_KEY_GENERATOR, unless = "#result == null")
public AuthenticationCommand getAuthenticationCommand(String serviceId, Authentication found, AuthSource authSource) {
// Authentication cannot be determined before load balancer
if (found instanceof LoadBalancerAuthentication) return loadBalancerCommand;
// if no instance exist or no metadata found, do nothing
if (found == null || found.isEmpty()) return AuthenticationCommand.EMPTY;

return getAuthenticationCommand(found, authSource);
return meAsProxy.getAuthenticationCommand(found, authSource);
}

public Optional<AuthSource> getAuthSourceByAuthentication(Authentication authentication) {
Expand Down Expand Up @@ -182,7 +195,7 @@ public void apply(InstanceInfo instanceInfo) {
boolean rejected = false;
try {
final Optional<AuthSource> authSource = authSourceService.getAuthSourceFromRequest();
cmd = getAuthenticationCommand(auth, authSource.orElse(null));
cmd = meAsProxy.getAuthenticationCommand(auth, authSource.orElse(null));

// if authentication schema required valid authentication source, check it
if (cmd.isRequiredValidSource()) {
Expand Down
Loading

0 comments on commit 720e14d

Please sign in to comment.