InfinispanUtil.java
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.connections.infinispan;
import org.infinispan.Cache;
import org.infinispan.client.hotrod.ProtocolVersion;
import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.commons.api.BasicCache;
import org.infinispan.commons.dataconversion.MediaType;
import org.infinispan.commons.time.TimeService;
import org.infinispan.commons.util.FileLookup;
import org.infinispan.commons.util.FileLookupFactory;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.configuration.global.GlobalConfigurationBuilder;
import org.infinispan.configuration.global.TransportConfigurationBuilder;
import org.infinispan.eviction.EvictionStrategy;
import org.infinispan.factories.ComponentRegistry;
import org.infinispan.factories.GlobalComponentRegistry;
import org.infinispan.factories.impl.BasicComponentRegistry;
import org.infinispan.factories.impl.ComponentRef;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.persistence.manager.PersistenceManager;
import org.infinispan.persistence.remote.RemoteStore;
import org.infinispan.remoting.transport.jgroups.JGroupsTransport;
import org.infinispan.util.EmbeddedTimeService;
import org.jboss.logging.Logger;
import org.jgroups.JChannel;
import org.keycloak.common.util.Time;
import org.keycloak.models.KeycloakSession;
import java.time.Instant;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class InfinispanUtil {
protected static final Logger logger = Logger.getLogger(InfinispanUtil.class);
public static final int MAXIMUM_REPLACE_RETRIES = 25;
// See if we have RemoteStore (external JDG) configured for cross-Data-Center scenario
public static Set<RemoteStore> getRemoteStores(Cache<?, ?> ispnCache) {
return ComponentRegistry.componentOf(ispnCache, PersistenceManager.class).getStores(RemoteStore.class);
}
public static RemoteCache getRemoteCache(Cache<?, ?> ispnCache) {
Set<RemoteStore> remoteStores = getRemoteStores(ispnCache);
if (remoteStores.isEmpty()) {
return null;
} else {
return remoteStores.iterator().next().getRemoteCache();
}
}
public static TopologyInfo getTopologyInfo(KeycloakSession session) {
return session.getProvider(InfinispanConnectionProvider.class).getTopologyInfo();
}
/**
* Convert the given value to the proper value, which can be used when calling operations for the infinispan remoteCache.
*
* Infinispan HotRod protocol of versions older than 3.0 uses the "lifespan" or "maxIdle" as the normal expiration time when the value is 30 days or less.
* However for the bigger values, it assumes that the value is unix timestamp.
*
* @param ispnCache
* @param lifespanOrigMs
* @return
*/
public static long toHotrodTimeMs(BasicCache<?, ?> ispnCache, long lifespanOrigMs) {
if (ispnCache instanceof RemoteCache<?, ?> remoteCache && lifespanOrigMs > 2592000000L) {
ProtocolVersion protocolVersion = remoteCache.getRemoteCacheContainer().getConfiguration().version();
if (ProtocolVersion.PROTOCOL_VERSION_30.compareTo(protocolVersion) > 0) {
return Time.currentTimeMillis() + lifespanOrigMs;
}
}
return lifespanOrigMs;
}
private static final Object CHANNEL_INIT_SYNCHRONIZER = new Object();
public static void configureTransport(GlobalConfigurationBuilder gcb, String nodeName, String siteName, String jgroupsUdpMcastAddr,
String jgroupsBindAddr, String jgroupsConfigPath) {
if (nodeName == null) {
gcb.transport().defaultTransport();
} else {
FileLookup fileLookup = FileLookupFactory.newInstance();
synchronized (CHANNEL_INIT_SYNCHRONIZER) {
String originalMcastAddr = System.getProperty(InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR);
if (jgroupsUdpMcastAddr == null) {
System.getProperties().remove(InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR);
} else {
System.setProperty(InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR, jgroupsUdpMcastAddr);
}
var originalBindAddr = System.getProperty(InfinispanConnectionProvider.JGROUPS_BIND_ADDR);
if (jgroupsBindAddr == null) {
System.getProperties().remove(InfinispanConnectionProvider.JGROUPS_BIND_ADDR);
} else {
System.setProperty(InfinispanConnectionProvider.JGROUPS_BIND_ADDR, jgroupsBindAddr);
}
try {
JChannel channel = new JChannel(fileLookup.lookupFileLocation(jgroupsConfigPath, InfinispanUtil.class.getClassLoader()).openStream());
channel.setName(nodeName);
JGroupsTransport transport = new JGroupsTransport(channel);
TransportConfigurationBuilder transportBuilder = gcb.transport()
.nodeName(nodeName)
.siteId(siteName)
.transport(transport);
// Use the cluster corresponding to current site. This is needed as the nodes in different DCs should not share same cluster
if (siteName != null) {
transportBuilder.clusterName(siteName);
}
transportBuilder.jmx()
.domain(InfinispanConnectionProvider.JMX_DOMAIN + "-" + nodeName)
.enable();
logger.infof("Configured jgroups transport with the channel name: %s", nodeName);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
if (originalMcastAddr == null) {
System.getProperties().remove(InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR);
} else {
System.setProperty(InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR, originalMcastAddr);
}
if (originalBindAddr == null) {
System.getProperties().remove(InfinispanConnectionProvider.JGROUPS_BIND_ADDR);
} else {
System.setProperty(InfinispanConnectionProvider.JGROUPS_BIND_ADDR, originalBindAddr);
}
}
}
}
}
public static ConfigurationBuilder createCacheConfigurationBuilder() {
ConfigurationBuilder builder = new ConfigurationBuilder();
// need to force the encoding to application/x-java-object to avoid unnecessary conversion of keys/values. See WFLY-14356.
builder.encoding().mediaType(MediaType.APPLICATION_OBJECT_TYPE);
// needs to be disabled if transaction is enabled
builder.simpleCache(true);
return builder;
}
public static ConfigurationBuilder getActionTokenCacheConfig() {
ConfigurationBuilder cb = createCacheConfigurationBuilder();
cb.memory()
.whenFull(EvictionStrategy.MANUAL)
.maxCount(InfinispanConnectionProvider.ACTION_TOKEN_CACHE_DEFAULT_MAX);
cb.expiration()
.maxIdle(InfinispanConnectionProvider.ACTION_TOKEN_MAX_IDLE_SECONDS, TimeUnit.SECONDS)
.wakeUpInterval(InfinispanConnectionProvider.ACTION_TOKEN_WAKE_UP_INTERVAL_SECONDS, TimeUnit.SECONDS);
return cb;
}
/**
* Replaces the {@link TimeService} in infinispan with the one that respects Keycloak {@link Time}.
* @param cacheManager
* @return Runnable to revert replacement of the infinispan time service
*/
public static Runnable setTimeServiceToKeycloakTime(EmbeddedCacheManager cacheManager) {
TimeService previousTimeService = replaceComponent(cacheManager, TimeService.class, KEYCLOAK_TIME_SERVICE, true);
AtomicReference<TimeService> ref = new AtomicReference<>(previousTimeService);
return () -> {
if (ref.get() == null) {
logger.warn("Calling revert of the TimeService when testing TimeService was already reverted");
return;
}
logger.info("Revert set KeycloakIspnTimeService to the infinispan cacheManager");
replaceComponent(cacheManager, TimeService.class, ref.getAndSet(null), true);
};
}
/**
* Forked from org.infinispan.test.TestingUtil class
*
* Replaces a component in a running cache manager (global component registry).
*
* @param cacheMgr cache in which to replace component
* @param componentType component type of which to replace
* @param replacementComponent new instance
* @param rewire if true, ComponentRegistry.rewire() is called after replacing.
*
* @return the original component that was replaced
*/
private static <T> T replaceComponent(EmbeddedCacheManager cacheMgr, Class<T> componentType, T replacementComponent, boolean rewire) {
GlobalComponentRegistry cr = GlobalComponentRegistry.of(cacheMgr);
BasicComponentRegistry bcr = cr.getComponent(BasicComponentRegistry.class);
ComponentRef<T> old = bcr.getComponent(componentType);
bcr.replaceComponent(componentType.getName(), replacementComponent, true);
if (rewire) {
cr.rewire();
cr.rewireNamedRegistries();
}
return old != null ? old.wired() : null;
}
public static final TimeService KEYCLOAK_TIME_SERVICE = new EmbeddedTimeService() {
private long getCurrentTimeMillis() {
return Time.currentTimeMillis();
}
@Override
public long wallClockTime() {
return getCurrentTimeMillis();
}
@Override
public long time() {
return TimeUnit.MILLISECONDS.toNanos(getCurrentTimeMillis());
}
@Override
public Instant instant() {
return Instant.ofEpochMilli(getCurrentTimeMillis());
}
};
}