Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Bill Burke 2017-07-28 16:15:53 -04:00
commit 852e9274d4
753 changed files with 9331 additions and 133835 deletions

4
.gitignore vendored
View file

@ -49,3 +49,7 @@ target
# Maven shade # Maven shade
############# #############
*dependency-reduced-pom.xml *dependency-reduced-pom.xml
# nodejs #
##########
node_modules

View file

@ -1,4 +1,5 @@
language: java language: java
dist: precise
cache: cache:
cache: false cache: false

View file

@ -19,6 +19,9 @@ package org.keycloak.adapters.springsecurity.facade;
import org.keycloak.KeycloakSecurityContext; import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.OIDCHttpFacade; import org.keycloak.adapters.OIDCHttpFacade;
import org.keycloak.adapters.spi.KeycloakAccount;
import org.keycloak.adapters.springsecurity.account.SimpleKeycloakAccount;
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -57,7 +60,8 @@ public class SimpleHttpFacade implements OIDCHttpFacade {
SecurityContext context = SecurityContextHolder.getContext(); SecurityContext context = SecurityContextHolder.getContext();
if (context != null && context.getAuthentication() != null) { if (context != null && context.getAuthentication() != null) {
return (KeycloakSecurityContext) context.getAuthentication().getDetails(); KeycloakAuthenticationToken authentication = (KeycloakAuthenticationToken) context.getAuthentication();
return authentication.getAccount().getKeycloakSecurityContext();
} }
return null; return null;

View file

@ -0,0 +1,41 @@
package org.keycloak.adapters.springsecurity.facade;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
import org.keycloak.adapters.spi.KeycloakAccount;
import org.keycloak.adapters.springsecurity.account.SimpleKeycloakAccount;
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
import org.mockito.internal.util.collections.Sets;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import java.security.Principal;
import java.util.Set;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.mock;
public class SimpleHttpFacadeTest {
@Before
public void setup() {
SecurityContext springSecurityContext = SecurityContextHolder.createEmptyContext();
SecurityContextHolder.setContext(springSecurityContext);
Set<String> roles = Sets.newSet("user");
Principal principal = mock(Principal.class);
RefreshableKeycloakSecurityContext keycloakSecurityContext = mock(RefreshableKeycloakSecurityContext.class);
KeycloakAccount account = new SimpleKeycloakAccount(principal, roles, keycloakSecurityContext);
KeycloakAuthenticationToken token = new KeycloakAuthenticationToken(account);
springSecurityContext.setAuthentication(token);
}
@Test
public void shouldRetrieveKeycloakSecurityContext() {
SimpleHttpFacade facade = new SimpleHttpFacade(new MockHttpServletRequest(), new MockHttpServletResponse());
assertNotNull(facade.getSecurityContext());
}
}

View file

@ -90,6 +90,11 @@ class ElytronHttpFacade implements OIDCHttpFacade {
void authenticationComplete() { void authenticationComplete() {
if (securityIdentity != null) { if (securityIdentity != null) {
HttpScope requestScope = request.getScope(Scope.EXCHANGE);
RefreshableKeycloakSecurityContext keycloakSecurityContext = account.getKeycloakSecurityContext();
requestScope.setAttachment(KeycloakSecurityContext.class.getName(), keycloakSecurityContext);
this.request.authenticationComplete(response -> { this.request.authenticationComplete(response -> {
if (!restored) { if (!restored) {
responseConsumer.accept(response); responseConsumer.accept(response);

View file

@ -71,7 +71,7 @@ class KeycloakHttpServerAuthenticationMechanism implements HttpServerAuthenticat
AdapterDeploymentContext deploymentContext = getDeploymentContext(request); AdapterDeploymentContext deploymentContext = getDeploymentContext(request);
if (deploymentContext == null) { if (deploymentContext == null) {
LOGGER.debugf("Ignoring request for path [%s] from mechanism [%s]. No deployment context found.", request.getRequestURI()); LOGGER.debugf("Ignoring request for path [%s] from mechanism [%s]. No deployment context found.", request.getRequestURI(), getMechanismName());
request.noAuthenticationInProgress(); request.noAuthenticationInProgress();
return; return;
} }

View file

@ -81,7 +81,7 @@ public final class FormattingXMLStreamWriter implements XMLExtendedStreamWriter,
public void writeStartElement(final String localName) throws XMLStreamException { public void writeStartElement(final String localName) throws XMLStreamException {
ArrayDeque<String> namespaces = unspecifiedNamespaces; ArrayDeque<String> namespaces = unspecifiedNamespaces;
String namespace = namespaces.getFirst(); String namespace = namespaces.getFirst();
if (namespace != NO_NAMESPACE) { if (namespace == null ? NO_NAMESPACE != null : ! namespace.equals(NO_NAMESPACE)) {
writeStartElement(namespace, localName); writeStartElement(namespace, localName);
return; return;
} }
@ -140,9 +140,9 @@ public final class FormattingXMLStreamWriter implements XMLExtendedStreamWriter,
attrQueue.add(new ArgRunnable() { attrQueue.add(new ArgRunnable() {
public void run(int arg) throws XMLStreamException { public void run(int arg) throws XMLStreamException {
if (arg == 0) { if (arg == 0) {
delegate.writeStartElement(prefix, namespaceURI, localName); delegate.writeStartElement(prefix, localName, namespaceURI);
} else { } else {
delegate.writeEmptyElement(prefix, namespaceURI, localName); delegate.writeEmptyElement(prefix, localName, namespaceURI);
} }
} }
}); });
@ -165,14 +165,14 @@ public final class FormattingXMLStreamWriter implements XMLExtendedStreamWriter,
runAttrQueue(); runAttrQueue();
nl(); nl();
indent(); indent();
delegate.writeEmptyElement(prefix, namespaceURI, localName); delegate.writeEmptyElement(prefix, localName, namespaceURI);
state = END_ELEMENT; state = END_ELEMENT;
} }
@Override @Override
public void writeEmptyElement(final String localName) throws XMLStreamException { public void writeEmptyElement(final String localName) throws XMLStreamException {
String namespace = unspecifiedNamespaces.getFirst(); String namespace = unspecifiedNamespaces.getFirst();
if (namespace != NO_NAMESPACE) { if (namespace == null ? NO_NAMESPACE != null : ! namespace.equals(NO_NAMESPACE)) {
writeEmptyElement(namespace, localName); writeEmptyElement(namespace, localName);
return; return;
} }

View file

@ -18,7 +18,10 @@
package org.keycloak.adapters.saml; package org.keycloak.adapters.saml;
import org.keycloak.adapters.spi.AuthenticationError; import org.keycloak.adapters.spi.AuthenticationError;
import org.keycloak.dom.saml.v2.protocol.StatusCodeType;
import org.keycloak.dom.saml.v2.protocol.StatusResponseType; import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import java.util.Objects;
/** /**
* Object that describes the SAML error that happened. * Object that describes the SAML error that happened.
@ -27,6 +30,7 @@ import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class SamlAuthenticationError implements AuthenticationError { public class SamlAuthenticationError implements AuthenticationError {
public static enum Reason { public static enum Reason {
EXTRACTION_FAILURE, EXTRACTION_FAILURE,
INVALID_SIGNATURE, INVALID_SIGNATURE,
@ -59,7 +63,18 @@ public class SamlAuthenticationError implements AuthenticationError {
@Override @Override
public String toString() { public String toString() {
return "SamlAuthenticationError [reason=" + reason + ", status=" + status + "]"; return "SamlAuthenticationError [reason=" + reason + ", status="
+ ((status == null || status.getStatus() == null) ? "UNKNOWN" : extractStatusCode(status.getStatus().getStatusCode()))
+ "]";
} }
private String extractStatusCode(StatusCodeType statusCode) {
if (statusCode == null || statusCode.getValue() == null) {
return "UNKNOWN";
}
if (Objects.equals(JBossSAMLURIConstants.STATUS_RESPONDER.get(), statusCode.getValue().toString())) {
return extractStatusCode(statusCode.getStatusCode());
}
return statusCode.getValue().toString();
}
} }

View file

@ -407,8 +407,8 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
SubjectType subject = assertion.getSubject(); SubjectType subject = assertion.getSubject();
SubjectType.STSubType subType = subject.getSubType(); SubjectType.STSubType subType = subject.getSubType();
NameIDType subjectNameID = (NameIDType) subType.getBaseID(); NameIDType subjectNameID = subType == null ? null : (NameIDType) subType.getBaseID();
String principalName = subjectNameID.getValue(); String principalName = subjectNameID == null ? null : subjectNameID.getValue();
final Set<String> roles = new HashSet<>(); final Set<String> roles = new HashSet<>();
MultivaluedHashMap<String, String> attributes = new MultivaluedHashMap<>(); MultivaluedHashMap<String, String> attributes = new MultivaluedHashMap<>();
@ -473,7 +473,7 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
} }
URI nameFormat = subjectNameID.getFormat(); URI nameFormat = subjectNameID == null ? null : subjectNameID.getFormat();
String nameFormatString = nameFormat == null ? JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get() : nameFormat.toString(); String nameFormatString = nameFormat == null ? JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get() : nameFormat.toString();
final SamlPrincipal principal = new SamlPrincipal(assertion, principalName, principalName, nameFormatString, attributes, friendlyAttributes); final SamlPrincipal principal = new SamlPrincipal(assertion, principalName, principalName, nameFormatString, attributes, friendlyAttributes);
String index = authn == null ? null : authn.getSessionIndex(); String index = authn == null ? null : authn.getSessionIndex();

View file

@ -41,10 +41,20 @@ import java.util.List;
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class SamlAuthenticatorValve extends AbstractSamlAuthenticatorValve { public class SamlAuthenticatorValve extends AbstractSamlAuthenticatorValve {
/**
* Method called by Tomcat &lt; 8.5.5
*/
public boolean authenticate(Request request, HttpServletResponse response) throws IOException { public boolean authenticate(Request request, HttpServletResponse response) throws IOException {
return authenticateInternal(request, response, request.getContext().getLoginConfig()); return authenticateInternal(request, response, request.getContext().getLoginConfig());
} }
/**
* Method called by Tomcat &gt;= 8.5.5
*/
protected boolean doAuthenticate(Request request, HttpServletResponse response) throws IOException {
return this.authenticate(request, response);
}
@Override @Override
protected boolean forwardToErrorPageInternal(Request request, HttpServletResponse response, Object loginConfig) throws IOException { protected boolean forwardToErrorPageInternal(Request request, HttpServletResponse response, Object loginConfig) throws IOException {
if (loginConfig == null) return false; if (loginConfig == null) return false;

View file

@ -47,10 +47,8 @@ import org.wildfly.security.auth.callback.AnonymousAuthorizationCallback;
import org.wildfly.security.auth.callback.AuthenticationCompleteCallback; import org.wildfly.security.auth.callback.AuthenticationCompleteCallback;
import org.wildfly.security.auth.callback.SecurityIdentityCallback; import org.wildfly.security.auth.callback.SecurityIdentityCallback;
import org.wildfly.security.auth.server.SecurityIdentity; import org.wildfly.security.auth.server.SecurityIdentity;
import org.wildfly.security.http.HttpAuthenticationException;
import org.wildfly.security.http.HttpScope; import org.wildfly.security.http.HttpScope;
import org.wildfly.security.http.HttpServerCookie; import org.wildfly.security.http.HttpServerCookie;
import org.wildfly.security.http.HttpServerMechanismsResponder;
import org.wildfly.security.http.HttpServerRequest; import org.wildfly.security.http.HttpServerRequest;
import org.wildfly.security.http.HttpServerResponse; import org.wildfly.security.http.HttpServerResponse;
import org.wildfly.security.http.Scope; import org.wildfly.security.http.Scope;
@ -87,11 +85,14 @@ class ElytronHttpFacade implements HttpFacade {
void authenticationComplete() { void authenticationComplete() {
this.securityIdentity = SecurityIdentityUtil.authorize(this.callbackHandler, samlSession.getPrincipal()); this.securityIdentity = SecurityIdentityUtil.authorize(this.callbackHandler, samlSession.getPrincipal());
this.request.authenticationComplete(response -> {
if (!restored) { if (this.securityIdentity != null) {
responseConsumer.accept(response); this.request.authenticationComplete(response -> {
} if (!restored) {
}, () -> ((ElytronTokeStore) sessionStore).logout(true)); responseConsumer.accept(response);
}
}, () -> ((ElytronTokeStore) sessionStore).logout(true));
}
} }
void authenticationCompleteAnonymous() { void authenticationCompleteAnonymous() {

View file

@ -65,7 +65,7 @@ class KeycloakHttpServerAuthenticationMechanism implements HttpServerAuthenticat
SamlDeploymentContext deploymentContext = getDeploymentContext(request); SamlDeploymentContext deploymentContext = getDeploymentContext(request);
if (deploymentContext == null) { if (deploymentContext == null) {
LOGGER.debugf("Ignoring request for path [%s] from mechanism [%s]. No deployment context found.", request.getRequestURI()); LOGGER.debugf("Ignoring request for path [%s] from mechanism [%s]. No deployment context found.", request.getRequestURI(), getMechanismName());
request.noAuthenticationInProgress(); request.noAuthenticationInProgress();
return; return;
} }

View file

@ -28,6 +28,7 @@
</resources> </resources>
<dependencies> <dependencies>
<module name="javax.api"/> <module name="javax.api"/>
<module name="javax.xml.soap.api"/>
<module name="org.jboss.logging"/> <module name="org.jboss.logging"/>
<module name="org.keycloak.keycloak-adapter-spi"/> <module name="org.keycloak.keycloak-adapter-spi"/>
<module name="org.keycloak.keycloak-saml-core-public"/> <module name="org.keycloak.keycloak-saml-core-public"/>

View file

@ -28,6 +28,7 @@
</resources> </resources>
<dependencies> <dependencies>
<module name="javax.api"/> <module name="javax.api"/>
<module name="javax.xml.soap.api"/>
<module name="org.jboss.logging"/> <module name="org.jboss.logging"/>
<module name="org.keycloak.keycloak-adapter-spi"/> <module name="org.keycloak.keycloak-adapter-spi"/>
<module name="org.keycloak.keycloak-saml-adapter-api-public"/> <module name="org.keycloak.keycloak-saml-adapter-api-public"/>

View file

@ -18,6 +18,7 @@
package org.keycloak.storage.ldap.mappers; package org.keycloak.storage.ldap.mappers;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.models.AbstractKeycloakTransaction;
import org.keycloak.models.KeycloakTransaction; import org.keycloak.models.KeycloakTransaction;
import org.keycloak.storage.ldap.LDAPStorageProvider; import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.idm.model.LDAPObject; import org.keycloak.storage.ldap.idm.model.LDAPObject;
@ -25,12 +26,10 @@ import org.keycloak.storage.ldap.idm.model.LDAPObject;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
public class LDAPTransaction implements KeycloakTransaction { public class LDAPTransaction extends AbstractKeycloakTransaction {
public static final Logger logger = Logger.getLogger(LDAPTransaction.class); public static final Logger logger = Logger.getLogger(LDAPTransaction.class);
protected TransactionState state = TransactionState.NOT_STARTED;
private final LDAPStorageProvider ldapProvider; private final LDAPStorageProvider ldapProvider;
private final LDAPObject ldapUser; private final LDAPObject ldapUser;
@ -39,57 +38,21 @@ public class LDAPTransaction implements KeycloakTransaction {
this.ldapUser = ldapUser; this.ldapUser = ldapUser;
} }
@Override
public void begin() {
if (state != TransactionState.NOT_STARTED) {
throw new IllegalStateException("Transaction already started");
}
state = TransactionState.STARTED;
}
@Override @Override
public void commit() { protected void commitImpl() {
if (state != TransactionState.STARTED) {
throw new IllegalStateException("Transaction in illegal state for commit: " + state);
}
if (logger.isTraceEnabled()) { if (logger.isTraceEnabled()) {
logger.trace("Transaction commit! Updating LDAP attributes for object " + ldapUser.getDn().toString() + ", attributes: " + ldapUser.getAttributes()); logger.trace("Transaction commit! Updating LDAP attributes for object " + ldapUser.getDn().toString() + ", attributes: " + ldapUser.getAttributes());
} }
ldapProvider.getLdapIdentityStore().update(ldapUser); ldapProvider.getLdapIdentityStore().update(ldapUser);
state = TransactionState.FINISHED;
} }
@Override
public void rollback() {
if (state != TransactionState.STARTED && state != TransactionState.ROLLBACK_ONLY) {
throw new IllegalStateException("Transaction in illegal state for rollback: " + state);
}
@Override
protected void rollbackImpl() {
logger.warn("Transaction rollback! Ignoring LDAP updates for object " + ldapUser.getDn().toString()); logger.warn("Transaction rollback! Ignoring LDAP updates for object " + ldapUser.getDn().toString());
state = TransactionState.FINISHED;
} }
@Override
public void setRollbackOnly() {
state = TransactionState.ROLLBACK_ONLY;
}
@Override
public boolean getRollbackOnly() {
return state == TransactionState.ROLLBACK_ONLY;
}
@Override
public boolean isActive() {
return state == TransactionState.STARTED || state == TransactionState.ROLLBACK_ONLY;
}
protected enum TransactionState {
NOT_STARTED, STARTED, ROLLBACK_ONLY, FINISHED
}
} }

View file

@ -41,7 +41,7 @@ public abstract class TxAwareLDAPUserModelDelegate extends UserModelDelegate {
protected void ensureTransactionStarted() { protected void ensureTransactionStarted() {
LDAPTransaction transaction = provider.getUserManager().getTransaction(getId()); LDAPTransaction transaction = provider.getUserManager().getTransaction(getId());
if (transaction.state == LDAPTransaction.TransactionState.NOT_STARTED) { if (transaction.getState() == LDAPTransaction.TransactionState.NOT_STARTED) {
if (logger.isTraceEnabled()) { if (logger.isTraceEnabled()) {
logger.trace("Starting and enlisting transaction for object " + ldapUser.getDn().toString()); logger.trace("Starting and enlisting transaction for object " + ldapUser.getDn().toString());
} }

View file

@ -29,6 +29,15 @@ When starting the server it can also import a realm from a json file:
mvn exec:java -Pkeycloak-server -Dimport=testrealm.json mvn exec:java -Pkeycloak-server -Dimport=testrealm.json
When starting the server, https transport can be set up by setting keystore containing the server certificate
and https port, optionally setting the truststore.
mvn exec:java -Pkeycloak-server \
-Djavax.net.ssl.trustStore=/path/to/truststore.jks \
-Djavax.net.ssl.keyStore=/path/to/keystore.jks \
-Djavax.net.ssl.keyStorePassword=CHANGEME \
-Dkeycloak.port.https=8443
### Live edit of html and styles ### Live edit of html and styles
The Keycloak test server can load resources directly from the filesystem instead of the classpath. This allows editing html, styles and updating images without restarting the server. To make the server use resources from the filesystem start with: The Keycloak test server can load resources directly from the filesystem instead of the classpath. This allows editing html, styles and updating images without restarting the server. To make the server use resources from the filesystem start with:

View file

@ -52,6 +52,12 @@ abstract class CrossDCAwareCacheFactory {
// For cross-DC scenario, we need to return underlying remoteCache for atomic operations to work properly // For cross-DC scenario, we need to return underlying remoteCache for atomic operations to work properly
RemoteStore remoteStore = remoteStores.iterator().next(); RemoteStore remoteStore = remoteStores.iterator().next();
RemoteCache remoteCache = remoteStore.getRemoteCache(); RemoteCache remoteCache = remoteStore.getRemoteCache();
if (remoteCache == null) {
String cacheName = remoteStore.getConfiguration().remoteCacheName();
throw new IllegalStateException("Remote cache '" + cacheName + "' is not available.");
}
return new RemoteCacheWrapperFactory(remoteCache); return new RemoteCacheWrapperFactory(remoteCache);
} }
} }

View file

@ -25,6 +25,11 @@ import org.keycloak.cluster.ExecutionResult;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
@ -43,11 +48,14 @@ public class InfinispanClusterProvider implements ClusterProvider {
private final CrossDCAwareCacheFactory crossDCAwareCacheFactory; private final CrossDCAwareCacheFactory crossDCAwareCacheFactory;
private final InfinispanNotificationsManager notificationsManager; // Just to extract notifications related stuff to separate class private final InfinispanNotificationsManager notificationsManager; // Just to extract notifications related stuff to separate class
public InfinispanClusterProvider(int clusterStartupTime, String myAddress, CrossDCAwareCacheFactory crossDCAwareCacheFactory, InfinispanNotificationsManager notificationsManager) { private final ExecutorService localExecutor;
public InfinispanClusterProvider(int clusterStartupTime, String myAddress, CrossDCAwareCacheFactory crossDCAwareCacheFactory, InfinispanNotificationsManager notificationsManager, ExecutorService localExecutor) {
this.myAddress = myAddress; this.myAddress = myAddress;
this.clusterStartupTime = clusterStartupTime; this.clusterStartupTime = clusterStartupTime;
this.crossDCAwareCacheFactory = crossDCAwareCacheFactory; this.crossDCAwareCacheFactory = crossDCAwareCacheFactory;
this.notificationsManager = notificationsManager; this.notificationsManager = notificationsManager;
this.localExecutor = localExecutor;
} }
@ -85,6 +93,34 @@ public class InfinispanClusterProvider implements ClusterProvider {
} }
@Override
public Future<Boolean> executeIfNotExecutedAsync(String taskKey, int taskTimeoutInSeconds, Callable task) {
TaskCallback newCallback = new TaskCallback();
TaskCallback callback = this.notificationsManager.registerTaskCallback(TASK_KEY_PREFIX + taskKey, newCallback);
// We successfully submitted our task
if (newCallback == callback) {
Callable<Boolean> wrappedTask = () -> {
boolean executed = executeIfNotExecuted(taskKey, taskTimeoutInSeconds, task).isExecuted();
if (!executed) {
logger.infof("Task already in progress on other cluster node. Will wait until it's finished");
}
callback.getTaskCompletedLatch().await(taskTimeoutInSeconds, TimeUnit.SECONDS);
return callback.isSuccess();
};
Future<Boolean> future = localExecutor.submit(wrappedTask);
callback.setFuture(future);
} else {
logger.infof("Task already in progress on this cluster node. Will wait until it's finished");
}
return callback.getFuture();
}
@Override @Override
public void registerListener(String taskKey, ClusterListener task) { public void registerListener(String taskKey, ClusterListener task) {
this.notificationsManager.registerListener(taskKey, task); this.notificationsManager.registerListener(taskKey, task);
@ -92,11 +128,10 @@ public class InfinispanClusterProvider implements ClusterProvider {
@Override @Override
public void notify(String taskKey, ClusterEvent event, boolean ignoreSender) { public void notify(String taskKey, ClusterEvent event, boolean ignoreSender, DCNotify dcNotify) {
this.notificationsManager.notify(taskKey, event, ignoreSender); this.notificationsManager.notify(taskKey, event, ignoreSender, dcNotify);
} }
private LockEntry createLockEntry() { private LockEntry createLockEntry() {
LockEntry lock = new LockEntry(); LockEntry lock = new LockEntry();
lock.setNode(myAddress); lock.setNode(myAddress);

View file

@ -35,12 +35,15 @@ import org.keycloak.common.util.Time;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
import java.io.Serializable; import java.io.Serializable;
import java.util.Collection; import java.util.Collection;
import java.util.Iterator; import java.util.Iterator;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -62,17 +65,18 @@ public class InfinispanClusterProviderFactory implements ClusterProviderFactory
// Ensure that atomic operations (like putIfAbsent) must work correctly in any of: non-clustered, clustered or cross-Data-Center (cross-DC) setups // Ensure that atomic operations (like putIfAbsent) must work correctly in any of: non-clustered, clustered or cross-Data-Center (cross-DC) setups
private CrossDCAwareCacheFactory crossDCAwareCacheFactory; private CrossDCAwareCacheFactory crossDCAwareCacheFactory;
private String myAddress;
private int clusterStartupTime; private int clusterStartupTime;
// Just to extract notifications related stuff to separate class // Just to extract notifications related stuff to separate class
private InfinispanNotificationsManager notificationsManager; private InfinispanNotificationsManager notificationsManager;
private ExecutorService localExecutor = Executors.newCachedThreadPool();
@Override @Override
public ClusterProvider create(KeycloakSession session) { public ClusterProvider create(KeycloakSession session) {
lazyInit(session); lazyInit(session);
return new InfinispanClusterProvider(clusterStartupTime, myAddress, crossDCAwareCacheFactory, notificationsManager); String myAddress = InfinispanUtil.getMyAddress(session);
return new InfinispanClusterProvider(clusterStartupTime, myAddress, crossDCAwareCacheFactory, notificationsManager, localExecutor);
} }
private void lazyInit(KeycloakSession session) { private void lazyInit(KeycloakSession session) {
@ -83,33 +87,23 @@ public class InfinispanClusterProviderFactory implements ClusterProviderFactory
workCache = ispnConnections.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME); workCache = ispnConnections.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME);
workCache.getCacheManager().addListener(new ViewChangeListener()); workCache.getCacheManager().addListener(new ViewChangeListener());
initMyAddress();
Set<RemoteStore> remoteStores = getRemoteStores(); // See if we have RemoteStore (external JDG) configured for cross-Data-Center scenario
Set<RemoteStore> remoteStores = InfinispanUtil.getRemoteStores(workCache);
crossDCAwareCacheFactory = CrossDCAwareCacheFactory.getFactory(workCache, remoteStores); crossDCAwareCacheFactory = CrossDCAwareCacheFactory.getFactory(workCache, remoteStores);
clusterStartupTime = initClusterStartupTime(session); clusterStartupTime = initClusterStartupTime(session);
notificationsManager = InfinispanNotificationsManager.create(workCache, myAddress, remoteStores); String myAddress = InfinispanUtil.getMyAddress(session);
String mySite = InfinispanUtil.getMySite(session);
notificationsManager = InfinispanNotificationsManager.create(workCache, myAddress, mySite, remoteStores);
} }
} }
} }
} }
// See if we have RemoteStore (external JDG) configured for cross-Data-Center scenario
private Set<RemoteStore> getRemoteStores() {
return workCache.getAdvancedCache().getComponentRegistry().getComponent(PersistenceManager.class).getStores(RemoteStore.class);
}
protected void initMyAddress() {
Transport transport = workCache.getCacheManager().getTransport();
this.myAddress = transport == null ? HostUtils.getHostName() + "-" + workCache.hashCode() : transport.getAddress().toString();
logger.debugf("My address: %s", this.myAddress);
}
protected int initClusterStartupTime(KeycloakSession session) { protected int initClusterStartupTime(KeycloakSession session) {
Integer existingClusterStartTime = (Integer) crossDCAwareCacheFactory.getCache().get(InfinispanClusterProvider.CLUSTER_STARTUP_TIME_KEY); Integer existingClusterStartTime = (Integer) crossDCAwareCacheFactory.getCache().get(InfinispanClusterProvider.CLUSTER_STARTUP_TIME_KEY);
if (existingClusterStartTime != null) { if (existingClusterStartTime != null) {
@ -201,6 +195,10 @@ public class InfinispanClusterProviderFactory implements ClusterProviderFactory
if (logger.isTraceEnabled()) { if (logger.isTraceEnabled()) {
logger.tracef("Removing task %s due it's node left cluster", rem); logger.tracef("Removing task %s due it's node left cluster", rem);
} }
// If we have task in progress, it needs to be notified
notificationsManager.taskFinished(rem, false);
cache.remove(rem); cache.remove(rem);
} }
} }

View file

@ -20,31 +20,38 @@ package org.keycloak.cluster.infinispan;
import java.io.Serializable; import java.io.Serializable;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.infinispan.Cache; import org.infinispan.Cache;
import org.infinispan.client.hotrod.RemoteCache; import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.client.hotrod.annotation.ClientCacheEntryCreated; import org.infinispan.client.hotrod.annotation.ClientCacheEntryCreated;
import org.infinispan.client.hotrod.annotation.ClientCacheEntryExpired;
import org.infinispan.client.hotrod.annotation.ClientCacheEntryModified; import org.infinispan.client.hotrod.annotation.ClientCacheEntryModified;
import org.infinispan.client.hotrod.annotation.ClientCacheEntryRemoved;
import org.infinispan.client.hotrod.annotation.ClientListener; import org.infinispan.client.hotrod.annotation.ClientListener;
import org.infinispan.client.hotrod.event.ClientCacheEntryCreatedEvent; import org.infinispan.client.hotrod.event.ClientCacheEntryCreatedEvent;
import org.infinispan.client.hotrod.event.ClientCacheEntryExpiredEvent;
import org.infinispan.client.hotrod.event.ClientCacheEntryModifiedEvent; import org.infinispan.client.hotrod.event.ClientCacheEntryModifiedEvent;
import org.infinispan.client.hotrod.event.ClientEvent; import org.infinispan.client.hotrod.event.ClientCacheEntryRemovedEvent;
import org.infinispan.context.Flag; import org.infinispan.context.Flag;
import org.infinispan.marshall.core.MarshalledEntry;
import org.infinispan.notifications.Listener; import org.infinispan.notifications.Listener;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryCreated; import org.infinispan.notifications.cachelistener.annotation.CacheEntryCreated;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryExpired;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryModified; import org.infinispan.notifications.cachelistener.annotation.CacheEntryModified;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryRemoved;
import org.infinispan.notifications.cachelistener.event.CacheEntryCreatedEvent; import org.infinispan.notifications.cachelistener.event.CacheEntryCreatedEvent;
import org.infinispan.notifications.cachelistener.event.CacheEntryExpiredEvent;
import org.infinispan.notifications.cachelistener.event.CacheEntryModifiedEvent; import org.infinispan.notifications.cachelistener.event.CacheEntryModifiedEvent;
import org.infinispan.persistence.manager.PersistenceManager; import org.infinispan.notifications.cachelistener.event.CacheEntryRemovedEvent;
import org.infinispan.persistence.remote.RemoteStore; import org.infinispan.persistence.remote.RemoteStore;
import org.infinispan.remoting.transport.Transport;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.cluster.ClusterEvent; import org.keycloak.cluster.ClusterEvent;
import org.keycloak.cluster.ClusterListener; import org.keycloak.cluster.ClusterListener;
import org.keycloak.cluster.ClusterProvider; import org.keycloak.cluster.ClusterProvider;
import org.keycloak.common.util.HostUtils;
import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.common.util.MultivaluedHashMap;
/** /**
@ -58,20 +65,25 @@ public class InfinispanNotificationsManager {
private final MultivaluedHashMap<String, ClusterListener> listeners = new MultivaluedHashMap<>(); private final MultivaluedHashMap<String, ClusterListener> listeners = new MultivaluedHashMap<>();
private final ConcurrentMap<String, TaskCallback> taskCallbacks = new ConcurrentHashMap<>();
private final Cache<String, Serializable> workCache; private final Cache<String, Serializable> workCache;
private final String myAddress; private final String myAddress;
private final String mySite;
protected InfinispanNotificationsManager(Cache<String, Serializable> workCache, String myAddress) {
protected InfinispanNotificationsManager(Cache<String, Serializable> workCache, String myAddress, String mySite) {
this.workCache = workCache; this.workCache = workCache;
this.myAddress = myAddress; this.myAddress = myAddress;
this.mySite = mySite;
} }
// Create and init manager including all listeners etc // Create and init manager including all listeners etc
public static InfinispanNotificationsManager create(Cache<String, Serializable> workCache, String myAddress, Set<RemoteStore> remoteStores) { public static InfinispanNotificationsManager create(Cache<String, Serializable> workCache, String myAddress, String mySite, Set<RemoteStore> remoteStores) {
InfinispanNotificationsManager manager = new InfinispanNotificationsManager(workCache, myAddress); InfinispanNotificationsManager manager = new InfinispanNotificationsManager(workCache, myAddress, mySite);
// We need CacheEntryListener just if we don't have remoteStore. With remoteStore will be all cluster nodes notified anyway from HotRod listener // We need CacheEntryListener just if we don't have remoteStore. With remoteStore will be all cluster nodes notified anyway from HotRod listener
if (remoteStores.isEmpty()) { if (remoteStores.isEmpty()) {
@ -85,6 +97,10 @@ public class InfinispanNotificationsManager {
logger.debugf("Added listener for HotRod remoteStore cache: %s", remoteCache.getName()); logger.debugf("Added listener for HotRod remoteStore cache: %s", remoteCache.getName());
} }
if (mySite == null) {
throw new IllegalStateException("Multiple datacenters available, but site name is not configured! Check your configuration");
}
} }
return manager; return manager;
@ -96,19 +112,37 @@ public class InfinispanNotificationsManager {
} }
void notify(String taskKey, ClusterEvent event, boolean ignoreSender) { TaskCallback registerTaskCallback(String taskKey, TaskCallback callback) {
TaskCallback existing = taskCallbacks.putIfAbsent(taskKey, callback);
if (existing != null) {
return existing;
} else {
return callback;
}
}
void notify(String taskKey, ClusterEvent event, boolean ignoreSender, ClusterProvider.DCNotify dcNotify) {
WrapperClusterEvent wrappedEvent = new WrapperClusterEvent(); WrapperClusterEvent wrappedEvent = new WrapperClusterEvent();
wrappedEvent.setEventKey(taskKey);
wrappedEvent.setDelegateEvent(event); wrappedEvent.setDelegateEvent(event);
wrappedEvent.setIgnoreSender(ignoreSender); wrappedEvent.setIgnoreSender(ignoreSender);
wrappedEvent.setIgnoreSenderSite(dcNotify == ClusterProvider.DCNotify.ALL_BUT_LOCAL_DC);
wrappedEvent.setSender(myAddress); wrappedEvent.setSender(myAddress);
wrappedEvent.setSenderSite(mySite);
if (logger.isTraceEnabled()) { if (logger.isTraceEnabled()) {
logger.tracef("Sending event %s: %s", taskKey, event); logger.tracef("Sending event: %s", event);
} }
Flag[] flags = dcNotify == ClusterProvider.DCNotify.LOCAL_DC_ONLY
? new Flag[] { Flag.IGNORE_RETURN_VALUES, Flag.SKIP_CACHE_STORE }
: new Flag[] { Flag.IGNORE_RETURN_VALUES };
// Put the value to the cache to notify listeners on all the nodes // Put the value to the cache to notify listeners on all the nodes
workCache.getAdvancedCache().withFlags(Flag.IGNORE_RETURN_VALUES) workCache.getAdvancedCache().withFlags(flags)
.put(taskKey, wrappedEvent, 120, TimeUnit.SECONDS); .put(UUID.randomUUID().toString(), wrappedEvent, 120, TimeUnit.SECONDS);
} }
@ -124,6 +158,12 @@ public class InfinispanNotificationsManager {
public void cacheEntryModified(CacheEntryModifiedEvent<String, Serializable> event) { public void cacheEntryModified(CacheEntryModifiedEvent<String, Serializable> event) {
eventReceived(event.getKey(), event.getValue()); eventReceived(event.getKey(), event.getValue());
} }
@CacheEntryRemoved
public void cacheEntryRemoved(CacheEntryRemovedEvent<String, Serializable> event) {
taskFinished(event.getKey(), true);
}
} }
@ -150,6 +190,14 @@ public class InfinispanNotificationsManager {
hotrodEventReceived(key); hotrodEventReceived(key);
} }
@ClientCacheEntryRemoved
public void removed(ClientCacheEntryRemovedEvent event) {
String key = event.getKey().toString();
taskFinished(key, true);
}
private void hotrodEventReceived(String key) { private void hotrodEventReceived(String key) {
// TODO: Look at CacheEventConverter stuff to possibly include value in the event and avoid additional remoteCache request // TODO: Look at CacheEventConverter stuff to possibly include value in the event and avoid additional remoteCache request
Object value = workCache.get(key); Object value = workCache.get(key);
@ -171,24 +219,39 @@ public class InfinispanNotificationsManager {
} }
} }
if (event.isIgnoreSenderSite()) {
if (this.mySite != null && this.mySite.equals(event.getSender())) {
return;
}
}
String eventKey = event.getEventKey();
if (logger.isTraceEnabled()) { if (logger.isTraceEnabled()) {
logger.tracef("Received event %s: %s", key, event); logger.tracef("Received event: %s", event);
} }
ClusterEvent wrappedEvent = event.getDelegateEvent(); ClusterEvent wrappedEvent = event.getDelegateEvent();
List<ClusterListener> myListeners = listeners.get(key); List<ClusterListener> myListeners = listeners.get(eventKey);
if (myListeners != null) {
for (ClusterListener listener : myListeners) {
listener.eventReceived(wrappedEvent);
}
}
myListeners = listeners.get(ClusterProvider.ALL);
if (myListeners != null) { if (myListeners != null) {
for (ClusterListener listener : myListeners) { for (ClusterListener listener : myListeners) {
listener.eventReceived(wrappedEvent); listener.eventReceived(wrappedEvent);
} }
} }
} }
void taskFinished(String taskKey, boolean success) {
TaskCallback callback = taskCallbacks.remove(taskKey);
if (callback != null) {
if (logger.isDebugEnabled()) {
logger.debugf("Finished task '%s' with '%b'", taskKey, success);
}
callback.setSuccess(success);
callback.getTaskCompletedLatch().countDown();
}
}
} }

View file

@ -0,0 +1,72 @@
/*
* 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.cluster.infinispan;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.jboss.logging.Logger;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
class TaskCallback {
protected static final Logger logger = Logger.getLogger(TaskCallback.class);
static final int LATCH_TIMEOUT_MS = 10000;
private volatile boolean success;
private volatile Future<Boolean> future;
private final CountDownLatch taskCompletedLatch = new CountDownLatch(1);
private final CountDownLatch futureAvailableLatch = new CountDownLatch(1);
public void setSuccess(boolean success) {
this.success = success;
}
public boolean isSuccess() {
return success;
}
public void setFuture(Future<Boolean> future) {
this.future = future;
this.futureAvailableLatch.countDown();
}
public Future<Boolean> getFuture() {
try {
this.futureAvailableLatch.await(LATCH_TIMEOUT_MS, TimeUnit.MILLISECONDS);
} catch (InterruptedException ie) {
logger.error("Interrupted thread!");
Thread.currentThread().interrupt();
}
return future;
}
public CountDownLatch getTaskCompletedLatch() {
return taskCompletedLatch;
}
}

View file

@ -24,10 +24,21 @@ import org.keycloak.cluster.ClusterEvent;
*/ */
public class WrapperClusterEvent implements ClusterEvent { public class WrapperClusterEvent implements ClusterEvent {
private String sender; // will be null in non-clustered environment private String eventKey;
private String sender;
private String senderSite;
private boolean ignoreSender; private boolean ignoreSender;
private boolean ignoreSenderSite;
private ClusterEvent delegateEvent; private ClusterEvent delegateEvent;
public String getEventKey() {
return eventKey;
}
public void setEventKey(String eventKey) {
this.eventKey = eventKey;
}
public String getSender() { public String getSender() {
return sender; return sender;
} }
@ -36,6 +47,14 @@ public class WrapperClusterEvent implements ClusterEvent {
this.sender = sender; this.sender = sender;
} }
public String getSenderSite() {
return senderSite;
}
public void setSenderSite(String senderSite) {
this.senderSite = senderSite;
}
public boolean isIgnoreSender() { public boolean isIgnoreSender() {
return ignoreSender; return ignoreSender;
} }
@ -44,6 +63,14 @@ public class WrapperClusterEvent implements ClusterEvent {
this.ignoreSender = ignoreSender; this.ignoreSender = ignoreSender;
} }
public boolean isIgnoreSenderSite() {
return ignoreSenderSite;
}
public void setIgnoreSenderSite(boolean ignoreSenderSite) {
this.ignoreSenderSite = ignoreSenderSite;
}
public ClusterEvent getDelegateEvent() { public ClusterEvent getDelegateEvent() {
return delegateEvent; return delegateEvent;
} }
@ -54,6 +81,6 @@ public class WrapperClusterEvent implements ClusterEvent {
@Override @Override
public String toString() { public String toString() {
return String.format("WrapperClusterEvent [ sender=%s, delegateEvent=%s ]", sender, delegateEvent.toString()); return String.format("WrapperClusterEvent [ eventKey=%s, sender=%s, senderSite=%s, delegateEvent=%s ]", eventKey, sender, senderSite, delegateEvent.toString());
} }
} }

View file

@ -26,9 +26,13 @@ import org.infinispan.manager.EmbeddedCacheManager;
public class DefaultInfinispanConnectionProvider implements InfinispanConnectionProvider { public class DefaultInfinispanConnectionProvider implements InfinispanConnectionProvider {
private EmbeddedCacheManager cacheManager; private EmbeddedCacheManager cacheManager;
private final String siteName;
private final String nodeName;
public DefaultInfinispanConnectionProvider(EmbeddedCacheManager cacheManager) { public DefaultInfinispanConnectionProvider(EmbeddedCacheManager cacheManager, String nodeName, String siteName) {
this.cacheManager = cacheManager; this.cacheManager = cacheManager;
this.nodeName = nodeName;
this.siteName = siteName;
} }
@Override @Override
@ -36,6 +40,16 @@ public class DefaultInfinispanConnectionProvider implements InfinispanConnection
return cacheManager.getCache(name); return cacheManager.getCache(name);
} }
@Override
public String getNodeName() {
return nodeName;
}
@Override
public String getSiteName() {
return siteName;
}
@Override @Override
public void close() { public void close() {
} }

View file

@ -17,6 +17,7 @@
package org.keycloak.connections.infinispan; package org.keycloak.connections.infinispan;
import java.security.SecureRandom;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.infinispan.commons.util.FileLookup; import org.infinispan.commons.util.FileLookup;
@ -30,6 +31,7 @@ import org.infinispan.eviction.EvictionType;
import org.infinispan.manager.DefaultCacheManager; import org.infinispan.manager.DefaultCacheManager;
import org.infinispan.manager.EmbeddedCacheManager; import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder; import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder;
import org.infinispan.remoting.transport.Transport;
import org.infinispan.remoting.transport.jgroups.JGroupsTransport; import org.infinispan.remoting.transport.jgroups.JGroupsTransport;
import org.infinispan.transaction.LockingMode; import org.infinispan.transaction.LockingMode;
import org.infinispan.transaction.TransactionMode; import org.infinispan.transaction.TransactionMode;
@ -38,8 +40,12 @@ import org.jboss.logging.Logger;
import org.jgroups.JChannel; import org.jgroups.JChannel;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory; import org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory;
import org.keycloak.common.util.HostUtils;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.sessions.infinispan.remotestore.KcRemoteStoreConfigurationBuilder;
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
import org.keycloak.models.utils.KeycloakModelUtils;
import javax.naming.InitialContext; import javax.naming.InitialContext;
@ -56,11 +62,15 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
protected boolean containerManaged; protected boolean containerManaged;
private String nodeName;
private String siteName;
@Override @Override
public InfinispanConnectionProvider create(KeycloakSession session) { public InfinispanConnectionProvider create(KeycloakSession session) {
lazyInit(); lazyInit();
return new DefaultInfinispanConnectionProvider(cacheManager); return new DefaultInfinispanConnectionProvider(cacheManager, nodeName, siteName);
} }
@Override @Override
@ -96,6 +106,8 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
} else { } else {
initEmbedded(); initEmbedded();
} }
logger.infof("Node name: %s, Site name: %s", nodeName, siteName);
} }
} }
} }
@ -134,7 +146,20 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
cacheManager.defineConfiguration(InfinispanConnectionProvider.AUTHORIZATION_REVISIONS_CACHE_NAME, getRevisionCacheConfig(authzRevisionsMaxEntries)); cacheManager.defineConfiguration(InfinispanConnectionProvider.AUTHORIZATION_REVISIONS_CACHE_NAME, getRevisionCacheConfig(authzRevisionsMaxEntries));
cacheManager.getCache(InfinispanConnectionProvider.AUTHORIZATION_REVISIONS_CACHE_NAME, true); cacheManager.getCache(InfinispanConnectionProvider.AUTHORIZATION_REVISIONS_CACHE_NAME, true);
Transport transport = cacheManager.getTransport();
if (transport != null) {
this.nodeName = transport.getAddress().toString();
this.siteName = cacheManager.getCacheManagerConfiguration().transport().siteId();
if (this.siteName == null) {
this.siteName = System.getProperty(InfinispanConnectionProvider.JBOSS_SITE_NAME);
}
} else {
this.nodeName = System.getProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME);
this.siteName = System.getProperty(InfinispanConnectionProvider.JBOSS_SITE_NAME);
}
if (this.nodeName == null || this.nodeName.equals("localhost")) {
this.nodeName = generateNodeName();
}
logger.debugv("Using container managed Infinispan cache container, lookup={1}", cacheContainerLookup); logger.debugv("Using container managed Infinispan cache container, lookup={1}", cacheContainerLookup);
} catch (Exception e) { } catch (Exception e) {
@ -152,13 +177,27 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
boolean async = config.getBoolean("async", false); boolean async = config.getBoolean("async", false);
boolean allowDuplicateJMXDomains = config.getBoolean("allowDuplicateJMXDomains", true); boolean allowDuplicateJMXDomains = config.getBoolean("allowDuplicateJMXDomains", true);
this.nodeName = config.get("nodeName", System.getProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME));
if (this.nodeName != null && this.nodeName.isEmpty()) {
this.nodeName = null;
}
this.siteName = config.get("siteName", System.getProperty(InfinispanConnectionProvider.JBOSS_SITE_NAME));
if (this.siteName != null && this.siteName.isEmpty()) {
this.siteName = null;
}
if (clustered) { if (clustered) {
String nodeName = config.get("nodeName", System.getProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME));
String jgroupsUdpMcastAddr = config.get("jgroupsUdpMcastAddr", System.getProperty(InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR)); String jgroupsUdpMcastAddr = config.get("jgroupsUdpMcastAddr", System.getProperty(InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR));
configureTransport(gcb, nodeName, jgroupsUdpMcastAddr); configureTransport(gcb, nodeName, siteName, jgroupsUdpMcastAddr);
gcb.globalJmxStatistics() gcb.globalJmxStatistics()
.jmxDomain(InfinispanConnectionProvider.JMX_DOMAIN + "-" + nodeName); .jmxDomain(InfinispanConnectionProvider.JMX_DOMAIN + "-" + nodeName);
} else {
if (nodeName == null) {
nodeName = generateNodeName();
}
} }
gcb.globalJmxStatistics() gcb.globalJmxStatistics()
.allowDuplicateDomains(allowDuplicateJMXDomains) .allowDuplicateDomains(allowDuplicateJMXDomains)
.enable(); .enable();
@ -166,6 +205,10 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
cacheManager = new DefaultCacheManager(gcb.build()); cacheManager = new DefaultCacheManager(gcb.build());
containerManaged = false; containerManaged = false;
if (cacheManager.getTransport() != null) {
nodeName = cacheManager.getTransport().getAddress().toString();
}
logger.debug("Started embedded Infinispan cache container"); logger.debug("Started embedded Infinispan cache container");
ConfigurationBuilder modelCacheConfigBuilder = new ConfigurationBuilder(); ConfigurationBuilder modelCacheConfigBuilder = new ConfigurationBuilder();
@ -198,11 +241,29 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
.build(); .build();
} }
// Base configuration doesn't contain any remote stores
Configuration sessionCacheConfigurationBase = sessionConfigBuilder.build();
boolean jdgEnabled = config.getBoolean("remoteStoreEnabled", false);
if (jdgEnabled) {
sessionConfigBuilder = new ConfigurationBuilder();
sessionConfigBuilder.read(sessionCacheConfigurationBase);
configureRemoteCacheStore(sessionConfigBuilder, async, InfinispanConnectionProvider.SESSION_CACHE_NAME, KcRemoteStoreConfigurationBuilder.class);
}
Configuration sessionCacheConfiguration = sessionConfigBuilder.build(); Configuration sessionCacheConfiguration = sessionConfigBuilder.build();
cacheManager.defineConfiguration(InfinispanConnectionProvider.SESSION_CACHE_NAME, sessionCacheConfiguration); cacheManager.defineConfiguration(InfinispanConnectionProvider.SESSION_CACHE_NAME, sessionCacheConfiguration);
if (jdgEnabled) {
sessionConfigBuilder = new ConfigurationBuilder();
sessionConfigBuilder.read(sessionCacheConfigurationBase);
configureRemoteCacheStore(sessionConfigBuilder, async, InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, KcRemoteStoreConfigurationBuilder.class);
}
sessionCacheConfiguration = sessionConfigBuilder.build();
cacheManager.defineConfiguration(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, sessionCacheConfiguration); cacheManager.defineConfiguration(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, sessionCacheConfiguration);
cacheManager.defineConfiguration(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME, sessionCacheConfiguration);
cacheManager.defineConfiguration(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME, sessionCacheConfiguration); cacheManager.defineConfiguration(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME, sessionCacheConfigurationBase);
cacheManager.defineConfiguration(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME, sessionCacheConfigurationBase);
// Retrieve caches to enforce rebalance // Retrieve caches to enforce rebalance
cacheManager.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME, true); cacheManager.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME, true);
@ -215,9 +276,8 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
replicationConfigBuilder.clustering().cacheMode(async ? CacheMode.REPL_ASYNC : CacheMode.REPL_SYNC); replicationConfigBuilder.clustering().cacheMode(async ? CacheMode.REPL_ASYNC : CacheMode.REPL_SYNC);
} }
boolean jdgEnabled = config.getBoolean("remoteStoreEnabled", false);
if (jdgEnabled) { if (jdgEnabled) {
configureRemoteCacheStore(replicationConfigBuilder, async); configureRemoteCacheStore(replicationConfigBuilder, async, InfinispanConnectionProvider.WORK_CACHE_NAME, RemoteStoreConfigurationBuilder.class);
} }
Configuration replicationEvictionCacheConfiguration = replicationConfigBuilder.build(); Configuration replicationEvictionCacheConfiguration = replicationConfigBuilder.build();
@ -267,6 +327,10 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
cacheManager.getCache(InfinispanConnectionProvider.AUTHORIZATION_REVISIONS_CACHE_NAME, true); cacheManager.getCache(InfinispanConnectionProvider.AUTHORIZATION_REVISIONS_CACHE_NAME, true);
} }
protected String generateNodeName() {
return InfinispanConnectionProvider.NODE_PREFIX + new SecureRandom().nextInt(1000000);
}
private Configuration getRevisionCacheConfig(long maxEntries) { private Configuration getRevisionCacheConfig(long maxEntries) {
ConfigurationBuilder cb = new ConfigurationBuilder(); ConfigurationBuilder cb = new ConfigurationBuilder();
cb.invocationBatching().enable().transaction().transactionMode(TransactionMode.TRANSACTIONAL); cb.invocationBatching().enable().transaction().transactionMode(TransactionMode.TRANSACTIONAL);
@ -281,19 +345,19 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
} }
// Used for cross-data centers scenario. Usually integration with external JDG server, which itself handles communication between DCs. // Used for cross-data centers scenario. Usually integration with external JDG server, which itself handles communication between DCs.
private void configureRemoteCacheStore(ConfigurationBuilder builder, boolean async) { private void configureRemoteCacheStore(ConfigurationBuilder builder, boolean async, String cacheName, Class<? extends RemoteStoreConfigurationBuilder> configBuilderClass) {
String jdgServer = config.get("remoteStoreServer", "localhost"); String jdgServer = config.get("remoteStoreServer", "localhost");
Integer jdgPort = config.getInt("remoteStorePort", 11222); Integer jdgPort = config.getInt("remoteStorePort", 11222);
builder.persistence() builder.persistence()
.passivation(false) .passivation(false)
.addStore(RemoteStoreConfigurationBuilder.class) .addStore(configBuilderClass)
.fetchPersistentState(false) .fetchPersistentState(false)
.ignoreModifications(false) .ignoreModifications(false)
.purgeOnStartup(false) .purgeOnStartup(false)
.preload(false) .preload(false)
.shared(true) .shared(true)
.remoteCacheName(InfinispanConnectionProvider.WORK_CACHE_NAME) .remoteCacheName(cacheName)
.rawValues(true) .rawValues(true)
.forceReturnValues(false) .forceReturnValues(false)
.marshaller(KeycloakHotRodMarshallerFactory.class.getName()) .marshaller(KeycloakHotRodMarshallerFactory.class.getName())
@ -355,7 +419,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
private static final Object CHANNEL_INIT_SYNCHRONIZER = new Object(); private static final Object CHANNEL_INIT_SYNCHRONIZER = new Object();
protected void configureTransport(GlobalConfigurationBuilder gcb, String nodeName, String jgroupsUdpMcastAddr) { protected void configureTransport(GlobalConfigurationBuilder gcb, String nodeName, String siteName, String jgroupsUdpMcastAddr) {
if (nodeName == null) { if (nodeName == null) {
gcb.transport().defaultTransport(); gcb.transport().defaultTransport();
} else { } else {
@ -376,6 +440,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
gcb.transport() gcb.transport()
.nodeName(nodeName) .nodeName(nodeName)
.siteId(siteName)
.transport(transport) .transport(transport)
.globalJmxStatistics() .globalJmxStatistics()
.jmxDomain(InfinispanConnectionProvider.JMX_DOMAIN + "-" + nodeName) .jmxDomain(InfinispanConnectionProvider.JMX_DOMAIN + "-" + nodeName)

View file

@ -55,8 +55,25 @@ public interface InfinispanConnectionProvider extends Provider {
String JBOSS_NODE_NAME = "jboss.node.name"; String JBOSS_NODE_NAME = "jboss.node.name";
String JGROUPS_UDP_MCAST_ADDR = "jgroups.udp.mcast_addr"; String JGROUPS_UDP_MCAST_ADDR = "jgroups.udp.mcast_addr";
// TODO This property is not in Wildfly. Check if corresponding property in Wildfly exists
String JBOSS_SITE_NAME = "jboss.site.name";
String JMX_DOMAIN = "jboss.datagrid-infinispan"; String JMX_DOMAIN = "jboss.datagrid-infinispan";
// Constant used as the prefix of the current node if "jboss.node.name" is not configured
String NODE_PREFIX = "node_";
<K, V> Cache<K, V> getCache(String name); <K, V> Cache<K, V> getCache(String name);
/**
* @return Address of current node in cluster. In non-cluster environment, it returns some other non-null value (eg. hostname with some random value like "host-123456" )
*/
String getNodeName();
/**
*
* @return siteName or null if we're not in environment with multiple sites (data centers)
*/
String getSiteName();
} }

View file

@ -69,7 +69,7 @@ public class InfinispanPublicKeyStorageProvider implements PublicKeyStorageProvi
public void clearCache() { public void clearCache() {
keys.clear(); keys.clear();
ClusterProvider cluster = session.getProvider(ClusterProvider.class); ClusterProvider cluster = session.getProvider(ClusterProvider.class);
cluster.notify(InfinispanPublicKeyStorageProviderFactory.KEYS_CLEAR_CACHE_EVENTS, new ClearCacheEvent(), true); cluster.notify(InfinispanPublicKeyStorageProviderFactory.KEYS_CLEAR_CACHE_EVENTS, new ClearCacheEvent(), true, ClusterProvider.DCNotify.ALL_DCS);
} }
@ -122,7 +122,7 @@ public class InfinispanPublicKeyStorageProvider implements PublicKeyStorageProvi
for (String cacheKey : invalidations) { for (String cacheKey : invalidations) {
keys.remove(cacheKey); keys.remove(cacheKey);
cluster.notify(cacheKey, PublicKeyStorageInvalidationEvent.create(cacheKey), true); cluster.notify(InfinispanPublicKeyStorageProviderFactory.PUBLIC_KEY_STORAGE_INVALIDATION_EVENT, PublicKeyStorageInvalidationEvent.create(cacheKey), true, ClusterProvider.DCNotify.ALL_DCS);
} }
} }

View file

@ -50,6 +50,8 @@ public class InfinispanPublicKeyStorageProviderFactory implements PublicKeyStora
public static final String KEYS_CLEAR_CACHE_EVENTS = "KEYS_CLEAR_CACHE_EVENTS"; public static final String KEYS_CLEAR_CACHE_EVENTS = "KEYS_CLEAR_CACHE_EVENTS";
public static final String PUBLIC_KEY_STORAGE_INVALIDATION_EVENT = "PUBLIC_KEY_STORAGE_INVALIDATION_EVENT";
private volatile Cache<String, PublicKeysEntry> keysCache; private volatile Cache<String, PublicKeysEntry> keysCache;
private final Map<String, FutureTask<PublicKeysEntry>> tasksInProgress = new ConcurrentHashMap<>(); private final Map<String, FutureTask<PublicKeysEntry>> tasksInProgress = new ConcurrentHashMap<>();
@ -69,12 +71,10 @@ public class InfinispanPublicKeyStorageProviderFactory implements PublicKeyStora
this.keysCache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.KEYS_CACHE_NAME); this.keysCache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.KEYS_CACHE_NAME);
ClusterProvider cluster = session.getProvider(ClusterProvider.class); ClusterProvider cluster = session.getProvider(ClusterProvider.class);
cluster.registerListener(ClusterProvider.ALL, (ClusterEvent event) -> { cluster.registerListener(PUBLIC_KEY_STORAGE_INVALIDATION_EVENT, (ClusterEvent event) -> {
if (event instanceof PublicKeyStorageInvalidationEvent) { PublicKeyStorageInvalidationEvent invalidationEvent = (PublicKeyStorageInvalidationEvent) event;
PublicKeyStorageInvalidationEvent invalidationEvent = (PublicKeyStorageInvalidationEvent) event; keysCache.remove(invalidationEvent.getCacheKey());
keysCache.remove(invalidationEvent.getCacheKey());
}
}); });

View file

@ -198,22 +198,15 @@ public abstract class CacheManager {
} }
public void sendInvalidationEvents(KeycloakSession session, Collection<InvalidationEvent> invalidationEvents) { public void sendInvalidationEvents(KeycloakSession session, Collection<InvalidationEvent> invalidationEvents, String eventKey) {
ClusterProvider clusterProvider = session.getProvider(ClusterProvider.class); ClusterProvider clusterProvider = session.getProvider(ClusterProvider.class);
// Maybe add InvalidationEvent, which will be collection of all invalidationEvents? That will reduce cluster traffic even more. // Maybe add InvalidationEvent, which will be collection of all invalidationEvents? That will reduce cluster traffic even more.
for (InvalidationEvent event : invalidationEvents) { for (InvalidationEvent event : invalidationEvents) {
clusterProvider.notify(generateEventId(event), event, true); clusterProvider.notify(eventKey, event, true, ClusterProvider.DCNotify.ALL_DCS);
} }
} }
protected String generateEventId(InvalidationEvent event) {
return new StringBuilder(event.getId())
.append("_")
.append(event.hashCode())
.toString();
}
public void invalidationEventReceived(InvalidationEvent event) { public void invalidationEventReceived(InvalidationEvent event) {
Set<String> invalidations = new HashSet<>(); Set<String> invalidations = new HashSet<>();

View file

@ -38,6 +38,7 @@ public class InfinispanCacheRealmProviderFactory implements CacheRealmProviderFa
private static final Logger log = Logger.getLogger(InfinispanCacheRealmProviderFactory.class); private static final Logger log = Logger.getLogger(InfinispanCacheRealmProviderFactory.class);
public static final String REALM_CLEAR_CACHE_EVENTS = "REALM_CLEAR_CACHE_EVENTS"; public static final String REALM_CLEAR_CACHE_EVENTS = "REALM_CLEAR_CACHE_EVENTS";
public static final String REALM_INVALIDATION_EVENTS = "REALM_INVALIDATION_EVENTS";
protected volatile RealmCacheManager realmCache; protected volatile RealmCacheManager realmCache;
@ -56,12 +57,11 @@ public class InfinispanCacheRealmProviderFactory implements CacheRealmProviderFa
realmCache = new RealmCacheManager(cache, revisions); realmCache = new RealmCacheManager(cache, revisions);
ClusterProvider cluster = session.getProvider(ClusterProvider.class); ClusterProvider cluster = session.getProvider(ClusterProvider.class);
cluster.registerListener(ClusterProvider.ALL, (ClusterEvent event) -> { cluster.registerListener(REALM_INVALIDATION_EVENTS, (ClusterEvent event) -> {
InvalidationEvent invalidationEvent = (InvalidationEvent) event;
realmCache.invalidationEventReceived(invalidationEvent);
if (event instanceof InvalidationEvent) {
InvalidationEvent invalidationEvent = (InvalidationEvent) event;
realmCache.invalidationEventReceived(invalidationEvent);
}
}); });
cluster.registerListener(REALM_CLEAR_CACHE_EVENTS, (ClusterEvent event) -> { cluster.registerListener(REALM_CLEAR_CACHE_EVENTS, (ClusterEvent event) -> {

View file

@ -37,6 +37,7 @@ public class InfinispanUserCacheProviderFactory implements UserCacheProviderFact
private static final Logger log = Logger.getLogger(InfinispanUserCacheProviderFactory.class); private static final Logger log = Logger.getLogger(InfinispanUserCacheProviderFactory.class);
public static final String USER_CLEAR_CACHE_EVENTS = "USER_CLEAR_CACHE_EVENTS"; public static final String USER_CLEAR_CACHE_EVENTS = "USER_CLEAR_CACHE_EVENTS";
public static final String USER_INVALIDATION_EVENTS = "USER_INVALIDATION_EVENTS";
protected volatile UserCacheManager userCache; protected volatile UserCacheManager userCache;
@ -58,12 +59,10 @@ public class InfinispanUserCacheProviderFactory implements UserCacheProviderFact
ClusterProvider cluster = session.getProvider(ClusterProvider.class); ClusterProvider cluster = session.getProvider(ClusterProvider.class);
cluster.registerListener(ClusterProvider.ALL, (ClusterEvent event) -> { cluster.registerListener(USER_INVALIDATION_EVENTS, (ClusterEvent event) -> {
if (event instanceof InvalidationEvent) { InvalidationEvent invalidationEvent = (InvalidationEvent) event;
InvalidationEvent invalidationEvent = (InvalidationEvent) event; userCache.invalidationEventReceived(invalidationEvent);
userCache.invalidationEventReceived(invalidationEvent);
}
}); });

View file

@ -95,11 +95,9 @@ public class RealmCacheManager extends CacheManager {
@Override @Override
protected void addInvalidationsFromEvent(InvalidationEvent event, Set<String> invalidations) { protected void addInvalidationsFromEvent(InvalidationEvent event, Set<String> invalidations) {
if (event instanceof RealmCacheInvalidationEvent) { invalidations.add(event.getId());
invalidations.add(event.getId());
((RealmCacheInvalidationEvent) event).addInvalidations(this, invalidations); ((RealmCacheInvalidationEvent) event).addInvalidations(this, invalidations);
}
} }
} }

View file

@ -166,7 +166,7 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override @Override
public void clear() { public void clear() {
ClusterProvider cluster = session.getProvider(ClusterProvider.class); ClusterProvider cluster = session.getProvider(ClusterProvider.class);
cluster.notify(InfinispanCacheRealmProviderFactory.REALM_CLEAR_CACHE_EVENTS, new ClearCacheEvent(), false); cluster.notify(InfinispanCacheRealmProviderFactory.REALM_CLEAR_CACHE_EVENTS, new ClearCacheEvent(), false, ClusterProvider.DCNotify.ALL_DCS);
} }
@Override @Override
@ -298,7 +298,7 @@ public class RealmCacheSession implements CacheRealmProvider {
cache.invalidateObject(id); cache.invalidateObject(id);
} }
cache.sendInvalidationEvents(session, invalidationEvents); cache.sendInvalidationEvents(session, invalidationEvents, InfinispanCacheRealmProviderFactory.REALM_INVALIDATION_EVENTS);
} }
private KeycloakTransaction getPrepareTransaction() { private KeycloakTransaction getPrepareTransaction() {

View file

@ -95,9 +95,7 @@ public class UserCacheManager extends CacheManager {
@Override @Override
protected void addInvalidationsFromEvent(InvalidationEvent event, Set<String> invalidations) { protected void addInvalidationsFromEvent(InvalidationEvent event, Set<String> invalidations) {
if (event instanceof UserCacheInvalidationEvent) { ((UserCacheInvalidationEvent) event).addInvalidations(this, invalidations);
((UserCacheInvalidationEvent) event).addInvalidations(this, invalidations);
}
} }
public void invalidateRealmUsers(String realm, Set<String> invalidations) { public void invalidateRealmUsers(String realm, Set<String> invalidations) {

View file

@ -90,7 +90,7 @@ public class UserCacheSession implements UserCache {
public void clear() { public void clear() {
cache.clear(); cache.clear();
ClusterProvider cluster = session.getProvider(ClusterProvider.class); ClusterProvider cluster = session.getProvider(ClusterProvider.class);
cluster.notify(InfinispanUserCacheProviderFactory.USER_CLEAR_CACHE_EVENTS, new ClearCacheEvent(), true); cluster.notify(InfinispanUserCacheProviderFactory.USER_CLEAR_CACHE_EVENTS, new ClearCacheEvent(), true, ClusterProvider.DCNotify.ALL_DCS);
} }
public UserProvider getDelegate() { public UserProvider getDelegate() {
@ -129,7 +129,7 @@ public class UserCacheSession implements UserCache {
cache.invalidateObject(invalidation); cache.invalidateObject(invalidation);
} }
cache.sendInvalidationEvents(session, invalidationEvents); cache.sendInvalidationEvents(session, invalidationEvents, InfinispanUserCacheProviderFactory.USER_INVALIDATION_EVENTS);
} }
private KeycloakTransaction getTransaction() { private KeycloakTransaction getTransaction() {

View file

@ -41,6 +41,7 @@ public class InfinispanCacheStoreFactoryProviderFactory implements CachedStorePr
private static final Logger log = Logger.getLogger(InfinispanCacheStoreFactoryProviderFactory.class); private static final Logger log = Logger.getLogger(InfinispanCacheStoreFactoryProviderFactory.class);
public static final String AUTHORIZATION_CLEAR_CACHE_EVENTS = "AUTHORIZATION_CLEAR_CACHE_EVENTS"; public static final String AUTHORIZATION_CLEAR_CACHE_EVENTS = "AUTHORIZATION_CLEAR_CACHE_EVENTS";
public static final String AUTHORIZATION_INVALIDATION_EVENTS = "AUTHORIZATION_INVALIDATION_EVENTS";
protected volatile StoreFactoryCacheManager storeCache; protected volatile StoreFactoryCacheManager storeCache;
@ -59,11 +60,11 @@ public class InfinispanCacheStoreFactoryProviderFactory implements CachedStorePr
storeCache = new StoreFactoryCacheManager(cache, revisions); storeCache = new StoreFactoryCacheManager(cache, revisions);
ClusterProvider cluster = session.getProvider(ClusterProvider.class); ClusterProvider cluster = session.getProvider(ClusterProvider.class);
cluster.registerListener(ClusterProvider.ALL, (ClusterEvent event) -> { cluster.registerListener(AUTHORIZATION_INVALIDATION_EVENTS, (ClusterEvent event) -> {
if (event instanceof InvalidationEvent) {
InvalidationEvent invalidationEvent = (InvalidationEvent) event; InvalidationEvent invalidationEvent = (InvalidationEvent) event;
storeCache.invalidationEventReceived(invalidationEvent); storeCache.invalidationEventReceived(invalidationEvent);
}
}); });
cluster.registerListener(AUTHORIZATION_CLEAR_CACHE_EVENTS, (ClusterEvent event) -> storeCache.clear()); cluster.registerListener(AUTHORIZATION_CLEAR_CACHE_EVENTS, (ClusterEvent event) -> storeCache.clear());

View file

@ -216,7 +216,7 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
cache.invalidateObject(id); cache.invalidateObject(id);
} }
cache.sendInvalidationEvents(session, invalidationEvents); cache.sendInvalidationEvents(session, invalidationEvents, InfinispanCacheStoreFactoryProviderFactory.AUTHORIZATION_INVALIDATION_EVENTS);
} }

View file

@ -22,13 +22,17 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import org.infinispan.Cache;
import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.sessions.infinispan.changes.InfinispanChangelogBasedTransaction;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask;
import org.keycloak.models.sessions.infinispan.changes.UserSessionClientSessionUpdateTask;
import org.keycloak.models.sessions.infinispan.changes.UserSessionUpdateTask;
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
/** /**
@ -39,19 +43,20 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
private final AuthenticatedClientSessionEntity entity; private final AuthenticatedClientSessionEntity entity;
private final ClientModel client; private final ClientModel client;
private final InfinispanUserSessionProvider provider; private final InfinispanUserSessionProvider provider;
private final Cache<String, SessionEntity> cache; private final InfinispanChangelogBasedTransaction updateTx;
private UserSessionAdapter userSession; private UserSessionAdapter userSession;
public AuthenticatedClientSessionAdapter(AuthenticatedClientSessionEntity entity, ClientModel client, UserSessionAdapter userSession, InfinispanUserSessionProvider provider, Cache<String, SessionEntity> cache) { public AuthenticatedClientSessionAdapter(AuthenticatedClientSessionEntity entity, ClientModel client, UserSessionAdapter userSession,
InfinispanUserSessionProvider provider, InfinispanChangelogBasedTransaction updateTx) {
this.provider = provider; this.provider = provider;
this.entity = entity; this.entity = entity;
this.client = client; this.client = client;
this.cache = cache; this.updateTx = updateTx;
this.userSession = userSession; this.userSession = userSession;
} }
private void update() { private void update(UserSessionUpdateTask task) {
provider.getTx().replace(cache, userSession.getEntity().getId(), userSession.getEntity()); updateTx.addTask(userSession.getId(), task);
} }
@ -62,15 +67,27 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
// Dettach userSession // Dettach userSession
if (userSession == null) { if (userSession == null) {
if (sessionEntity.getAuthenticatedClientSessions() != null) { UserSessionUpdateTask task = new UserSessionUpdateTask() {
sessionEntity.getAuthenticatedClientSessions().remove(clientUUID);
update(); @Override
this.userSession = null; public void runUpdate(UserSessionEntity sessionEntity) {
} sessionEntity.getAuthenticatedClientSessions().remove(clientUUID);
}
};
update(task);
this.userSession = null;
} else { } else {
this.userSession = (UserSessionAdapter) userSession; this.userSession = (UserSessionAdapter) userSession;
sessionEntity.getAuthenticatedClientSessions().put(clientUUID, entity); UserSessionUpdateTask task = new UserSessionUpdateTask() {
update();
@Override
public void runUpdate(UserSessionEntity sessionEntity) {
sessionEntity.getAuthenticatedClientSessions().put(clientUUID, entity);
}
};
update(task);
} }
} }
@ -86,8 +103,16 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
@Override @Override
public void setRedirectUri(String uri) { public void setRedirectUri(String uri) {
entity.setRedirectUri(uri); UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) {
update();
@Override
protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) {
entity.setRedirectUri(uri);
}
};
update(task);
} }
@Override @Override
@ -112,8 +137,22 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
@Override @Override
public void setTimestamp(int timestamp) { public void setTimestamp(int timestamp) {
entity.setTimestamp(timestamp); UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) {
update();
@Override
protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) {
entity.setTimestamp(timestamp);
}
@Override
public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<UserSessionEntity> sessionWrapper) {
// We usually update lastSessionRefresh at the same time. That would handle it.
return CrossDCMessageStatus.NOT_NEEDED;
}
};
update(task);
} }
@Override @Override
@ -123,8 +162,16 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
@Override @Override
public void setAction(String action) { public void setAction(String action) {
entity.setAction(action); UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) {
update();
@Override
protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) {
entity.setAction(action);
}
};
update(task);
} }
@Override @Override
@ -134,8 +181,16 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
@Override @Override
public void setProtocol(String method) { public void setProtocol(String method) {
entity.setAuthMethod(method); UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) {
update();
@Override
protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) {
entity.setAuthMethod(method);
}
};
update(task);
} }
@Override @Override
@ -145,8 +200,16 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
@Override @Override
public void setRoles(Set<String> roles) { public void setRoles(Set<String> roles) {
entity.setRoles(roles); UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) {
update();
@Override
protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) {
entity.setRoles(roles); // TODO not thread-safe. But we will remove setRoles anyway...?
}
};
update(task);
} }
@Override @Override
@ -156,35 +219,54 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
@Override @Override
public void setProtocolMappers(Set<String> protocolMappers) { public void setProtocolMappers(Set<String> protocolMappers) {
entity.setProtocolMappers(protocolMappers); UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) {
update();
@Override
protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) {
entity.setProtocolMappers(protocolMappers); // TODO not thread-safe. But we will remove setProtocolMappers anyway...?
}
};
update(task);
} }
@Override @Override
public String getNote(String name) { public String getNote(String name) {
return entity.getNotes()==null ? null : entity.getNotes().get(name); return entity.getNotes().get(name);
} }
@Override @Override
public void setNote(String name, String value) { public void setNote(String name, String value) {
if (entity.getNotes() == null) { UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) {
entity.setNotes(new HashMap<>());
} @Override
entity.getNotes().put(name, value); protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) {
update(); entity.getNotes().put(name, value);
}
};
update(task);
} }
@Override @Override
public void removeNote(String name) { public void removeNote(String name) {
if (entity.getNotes() != null) { UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) {
entity.getNotes().remove(name);
update(); @Override
} protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) {
entity.getNotes().remove(name);
}
};
update(task);
} }
@Override @Override
public Map<String, String> getNotes() { public Map<String, String> getNotes() {
if (entity.getNotes() == null || entity.getNotes().isEmpty()) return Collections.emptyMap(); if (entity.getNotes().isEmpty()) return Collections.emptyMap();
Map<String, String> copy = new HashMap<>(); Map<String, String> copy = new HashMap<>();
copy.putAll(entity.getNotes()); copy.putAll(entity.getNotes());
return copy; return copy;

View file

@ -15,27 +15,24 @@
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.models.sessions.infinispan.mapreduce; package org.keycloak.models.sessions.infinispan;
import org.infinispan.distexec.mapreduce.Reducer; import org.infinispan.AdvancedCache;
import org.infinispan.Cache;
import java.util.Iterator; import org.infinispan.context.Flag;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
public class LargestResultReducer implements Reducer<String, Integer> { public class CacheDecorators {
@Override public static <K, V> AdvancedCache<K, V> localCache(Cache<K, V> cache) {
public Integer reduce(String reducedKey, Iterator<Integer> itr) { return cache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL);
Integer largest = itr.next();
while (itr.hasNext()) {
Integer next = itr.next();
if (next > largest) {
largest = next;
}
}
return largest;
} }
public static <K, V> AdvancedCache<K, V> skipCacheLoaders(Cache<K, V> cache) {
return cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_LOAD, Flag.SKIP_CACHE_STORE);
}
} }

View file

@ -61,10 +61,6 @@ public class InfinispanActionTokenStoreProvider implements ActionTokenStoreProvi
this.tx.put(actionKeyCache, tokenKey, tokenValue, key.getExpiration() - Time.currentTime(), TimeUnit.SECONDS); this.tx.put(actionKeyCache, tokenKey, tokenValue, key.getExpiration() - Time.currentTime(), TimeUnit.SECONDS);
} }
private static String generateActionTokenEventId() {
return InfinispanActionTokenStoreProviderFactory.ACTION_TOKEN_EVENTS + "/" + UUID.randomUUID();
}
@Override @Override
public ActionTokenValueModel get(ActionTokenKeyModel actionTokenKey) { public ActionTokenValueModel get(ActionTokenKeyModel actionTokenKey) {
if (actionTokenKey == null || actionTokenKey.getUserId() == null || actionTokenKey.getActionId() == null) { if (actionTokenKey == null || actionTokenKey.getUserId() == null || actionTokenKey.getActionId() == null) {
@ -98,6 +94,6 @@ public class InfinispanActionTokenStoreProvider implements ActionTokenStoreProvi
} }
ClusterProvider cluster = session.getProvider(ClusterProvider.class); ClusterProvider cluster = session.getProvider(ClusterProvider.class);
this.tx.notify(cluster, generateActionTokenEventId(), new RemoveActionTokensSpecificEvent(userId, actionId), false); this.tx.notify(cluster, InfinispanActionTokenStoreProviderFactory.ACTION_TOKEN_EVENTS, new RemoveActionTokensSpecificEvent(userId, actionId), false);
} }
} }

View file

@ -70,24 +70,24 @@ public class InfinispanActionTokenStoreProviderFactory implements ActionTokenSto
ClusterProvider cluster = session.getProvider(ClusterProvider.class); ClusterProvider cluster = session.getProvider(ClusterProvider.class);
cluster.registerListener(ClusterProvider.ALL, event -> { cluster.registerListener(ACTION_TOKEN_EVENTS, event -> {
if (event instanceof RemoveActionTokensSpecificEvent) {
RemoveActionTokensSpecificEvent e = (RemoveActionTokensSpecificEvent) event;
LOG.debugf("[%s] Removing token invalidation for user+action: userId=%s, actionId=%s", cacheAddress, e.getUserId(), e.getActionId()); RemoveActionTokensSpecificEvent e = (RemoveActionTokensSpecificEvent) event;
AdvancedCache<ActionTokenReducedKey, ActionTokenValueEntity> localCache = cache LOG.debugf("[%s] Removing token invalidation for user+action: userId=%s, actionId=%s", cacheAddress, e.getUserId(), e.getActionId());
.getAdvancedCache()
.withFlags(Flag.CACHE_MODE_LOCAL, Flag.SKIP_CACHE_LOAD);
List<ActionTokenReducedKey> toRemove = localCache AdvancedCache<ActionTokenReducedKey, ActionTokenValueEntity> localCache = cache
.keySet() .getAdvancedCache()
.stream() .withFlags(Flag.CACHE_MODE_LOCAL, Flag.SKIP_CACHE_LOAD);
.filter(k -> Objects.equals(k.getUserId(), e.getUserId()) && Objects.equals(k.getActionId(), e.getActionId()))
.collect(Collectors.toList()); List<ActionTokenReducedKey> toRemove = localCache
.keySet()
.stream()
.filter(k -> Objects.equals(k.getUserId(), e.getUserId()) && Objects.equals(k.getActionId(), e.getActionId()))
.collect(Collectors.toList());
toRemove.forEach(localCache::remove);
toRemove.forEach(localCache::remove);
}
}); });
LOG.debugf("[%s] Registered cluster listeners", cacheAddress); LOG.debugf("[%s] Registered cluster listeners", cacheAddress);

View file

@ -30,6 +30,9 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.cache.infinispan.events.AuthenticationSessionAuthNoteUpdateEvent; import org.keycloak.models.cache.infinispan.events.AuthenticationSessionAuthNoteUpdateEvent;
import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity; import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity;
import org.keycloak.models.sessions.infinispan.events.ClientRemovedSessionEvent;
import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent;
import org.keycloak.models.sessions.infinispan.events.SessionEventsSenderTransaction;
import org.keycloak.models.sessions.infinispan.stream.AuthenticationSessionPredicate; import org.keycloak.models.sessions.infinispan.stream.AuthenticationSessionPredicate;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.RealmInfoUtil; import org.keycloak.models.utils.RealmInfoUtil;
@ -46,13 +49,17 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
private final KeycloakSession session; private final KeycloakSession session;
private final Cache<String, AuthenticationSessionEntity> cache; private final Cache<String, AuthenticationSessionEntity> cache;
protected final InfinispanKeycloakTransaction tx; protected final InfinispanKeycloakTransaction tx;
protected final SessionEventsSenderTransaction clusterEventsSenderTx;
public InfinispanAuthenticationSessionProvider(KeycloakSession session, Cache<String, AuthenticationSessionEntity> cache) { public InfinispanAuthenticationSessionProvider(KeycloakSession session, Cache<String, AuthenticationSessionEntity> cache) {
this.session = session; this.session = session;
this.cache = cache; this.cache = cache;
this.tx = new InfinispanKeycloakTransaction(); this.tx = new InfinispanKeycloakTransaction();
this.clusterEventsSenderTx = new SessionEventsSenderTransaction(session);
session.getTransactionManager().enlistAfterCompletion(tx); session.getTransactionManager().enlistAfterCompletion(tx);
session.getTransactionManager().enlistAfterCompletion(clusterEventsSenderTx);
} }
@Override @Override
@ -109,37 +116,61 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
// Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account) // Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account)
Iterator<Map.Entry<String, AuthenticationSessionEntity>> itr = cache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL) Iterator<Map.Entry<String, AuthenticationSessionEntity>> itr = CacheDecorators.localCache(cache)
.entrySet().stream().filter(AuthenticationSessionPredicate.create(realm.getId()).expired(expired)).iterator(); .entrySet()
.stream()
.filter(AuthenticationSessionPredicate.create(realm.getId()).expired(expired))
.iterator();
int counter = 0; int counter = 0;
while (itr.hasNext()) { while (itr.hasNext()) {
counter++; counter++;
AuthenticationSessionEntity entity = itr.next().getValue(); AuthenticationSessionEntity entity = itr.next().getValue();
tx.remove(cache, entity.getId()); tx.remove(CacheDecorators.localCache(cache), entity.getId());
} }
log.debugf("Removed %d expired user sessions for realm '%s'", counter, realm.getName()); log.debugf("Removed %d expired authentication sessions for realm '%s'", counter, realm.getName());
} }
// TODO: Should likely listen to "RealmRemovedEvent" received from cluster and clean just local sessions
@Override @Override
public void onRealmRemoved(RealmModel realm) { public void onRealmRemoved(RealmModel realm) {
Iterator<Map.Entry<String, AuthenticationSessionEntity>> itr = cache.entrySet().stream().filter(AuthenticationSessionPredicate.create(realm.getId())).iterator(); clusterEventsSenderTx.addEvent(InfinispanAuthenticationSessionProviderFactory.REALM_REMOVED_AUTHSESSION_EVENT, RealmRemovedSessionEvent.create(realm.getId()), true);
}
protected void onRealmRemovedEvent(String realmId) {
Iterator<Map.Entry<String, AuthenticationSessionEntity>> itr = CacheDecorators.localCache(cache)
.entrySet()
.stream()
.filter(AuthenticationSessionPredicate.create(realmId))
.iterator();
while (itr.hasNext()) { while (itr.hasNext()) {
cache.remove(itr.next().getKey()); CacheDecorators.localCache(cache)
.remove(itr.next().getKey());
} }
} }
// TODO: Should likely listen to "ClientRemovedEvent" received from cluster and clean just local sessions
@Override @Override
public void onClientRemoved(RealmModel realm, ClientModel client) { public void onClientRemoved(RealmModel realm, ClientModel client) {
Iterator<Map.Entry<String, AuthenticationSessionEntity>> itr = cache.entrySet().stream().filter(AuthenticationSessionPredicate.create(realm.getId()).client(client.getId())).iterator(); clusterEventsSenderTx.addEvent(InfinispanAuthenticationSessionProviderFactory.CLIENT_REMOVED_AUTHSESSION_EVENT, ClientRemovedSessionEvent.create(realm.getId(), client.getId()), true);
}
protected void onClientRemovedEvent(String realmId, String clientUuid) {
Iterator<Map.Entry<String, AuthenticationSessionEntity>> itr = CacheDecorators.localCache(cache)
.entrySet()
.stream()
.filter(AuthenticationSessionPredicate.create(realmId).client(clientUuid))
.iterator();
while (itr.hasNext()) { while (itr.hasNext()) {
cache.remove(itr.next().getKey()); CacheDecorators.localCache(cache)
.remove(itr.next().getKey());
} }
} }
@Override @Override
public void updateNonlocalSessionAuthNotes(String authSessionId, Map<String, String> authNotesFragment) { public void updateNonlocalSessionAuthNotes(String authSessionId, Map<String, String> authNotesFragment) {
if (authSessionId == null) { if (authSessionId == null) {
@ -150,7 +181,8 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
cluster.notify( cluster.notify(
InfinispanAuthenticationSessionProviderFactory.AUTHENTICATION_SESSION_EVENTS, InfinispanAuthenticationSessionProviderFactory.AUTHENTICATION_SESSION_EVENTS,
AuthenticationSessionAuthNoteUpdateEvent.create(authSessionId, authNotesFragment), AuthenticationSessionAuthNoteUpdateEvent.create(authSessionId, authNotesFragment),
true true,
ClusterProvider.DCNotify.ALL_BUT_LOCAL_DC
); );
} }
@ -159,4 +191,7 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
} }
public Cache<String, AuthenticationSessionEntity> getCache() {
return cache;
}
} }

View file

@ -26,6 +26,12 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.cache.infinispan.events.AuthenticationSessionAuthNoteUpdateEvent; import org.keycloak.models.cache.infinispan.events.AuthenticationSessionAuthNoteUpdateEvent;
import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity; import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity;
import org.keycloak.models.sessions.infinispan.events.AbstractAuthSessionClusterListener;
import org.keycloak.models.sessions.infinispan.events.ClientRemovedSessionEvent;
import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent;
import org.keycloak.models.utils.PostMigrationEvent;
import org.keycloak.provider.ProviderEvent;
import org.keycloak.provider.ProviderEventListener;
import org.keycloak.sessions.AuthenticationSessionProvider; import org.keycloak.sessions.AuthenticationSessionProvider;
import org.keycloak.sessions.AuthenticationSessionProviderFactory; import org.keycloak.sessions.AuthenticationSessionProviderFactory;
import java.util.Map; import java.util.Map;
@ -42,13 +48,59 @@ public class InfinispanAuthenticationSessionProviderFactory implements Authentic
private volatile Cache<String, AuthenticationSessionEntity> authSessionsCache; private volatile Cache<String, AuthenticationSessionEntity> authSessionsCache;
public static final String PROVIDER_ID = "infinispan";
public static final String AUTHENTICATION_SESSION_EVENTS = "AUTHENTICATION_SESSION_EVENTS"; public static final String AUTHENTICATION_SESSION_EVENTS = "AUTHENTICATION_SESSION_EVENTS";
public static final String REALM_REMOVED_AUTHSESSION_EVENT = "REALM_REMOVED_EVENT_AUTHSESSIONS";
public static final String CLIENT_REMOVED_AUTHSESSION_EVENT = "CLIENT_REMOVED_SESSION_AUTHSESSIONS";
@Override @Override
public void init(Config.Scope config) { public void init(Config.Scope config) {
} }
@Override
public void postInit(KeycloakSessionFactory factory) {
factory.register(new ProviderEventListener() {
@Override
public void onEvent(ProviderEvent event) {
if (event instanceof PostMigrationEvent) {
registerClusterListeners(((PostMigrationEvent) event).getSession());
}
}
});
}
protected void registerClusterListeners(KeycloakSession session) {
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
cluster.registerListener(REALM_REMOVED_AUTHSESSION_EVENT, new AbstractAuthSessionClusterListener<RealmRemovedSessionEvent>(sessionFactory) {
@Override
protected void eventReceived(KeycloakSession session, InfinispanAuthenticationSessionProvider provider, RealmRemovedSessionEvent sessionEvent) {
provider.onRealmRemovedEvent(sessionEvent.getRealmId());
}
});
cluster.registerListener(CLIENT_REMOVED_AUTHSESSION_EVENT, new AbstractAuthSessionClusterListener<ClientRemovedSessionEvent>(sessionFactory) {
@Override
protected void eventReceived(KeycloakSession session, InfinispanAuthenticationSessionProvider provider, ClientRemovedSessionEvent sessionEvent) {
provider.onClientRemovedEvent(sessionEvent.getRealmId(), sessionEvent.getClientUuid());
}
});
log.debug("Registered cluster listeners");
}
@Override @Override
public AuthenticationSessionProvider create(KeycloakSession session) { public AuthenticationSessionProvider create(KeycloakSession session) {
lazyInit(session); lazyInit(session);
@ -98,16 +150,12 @@ public class InfinispanAuthenticationSessionProviderFactory implements Authentic
} }
} }
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override @Override
public void close() { public void close() {
} }
@Override @Override
public String getId() { public String getId() {
return "infinispan"; return PROVIDER_ID;
} }
} }

View file

@ -155,7 +155,7 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
theTaskKey = taskKey + "-" + (i++); theTaskKey = taskKey + "-" + (i++);
} }
tasks.put(taskKey, () -> clusterProvider.notify(taskKey, event, ignoreSender)); tasks.put(taskKey, () -> clusterProvider.notify(taskKey, event, ignoreSender, ClusterProvider.DCNotify.ALL_DCS));
} }
public <K, V> void remove(Cache<K, V> cache, K key) { public <K, V> void remove(Cache<K, V> cache, K key) {
@ -168,7 +168,7 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
// This is for possibility to lookup for session by id, which was created in this transaction // This is for possibility to lookup for session by id, which was created in this transaction
public <K, V> V get(Cache<K, V> cache, K key) { public <K, V> V get(Cache<K, V> cache, K key) {
Object taskKey = getTaskKey(cache, key); Object taskKey = getTaskKey(cache, key);
CacheTask<V> current = tasks.get(taskKey); CacheTask current = tasks.get(taskKey);
if (current != null) { if (current != null) {
if (current instanceof CacheTaskWithValue) { if (current instanceof CacheTaskWithValue) {
return ((CacheTaskWithValue<V>) current).getValue(); return ((CacheTaskWithValue<V>) current).getValue();
@ -190,11 +190,11 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
} }
} }
public interface CacheTask<V> { public interface CacheTask {
void execute(); void execute();
} }
public abstract class CacheTaskWithValue<V> implements CacheTask<V> { public abstract class CacheTaskWithValue<V> implements CacheTask {
protected V value; protected V value;
public CacheTaskWithValue(V value) { public CacheTaskWithValue(V value) {

View file

@ -21,6 +21,7 @@ import org.keycloak.Config;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
import org.keycloak.sessions.StickySessionEncoderProvider; import org.keycloak.sessions.StickySessionEncoderProvider;
import org.keycloak.sessions.StickySessionEncoderProviderFactory; import org.keycloak.sessions.StickySessionEncoderProviderFactory;
@ -29,16 +30,22 @@ import org.keycloak.sessions.StickySessionEncoderProviderFactory;
*/ */
public class InfinispanStickySessionEncoderProviderFactory implements StickySessionEncoderProviderFactory { public class InfinispanStickySessionEncoderProviderFactory implements StickySessionEncoderProviderFactory {
private String myNodeName;
@Override @Override
public StickySessionEncoderProvider create(KeycloakSession session) { public StickySessionEncoderProvider create(KeycloakSession session) {
String myNodeName = InfinispanUtil.getMyAddress(session);
if (myNodeName != null && myNodeName.startsWith(InfinispanConnectionProvider.NODE_PREFIX)) {
// Node name was randomly generated. We won't use anything for sticky sessions in this case
myNodeName = null;
}
return new InfinispanStickySessionEncoderProvider(session, myNodeName); return new InfinispanStickySessionEncoderProvider(session, myNodeName);
} }
@Override @Override
public void init(Config.Scope config) { public void init(Config.Scope config) {
myNodeName = config.get("nodeName", System.getProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME));
} }
@Override @Override

View file

@ -18,10 +18,11 @@
package org.keycloak.models.sessions.infinispan; package org.keycloak.models.sessions.infinispan;
import org.infinispan.Cache; import org.infinispan.Cache;
import org.infinispan.CacheStream; import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.context.Flag; import org.infinispan.context.Flag;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
@ -31,19 +32,27 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider; import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.session.UserSessionPersisterProvider; import org.keycloak.models.session.UserSessionPersisterProvider;
import org.keycloak.models.sessions.infinispan.changes.sessions.LastSessionRefreshStore;
import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.changes.InfinispanChangelogBasedTransaction;
import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask;
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity; import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey; import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import org.keycloak.models.sessions.infinispan.events.ClientRemovedSessionEvent;
import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent;
import org.keycloak.models.sessions.infinispan.events.RemoveAllUserLoginFailuresEvent;
import org.keycloak.models.sessions.infinispan.events.RemoveUserSessionsEvent;
import org.keycloak.models.sessions.infinispan.events.SessionEventsSenderTransaction;
import org.keycloak.models.sessions.infinispan.stream.Comparators; import org.keycloak.models.sessions.infinispan.stream.Comparators;
import org.keycloak.models.sessions.infinispan.stream.Mappers; import org.keycloak.models.sessions.infinispan.stream.Mappers;
import org.keycloak.models.sessions.infinispan.stream.SessionPredicate; import org.keycloak.models.sessions.infinispan.stream.SessionPredicate;
import org.keycloak.models.sessions.infinispan.stream.UserLoginFailurePredicate; import org.keycloak.models.sessions.infinispan.stream.UserLoginFailurePredicate;
import org.keycloak.models.sessions.infinispan.stream.UserSessionPredicate; import org.keycloak.models.sessions.infinispan.stream.UserSessionPredicate;
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@ -51,7 +60,6 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
/** /**
@ -62,31 +70,71 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
private static final Logger log = Logger.getLogger(InfinispanUserSessionProvider.class); private static final Logger log = Logger.getLogger(InfinispanUserSessionProvider.class);
protected final KeycloakSession session; protected final KeycloakSession session;
protected final Cache<String, SessionEntity> sessionCache;
protected final Cache<String, SessionEntity> offlineSessionCache; protected final Cache<String, SessionEntityWrapper<UserSessionEntity>> sessionCache;
protected final Cache<String, SessionEntityWrapper<UserSessionEntity>> offlineSessionCache;
protected final Cache<LoginFailureKey, LoginFailureEntity> loginFailureCache; protected final Cache<LoginFailureKey, LoginFailureEntity> loginFailureCache;
protected final InfinispanChangelogBasedTransaction<UserSessionEntity> sessionTx;
protected final InfinispanChangelogBasedTransaction<UserSessionEntity> offlineSessionTx;
protected final InfinispanKeycloakTransaction tx; protected final InfinispanKeycloakTransaction tx;
public InfinispanUserSessionProvider(KeycloakSession session, Cache<String, SessionEntity> sessionCache, Cache<String, SessionEntity> offlineSessionCache, protected final SessionEventsSenderTransaction clusterEventsSenderTx;
protected final LastSessionRefreshStore lastSessionRefreshStore;
protected final LastSessionRefreshStore offlineLastSessionRefreshStore;
public InfinispanUserSessionProvider(KeycloakSession session,
RemoteCacheInvoker remoteCacheInvoker,
LastSessionRefreshStore lastSessionRefreshStore,
LastSessionRefreshStore offlineLastSessionRefreshStore,
Cache<String, SessionEntityWrapper<UserSessionEntity>> sessionCache,
Cache<String, SessionEntityWrapper<UserSessionEntity>> offlineSessionCache,
Cache<LoginFailureKey, LoginFailureEntity> loginFailureCache) { Cache<LoginFailureKey, LoginFailureEntity> loginFailureCache) {
this.session = session; this.session = session;
this.sessionCache = sessionCache; this.sessionCache = sessionCache;
this.offlineSessionCache = offlineSessionCache; this.offlineSessionCache = offlineSessionCache;
this.loginFailureCache = loginFailureCache; this.loginFailureCache = loginFailureCache;
this.sessionTx = new InfinispanChangelogBasedTransaction<>(session, InfinispanConnectionProvider.SESSION_CACHE_NAME, sessionCache, remoteCacheInvoker);
this.offlineSessionTx = new InfinispanChangelogBasedTransaction<>(session, InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, offlineSessionCache, remoteCacheInvoker);
this.tx = new InfinispanKeycloakTransaction(); this.tx = new InfinispanKeycloakTransaction();
this.clusterEventsSenderTx = new SessionEventsSenderTransaction(session);
this.lastSessionRefreshStore = lastSessionRefreshStore;
this.offlineLastSessionRefreshStore = offlineLastSessionRefreshStore;
session.getTransactionManager().enlistAfterCompletion(tx); session.getTransactionManager().enlistAfterCompletion(tx);
session.getTransactionManager().enlistAfterCompletion(clusterEventsSenderTx);
session.getTransactionManager().enlistAfterCompletion(sessionTx);
session.getTransactionManager().enlistAfterCompletion(offlineSessionTx);
} }
protected Cache<String, SessionEntity> getCache(boolean offline) { protected Cache<String, SessionEntityWrapper<UserSessionEntity>> getCache(boolean offline) {
return offline ? offlineSessionCache : sessionCache; return offline ? offlineSessionCache : sessionCache;
} }
protected InfinispanChangelogBasedTransaction<UserSessionEntity> getTransaction(boolean offline) {
return offline ? offlineSessionTx : sessionTx;
}
protected LastSessionRefreshStore getLastSessionRefreshStore() {
return lastSessionRefreshStore;
}
protected LastSessionRefreshStore getOfflineLastSessionRefreshStore() {
return offlineLastSessionRefreshStore;
}
@Override @Override
public AuthenticatedClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession) { public AuthenticatedClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession) {
AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity(); AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity();
AuthenticatedClientSessionAdapter adapter = new AuthenticatedClientSessionAdapter(entity, client, (UserSessionAdapter) userSession, this, sessionCache); InfinispanChangelogBasedTransaction<UserSessionEntity> updateTx = getTransaction(false);
AuthenticatedClientSessionAdapter adapter = new AuthenticatedClientSessionAdapter(entity, client, (UserSessionAdapter) userSession, this, updateTx);
adapter.setUserSession(userSession); adapter.setUserSession(userSession);
return adapter; return adapter;
} }
@ -95,10 +143,28 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
public UserSessionModel createUserSession(String id, RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) { public UserSessionModel createUserSession(String id, RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) {
UserSessionEntity entity = new UserSessionEntity(); UserSessionEntity entity = new UserSessionEntity();
entity.setId(id); entity.setId(id);
updateSessionEntity(entity, realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, brokerUserId); updateSessionEntity(entity, realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, brokerUserId);
tx.putIfAbsent(sessionCache, id, entity); SessionUpdateTask<UserSessionEntity> createSessionTask = new SessionUpdateTask<UserSessionEntity>() {
@Override
public void runUpdate(UserSessionEntity session) {
}
@Override
public CacheOperation getOperation(UserSessionEntity session) {
return CacheOperation.ADD_IF_ABSENT;
}
@Override
public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<UserSessionEntity> sessionWrapper) {
return CrossDCMessageStatus.SYNC;
}
};
sessionTx.addTask(id, createSessionTask, entity);
return wrap(realm, entity, false); return wrap(realm, entity, false);
} }
@ -121,31 +187,43 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
} }
@Override @Override
public UserSessionModel getUserSession(RealmModel realm, String id) { public UserSessionModel getUserSession(RealmModel realm, String id) {
return getUserSession(realm, id, false); return getUserSession(realm, id, false);
} }
protected UserSessionAdapter getUserSession(RealmModel realm, String id, boolean offline) { protected UserSessionAdapter getUserSession(RealmModel realm, String id, boolean offline) {
Cache<String, SessionEntity> cache = getCache(offline); UserSessionEntity entity = getUserSessionEntity(id, offline);
UserSessionEntity entity = (UserSessionEntity) tx.get(cache, id); // Chance created in this transaction
if (entity == null) {
entity = (UserSessionEntity) cache.get(id);
}
return wrap(realm, entity, offline); return wrap(realm, entity, offline);
} }
protected List<UserSessionModel> getUserSessions(RealmModel realm, Predicate<Map.Entry<String, SessionEntity>> predicate, boolean offline) { private UserSessionEntity getUserSessionEntity(String id, boolean offline) {
CacheStream<Map.Entry<String, SessionEntity>> cacheStream = getCache(offline).entrySet().stream(); InfinispanChangelogBasedTransaction<UserSessionEntity> tx = getTransaction(offline);
Iterator<Map.Entry<String, SessionEntity>> itr = cacheStream.filter(predicate).iterator(); SessionEntityWrapper<UserSessionEntity> entityWrapper = tx.get(id);
List<UserSessionModel> sessions = new LinkedList<>(); return entityWrapper==null ? null : entityWrapper.getEntity();
}
protected List<UserSessionModel> getUserSessions(RealmModel realm, Predicate<Map.Entry<String, SessionEntityWrapper<UserSessionEntity>>> predicate, boolean offline) {
Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = getCache(offline);
cache = CacheDecorators.skipCacheLoaders(cache);
Stream<Map.Entry<String, SessionEntityWrapper<UserSessionEntity>>> cacheStream = cache.entrySet().stream();
List<UserSessionModel> resultSessions = new LinkedList<>();
Iterator<UserSessionEntity> itr = cacheStream.filter(predicate)
.map(Mappers.userSessionEntity())
.iterator();
while (itr.hasNext()) { while (itr.hasNext()) {
UserSessionEntity e = (UserSessionEntity) itr.next().getValue(); UserSessionEntity userSessionEntity = itr.next();
sessions.add(wrap(realm, e, offline)); resultSessions.add(wrap(realm, userSessionEntity, offline));
} }
return sessions;
return resultSessions;
} }
@Override @Override
@ -175,65 +253,90 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
} }
protected List<UserSessionModel> getUserSessions(final RealmModel realm, ClientModel client, int firstResult, int maxResults, final boolean offline) { protected List<UserSessionModel> getUserSessions(final RealmModel realm, ClientModel client, int firstResult, int maxResults, final boolean offline) {
final Cache<String, SessionEntity> cache = getCache(offline); Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = getCache(offline);
cache = CacheDecorators.skipCacheLoaders(cache);
Stream<UserSessionEntity> stream = cache.entrySet().stream() Stream<UserSessionEntity> stream = cache.entrySet().stream()
.filter(UserSessionPredicate.create(realm.getId()).client(client.getId())) .filter(UserSessionPredicate.create(realm.getId()).client(client.getId()))
.map(Mappers.userSessionEntity()) .map(Mappers.userSessionEntity())
.sorted(Comparators.userSessionLastSessionRefresh()); .sorted(Comparators.userSessionLastSessionRefresh());
// Doesn't work due to ISPN-6575 . TODO Fix once infinispan upgraded to 8.2.2.Final or 9.0 if (firstResult > 0) {
// if (firstResult > 0) { stream = stream.skip(firstResult);
// stream = stream.skip(firstResult);
// }
//
// if (maxResults > 0) {
// stream = stream.limit(maxResults);
// }
//
// List<UserSessionEntity> entities = stream.collect(Collectors.toList());
// Workaround for ISPN-6575 TODO Fix once infinispan upgraded to 8.2.2.Final or 9.0 and replace with the more effective code above
if (firstResult < 0) {
firstResult = 0;
}
if (maxResults < 0) {
maxResults = Integer.MAX_VALUE;
} }
int count = firstResult + maxResults; if (maxResults > 0) {
if (count > 0) { stream = stream.limit(maxResults);
stream = stream.limit(count);
} }
List<UserSessionEntity> entities = stream.collect(Collectors.toList());
if (firstResult > entities.size()) {
return Collections.emptyList();
}
maxResults = Math.min(maxResults, entities.size() - firstResult);
entities = entities.subList(firstResult, firstResult + maxResults);
final List<UserSessionModel> sessions = new LinkedList<>(); final List<UserSessionModel> sessions = new LinkedList<>();
entities.stream().forEach(new Consumer<UserSessionEntity>() { Iterator<UserSessionEntity> itr = stream.iterator();
@Override
public void accept(UserSessionEntity userSessionEntity) { while (itr.hasNext()) {
sessions.add(wrap(realm, userSessionEntity, offline)); UserSessionEntity userSessionEntity = itr.next();
} sessions.add(wrap(realm, userSessionEntity, offline));
}); }
return sessions; return sessions;
} }
@Override
public UserSessionModel getUserSessionWithPredicate(RealmModel realm, String id, boolean offline, Predicate<UserSessionModel> predicate) {
UserSessionModel userSession = getUserSession(realm, id, offline);
if (userSession == null) {
return null;
}
// We have userSession, which passes predicate. No need for remote lookup.
if (predicate.test(userSession)) {
return userSession;
}
// Try lookup userSession from remoteCache
Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = getCache(offline);
RemoteCache remoteCache = InfinispanUtil.getRemoteCache(cache);
if (remoteCache != null) {
UserSessionEntity remoteSessionEntity = (UserSessionEntity) remoteCache.get(id);
if (remoteSessionEntity != null) {
UserSessionModel remoteSessionAdapter = wrap(realm, remoteSessionEntity, offline);
if (predicate.test(remoteSessionAdapter)) {
InfinispanChangelogBasedTransaction<UserSessionEntity> tx = getTransaction(offline);
// Remote entity contains our predicate. Update local cache with the remote entity
SessionEntityWrapper<UserSessionEntity> sessionWrapper = remoteSessionEntity.mergeRemoteEntityWithLocalEntity(tx.get(id));
// Replace entity just in ispn cache. Skip remoteStore
cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD, Flag.IGNORE_RETURN_VALUES)
.replace(id, sessionWrapper);
tx.reloadEntityInCurrentTransaction(realm, id, sessionWrapper);
// Recursion. We should have it locally now
return getUserSessionWithPredicate(realm, id, offline, predicate);
}
}
}
return null;
}
@Override @Override
public long getActiveUserSessions(RealmModel realm, ClientModel client) { public long getActiveUserSessions(RealmModel realm, ClientModel client) {
return getUserSessionsCount(realm, client, false); return getUserSessionsCount(realm, client, false);
} }
protected long getUserSessionsCount(RealmModel realm, ClientModel client, boolean offline) { protected long getUserSessionsCount(RealmModel realm, ClientModel client, boolean offline) {
return getCache(offline).entrySet().stream() Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = getCache(offline);
cache = CacheDecorators.skipCacheLoaders(cache);
return cache.entrySet().stream()
.filter(UserSessionPredicate.create(realm.getId()).client(client.getId())) .filter(UserSessionPredicate.create(realm.getId()).client(client.getId()))
.count(); .count();
} }
@ -242,7 +345,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
public void removeUserSession(RealmModel realm, UserSessionModel session) { public void removeUserSession(RealmModel realm, UserSessionModel session) {
UserSessionEntity entity = getUserSessionEntity(session, false); UserSessionEntity entity = getUserSessionEntity(session, false);
if (entity != null) { if (entity != null) {
removeUserSession(realm, entity, false); removeUserSession(entity, false);
} }
} }
@ -252,12 +355,15 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
} }
protected void removeUserSessions(RealmModel realm, UserModel user, boolean offline) { protected void removeUserSessions(RealmModel realm, UserModel user, boolean offline) {
Cache<String, SessionEntity> cache = getCache(offline); Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = getCache(offline);
cache = CacheDecorators.skipCacheLoaders(cache);
Iterator<UserSessionEntity> itr = cache.entrySet().stream().filter(UserSessionPredicate.create(realm.getId()).user(user.getId())).map(Mappers.userSessionEntity()).iterator();
Iterator<SessionEntity> itr = cache.entrySet().stream().filter(UserSessionPredicate.create(realm.getId()).user(user.getId())).map(Mappers.sessionEntity()).iterator();
while (itr.hasNext()) { while (itr.hasNext()) {
UserSessionEntity userSessionEntity = (UserSessionEntity) itr.next(); UserSessionEntity userSessionEntity = itr.next();
removeUserSession(realm, userSessionEntity, offline); removeUserSession(userSessionEntity, offline);
} }
} }
@ -273,17 +379,30 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
int expiredRefresh = Time.currentTime() - realm.getSsoSessionIdleTimeout(); int expiredRefresh = Time.currentTime() - realm.getSsoSessionIdleTimeout();
// Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account) // Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account)
Iterator<Map.Entry<String, SessionEntity>> itr = sessionCache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL) Cache<String, SessionEntityWrapper<UserSessionEntity>> localCache = CacheDecorators.localCache(sessionCache);
.entrySet().stream().filter(UserSessionPredicate.create(realm.getId()).expired(expired, expiredRefresh)).iterator();
int counter = 0; int[] counter = { 0 };
while (itr.hasNext()) {
counter++;
UserSessionEntity entity = (UserSessionEntity) itr.next().getValue();
tx.remove(sessionCache, entity.getId());
}
log.debugf("Removed %d expired user sessions for realm '%s'", counter, realm.getName()); Cache<String, SessionEntityWrapper<UserSessionEntity>> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache);
// Ignore remoteStore for stream iteration. But we will invoke remoteStore for userSession removal propagate
localCacheStoreIgnore
.entrySet()
.stream()
.filter(UserSessionPredicate.create(realm.getId()).expired(expired, expiredRefresh))
.map(Mappers.sessionId())
.forEach(new Consumer<String>() {
@Override
public void accept(String sessionId) {
counter[0]++;
tx.remove(localCache, sessionId);
}
});
log.debugf("Removed %d expired user sessions for realm '%s'", counter[0], realm.getName());
} }
private void removeExpiredOfflineUserSessions(RealmModel realm) { private void removeExpiredOfflineUserSessions(RealmModel realm) {
@ -291,38 +410,69 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
int expiredOffline = Time.currentTime() - realm.getOfflineSessionIdleTimeout(); int expiredOffline = Time.currentTime() - realm.getOfflineSessionIdleTimeout();
// Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account) // Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account)
Cache<String, SessionEntityWrapper<UserSessionEntity>> localCache = CacheDecorators.localCache(offlineSessionCache);
UserSessionPredicate predicate = UserSessionPredicate.create(realm.getId()).expired(null, expiredOffline); UserSessionPredicate predicate = UserSessionPredicate.create(realm.getId()).expired(null, expiredOffline);
Iterator<Map.Entry<String, SessionEntity>> itr = offlineSessionCache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL)
.entrySet().stream().filter(predicate).iterator();
int counter = 0; final int[] counter = { 0 };
while (itr.hasNext()) {
counter++;
UserSessionEntity entity = (UserSessionEntity) itr.next().getValue();
tx.remove(offlineSessionCache, entity.getId());
persister.removeUserSession(entity.getId(), true); Cache<String, SessionEntityWrapper<UserSessionEntity>> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache);
for (String clientUUID : entity.getAuthenticatedClientSessions().keySet()) { // Ignore remoteStore for stream iteration. But we will invoke remoteStore for userSession removal propagate
persister.removeClientSession(entity.getId(), clientUUID, true); localCacheStoreIgnore
} .entrySet()
} .stream()
.filter(predicate)
.map(Mappers.userSessionEntity())
.forEach(new Consumer<UserSessionEntity>() {
@Override
public void accept(UserSessionEntity userSessionEntity) {
counter[0]++;
tx.remove(localCache, userSessionEntity.getId());
// TODO:mposolda can be likely optimized to delete all expired at one step
persister.removeUserSession( userSessionEntity.getId(), true);
// TODO can be likely optimized to delete all at one step
for (String clientUUID : userSessionEntity.getAuthenticatedClientSessions().keySet()) {
persister.removeClientSession(userSessionEntity.getId(), clientUUID, true);
}
}
});
log.debugf("Removed %d expired offline user sessions for realm '%s'", counter, realm.getName()); log.debugf("Removed %d expired offline user sessions for realm '%s'", counter, realm.getName());
} }
@Override @Override
public void removeUserSessions(RealmModel realm) { public void removeUserSessions(RealmModel realm) {
removeUserSessions(realm, false); // Don't send message to all DCs, just to all cluster nodes in current DC. The remoteCache will notify client listeners for removed userSessions. This assumes that 2nd DC contains same userSessions like current one.
clusterEventsSenderTx.addEvent(InfinispanUserSessionProviderFactory.REMOVE_USER_SESSIONS_EVENT, RemoveUserSessionsEvent.create(realm.getId()), false);
} }
protected void removeUserSessions(RealmModel realm, boolean offline) { protected void onRemoveUserSessionsEvent(String realmId) {
Cache<String, SessionEntity> cache = getCache(offline); removeLocalUserSessions(realmId, false);
}
Iterator<String> itr = cache.entrySet().stream().filter(SessionPredicate.create(realm.getId())).map(Mappers.sessionId()).iterator(); private void removeLocalUserSessions(String realmId, boolean offline) {
while (itr.hasNext()) { Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = getCache(offline);
cache.remove(itr.next()); Cache<String, SessionEntityWrapper<UserSessionEntity>> localCache = CacheDecorators.localCache(cache);
}
Cache<String, SessionEntityWrapper<UserSessionEntity>> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache);
localCacheStoreIgnore
.entrySet()
.stream()
.filter(SessionPredicate.create(realmId))
.map(Mappers.sessionId())
.forEach(new Consumer<String>() {
@Override
public void accept(String sessionId) {
localCache.remove(sessionId);
}
});
} }
@Override @Override
@ -348,22 +498,48 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
@Override @Override
public void removeAllUserLoginFailures(RealmModel realm) { public void removeAllUserLoginFailures(RealmModel realm) {
Iterator<LoginFailureKey> itr = loginFailureCache.entrySet().stream().filter(UserLoginFailurePredicate.create(realm.getId())).map(Mappers.loginFailureId()).iterator(); clusterEventsSenderTx.addEvent(InfinispanUserSessionProviderFactory.REMOVE_ALL_LOGIN_FAILURES_EVENT, RemoveAllUserLoginFailuresEvent.create(realm.getId()), false);
}
protected void onRemoveAllUserLoginFailuresEvent(String realmId) {
removeAllLocalUserLoginFailuresEvent(realmId);
}
private void removeAllLocalUserLoginFailuresEvent(String realmId) {
Cache<LoginFailureKey, LoginFailureEntity> localCache = CacheDecorators.localCache(loginFailureCache);
Cache<LoginFailureKey, LoginFailureEntity> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache);
Iterator<LoginFailureKey> itr = localCacheStoreIgnore
.entrySet()
.stream()
.filter(UserLoginFailurePredicate.create(realmId))
.map(Mappers.loginFailureId())
.iterator();
while (itr.hasNext()) { while (itr.hasNext()) {
LoginFailureKey key = itr.next(); LoginFailureKey key = itr.next();
tx.remove(loginFailureCache, key); localCache.remove(key);
} }
} }
@Override @Override
public void onRealmRemoved(RealmModel realm) { public void onRealmRemoved(RealmModel realm) {
removeUserSessions(realm, true); clusterEventsSenderTx.addEvent(InfinispanUserSessionProviderFactory.REALM_REMOVED_SESSION_EVENT, RealmRemovedSessionEvent.create(realm.getId()), false);
removeUserSessions(realm, false); }
removeAllUserLoginFailures(realm);
protected void onRealmRemovedEvent(String realmId) {
removeLocalUserSessions(realmId, true);
removeLocalUserSessions(realmId, false);
removeAllLocalUserLoginFailuresEvent(realmId);
} }
@Override @Override
public void onClientRemoved(RealmModel realm, ClientModel client) { public void onClientRemoved(RealmModel realm, ClientModel client) {
clusterEventsSenderTx.addEvent(InfinispanUserSessionProviderFactory.CLIENT_REMOVED_SESSION_EVENT, ClientRemovedSessionEvent.create(realm.getId(), client.getId()), false);
}
protected void onClientRemovedEvent(String realmId, String clientUuid) {
// Nothing for now. userSession.getAuthenticatedClientSessions() will check lazily if particular client exists and update userSession on-the-fly. // Nothing for now. userSession.getAuthenticatedClientSessions() will check lazily if particular client exists and update userSession on-the-fly.
} }
@ -380,10 +556,29 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
public void close() { public void close() {
} }
protected void removeUserSession(RealmModel realm, UserSessionEntity sessionEntity, boolean offline) { protected void removeUserSession(UserSessionEntity sessionEntity, boolean offline) {
Cache<String, SessionEntity> cache = getCache(offline); InfinispanChangelogBasedTransaction<UserSessionEntity> tx = getTransaction(offline);
tx.remove(cache, sessionEntity.getId()); SessionUpdateTask<UserSessionEntity> removeTask = new SessionUpdateTask<UserSessionEntity>() {
@Override
public void runUpdate(UserSessionEntity entity) {
}
@Override
public CacheOperation getOperation(UserSessionEntity entity) {
return CacheOperation.REMOVE;
}
@Override
public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<UserSessionEntity> sessionWrapper) {
return CrossDCMessageStatus.SYNC;
}
};
tx.addTask(sessionEntity.getId(), removeTask);
} }
InfinispanKeycloakTransaction getTx() { InfinispanKeycloakTransaction getTx() {
@ -391,16 +586,8 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
} }
UserSessionAdapter wrap(RealmModel realm, UserSessionEntity entity, boolean offline) { UserSessionAdapter wrap(RealmModel realm, UserSessionEntity entity, boolean offline) {
Cache<String, SessionEntity> cache = getCache(offline); InfinispanChangelogBasedTransaction<UserSessionEntity> tx = getTransaction(offline);
return entity != null ? new UserSessionAdapter(session, this, cache, realm, entity, offline) : null; return entity != null ? new UserSessionAdapter(session, this, tx, realm, entity, offline) : null;
}
List<UserSessionModel> wrapUserSessions(RealmModel realm, Collection<UserSessionEntity> entities, boolean offline) {
List<UserSessionModel> models = new LinkedList<>();
for (UserSessionEntity e : entities) {
models.add(wrap(realm, e, offline));
}
return models;
} }
UserLoginFailureModel wrap(LoginFailureKey key, LoginFailureEntity entity) { UserLoginFailureModel wrap(LoginFailureKey key, LoginFailureEntity entity) {
@ -411,8 +598,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
if (userSession instanceof UserSessionAdapter) { if (userSession instanceof UserSessionAdapter) {
return ((UserSessionAdapter) userSession).getEntity(); return ((UserSessionAdapter) userSession).getEntity();
} else { } else {
Cache<String, SessionEntity> cache = getCache(offline); return getUserSessionEntity(userSession.getId(), offline);
return cache != null ? (UserSessionEntity) cache.get(userSession.getId()) : null;
} }
} }
@ -438,7 +624,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
public void removeOfflineUserSession(RealmModel realm, UserSessionModel userSession) { public void removeOfflineUserSession(RealmModel realm, UserSessionModel userSession) {
UserSessionEntity userSessionEntity = getUserSessionEntity(userSession, true); UserSessionEntity userSessionEntity = getUserSessionEntity(userSession, true);
if (userSessionEntity != null) { if (userSessionEntity != null) {
removeUserSession(realm, userSessionEntity, true); removeUserSession(userSessionEntity, true);
} }
} }
@ -449,7 +635,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
UserSessionAdapter userSessionAdapter = (offlineUserSession instanceof UserSessionAdapter) ? (UserSessionAdapter) offlineUserSession : UserSessionAdapter userSessionAdapter = (offlineUserSession instanceof UserSessionAdapter) ? (UserSessionAdapter) offlineUserSession :
getOfflineUserSession(offlineUserSession.getRealm(), offlineUserSession.getId()); getOfflineUserSession(offlineUserSession.getRealm(), offlineUserSession.getId());
AuthenticatedClientSessionAdapter offlineClientSession = importClientSession(userSessionAdapter, clientSession); AuthenticatedClientSessionAdapter offlineClientSession = importClientSession(userSessionAdapter, clientSession, getTransaction(true));
// update timestamp to current time // update timestamp to current time
offlineClientSession.setTimestamp(Time.currentTime()); offlineClientSession.setTimestamp(Time.currentTime());
@ -459,12 +645,18 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
@Override @Override
public List<UserSessionModel> getOfflineUserSessions(RealmModel realm, UserModel user) { public List<UserSessionModel> getOfflineUserSessions(RealmModel realm, UserModel user) {
Iterator<Map.Entry<String, SessionEntity>> itr = offlineSessionCache.entrySet().stream().filter(UserSessionPredicate.create(realm.getId()).user(user.getId())).iterator();
List<UserSessionModel> userSessions = new LinkedList<>(); List<UserSessionModel> userSessions = new LinkedList<>();
while(itr.hasNext()) { Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = CacheDecorators.skipCacheLoaders(offlineSessionCache);
UserSessionEntity entity = (UserSessionEntity) itr.next().getValue();
UserSessionModel userSession = wrap(realm, entity, true); Iterator<UserSessionEntity> itr = cache.entrySet().stream()
.filter(UserSessionPredicate.create(realm.getId()).user(user.getId()))
.map(Mappers.userSessionEntity())
.iterator();
while (itr.hasNext()) {
UserSessionEntity userSessionEntity = itr.next();
UserSessionModel userSession = wrap(realm, userSessionEntity, true);
userSessions.add(userSession); userSessions.add(userSession);
} }
@ -492,7 +684,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
entity.setBrokerUserId(userSession.getBrokerUserId()); entity.setBrokerUserId(userSession.getBrokerUserId());
entity.setIpAddress(userSession.getIpAddress()); entity.setIpAddress(userSession.getIpAddress());
entity.setLoginUsername(userSession.getLoginUsername()); entity.setLoginUsername(userSession.getLoginUsername());
entity.setNotes(userSession.getNotes()== null ? new ConcurrentHashMap<>() : userSession.getNotes()); entity.setNotes(userSession.getNotes() == null ? new ConcurrentHashMap<>() : userSession.getNotes());
entity.setAuthenticatedClientSessions(new ConcurrentHashMap<>()); entity.setAuthenticatedClientSessions(new ConcurrentHashMap<>());
entity.setRememberMe(userSession.isRememberMe()); entity.setRememberMe(userSession.isRememberMe());
entity.setState(userSession.getState()); entity.setState(userSession.getState());
@ -502,14 +694,34 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
entity.setLastSessionRefresh(userSession.getLastSessionRefresh()); entity.setLastSessionRefresh(userSession.getLastSessionRefresh());
Cache<String, SessionEntity> cache = getCache(offline); InfinispanChangelogBasedTransaction<UserSessionEntity> tx = getTransaction(offline);
tx.put(cache, userSession.getId(), entity);
SessionUpdateTask importTask = new SessionUpdateTask<UserSessionEntity>() {
@Override
public void runUpdate(UserSessionEntity session) {
}
@Override
public CacheOperation getOperation(UserSessionEntity session) {
return CacheOperation.ADD_IF_ABSENT;
}
@Override
public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<UserSessionEntity> sessionWrapper) {
return CrossDCMessageStatus.SYNC;
}
};
tx.addTask(userSession.getId(), importTask, entity);
UserSessionAdapter importedSession = wrap(userSession.getRealm(), entity, offline); UserSessionAdapter importedSession = wrap(userSession.getRealm(), entity, offline);
// Handle client sessions // Handle client sessions
if (importAuthenticatedClientSessions) { if (importAuthenticatedClientSessions) {
for (AuthenticatedClientSessionModel clientSession : userSession.getAuthenticatedClientSessions().values()) { for (AuthenticatedClientSessionModel clientSession : userSession.getAuthenticatedClientSessions().values()) {
importClientSession(importedSession, clientSession); importClientSession(importedSession, clientSession, tx);
} }
} }
@ -517,25 +729,46 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
} }
private AuthenticatedClientSessionAdapter importClientSession(UserSessionAdapter importedUserSession, AuthenticatedClientSessionModel clientSession) { private AuthenticatedClientSessionAdapter importClientSession(UserSessionAdapter importedUserSession, AuthenticatedClientSessionModel clientSession,
InfinispanChangelogBasedTransaction<UserSessionEntity> updateTx) {
AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity(); AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity();
entity.setAction(clientSession.getAction()); entity.setAction(clientSession.getAction());
entity.setAuthMethod(clientSession.getProtocol()); entity.setAuthMethod(clientSession.getProtocol());
entity.setNotes(clientSession.getNotes()); entity.setNotes(clientSession.getNotes() == null ? new ConcurrentHashMap<>() : clientSession.getNotes());
entity.setProtocolMappers(clientSession.getProtocolMappers()); entity.setProtocolMappers(clientSession.getProtocolMappers());
entity.setRedirectUri(clientSession.getRedirectUri()); entity.setRedirectUri(clientSession.getRedirectUri());
entity.setRoles(clientSession.getRoles()); entity.setRoles(clientSession.getRoles());
entity.setTimestamp(clientSession.getTimestamp()); entity.setTimestamp(clientSession.getTimestamp());
Map<String, AuthenticatedClientSessionEntity> clientSessions = importedUserSession.getEntity().getAuthenticatedClientSessions(); Map<String, AuthenticatedClientSessionEntity> clientSessions = importedUserSession.getEntity().getAuthenticatedClientSessions();
clientSessions.put(clientSession.getClient().getId(), entity); clientSessions.put(clientSession.getClient().getId(), entity);
importedUserSession.update(); SessionUpdateTask importTask = new SessionUpdateTask<UserSessionEntity>() {
return new AuthenticatedClientSessionAdapter(entity, clientSession.getClient(), importedUserSession, this, importedUserSession.getCache()); @Override
public void runUpdate(UserSessionEntity session) {
Map<String, AuthenticatedClientSessionEntity> clientSessions = session.getAuthenticatedClientSessions();
clientSessions.put(clientSession.getClient().getId(), entity);
}
@Override
public CacheOperation getOperation(UserSessionEntity session) {
return CacheOperation.REPLACE;
}
@Override
public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<UserSessionEntity> sessionWrapper) {
return CrossDCMessageStatus.SYNC;
}
};
updateTx.addTask(importedUserSession.getId(), importTask);
return new AuthenticatedClientSessionAdapter(entity, clientSession.getClient(), importedUserSession, this, updateTx);
} }
} }

View file

@ -18,41 +18,76 @@
package org.keycloak.models.sessions.infinispan; package org.keycloak.models.sessions.infinispan;
import org.infinispan.Cache; import org.infinispan.Cache;
import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.persistence.remote.RemoteStore;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakSessionTask; import org.keycloak.models.KeycloakSessionTask;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionProvider; import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.UserSessionProviderFactory; import org.keycloak.models.UserSessionProviderFactory;
import org.keycloak.models.sessions.infinispan.changes.sessions.LastSessionRefreshStore;
import org.keycloak.models.sessions.infinispan.changes.sessions.LastSessionRefreshStoreFactory;
import org.keycloak.models.sessions.infinispan.initializer.BaseCacheInitializer;
import org.keycloak.models.sessions.infinispan.initializer.CacheInitializer;
import org.keycloak.models.sessions.infinispan.initializer.DBLockBasedCacheInitializer;
import org.keycloak.models.sessions.infinispan.initializer.SingleWorkerCacheInitializer;
import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity; import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey; import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity; import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import org.keycloak.models.sessions.infinispan.initializer.InfinispanUserSessionInitializer; import org.keycloak.models.sessions.infinispan.events.AbstractUserSessionClusterListener;
import org.keycloak.models.sessions.infinispan.initializer.OfflineUserSessionLoader; import org.keycloak.models.sessions.infinispan.events.ClientRemovedSessionEvent;
import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent;
import org.keycloak.models.sessions.infinispan.events.RemoveAllUserLoginFailuresEvent;
import org.keycloak.models.sessions.infinispan.events.RemoveUserSessionsEvent;
import org.keycloak.models.sessions.infinispan.initializer.InfinispanCacheInitializer;
import org.keycloak.models.sessions.infinispan.initializer.OfflinePersistentUserSessionLoader;
import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheSessionListener;
import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheSessionsLoader;
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.PostMigrationEvent; import org.keycloak.models.utils.PostMigrationEvent;
import org.keycloak.provider.ProviderEvent; import org.keycloak.provider.ProviderEvent;
import org.keycloak.provider.ProviderEventListener; import org.keycloak.provider.ProviderEventListener;
import java.io.Serializable; import java.io.Serializable;
import java.util.Set;
public class InfinispanUserSessionProviderFactory implements UserSessionProviderFactory { public class InfinispanUserSessionProviderFactory implements UserSessionProviderFactory {
private static final Logger log = Logger.getLogger(InfinispanUserSessionProviderFactory.class); private static final Logger log = Logger.getLogger(InfinispanUserSessionProviderFactory.class);
public static final String PROVIDER_ID = "infinispan";
public static final String REALM_REMOVED_SESSION_EVENT = "REALM_REMOVED_EVENT_SESSIONS";
public static final String CLIENT_REMOVED_SESSION_EVENT = "CLIENT_REMOVED_SESSION_SESSIONS";
public static final String REMOVE_USER_SESSIONS_EVENT = "REMOVE_USER_SESSIONS_EVENT";
public static final String REMOVE_ALL_LOGIN_FAILURES_EVENT = "REMOVE_ALL_LOGIN_FAILURES_EVENT";
private Config.Scope config; private Config.Scope config;
private RemoteCacheInvoker remoteCacheInvoker;
private LastSessionRefreshStore lastSessionRefreshStore;
private LastSessionRefreshStore offlineLastSessionRefreshStore;
@Override @Override
public InfinispanUserSessionProvider create(KeycloakSession session) { public InfinispanUserSessionProvider create(KeycloakSession session) {
InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class); InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
Cache<String, SessionEntity> cache = connections.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME); Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = connections.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
Cache<String, SessionEntity> offlineSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME); Cache<String, SessionEntityWrapper<UserSessionEntity>> offlineSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME);
Cache<LoginFailureKey, LoginFailureEntity> loginFailures = connections.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME); Cache<LoginFailureKey, LoginFailureEntity> loginFailures = connections.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME);
return new InfinispanUserSessionProvider(session, cache, offlineSessionsCache, loginFailures); return new InfinispanUserSessionProvider(session, remoteCacheInvoker, lastSessionRefreshStore, offlineLastSessionRefreshStore, cache, offlineSessionsCache, loginFailures);
} }
@Override @Override
@ -62,18 +97,19 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
@Override @Override
public void postInit(final KeycloakSessionFactory factory) { public void postInit(final KeycloakSessionFactory factory) {
// Max count of worker errors. Initialization will end with exception when this number is reached
final int maxErrors = config.getInt("maxErrors", 20);
// Count of sessions to be computed in each segment
final int sessionsPerSegment = config.getInt("sessionsPerSegment", 100);
factory.register(new ProviderEventListener() { factory.register(new ProviderEventListener() {
@Override @Override
public void onEvent(ProviderEvent event) { public void onEvent(ProviderEvent event) {
if (event instanceof PostMigrationEvent) { if (event instanceof PostMigrationEvent) {
loadPersistentSessions(factory, maxErrors, sessionsPerSegment); KeycloakSession session = ((PostMigrationEvent) event).getSession();
checkRemoteCaches(session);
loadPersistentSessions(factory, getMaxErrors(), getSessionsPerSegment());
registerClusterListeners(session);
loadSessionsFromRemoteCaches(session);
} else if (event instanceof UserModel.UserRemovedEvent) { } else if (event instanceof UserModel.UserRemovedEvent) {
UserModel.UserRemovedEvent userRemovedEvent = (UserModel.UserRemovedEvent) event; UserModel.UserRemovedEvent userRemovedEvent = (UserModel.UserRemovedEvent) event;
@ -84,35 +120,169 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
}); });
} }
// Max count of worker errors. Initialization will end with exception when this number is reached
private int getMaxErrors() {
return config.getInt("maxErrors", 20);
}
// Count of sessions to be computed in each segment
private int getSessionsPerSegment() {
return config.getInt("sessionsPerSegment", 100);
}
@Override @Override
public void loadPersistentSessions(final KeycloakSessionFactory sessionFactory, final int maxErrors, final int sessionsPerSegment) { public void loadPersistentSessions(final KeycloakSessionFactory sessionFactory, final int maxErrors, final int sessionsPerSegment) {
log.debug("Start pre-loading userSessions and clientSessions from persistent storage"); log.debug("Start pre-loading userSessions from persistent storage");
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() { KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
@Override @Override
public void run(KeycloakSession session) { public void run(KeycloakSession session) {
InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class); InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
Cache<String, Serializable> cache = connections.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME); Cache<String, Serializable> workCache = connections.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME);
InfinispanCacheInitializer ispnInitializer = new InfinispanCacheInitializer(sessionFactory, workCache, new OfflinePersistentUserSessionLoader(), "offlineUserSessions", sessionsPerSegment, maxErrors);
// DB-lock to ensure that persistent sessions are loaded from DB just on one DC. The other DCs will load them from remote cache.
CacheInitializer initializer = new DBLockBasedCacheInitializer(session, ispnInitializer);
InfinispanUserSessionInitializer initializer = new InfinispanUserSessionInitializer(sessionFactory, cache, new OfflineUserSessionLoader(), maxErrors, sessionsPerSegment, "offlineUserSessions");
initializer.initCache(); initializer.initCache();
initializer.loadPersistentSessions(); initializer.loadSessions();
} }
}); });
log.debug("Pre-loading userSessions and clientSessions from persistent storage finished"); log.debug("Pre-loading userSessions from persistent storage finished");
} }
protected void registerClusterListeners(KeycloakSession session) {
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
cluster.registerListener(REALM_REMOVED_SESSION_EVENT, new AbstractUserSessionClusterListener<RealmRemovedSessionEvent>(sessionFactory) {
@Override
protected void eventReceived(KeycloakSession session, InfinispanUserSessionProvider provider, RealmRemovedSessionEvent sessionEvent) {
provider.onRealmRemovedEvent(sessionEvent.getRealmId());
}
});
cluster.registerListener(CLIENT_REMOVED_SESSION_EVENT, new AbstractUserSessionClusterListener<ClientRemovedSessionEvent>(sessionFactory) {
@Override
protected void eventReceived(KeycloakSession session, InfinispanUserSessionProvider provider, ClientRemovedSessionEvent sessionEvent) {
provider.onClientRemovedEvent(sessionEvent.getRealmId(), sessionEvent.getClientUuid());
}
});
cluster.registerListener(REMOVE_USER_SESSIONS_EVENT, new AbstractUserSessionClusterListener<RemoveUserSessionsEvent>(sessionFactory) {
@Override
protected void eventReceived(KeycloakSession session, InfinispanUserSessionProvider provider, RemoveUserSessionsEvent sessionEvent) {
provider.onRemoveUserSessionsEvent(sessionEvent.getRealmId());
}
});
cluster.registerListener(REMOVE_ALL_LOGIN_FAILURES_EVENT, new AbstractUserSessionClusterListener<RemoveAllUserLoginFailuresEvent>(sessionFactory) {
@Override
protected void eventReceived(KeycloakSession session, InfinispanUserSessionProvider provider, RemoveAllUserLoginFailuresEvent sessionEvent) {
provider.onRemoveAllUserLoginFailuresEvent(sessionEvent.getRealmId());
}
});
log.debug("Registered cluster listeners");
}
protected void checkRemoteCaches(KeycloakSession session) {
this.remoteCacheInvoker = new RemoteCacheInvoker();
InfinispanConnectionProvider ispn = session.getProvider(InfinispanConnectionProvider.class);
Cache sessionsCache = ispn.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
boolean sessionsRemoteCache = checkRemoteCache(session, sessionsCache, (RealmModel realm) -> {
return realm.getSsoSessionIdleTimeout() * 1000;
});
if (sessionsRemoteCache) {
lastSessionRefreshStore = new LastSessionRefreshStoreFactory().createAndInit(session, sessionsCache, false);
}
Cache offlineSessionsCache = ispn.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME);
boolean offlineSessionsRemoteCache = checkRemoteCache(session, offlineSessionsCache, (RealmModel realm) -> {
return realm.getOfflineSessionIdleTimeout() * 1000;
});
if (offlineSessionsRemoteCache) {
offlineLastSessionRefreshStore = new LastSessionRefreshStoreFactory().createAndInit(session, offlineSessionsCache, true);
}
}
private boolean checkRemoteCache(KeycloakSession session, Cache ispnCache, RemoteCacheInvoker.MaxIdleTimeLoader maxIdleLoader) {
Set<RemoteStore> remoteStores = InfinispanUtil.getRemoteStores(ispnCache);
if (remoteStores.isEmpty()) {
log.debugf("No remote store configured for cache '%s'", ispnCache.getName());
return false;
} else {
log.infof("Remote store configured for cache '%s'", ispnCache.getName());
RemoteCache remoteCache = remoteStores.iterator().next().getRemoteCache();
remoteCacheInvoker.addRemoteCache(ispnCache.getName(), remoteCache, maxIdleLoader);
RemoteCacheSessionListener hotrodListener = RemoteCacheSessionListener.createListener(session, ispnCache, remoteCache);
remoteCache.addClientListener(hotrodListener);
return true;
}
}
private void loadSessionsFromRemoteCaches(KeycloakSession session) {
for (String cacheName : remoteCacheInvoker.getRemoteCacheNames()) {
loadSessionsFromRemoteCache(session.getKeycloakSessionFactory(), cacheName, getMaxErrors());
}
}
private void loadSessionsFromRemoteCache(final KeycloakSessionFactory sessionFactory, String cacheName, final int maxErrors) {
log.debugf("Check pre-loading userSessions from remote cache '%s'", cacheName);
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
@Override
public void run(KeycloakSession session) {
InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
Cache<String, Serializable> workCache = connections.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME);
// Use limit for sessionsPerSegment as RemoteCache bulk load doesn't have support for pagination :/
BaseCacheInitializer initializer = new SingleWorkerCacheInitializer(session, workCache, new RemoteCacheSessionsLoader(cacheName), "remoteCacheLoad::" + cacheName);
initializer.initCache();
initializer.loadSessions();
}
});
log.debugf("Pre-loading userSessions from remote cache '%s' finished", cacheName);
}
@Override @Override
public void close() { public void close() {
} }
@Override @Override
public String getId() { public String getId() {
return "infinispan"; return PROVIDER_ID;
} }
} }

View file

@ -17,15 +17,17 @@
package org.keycloak.models.sessions.infinispan; package org.keycloak.models.sessions.infinispan;
import org.infinispan.Cache;
import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.sessions.infinispan.changes.InfinispanChangelogBasedTransaction;
import org.keycloak.models.sessions.infinispan.changes.sessions.LastSessionRefreshChecker;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.changes.UserSessionUpdateTask;
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import java.util.Collections; import java.util.Collections;
@ -33,7 +35,6 @@ import java.util.HashMap;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -44,7 +45,7 @@ public class UserSessionAdapter implements UserSessionModel {
private final InfinispanUserSessionProvider provider; private final InfinispanUserSessionProvider provider;
private final Cache<String, SessionEntity> cache; private final InfinispanChangelogBasedTransaction updateTx;
private final RealmModel realm; private final RealmModel realm;
@ -52,11 +53,11 @@ public class UserSessionAdapter implements UserSessionModel {
private final boolean offline; private final boolean offline;
public UserSessionAdapter(KeycloakSession session, InfinispanUserSessionProvider provider, Cache<String, SessionEntity> cache, RealmModel realm, public UserSessionAdapter(KeycloakSession session, InfinispanUserSessionProvider provider, InfinispanChangelogBasedTransaction updateTx, RealmModel realm,
UserSessionEntity entity, boolean offline) { UserSessionEntity entity, boolean offline) {
this.session = session; this.session = session;
this.provider = provider; this.provider = provider;
this.cache = cache; this.updateTx = updateTx;
this.realm = realm; this.realm = realm;
this.entity = entity; this.entity = entity;
this.offline = offline; this.offline = offline;
@ -74,7 +75,7 @@ public class UserSessionAdapter implements UserSessionModel {
// Check if client still exists // Check if client still exists
ClientModel client = realm.getClientById(key); ClientModel client = realm.getClientById(key);
if (client != null) { if (client != null) {
result.put(key, new AuthenticatedClientSessionAdapter(value, client, this, provider, cache)); result.put(key, new AuthenticatedClientSessionAdapter(value, client, this, provider, updateTx));
} else { } else {
removedClientUUIDS.add(key); removedClientUUIDS.add(key);
} }
@ -83,10 +84,18 @@ public class UserSessionAdapter implements UserSessionModel {
// Update user session // Update user session
if (!removedClientUUIDS.isEmpty()) { if (!removedClientUUIDS.isEmpty()) {
for (String clientUUID : removedClientUUIDS) { UserSessionUpdateTask task = new UserSessionUpdateTask() {
entity.getAuthenticatedClientSessions().remove(clientUUID);
} @Override
update(); public void runUpdate(UserSessionEntity entity) {
for (String clientUUID : removedClientUUIDS) {
entity.getAuthenticatedClientSessions().remove(clientUUID);
}
}
};
update(task);
} }
return Collections.unmodifiableMap(result); return Collections.unmodifiableMap(result);
@ -114,12 +123,6 @@ public class UserSessionAdapter implements UserSessionModel {
return session.users().getUserById(entity.getUser(), realm); return session.users().getUserById(entity.getUser(), realm);
} }
@Override
public void setUser(UserModel user) {
entity.setUser(user.getId());
update();
}
@Override @Override
public String getLoginUsername() { public String getLoginUsername() {
return entity.getLoginUsername(); return entity.getLoginUsername();
@ -148,8 +151,21 @@ public class UserSessionAdapter implements UserSessionModel {
} }
public void setLastSessionRefresh(int lastSessionRefresh) { public void setLastSessionRefresh(int lastSessionRefresh) {
entity.setLastSessionRefresh(lastSessionRefresh); UserSessionUpdateTask task = new UserSessionUpdateTask() {
update();
@Override
public void runUpdate(UserSessionEntity entity) {
entity.setLastSessionRefresh(lastSessionRefresh);
}
@Override
public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<UserSessionEntity> sessionWrapper) {
return new LastSessionRefreshChecker(provider.getLastSessionRefreshStore(), provider.getOfflineLastSessionRefreshStore())
.getCrossDCMessageStatus(UserSessionAdapter.this.session, UserSessionAdapter.this.realm, sessionWrapper, offline, lastSessionRefresh);
}
};
update(task);
} }
@Override @Override
@ -159,22 +175,36 @@ public class UserSessionAdapter implements UserSessionModel {
@Override @Override
public void setNote(String name, String value) { public void setNote(String name, String value) {
if (value == null) { UserSessionUpdateTask task = new UserSessionUpdateTask() {
if (entity.getNotes().containsKey(name)) {
removeNote(name); @Override
public void runUpdate(UserSessionEntity entity) {
if (value == null) {
if (entity.getNotes().containsKey(name)) {
removeNote(name);
}
return;
}
entity.getNotes().put(name, value);
} }
return;
} };
entity.getNotes().put(name, value);
update(); update(task);
} }
@Override @Override
public void removeNote(String name) { public void removeNote(String name) {
if (entity.getNotes() != null) { UserSessionUpdateTask task = new UserSessionUpdateTask() {
entity.getNotes().remove(name);
update(); @Override
} public void runUpdate(UserSessionEntity entity) {
entity.getNotes().remove(name);
}
};
update(task);
} }
@Override @Override
@ -189,19 +219,34 @@ public class UserSessionAdapter implements UserSessionModel {
@Override @Override
public void setState(State state) { public void setState(State state) {
entity.setState(state); UserSessionUpdateTask task = new UserSessionUpdateTask() {
update();
@Override
public void runUpdate(UserSessionEntity entity) {
entity.setState(state);
}
};
update(task);
} }
@Override @Override
public void restartSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) { public void restartSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) {
provider.updateSessionEntity(entity, realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, brokerUserId); UserSessionUpdateTask task = new UserSessionUpdateTask() {
entity.setState(null); @Override
entity.getNotes().clear(); public void runUpdate(UserSessionEntity entity) {
entity.getAuthenticatedClientSessions().clear(); provider.updateSessionEntity(entity, realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, brokerUserId);
update(); entity.setState(null);
entity.getNotes().clear();
entity.getAuthenticatedClientSessions().clear();
}
};
update(task);
} }
@Override @Override
@ -222,11 +267,8 @@ public class UserSessionAdapter implements UserSessionModel {
return entity; return entity;
} }
void update() { void update(UserSessionUpdateTask task) {
provider.getTx().replace(cache, entity.getId(), entity); updateTx.addTask(getId(), task);
} }
Cache<String, SessionEntity> getCache() {
return cache;
}
} }

View file

@ -0,0 +1,233 @@
/*
* 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.models.sessions.infinispan.changes;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.infinispan.Cache;
import org.infinispan.context.Flag;
import org.jboss.logging.Logger;
import org.keycloak.models.AbstractKeycloakTransaction;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class InfinispanChangelogBasedTransaction<S extends SessionEntity> extends AbstractKeycloakTransaction {
public static final Logger logger = Logger.getLogger(InfinispanChangelogBasedTransaction.class);
private final KeycloakSession kcSession;
private final String cacheName;
private final Cache<String, SessionEntityWrapper<S>> cache;
private final RemoteCacheInvoker remoteCacheInvoker;
private final Map<String, SessionUpdatesList<S>> updates = new HashMap<>();
public InfinispanChangelogBasedTransaction(KeycloakSession kcSession, String cacheName, Cache<String, SessionEntityWrapper<S>> cache, RemoteCacheInvoker remoteCacheInvoker) {
this.kcSession = kcSession;
this.cacheName = cacheName;
this.cache = cache;
this.remoteCacheInvoker = remoteCacheInvoker;
}
public void addTask(String key, SessionUpdateTask<S> task) {
SessionUpdatesList<S> myUpdates = updates.get(key);
if (myUpdates == null) {
// Lookup entity from cache
SessionEntityWrapper<S> wrappedEntity = cache.get(key);
if (wrappedEntity == null) {
logger.warnf("Not present cache item for key %s", key);
return;
}
RealmModel realm = kcSession.realms().getRealm(wrappedEntity.getEntity().getRealm());
myUpdates = new SessionUpdatesList<>(realm, wrappedEntity);
updates.put(key, myUpdates);
}
// Run the update now, so reader in same transaction can see it (TODO: Rollback may not work correctly. See if it's an issue..)
task.runUpdate(myUpdates.getEntityWrapper().getEntity());
myUpdates.add(task);
}
// Create entity and new version for it
public void addTask(String key, SessionUpdateTask<S> task, S entity) {
if (entity == null) {
throw new IllegalArgumentException("Null entity not allowed");
}
RealmModel realm = kcSession.realms().getRealm(entity.getRealm());
SessionEntityWrapper<S> wrappedEntity = new SessionEntityWrapper<>(entity);
SessionUpdatesList<S> myUpdates = new SessionUpdatesList<>(realm, wrappedEntity);
updates.put(key, myUpdates);
// Run the update now, so reader in same transaction can see it
task.runUpdate(entity);
myUpdates.add(task);
}
public void reloadEntityInCurrentTransaction(RealmModel realm, String key, SessionEntityWrapper<S> entity) {
if (entity == null) {
throw new IllegalArgumentException("Null entity not allowed");
}
SessionEntityWrapper<S> latestEntity = cache.get(key);
if (latestEntity == null) {
return;
}
SessionUpdatesList<S> newUpdates = new SessionUpdatesList<>(realm, latestEntity);
SessionUpdatesList<S> existingUpdates = updates.get(key);
if (existingUpdates != null) {
newUpdates.setUpdateTasks(existingUpdates.getUpdateTasks());
}
updates.put(key, newUpdates);
}
public SessionEntityWrapper<S> get(String key) {
SessionUpdatesList<S> myUpdates = updates.get(key);
if (myUpdates == null) {
SessionEntityWrapper<S> wrappedEntity = cache.get(key);
if (wrappedEntity == null) {
return null;
}
RealmModel realm = kcSession.realms().getRealm(wrappedEntity.getEntity().getRealm());
myUpdates = new SessionUpdatesList<>(realm, wrappedEntity);
updates.put(key, myUpdates);
return wrappedEntity;
} else {
return myUpdates.getEntityWrapper();
}
}
@Override
protected void commitImpl() {
for (Map.Entry<String, SessionUpdatesList<S>> entry : updates.entrySet()) {
SessionUpdatesList<S> sessionUpdates = entry.getValue();
SessionEntityWrapper<S> sessionWrapper = sessionUpdates.getEntityWrapper();
RealmModel realm = sessionUpdates.getRealm();
MergedUpdate<S> merged = MergedUpdate.computeUpdate(sessionUpdates.getUpdateTasks(), sessionWrapper);
if (merged != null) {
// Now run the operation in our cluster
runOperationInCluster(entry.getKey(), merged, sessionWrapper);
// Check if we need to send message to second DC
remoteCacheInvoker.runTask(kcSession, realm, cacheName, entry.getKey(), merged, sessionWrapper);
}
}
}
private void runOperationInCluster(String key, MergedUpdate<S> task, SessionEntityWrapper<S> sessionWrapper) {
S session = sessionWrapper.getEntity();
SessionUpdateTask.CacheOperation operation = task.getOperation(session);
// Don't need to run update of underlying entity. Local updates were already run
//task.runUpdate(session);
switch (operation) {
case REMOVE:
// Just remove it
cache
.getAdvancedCache().withFlags(Flag.IGNORE_RETURN_VALUES)
.remove(key);
break;
case ADD:
cache
.getAdvancedCache().withFlags(Flag.IGNORE_RETURN_VALUES)
.put(key, sessionWrapper, task.getLifespanMs(), TimeUnit.MILLISECONDS);
break;
case ADD_IF_ABSENT:
SessionEntityWrapper existing = cache.putIfAbsent(key, sessionWrapper);
if (existing != null) {
throw new IllegalStateException("There is already existing value in cache for key " + key);
}
break;
case REPLACE:
replace(key, task, sessionWrapper);
break;
default:
throw new IllegalStateException("Unsupported state " + operation);
}
}
private void replace(String key, MergedUpdate<S> task, SessionEntityWrapper<S> oldVersionEntity) {
boolean replaced = false;
S session = oldVersionEntity.getEntity();
while (!replaced) {
SessionEntityWrapper<S> newVersionEntity = generateNewVersionAndWrapEntity(session, oldVersionEntity.getLocalMetadata());
// Atomic cluster-aware replace
replaced = cache.replace(key, oldVersionEntity, newVersionEntity);
// Replace fail. Need to load latest entity from cache, apply updates again and try to replace in cache again
if (!replaced) {
logger.debugf("Replace failed for entity: %s . Will try again", key);
oldVersionEntity = cache.get(key);
if (oldVersionEntity == null) {
logger.debugf("Entity %s not found. Maybe removed in the meantime. Replace task will be ignored", key);
return;
}
session = oldVersionEntity.getEntity();
task.runUpdate(session);
} else {
if (logger.isTraceEnabled()) {
logger.tracef("Replace SUCCESS for entity: %s . old version: %d, new version: %d", key, oldVersionEntity.getVersion(), newVersionEntity.getVersion());
}
}
}
}
@Override
protected void rollbackImpl() {
}
private SessionEntityWrapper<S> generateNewVersionAndWrapEntity(S entity, Map<String, String> localMetadata) {
return new SessionEntityWrapper<>(localMetadata, entity);
}
}

View file

@ -0,0 +1,99 @@
/*
* 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.models.sessions.infinispan.changes;
import java.util.LinkedList;
import java.util.List;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
class MergedUpdate<S extends SessionEntity> implements SessionUpdateTask<S> {
private List<SessionUpdateTask<S>> childUpdates = new LinkedList<>();
private CacheOperation operation;
private CrossDCMessageStatus crossDCMessageStatus;
public MergedUpdate(CacheOperation operation, CrossDCMessageStatus crossDCMessageStatus) {
this.operation = operation;
this.crossDCMessageStatus = crossDCMessageStatus;
}
@Override
public void runUpdate(S session) {
for (SessionUpdateTask<S> child : childUpdates) {
child.runUpdate(session);
}
}
@Override
public CacheOperation getOperation(S session) {
return operation;
}
@Override
public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<S> sessionWrapper) {
return crossDCMessageStatus;
}
public static <S extends SessionEntity> MergedUpdate<S> computeUpdate(List<SessionUpdateTask<S>> childUpdates, SessionEntityWrapper<S> sessionWrapper) {
if (childUpdates == null || childUpdates.isEmpty()) {
return null;
}
MergedUpdate<S> result = null;
S session = sessionWrapper.getEntity();
for (SessionUpdateTask<S> child : childUpdates) {
if (result == null) {
result = new MergedUpdate<>(child.getOperation(session), child.getCrossDCMessageStatus(sessionWrapper));
result.childUpdates.add(child);
} else {
// Merge the operations. REMOVE is special case as other operations are not needed then.
CacheOperation mergedOp = result.getOperation(session).merge(child.getOperation(session), session);
if (mergedOp == CacheOperation.REMOVE) {
result = new MergedUpdate<>(child.getOperation(session), child.getCrossDCMessageStatus(sessionWrapper));
result.childUpdates.add(child);
return result;
}
result.operation = mergedOp;
// Check if we need to send message to other DCs and how critical it is
CrossDCMessageStatus currentDCStatus = result.getCrossDCMessageStatus(sessionWrapper);
// Optimization. If we already have SYNC, we don't need to retrieve childDCStatus
if (currentDCStatus != CrossDCMessageStatus.SYNC) {
CrossDCMessageStatus childDCStatus = child.getCrossDCMessageStatus(sessionWrapper);
result.crossDCMessageStatus = currentDCStatus.merge(childDCStatus);
}
// Finally add another update to the result
result.childUpdates.add(child);
}
}
return result;
}
}

View file

@ -0,0 +1,151 @@
/*
* 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.models.sessions.infinispan.changes;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import org.infinispan.commons.marshall.Externalizer;
import org.infinispan.commons.marshall.MarshallUtil;
import org.infinispan.commons.marshall.SerializeWith;
import org.keycloak.models.sessions.infinispan.changes.sessions.SessionData;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@SerializeWith(SessionEntityWrapper.ExternalizerImpl.class)
public class SessionEntityWrapper<S extends SessionEntity> {
private UUID version;
private final S entity;
private final Map<String, String> localMetadata;
protected SessionEntityWrapper(UUID version, Map<String, String> localMetadata, S entity) {
if (version == null) {
throw new IllegalArgumentException("Version UUID can't be null");
}
this.version = version;
this.localMetadata = localMetadata;
this.entity = entity;
}
public SessionEntityWrapper(Map<String, String> localMetadata, S entity) {
this(UUID.randomUUID(),localMetadata, entity);
}
public SessionEntityWrapper(S entity) {
this(new ConcurrentHashMap<>(), entity);
}
public UUID getVersion() {
return version;
}
public void setVersion(UUID version) {
this.version = version;
}
public S getEntity() {
return entity;
}
public String getLocalMetadataNote(String key) {
return localMetadata.get(key);
}
public void putLocalMetadataNote(String key, String value) {
localMetadata.put(key, value);
}
public Integer getLocalMetadataNoteInt(String key) {
String note = getLocalMetadataNote(key);
return note==null ? null : Integer.parseInt(note);
}
public void putLocalMetadataNoteInt(String key, int value) {
localMetadata.put(key, String.valueOf(value));
}
public Map<String, String> getLocalMetadata() {
return localMetadata;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof SessionEntityWrapper)) return false;
SessionEntityWrapper that = (SessionEntityWrapper) o;
if (!Objects.equals(version, that.version)) {
return false;
}
return Objects.equals(entity, that.entity);
}
@Override
public int hashCode() {
return Objects.hashCode(version) * 17
+ Objects.hashCode(entity);
}
public static class ExternalizerImpl implements Externalizer<SessionEntityWrapper> {
@Override
public void writeObject(ObjectOutput output, SessionEntityWrapper obj) throws IOException {
MarshallUtil.marshallUUID(obj.version, output, false);
MarshallUtil.marshallMap(obj.localMetadata, output);
output.writeObject(obj.getEntity());
}
@Override
public SessionEntityWrapper readObject(ObjectInput input) throws IOException, ClassNotFoundException {
UUID objVersion = MarshallUtil.unmarshallUUID(input, false);
Map<String, String> localMetadata = MarshallUtil.unmarshallMap(input, new MarshallUtil.MapBuilder<String, String, Map<String, String>>() {
@Override
public Map<String, String> build(int size) {
return new ConcurrentHashMap<>(size);
}
});
SessionEntity entity = (SessionEntity) input.readObject();
return new SessionEntityWrapper<>(objVersion, localMetadata, entity);
}
}
}

View file

@ -0,0 +1,87 @@
/*
* 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.models.sessions.infinispan.changes;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface SessionUpdateTask<S extends SessionEntity> {
void runUpdate(S entity);
CacheOperation getOperation(S entity);
CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<S> sessionWrapper);
default long getLifespanMs() {
return -1;
}
enum CacheOperation {
ADD,
ADD_IF_ABSENT, // ADD_IF_ABSENT throws an exception if there is existing value
REMOVE,
REPLACE;
CacheOperation merge(CacheOperation other, SessionEntity entity) {
if (this == REMOVE || other == REMOVE) {
return REMOVE;
}
if (this == ADD | this == ADD_IF_ABSENT) {
if (other == ADD | other == ADD_IF_ABSENT) {
throw new IllegalStateException("Illegal state. Task already in progress for session " + entity.getId());
}
return this;
}
// Lowest priority
return REPLACE;
}
}
enum CrossDCMessageStatus {
SYNC,
//ASYNC,
// QUEUE,
NOT_NEEDED;
CrossDCMessageStatus merge(CrossDCMessageStatus other) {
if (this == SYNC || other == SYNC) {
return SYNC;
}
/*if (this == ASYNC || other == ASYNC) {
return ASYNC;
}*/
return NOT_NEEDED;
}
}
}

View file

@ -0,0 +1,64 @@
/*
* 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.models.sessions.infinispan.changes;
import java.util.LinkedList;
import java.util.List;
import org.keycloak.models.RealmModel;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
/**
* tracks all changes to the underlying session in this transaction
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
class SessionUpdatesList<S extends SessionEntity> {
private final RealmModel realm;
private final SessionEntityWrapper<S> entityWrapper;
private List<SessionUpdateTask<S>> updateTasks = new LinkedList<>();
public SessionUpdatesList(RealmModel realm, SessionEntityWrapper<S> entityWrapper) {
this.realm = realm;
this.entityWrapper = entityWrapper;
}
public RealmModel getRealm() {
return realm;
}
public SessionEntityWrapper<S> getEntityWrapper() {
return entityWrapper;
}
public void add(SessionUpdateTask<S> task) {
updateTasks.add(task);
}
public List<SessionUpdateTask<S>> getUpdateTasks() {
return updateTasks;
}
public void setUpdateTasks(List<SessionUpdateTask<S>> updateTasks) {
this.updateTasks = updateTasks;
}
}

View file

@ -0,0 +1,51 @@
/*
* 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.models.sessions.infinispan.changes;
import org.jboss.logging.Logger;
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
/**
* Task for create or update AuthenticatedClientSessionEntity within userSession
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public abstract class UserSessionClientSessionUpdateTask extends UserSessionUpdateTask {
public static final Logger logger = Logger.getLogger(UserSessionClientSessionUpdateTask.class);
private final String clientUUID;
public UserSessionClientSessionUpdateTask(String clientUUID) {
this.clientUUID = clientUUID;
}
@Override
public void runUpdate(UserSessionEntity userSession) {
AuthenticatedClientSessionEntity clientSession = userSession.getAuthenticatedClientSessions().get(clientUUID);
if (clientSession == null) {
logger.warnf("Not found authenticated client session entity for client %s in userSession %s", clientUUID, userSession.getId());
return;
}
runClientSessionUpdate(clientSession);
}
protected abstract void runClientSessionUpdate(AuthenticatedClientSessionEntity entity);
}

View file

@ -0,0 +1,38 @@
/*
* 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.models.sessions.infinispan.changes;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public abstract class UserSessionUpdateTask implements SessionUpdateTask<UserSessionEntity> {
@Override
public CacheOperation getOperation(UserSessionEntity session) {
return CacheOperation.REPLACE;
}
@Override
public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<UserSessionEntity> sessionWrapper) {
return CrossDCMessageStatus.SYNC;
}
}

View file

@ -0,0 +1,85 @@
/*
* Copyright 2017 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.models.sessions.infinispan.changes.sessions;
import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class LastSessionRefreshChecker {
public static final Logger logger = Logger.getLogger(LastSessionRefreshChecker.class);
private final LastSessionRefreshStore store;
private final LastSessionRefreshStore offlineStore;
public LastSessionRefreshChecker(LastSessionRefreshStore store, LastSessionRefreshStore offlineStore) {
this.store = store;
this.offlineStore = offlineStore;
}
// Metadata attribute, which contains the lastSessionRefresh available on remoteCache. Used in decide whether we need to write to remoteCache (DC) or not
public static final String LAST_SESSION_REFRESH_REMOTE = "lsrr";
public SessionUpdateTask.CrossDCMessageStatus getCrossDCMessageStatus(KeycloakSession kcSession, RealmModel realm, SessionEntityWrapper<UserSessionEntity> sessionWrapper, boolean offline, int newLastSessionRefresh) {
// revokeRefreshToken always writes everything to remoteCache immediately
if (realm.isRevokeRefreshToken()) {
return SessionUpdateTask.CrossDCMessageStatus.SYNC;
}
// We're likely not in cross-dc environment. Doesn't matter what we return
LastSessionRefreshStore storeToUse = offline ? offlineStore : store;
if (storeToUse == null) {
return SessionUpdateTask.CrossDCMessageStatus.SYNC;
}
Boolean ignoreRemoteCacheUpdate = (Boolean) kcSession.getAttribute(LastSessionRefreshListener.IGNORE_REMOTE_CACHE_UPDATE);
if (ignoreRemoteCacheUpdate != null && ignoreRemoteCacheUpdate) {
return SessionUpdateTask.CrossDCMessageStatus.NOT_NEEDED;
}
Integer lsrr = sessionWrapper.getLocalMetadataNoteInt(LAST_SESSION_REFRESH_REMOTE);
if (lsrr == null) {
logger.warnf("Not available lsrr note on user session %s.", sessionWrapper.getEntity().getId());
return SessionUpdateTask.CrossDCMessageStatus.SYNC;
}
int idleTimeout = offline ? realm.getOfflineSessionIdleTimeout() : realm.getSsoSessionIdleTimeout();
if (lsrr + (idleTimeout / 2) <= newLastSessionRefresh) {
logger.debugf("We are going to write remotely. Remote last session refresh: %d, New last session refresh: %d", (int) lsrr, newLastSessionRefresh);
return SessionUpdateTask.CrossDCMessageStatus.SYNC;
}
logger.debugf("Skip writing last session refresh to the remoteCache. Session %s newLastSessionRefresh %d", sessionWrapper.getEntity().getId(), newLastSessionRefresh);
storeToUse.putLastSessionRefresh(kcSession, sessionWrapper.getEntity().getId(), realm.getId(), newLastSessionRefresh);
return SessionUpdateTask.CrossDCMessageStatus.NOT_NEEDED;
}
}

View file

@ -0,0 +1,73 @@
/*
* Copyright 2017 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.models.sessions.infinispan.changes.sessions;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.HashMap;
import java.util.Map;
import org.infinispan.commons.marshall.Externalizer;
import org.infinispan.commons.marshall.MarshallUtil;
import org.infinispan.commons.marshall.SerializeWith;
import org.keycloak.cluster.ClusterEvent;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@SerializeWith(LastSessionRefreshEvent.ExternalizerImpl.class)
public class LastSessionRefreshEvent implements ClusterEvent {
private final Map<String, SessionData> lastSessionRefreshes;
public LastSessionRefreshEvent(Map<String, SessionData> lastSessionRefreshes) {
this.lastSessionRefreshes = lastSessionRefreshes;
}
public Map<String, SessionData> getLastSessionRefreshes() {
return lastSessionRefreshes;
}
public static class ExternalizerImpl implements Externalizer<LastSessionRefreshEvent> {
@Override
public void writeObject(ObjectOutput output, LastSessionRefreshEvent obj) throws IOException {
MarshallUtil.marshallMap(obj.lastSessionRefreshes, output);
}
@Override
public LastSessionRefreshEvent readObject(ObjectInput input) throws IOException, ClassNotFoundException {
Map<String, SessionData> map = MarshallUtil.unmarshallMap(input, new MarshallUtil.MapBuilder<String, SessionData, Map<String, SessionData>>() {
@Override
public Map<String, SessionData> build(int size) {
return new HashMap<>(size);
}
});
LastSessionRefreshEvent event = new LastSessionRefreshEvent(map);
return event;
}
}
}

View file

@ -0,0 +1,112 @@
/*
* Copyright 2017 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.models.sessions.infinispan.changes.sessions;
import java.util.Map;
import org.infinispan.Cache;
import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.client.hotrod.event.ClientEvent;
import org.jboss.logging.Logger;
import org.keycloak.cluster.ClusterEvent;
import org.keycloak.cluster.ClusterListener;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
import org.keycloak.models.utils.KeycloakModelUtils;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class LastSessionRefreshListener implements ClusterListener {
public static final Logger logger = Logger.getLogger(LastSessionRefreshListener.class);
public static final String IGNORE_REMOTE_CACHE_UPDATE = "IGNORE_REMOTE_CACHE_UPDATE";
private final boolean offline;
private final KeycloakSessionFactory sessionFactory;
private final Cache<String, SessionEntityWrapper> cache;
private final boolean distributed;
private final String myAddress;
public LastSessionRefreshListener(KeycloakSession session, Cache<String, SessionEntityWrapper> cache, boolean offline) {
this.sessionFactory = session.getKeycloakSessionFactory();
this.cache = cache;
this.offline = offline;
this.distributed = InfinispanUtil.isDistributedCache(cache);
if (this.distributed) {
this.myAddress = InfinispanUtil.getMyAddress(session);
} else {
this.myAddress = null;
}
}
@Override
public void eventReceived(ClusterEvent event) {
Map<String, SessionData> lastSessionRefreshes = ((LastSessionRefreshEvent) event).getLastSessionRefreshes();
if (logger.isDebugEnabled()) {
logger.debugf("Received refreshes. Offline %b, refreshes: %s", offline, lastSessionRefreshes);
}
lastSessionRefreshes.entrySet().stream().forEach((entry) -> {
String sessionId = entry.getKey();
String realmId = entry.getValue().getRealmId();
int lastSessionRefresh = entry.getValue().getLastSessionRefresh();
// All nodes will receive the message. So ensure that each node updates just lastSessionRefreshes owned by him.
if (shouldUpdateLocalCache(sessionId)) {
KeycloakModelUtils.runJobInTransaction(sessionFactory, (kcSession) -> {
RealmModel realm = kcSession.realms().getRealm(realmId);
UserSessionModel userSession = kcSession.sessions().getUserSession(realm, sessionId);
if (userSession == null) {
logger.debugf("User session %s not available on node %s", sessionId, myAddress);
} else {
// Update just if lastSessionRefresh from event is bigger than ours
if (lastSessionRefresh > userSession.getLastSessionRefresh()) {
// Ensure that remoteCache won't be updated due to this
kcSession.setAttribute(IGNORE_REMOTE_CACHE_UPDATE, true);
userSession.setLastSessionRefresh(lastSessionRefresh);
}
}
});
}
});
}
// For distributed caches, ensure that local modification is executed just on owner
protected boolean shouldUpdateLocalCache(String key) {
if (!distributed) {
return true;
} else {
String keyAddress = InfinispanUtil.getKeyPrimaryOwnerAddress(cache, key);
return myAddress.equals(keyAddress);
}
}
}

View file

@ -0,0 +1,101 @@
/*
* Copyright 2017 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.models.sessions.infinispan.changes.sessions;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.jboss.logging.Logger;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.common.util.Time;
import org.keycloak.models.KeycloakSession;
/**
* Tracks the queue of lastSessionRefreshes, which were updated on this host. Those will be sent to the second DC in bulk, so second DC can update
* lastSessionRefreshes on it's side. Message is sent either periodically or if there are lots of stored lastSessionRefreshes.
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class LastSessionRefreshStore {
protected static final Logger logger = Logger.getLogger(LastSessionRefreshStore.class);
private final int maxIntervalBetweenMessagesSeconds;
private final int maxCount;
private final String eventKey;
private volatile Map<String, SessionData> lastSessionRefreshes = new ConcurrentHashMap<>();
private volatile int lastRun = Time.currentTime();
protected LastSessionRefreshStore(int maxIntervalBetweenMessagesSeconds, int maxCount, String eventKey) {
this.maxIntervalBetweenMessagesSeconds = maxIntervalBetweenMessagesSeconds;
this.maxCount = maxCount;
this.eventKey = eventKey;
}
public void putLastSessionRefresh(KeycloakSession kcSession, String sessionId, String realmId, int lastSessionRefresh) {
lastSessionRefreshes.put(sessionId, new SessionData(realmId, lastSessionRefresh));
// Assume that lastSessionRefresh is same or close to current time
checkSendingMessage(kcSession, lastSessionRefresh);
}
void checkSendingMessage(KeycloakSession kcSession, int currentTime) {
if (lastSessionRefreshes.size() >= maxCount || lastRun + maxIntervalBetweenMessagesSeconds <= currentTime) {
Map<String, SessionData> refreshesToSend = prepareSendingMessage(currentTime);
// Sending message doesn't need to be synchronized
if (refreshesToSend != null) {
sendMessage(kcSession, refreshesToSend);
}
}
}
// synchronized manipulation with internal object instances. Will return map if message should be sent. Otherwise return null
private synchronized Map<String, SessionData> prepareSendingMessage(int currentTime) {
if (lastSessionRefreshes.size() >= maxCount || lastRun + maxIntervalBetweenMessagesSeconds <= currentTime) {
// Create new map instance, so that new writers will use that one
Map<String, SessionData> copiedRefreshesToSend = lastSessionRefreshes;
lastSessionRefreshes = new ConcurrentHashMap<>();
lastRun = currentTime;
return copiedRefreshesToSend;
} else {
return null;
}
}
protected void sendMessage(KeycloakSession kcSession, Map<String, SessionData> refreshesToSend) {
LastSessionRefreshEvent event = new LastSessionRefreshEvent(refreshesToSend);
if (logger.isDebugEnabled()) {
logger.debugf("Sending lastSessionRefreshes: %s", event.getLastSessionRefreshes().toString());
}
// Don't notify local DC about the lastSessionRefreshes. They were processed here already
ClusterProvider cluster = kcSession.getProvider(ClusterProvider.class);
cluster.notify(eventKey, event, true, ClusterProvider.DCNotify.ALL_BUT_LOCAL_DC);
}
}

View file

@ -0,0 +1,74 @@
/*
* Copyright 2017 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.models.sessions.infinispan.changes.sessions;
import org.infinispan.Cache;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.common.util.Time;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import org.keycloak.timer.TimerProvider;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class LastSessionRefreshStoreFactory {
// Timer interval. The store will be checked every 5 seconds whether the message with stored lastSessionRefreshes
public static final long DEFAULT_TIMER_INTERVAL_MS = 5000;
// Max interval between messages. It means that when message is sent to second DC, then another message will be sent at least after 60 seconds.
public static final int DEFAULT_MAX_INTERVAL_BETWEEN_MESSAGES_SECONDS = 60;
// Max count of lastSessionRefreshes. It count of lastSessionRefreshes reach this value, the message is sent to second DC
public static final int DEFAULT_MAX_COUNT = 100;
public LastSessionRefreshStore createAndInit(KeycloakSession kcSession, Cache<String, SessionEntityWrapper> cache, boolean offline) {
return createAndInit(kcSession, cache, DEFAULT_TIMER_INTERVAL_MS, DEFAULT_MAX_INTERVAL_BETWEEN_MESSAGES_SECONDS, DEFAULT_MAX_COUNT, offline);
}
public LastSessionRefreshStore createAndInit(KeycloakSession kcSession, Cache<String, SessionEntityWrapper> cache, long timerIntervalMs, int maxIntervalBetweenMessagesSeconds, int maxCount, boolean offline) {
String eventKey = offline ? "lastSessionRefreshes-offline" : "lastSessionRefreshes";
LastSessionRefreshStore store = createStoreInstance(maxIntervalBetweenMessagesSeconds, maxCount, eventKey);
// Register listener
ClusterProvider cluster = kcSession.getProvider(ClusterProvider.class);
cluster.registerListener(eventKey, new LastSessionRefreshListener(kcSession, cache, offline));
// Setup periodic timer check
TimerProvider timer = kcSession.getProvider(TimerProvider.class);
timer.scheduleTask((KeycloakSession keycloakSession) -> {
store.checkSendingMessage(keycloakSession, Time.currentTime());
}, timerIntervalMs, eventKey);
return store;
}
protected LastSessionRefreshStore createStoreInstance(int maxIntervalBetweenMessagesSeconds, int maxCount, String eventKey) {
return new LastSessionRefreshStore(maxIntervalBetweenMessagesSeconds, maxCount, eventKey);
}
}

View file

@ -0,0 +1,74 @@
/*
* Copyright 2017 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.models.sessions.infinispan.changes.sessions;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import org.infinispan.commons.marshall.Externalizer;
import org.infinispan.commons.marshall.MarshallUtil;
import org.infinispan.commons.marshall.SerializeWith;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@SerializeWith(SessionData.ExternalizerImpl.class)
public class SessionData {
private final String realmId;
private final int lastSessionRefresh;
public SessionData(String realmId, int lastSessionRefresh) {
this.realmId = realmId;
this.lastSessionRefresh = lastSessionRefresh;
}
public String getRealmId() {
return realmId;
}
public int getLastSessionRefresh() {
return lastSessionRefresh;
}
@Override
public String toString() {
return String.format("realmId: %s, lastSessionRefresh: %d", realmId, lastSessionRefresh);
}
public static class ExternalizerImpl implements Externalizer<SessionData> {
@Override
public void writeObject(ObjectOutput output, SessionData obj) throws IOException {
MarshallUtil.marshallString(obj.realmId, output);
MarshallUtil.marshallInt(output, obj.lastSessionRefresh);
}
@Override
public SessionData readObject(ObjectInput input) throws IOException, ClassNotFoundException {
String realmId = MarshallUtil.unmarshallString(input);
int lastSessionRefresh = MarshallUtil.unmarshallInt(input);
return new SessionData(realmId, lastSessionRefresh);
}
}
}

View file

@ -17,13 +17,24 @@
package org.keycloak.models.sessions.infinispan.entities; package org.keycloak.models.sessions.infinispan.entities;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.Serializable; import java.io.Serializable;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.infinispan.commons.marshall.Externalizer;
import org.infinispan.commons.marshall.MarshallUtil;
import org.infinispan.commons.marshall.SerializeWith;
import org.keycloak.models.sessions.infinispan.util.KeycloakMarshallUtil;
/** /**
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
@SerializeWith(AuthenticatedClientSessionEntity.ExternalizerImpl.class)
public class AuthenticatedClientSessionEntity implements Serializable { public class AuthenticatedClientSessionEntity implements Serializable {
private String authMethod; private String authMethod;
@ -33,7 +44,7 @@ public class AuthenticatedClientSessionEntity implements Serializable {
private Set<String> roles; private Set<String> roles;
private Set<String> protocolMappers; private Set<String> protocolMappers;
private Map<String, String> notes; private Map<String, String> notes = new ConcurrentHashMap<>();
public String getAuthMethod() { public String getAuthMethod() {
return authMethod; return authMethod;
@ -91,4 +102,46 @@ public class AuthenticatedClientSessionEntity implements Serializable {
this.notes = notes; this.notes = notes;
} }
public static class ExternalizerImpl implements Externalizer<AuthenticatedClientSessionEntity> {
@Override
public void writeObject(ObjectOutput output, AuthenticatedClientSessionEntity session) throws IOException {
MarshallUtil.marshallString(session.getAuthMethod(), output);
MarshallUtil.marshallString(session.getRedirectUri(), output);
MarshallUtil.marshallInt(output, session.getTimestamp());
MarshallUtil.marshallString(session.getAction(), output);
Map<String, String> notes = session.getNotes();
KeycloakMarshallUtil.writeMap(notes, KeycloakMarshallUtil.STRING_EXT, KeycloakMarshallUtil.STRING_EXT, output);
KeycloakMarshallUtil.writeCollection(session.getProtocolMappers(), KeycloakMarshallUtil.STRING_EXT, output);
KeycloakMarshallUtil.writeCollection(session.getRoles(), KeycloakMarshallUtil.STRING_EXT, output);
}
@Override
public AuthenticatedClientSessionEntity readObject(ObjectInput input) throws IOException, ClassNotFoundException {
AuthenticatedClientSessionEntity sessionEntity = new AuthenticatedClientSessionEntity();
sessionEntity.setAuthMethod(MarshallUtil.unmarshallString(input));
sessionEntity.setRedirectUri(MarshallUtil.unmarshallString(input));
sessionEntity.setTimestamp(MarshallUtil.unmarshallInt(input));
sessionEntity.setAction(MarshallUtil.unmarshallString(input));
Map<String, String> notes = KeycloakMarshallUtil.readMap(input, KeycloakMarshallUtil.STRING_EXT, KeycloakMarshallUtil.STRING_EXT,
new KeycloakMarshallUtil.ConcurrentHashMapBuilder<>());
sessionEntity.setNotes(notes);
Set<String> protocolMappers = KeycloakMarshallUtil.readCollection(input, KeycloakMarshallUtil.STRING_EXT, new KeycloakMarshallUtil.HashSetBuilder<>());
sessionEntity.setProtocolMappers(protocolMappers);
Set<String> roles = KeycloakMarshallUtil.readCollection(input, KeycloakMarshallUtil.STRING_EXT, new KeycloakMarshallUtil.HashSetBuilder<>());
sessionEntity.setRoles(roles);
return sessionEntity;
}
}
} }

View file

@ -19,6 +19,8 @@ package org.keycloak.models.sessions.infinispan.entities;
import java.io.Serializable; import java.io.Serializable;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
@ -60,4 +62,10 @@ public class SessionEntity implements Serializable {
public int hashCode() { public int hashCode() {
return id != null ? id.hashCode() : 0; return id != null ? id.hashCode() : 0;
} }
public SessionEntityWrapper mergeRemoteEntityWithLocalEntity(SessionEntityWrapper localEntityWrapper) {
throw new IllegalStateException("Not yet implemented");
};
} }

View file

@ -17,18 +17,31 @@
package org.keycloak.models.sessions.infinispan.entities; package org.keycloak.models.sessions.infinispan.entities;
import org.infinispan.commons.marshall.Externalizer;
import org.infinispan.commons.marshall.MarshallUtil;
import org.infinispan.commons.marshall.SerializeWith;
import org.jboss.logging.Logger;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.util.KeycloakMarshallUtil;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
@SerializeWith(UserSessionEntity.ExternalizerImpl.class)
public class UserSessionEntity extends SessionEntity { public class UserSessionEntity extends SessionEntity {
public static final Logger logger = Logger.getLogger(UserSessionEntity.class);
// Tracks the "lastSessionRefresh" from userSession entity from remote cache
public static final String LAST_SESSION_REFRESH_REMOTE = "lsrr";
private String user; private String user;
private String brokerSessionId; private String brokerSessionId;
@ -147,4 +160,106 @@ public class UserSessionEntity extends SessionEntity {
public void setBrokerUserId(String brokerUserId) { public void setBrokerUserId(String brokerUserId) {
this.brokerUserId = brokerUserId; this.brokerUserId = brokerUserId;
} }
@Override
public String toString() {
return String.format("UserSessionEntity [ id=%s, realm=%s, lastSessionRefresh=%d]", getId(), getRealm(), getLastSessionRefresh());
}
@Override
public SessionEntityWrapper mergeRemoteEntityWithLocalEntity(SessionEntityWrapper localEntityWrapper) {
int lsrRemote = getLastSessionRefresh();
SessionEntityWrapper entityWrapper;
if (localEntityWrapper == null) {
entityWrapper = new SessionEntityWrapper<>(this);
} else {
UserSessionEntity localUserSession = (UserSessionEntity) localEntityWrapper.getEntity();
// local lastSessionRefresh should always contain the bigger
if (lsrRemote < localUserSession.getLastSessionRefresh()) {
setLastSessionRefresh(localUserSession.getLastSessionRefresh());
}
entityWrapper = new SessionEntityWrapper<>(localEntityWrapper.getLocalMetadata(), this);
}
entityWrapper.putLocalMetadataNoteInt(LAST_SESSION_REFRESH_REMOTE, lsrRemote);
logger.debugf("Updating session entity. lastSessionRefresh=%d, lastSessionRefreshRemote=%d", getLastSessionRefresh(), lsrRemote);
return entityWrapper;
}
public static class ExternalizerImpl implements Externalizer<UserSessionEntity> {
@Override
public void writeObject(ObjectOutput output, UserSessionEntity session) throws IOException {
MarshallUtil.marshallString(session.getAuthMethod(), output);
MarshallUtil.marshallString(session.getBrokerSessionId(), output);
MarshallUtil.marshallString(session.getBrokerUserId(), output);
MarshallUtil.marshallString(session.getId(), output);
MarshallUtil.marshallString(session.getIpAddress(), output);
MarshallUtil.marshallString(session.getLoginUsername(), output);
MarshallUtil.marshallString(session.getRealm(), output);
MarshallUtil.marshallString(session.getUser(), output);
MarshallUtil.marshallInt(output, session.getLastSessionRefresh());
MarshallUtil.marshallInt(output, session.getStarted());
output.writeBoolean(session.isRememberMe());
int state = session.getState() == null ? 0 :
((session.getState() == UserSessionModel.State.LOGGED_IN) ? 1 : (session.getState() == UserSessionModel.State.LOGGED_OUT ? 2 : 3));
output.writeInt(state);
Map<String, String> notes = session.getNotes();
KeycloakMarshallUtil.writeMap(notes, KeycloakMarshallUtil.STRING_EXT, KeycloakMarshallUtil.STRING_EXT, output);
Map<String, AuthenticatedClientSessionEntity> authSessions = session.getAuthenticatedClientSessions();
KeycloakMarshallUtil.writeMap(authSessions, KeycloakMarshallUtil.STRING_EXT, new AuthenticatedClientSessionEntity.ExternalizerImpl(), output);
}
@Override
public UserSessionEntity readObject(ObjectInput input) throws IOException, ClassNotFoundException {
UserSessionEntity sessionEntity = new UserSessionEntity();
sessionEntity.setAuthMethod(MarshallUtil.unmarshallString(input));
sessionEntity.setBrokerSessionId(MarshallUtil.unmarshallString(input));
sessionEntity.setBrokerUserId(MarshallUtil.unmarshallString(input));
sessionEntity.setId(MarshallUtil.unmarshallString(input));
sessionEntity.setIpAddress(MarshallUtil.unmarshallString(input));
sessionEntity.setLoginUsername(MarshallUtil.unmarshallString(input));
sessionEntity.setRealm(MarshallUtil.unmarshallString(input));
sessionEntity.setUser(MarshallUtil.unmarshallString(input));
sessionEntity.setLastSessionRefresh(MarshallUtil.unmarshallInt(input));
sessionEntity.setStarted(MarshallUtil.unmarshallInt(input));
sessionEntity.setRememberMe(input.readBoolean());
int state = input.readInt();
switch(state) {
case 1: sessionEntity.setState(UserSessionModel.State.LOGGED_IN);
break;
case 2: sessionEntity.setState(UserSessionModel.State.LOGGED_OUT);
break;
case 3: sessionEntity.setState(UserSessionModel.State.LOGGING_OUT);
break;
default:
sessionEntity.setState(null);
}
Map<String, String> notes = KeycloakMarshallUtil.readMap(input, KeycloakMarshallUtil.STRING_EXT, KeycloakMarshallUtil.STRING_EXT,
new KeycloakMarshallUtil.ConcurrentHashMapBuilder<>());
sessionEntity.setNotes(notes);
Map<String, AuthenticatedClientSessionEntity> authSessions = KeycloakMarshallUtil.readMap(input, KeycloakMarshallUtil.STRING_EXT, new AuthenticatedClientSessionEntity.ExternalizerImpl(),
new KeycloakMarshallUtil.ConcurrentHashMapBuilder<>());
sessionEntity.setAuthenticatedClientSessions(authSessions);
return sessionEntity;
}
}
} }

View file

@ -0,0 +1,64 @@
/*
* 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.models.sessions.infinispan.events;
import org.jboss.logging.Logger;
import org.keycloak.cluster.ClusterEvent;
import org.keycloak.cluster.ClusterListener;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.sessions.infinispan.InfinispanAuthenticationSessionProvider;
import org.keycloak.models.sessions.infinispan.InfinispanAuthenticationSessionProviderFactory;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.sessions.AuthenticationSessionProvider;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public abstract class AbstractAuthSessionClusterListener <SE extends SessionClusterEvent> implements ClusterListener {
private static final Logger log = Logger.getLogger(AbstractAuthSessionClusterListener.class);
private final KeycloakSessionFactory sessionFactory;
public AbstractAuthSessionClusterListener(KeycloakSessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
@Override
public void eventReceived(ClusterEvent event) {
KeycloakModelUtils.runJobInTransaction(sessionFactory, (KeycloakSession session) -> {
InfinispanAuthenticationSessionProvider provider = (InfinispanAuthenticationSessionProvider) session.getProvider(AuthenticationSessionProvider.class,
InfinispanAuthenticationSessionProviderFactory.PROVIDER_ID);
SE sessionEvent = (SE) event;
if (!provider.getCache().getStatus().allowInvocations()) {
log.debugf("Cache in state '%s' doesn't allow invocations", provider.getCache().getStatus());
return;
}
log.debugf("Received authentication session event '%s'", sessionEvent.toString());
eventReceived(session, provider, sessionEvent);
});
}
protected abstract void eventReceived(KeycloakSession session, InfinispanAuthenticationSessionProvider provider, SE sessionEvent);
}

View file

@ -0,0 +1,64 @@
/*
* 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.models.sessions.infinispan.events;
import org.jboss.logging.Logger;
import org.keycloak.cluster.ClusterEvent;
import org.keycloak.cluster.ClusterListener;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.sessions.infinispan.InfinispanUserSessionProvider;
import org.keycloak.models.sessions.infinispan.InfinispanUserSessionProviderFactory;
import org.keycloak.models.utils.KeycloakModelUtils;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public abstract class AbstractUserSessionClusterListener<SE extends SessionClusterEvent> implements ClusterListener {
private static final Logger log = Logger.getLogger(AbstractUserSessionClusterListener.class);
private final KeycloakSessionFactory sessionFactory;
public AbstractUserSessionClusterListener(KeycloakSessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
@Override
public void eventReceived(ClusterEvent event) {
KeycloakModelUtils.runJobInTransaction(sessionFactory, (KeycloakSession session) -> {
InfinispanUserSessionProvider provider = (InfinispanUserSessionProvider) session.getProvider(UserSessionProvider.class, InfinispanUserSessionProviderFactory.PROVIDER_ID);
SE sessionEvent = (SE) event;
String realmId = sessionEvent.getRealmId();
if (log.isDebugEnabled()) {
log.debugf("Received user session event '%s'", sessionEvent.toString());
}
eventReceived(session, provider, sessionEvent);
});
}
protected abstract void eventReceived(KeycloakSession session, InfinispanUserSessionProvider provider, SE sessionEvent);
}

View file

@ -0,0 +1,50 @@
/*
* 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.models.sessions.infinispan.events;
import org.keycloak.cluster.ClusterEvent;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ClientRemovedSessionEvent implements SessionClusterEvent {
private String realmId;
private String clientUuid;
public static ClientRemovedSessionEvent create(String realmId, String clientUuid) {
ClientRemovedSessionEvent event = new ClientRemovedSessionEvent();
event.realmId = realmId;
event.clientUuid = clientUuid;
return event;
}
@Override
public String toString() {
return String.format("ClientRemovedSessionEvent [ realmId=%s , clientUuid=%s ]", realmId, clientUuid);
}
@Override
public String getRealmId() {
return realmId;
}
public String getClientUuid() {
return clientUuid;
}
}

View file

@ -0,0 +1,42 @@
/*
* 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.models.sessions.infinispan.events;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class RealmRemovedSessionEvent implements SessionClusterEvent {
private String realmId;
public static RealmRemovedSessionEvent create(String realmId) {
RealmRemovedSessionEvent event = new RealmRemovedSessionEvent();
event.realmId = realmId;
return event;
}
@Override
public String toString() {
return String.format("RealmRemovedSessionEvent [ realmId=%s ]", realmId);
}
@Override
public String getRealmId() {
return realmId;
}
}

View file

@ -0,0 +1,42 @@
/*
* 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.models.sessions.infinispan.events;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class RemoveAllUserLoginFailuresEvent implements SessionClusterEvent {
private String realmId;
public static RemoveAllUserLoginFailuresEvent create(String realmId) {
RemoveAllUserLoginFailuresEvent event = new RemoveAllUserLoginFailuresEvent();
event.realmId = realmId;
return event;
}
@Override
public String toString() {
return String.format("RemoveAllUserLoginFailuresEvent [ realmId=%s ]", realmId);
}
@Override
public String getRealmId() {
return realmId;
}
}

View file

@ -0,0 +1,42 @@
/*
* 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.models.sessions.infinispan.events;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class RemoveUserSessionsEvent implements SessionClusterEvent {
private String realmId;
public static RemoveUserSessionsEvent create(String realmId) {
RemoveUserSessionsEvent event = new RemoveUserSessionsEvent();
event.realmId = realmId;
return event;
}
@Override
public String toString() {
return String.format("RemoveUserSessionsEvent [ realmId=%s ]", realmId);
}
@Override
public String getRealmId() {
return realmId;
}
}

View file

@ -15,21 +15,15 @@
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.models.sessions.infinispan.mapreduce; package org.keycloak.models.sessions.infinispan.events;
import org.infinispan.distexec.mapreduce.Reducer; import org.keycloak.cluster.ClusterEvent;
import java.io.Serializable;
import java.util.Iterator;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
public class FirstResultReducer implements Reducer<Object, Object>, Serializable { public interface SessionClusterEvent extends ClusterEvent {
@Override String getRealmId();
public Object reduce(Object reducedKey, Iterator<Object> itr) {
return itr.next();
}
} }

View file

@ -0,0 +1,74 @@
/*
* 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.models.sessions.infinispan.events;
import java.util.List;
import java.util.Map;
import org.keycloak.cluster.ClusterProvider;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.models.AbstractKeycloakTransaction;
import org.keycloak.models.KeycloakSession;
/**
* Postpone sending notifications of session events to the commit of Keycloak transaction
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class SessionEventsSenderTransaction extends AbstractKeycloakTransaction {
private final KeycloakSession session;
private final MultivaluedHashMap<String, SessionClusterEvent> sessionEvents = new MultivaluedHashMap<>();
private final MultivaluedHashMap<String, SessionClusterEvent> localDCSessionEvents = new MultivaluedHashMap<>();
public SessionEventsSenderTransaction(KeycloakSession session) {
this.session = session;
}
public void addEvent(String eventName, SessionClusterEvent event, boolean sendToAllDCs) {
if (sendToAllDCs) {
sessionEvents.add(eventName, event);
} else {
localDCSessionEvents.add(eventName, event);
}
}
@Override
protected void commitImpl() {
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
// TODO bulk notify (send whole list instead of separate events?)
for (Map.Entry<String, List<SessionClusterEvent>> entry : sessionEvents.entrySet()) {
for (SessionClusterEvent event : entry.getValue()) {
cluster.notify(entry.getKey(), event, false, ClusterProvider.DCNotify.ALL_DCS);
}
}
for (Map.Entry<String, List<SessionClusterEvent>> entry : localDCSessionEvents.entrySet()) {
for (SessionClusterEvent event : entry.getValue()) {
cluster.notify(entry.getKey(), event, false, ClusterProvider.DCNotify.LOCAL_DC_ONLY);
}
}
}
@Override
protected void rollbackImpl() {
}
}

View file

@ -0,0 +1,159 @@
/*
* 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.models.sessions.infinispan.initializer;
import java.io.Serializable;
import org.infinispan.Cache;
import org.infinispan.context.Flag;
import org.infinispan.lifecycle.ComponentStatus;
import org.infinispan.remoting.transport.Transport;
import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakSessionTask;
import org.keycloak.models.utils.KeycloakModelUtils;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public abstract class BaseCacheInitializer extends CacheInitializer {
private static final String STATE_KEY_PREFIX = "distributed::";
private static final Logger log = Logger.getLogger(BaseCacheInitializer.class);
protected final KeycloakSessionFactory sessionFactory;
protected final Cache<String, Serializable> workCache;
protected final SessionLoader sessionLoader;
protected final int sessionsPerSegment;
protected final String stateKey;
public BaseCacheInitializer(KeycloakSessionFactory sessionFactory, Cache<String, Serializable> workCache, SessionLoader sessionLoader, String stateKeySuffix, int sessionsPerSegment) {
this.sessionFactory = sessionFactory;
this.workCache = workCache;
this.sessionLoader = sessionLoader;
this.sessionsPerSegment = sessionsPerSegment;
this.stateKey = STATE_KEY_PREFIX + stateKeySuffix;
}
@Override
protected boolean isFinished() {
// Check if we should skipLoadingSessions. This can happen if someone else already did the task (For example in cross-dc environment, it was done by different DC)
boolean isFinishedAlready = this.sessionLoader.isFinished(this);
if (isFinishedAlready) {
return true;
}
InitializerState state = getStateFromCache();
return state != null && state.isFinished();
}
@Override
protected boolean isCoordinator() {
Transport transport = workCache.getCacheManager().getTransport();
return transport == null || transport.isCoordinator();
}
protected InitializerState getOrCreateInitializerState() {
InitializerState state = getStateFromCache();
if (state == null) {
final int[] count = new int[1];
// Rather use separate transactions for update and counting
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
@Override
public void run(KeycloakSession session) {
sessionLoader.init(session);
}
});
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
@Override
public void run(KeycloakSession session) {
count[0] = sessionLoader.getSessionsCount(session);
}
});
state = new InitializerState();
state.init(count[0], sessionsPerSegment);
saveStateToCache(state);
}
return state;
}
private InitializerState getStateFromCache() {
// We ignore cacheStore for now, so that in Cross-DC scenario (with RemoteStore enabled) is the remoteStore ignored. This means that every DC needs to load offline sessions separately.
return (InitializerState) workCache.getAdvancedCache()
.withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD)
.get(stateKey);
}
protected void saveStateToCache(final InitializerState state) {
// 3 attempts to send the message (it may fail if some node fails in the meantime)
retry(3, new Runnable() {
@Override
public void run() {
// Save this synchronously to ensure all nodes read correct state
// We ignore cacheStore for now, so that in Cross-DC scenario (with RemoteStore enabled) is the remoteStore ignored. This means that every DC needs to load offline sessions separately.
BaseCacheInitializer.this.workCache.getAdvancedCache().
withFlags(Flag.IGNORE_RETURN_VALUES, Flag.FORCE_SYNCHRONOUS, Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD)
.put(stateKey, state);
}
});
}
private void retry(int retry, Runnable runnable) {
while (true) {
try {
runnable.run();
return;
} catch (RuntimeException e) {
ComponentStatus status = workCache.getStatus();
if (status.isStopping() || status.isTerminated()) {
log.warn("Failed to put initializerState to the cache. Cache is already terminating");
log.debug(e.getMessage(), e);
return;
}
retry--;
if (retry == 0) {
throw e;
}
}
}
}
public Cache<String, Serializable> getWorkCache() {
return workCache;
}
}

View file

@ -0,0 +1,55 @@
/*
* 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.models.sessions.infinispan.initializer;
import org.jboss.logging.Logger;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public abstract class CacheInitializer {
private static final Logger log = Logger.getLogger(CacheInitializer.class);
public void initCache() {
}
public void loadSessions() {
while (!isFinished()) {
if (!isCoordinator()) {
try {
Thread.sleep(1000);
} catch (InterruptedException ie) {
log.error("Interrupted", ie);
}
} else {
startLoading();
}
}
}
protected abstract boolean isFinished();
protected abstract boolean isCoordinator();
/**
* Just coordinator will run this
*/
protected abstract void startLoading();
}

View file

@ -0,0 +1,81 @@
/*
* 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.models.sessions.infinispan.initializer;
import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.dblock.DBLockManager;
import org.keycloak.models.dblock.DBLockProvider;
/**
* Encapsulates preloading of sessions within the DB Lock. This DB-aware lock ensures that "startLoading" is done on single DC and the other DCs need to wait.
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class DBLockBasedCacheInitializer extends CacheInitializer {
private static final Logger log = Logger.getLogger(DBLockBasedCacheInitializer.class);
private final KeycloakSession session;
private final CacheInitializer delegate;
public DBLockBasedCacheInitializer(KeycloakSession session, CacheInitializer delegate) {
this.session = session;
this.delegate = delegate;
}
@Override
public void initCache() {
delegate.initCache();
}
@Override
protected boolean isFinished() {
return delegate.isFinished();
}
@Override
protected boolean isCoordinator() {
return delegate.isCoordinator();
}
/**
* Just coordinator will run this. And there is DB-lock, so the delegate.startLoading() will be permitted just by the single DC
*/
@Override
protected void startLoading() {
DBLockManager dbLockManager = new DBLockManager(session);
dbLockManager.checkForcedUnlock();
DBLockProvider dbLock = dbLockManager.getDBLock();
dbLock.waitForLock();
try {
if (isFinished()) {
log.infof("Task already finished when DBLock retrieved");
} else {
delegate.startLoading();
}
} finally {
dbLock.releaseLock();
}
}
}

View file

@ -18,15 +18,10 @@
package org.keycloak.models.sessions.infinispan.initializer; package org.keycloak.models.sessions.infinispan.initializer;
import org.infinispan.Cache; import org.infinispan.Cache;
import org.infinispan.context.Flag;
import org.infinispan.distexec.DefaultExecutorService; import org.infinispan.distexec.DefaultExecutorService;
import org.infinispan.lifecycle.ComponentStatus;
import org.infinispan.remoting.transport.Transport; import org.infinispan.remoting.transport.Transport;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakSessionTask;
import org.keycloak.models.utils.KeycloakModelUtils;
import java.io.Serializable; import java.io.Serializable;
import java.util.LinkedList; import java.util.LinkedList;
@ -37,131 +32,34 @@ import java.util.concurrent.Executors;
import java.util.concurrent.Future; import java.util.concurrent.Future;
/** /**
* Startup initialization for reading persistent userSessions/clientSessions to be filled into infinispan/memory . In cluster, * Startup initialization for reading persistent userSessions to be filled into infinispan/memory . In cluster,
* the initialization is distributed among all cluster nodes, so the startup time is even faster * the initialization is distributed among all cluster nodes, so the startup time is even faster
* *
* TODO: Move to clusterService. Implementation is already pretty generic and doesn't contain any "userSession" specific stuff. All sessions-specific logic is in the SessionLoader implementation * TODO: Move to clusterService. Implementation is already pretty generic and doesn't contain any "userSession" specific stuff. All sessions-specific logic is in the SessionLoader implementation
* *
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
public class InfinispanUserSessionInitializer { public class InfinispanCacheInitializer extends BaseCacheInitializer {
private static final String STATE_KEY_PREFIX = "distributed::"; private static final Logger log = Logger.getLogger(InfinispanCacheInitializer.class);
private static final Logger log = Logger.getLogger(InfinispanUserSessionInitializer.class);
private final KeycloakSessionFactory sessionFactory;
private final Cache<String, Serializable> workCache;
private final SessionLoader sessionLoader;
private final int maxErrors; private final int maxErrors;
private final int sessionsPerSegment;
private final String stateKey;
public InfinispanUserSessionInitializer(KeycloakSessionFactory sessionFactory, Cache<String, Serializable> workCache, SessionLoader sessionLoader, int maxErrors, int sessionsPerSegment, String stateKeySuffix) { public InfinispanCacheInitializer(KeycloakSessionFactory sessionFactory, Cache<String, Serializable> workCache, SessionLoader sessionLoader, String stateKeySuffix, int sessionsPerSegment, int maxErrors) {
this.sessionFactory = sessionFactory; super(sessionFactory, workCache, sessionLoader, stateKeySuffix, sessionsPerSegment);
this.workCache = workCache;
this.sessionLoader = sessionLoader;
this.maxErrors = maxErrors; this.maxErrors = maxErrors;
this.sessionsPerSegment = sessionsPerSegment;
this.stateKey = STATE_KEY_PREFIX + stateKeySuffix;
} }
@Override
public void initCache() { public void initCache() {
this.workCache.getAdvancedCache().getComponentRegistry().registerComponent(sessionFactory, KeycloakSessionFactory.class); this.workCache.getAdvancedCache().getComponentRegistry().registerComponent(sessionFactory, KeycloakSessionFactory.class);
} }
public void loadPersistentSessions() {
if (isFinished()) {
return;
}
while (!isFinished()) {
if (!isCoordinator()) {
try {
Thread.sleep(1000);
} catch (InterruptedException ie) {
log.error("Interrupted", ie);
}
} else {
startLoading();
}
}
}
private boolean isFinished() {
InitializerState state = getStateFromCache();
return state != null && state.isFinished();
}
private InitializerState getOrCreateInitializerState() {
InitializerState state = getStateFromCache();
if (state == null) {
final int[] count = new int[1];
// Rather use separate transactions for update and counting
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
@Override
public void run(KeycloakSession session) {
sessionLoader.init(session);
}
});
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
@Override
public void run(KeycloakSession session) {
count[0] = sessionLoader.getSessionsCount(session);
}
});
state = new InitializerState();
state.init(count[0], sessionsPerSegment);
saveStateToCache(state);
}
return state;
}
private InitializerState getStateFromCache() {
// TODO: We ignore cacheStore for now, so that in Cross-DC scenario (with RemoteStore enabled) is the remoteStore ignored. This means that every DC needs to load offline sessions separately.
return (InitializerState) workCache.getAdvancedCache()
.withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD)
.get(stateKey);
}
private void saveStateToCache(final InitializerState state) {
// 3 attempts to send the message (it may fail if some node fails in the meantime)
retry(3, new Runnable() {
@Override
public void run() {
// Save this synchronously to ensure all nodes read correct state
// TODO: We ignore cacheStore for now, so that in Cross-DC scenario (with RemoteStore enabled) is the remoteStore ignored. This means that every DC needs to load offline sessions separately.
InfinispanUserSessionInitializer.this.workCache.getAdvancedCache().
withFlags(Flag.IGNORE_RETURN_VALUES, Flag.FORCE_SYNCHRONOUS, Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD)
.put(stateKey, state);
}
});
}
private boolean isCoordinator() {
Transport transport = workCache.getCacheManager().getTransport();
return transport == null || transport.isCoordinator();
}
// Just coordinator will run this // Just coordinator will run this
private void startLoading() { @Override
protected void startLoading() {
InitializerState state = getOrCreateInitializerState(); InitializerState state = getOrCreateInitializerState();
// Assume each worker has same processor's count // Assume each worker has same processor's count
@ -230,6 +128,10 @@ public class InfinispanUserSessionInitializer {
log.debug("New initializer state pushed. The state is: " + state.printState()); log.debug("New initializer state pushed. The state is: " + state.printState());
} }
} }
// Loader callback after the task is finished
this.sessionLoader.afterAllSessionsLoaded(this);
} finally { } finally {
if (distributed) { if (distributed) {
executorService.shutdown(); executorService.shutdown();
@ -238,25 +140,6 @@ public class InfinispanUserSessionInitializer {
} }
} }
private void retry(int retry, Runnable runnable) {
while (true) {
try {
runnable.run();
return;
} catch (RuntimeException e) {
ComponentStatus status = workCache.getStatus();
if (status.isStopping() || status.isTerminated()) {
log.warn("Failed to put initializerState to the cache. Cache is already terminating");
log.debug(e.getMessage(), e);
return;
}
retry--;
if (retry == 0) {
throw e;
}
}
}
}
public static class WorkerResult implements Serializable { public static class WorkerResult implements Serializable {

View file

@ -17,20 +17,30 @@
package org.keycloak.models.sessions.infinispan.initializer; package org.keycloak.models.sessions.infinispan.initializer;
import org.infinispan.Cache;
import org.infinispan.context.Flag;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.cluster.ClusterProvider; import org.keycloak.cluster.ClusterProvider;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.session.UserSessionPersisterProvider; import org.keycloak.models.session.UserSessionPersisterProvider;
import java.io.Serializable;
import java.util.List; import java.util.List;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
public class OfflineUserSessionLoader implements SessionLoader { public class OfflinePersistentUserSessionLoader implements SessionLoader, Serializable {
private static final Logger log = Logger.getLogger(OfflinePersistentUserSessionLoader.class);
// Cross-DC aware flag
public static final String PERSISTENT_SESSIONS_LOADED = "PERSISTENT_SESSIONS_LOADED";
// Just local-DC aware flag
public static final String PERSISTENT_SESSIONS_LOADED_IN_CURRENT_DC = "PERSISTENT_SESSIONS_LOADED_IN_CURRENT_DC";
private static final Logger log = Logger.getLogger(OfflineUserSessionLoader.class);
@Override @Override
public void init(KeycloakSession session) { public void init(KeycloakSession session) {
@ -45,12 +55,14 @@ public class OfflineUserSessionLoader implements SessionLoader {
persister.updateAllTimestamps(clusterStartupTime); persister.updateAllTimestamps(clusterStartupTime);
} }
@Override @Override
public int getSessionsCount(KeycloakSession session) { public int getSessionsCount(KeycloakSession session) {
UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
return persister.getUserSessionsCount(true); return persister.getUserSessionsCount(true);
} }
@Override @Override
public boolean loadSessions(KeycloakSession session, int first, int max) { public boolean loadSessions(KeycloakSession session, int first, int max) {
if (log.isTraceEnabled()) { if (log.isTraceEnabled()) {
@ -70,4 +82,37 @@ public class OfflineUserSessionLoader implements SessionLoader {
} }
@Override
public boolean isFinished(BaseCacheInitializer initializer) {
Cache<String, Serializable> workCache = initializer.getWorkCache();
Boolean sessionsLoaded = (Boolean) workCache.get(PERSISTENT_SESSIONS_LOADED);
if (sessionsLoaded != null && sessionsLoaded) {
log.debugf("Persistent sessions loaded already.");
return true;
} else {
log.debugf("Persistent sessions not yet loaded.");
return false;
}
}
@Override
public void afterAllSessionsLoaded(BaseCacheInitializer initializer) {
Cache<String, Serializable> workCache = initializer.getWorkCache();
// Cross-DC aware flag
workCache
.getAdvancedCache().withFlags(Flag.SKIP_REMOTE_LOOKUP)
.put(PERSISTENT_SESSIONS_LOADED, true);
// Just local-DC aware flag
workCache
.getAdvancedCache().withFlags(Flag.SKIP_REMOTE_LOOKUP, Flag.SKIP_CACHE_LOAD, Flag.SKIP_CACHE_STORE)
.put(PERSISTENT_SESSIONS_LOADED_IN_CURRENT_DC, true);
log.debugf("Persistent sessions loaded successfully!");
}
} }

View file

@ -31,7 +31,7 @@ import java.util.Set;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
public class SessionInitializerWorker implements DistributedCallable<String, Serializable, InfinispanUserSessionInitializer.WorkerResult>, Serializable { public class SessionInitializerWorker implements DistributedCallable<String, Serializable, InfinispanCacheInitializer.WorkerResult>, Serializable {
private static final Logger log = Logger.getLogger(SessionInitializerWorker.class); private static final Logger log = Logger.getLogger(SessionInitializerWorker.class);
@ -53,7 +53,7 @@ public class SessionInitializerWorker implements DistributedCallable<String, Ser
} }
@Override @Override
public InfinispanUserSessionInitializer.WorkerResult call() throws Exception { public InfinispanCacheInitializer.WorkerResult call() throws Exception {
if (log.isTraceEnabled()) { if (log.isTraceEnabled()) {
log.tracef("Running computation for segment: %d", segment); log.tracef("Running computation for segment: %d", segment);
} }
@ -61,7 +61,7 @@ public class SessionInitializerWorker implements DistributedCallable<String, Ser
KeycloakSessionFactory sessionFactory = workCache.getAdvancedCache().getComponentRegistry().getComponent(KeycloakSessionFactory.class); KeycloakSessionFactory sessionFactory = workCache.getAdvancedCache().getComponentRegistry().getComponent(KeycloakSessionFactory.class);
if (sessionFactory == null) { if (sessionFactory == null) {
log.debugf("KeycloakSessionFactory not yet set in cache. Worker skipped"); log.debugf("KeycloakSessionFactory not yet set in cache. Worker skipped");
return InfinispanUserSessionInitializer.WorkerResult.create(segment, false); return InfinispanCacheInitializer.WorkerResult.create(segment, false);
} }
final int first = segment * sessionsPerSegment; final int first = segment * sessionsPerSegment;
@ -76,7 +76,7 @@ public class SessionInitializerWorker implements DistributedCallable<String, Ser
}); });
return InfinispanUserSessionInitializer.WorkerResult.create(segment, true); return InfinispanCacheInitializer.WorkerResult.create(segment, true);
} }
} }

View file

@ -24,11 +24,51 @@ import java.io.Serializable;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
public interface SessionLoader extends Serializable { public interface SessionLoader {
/**
* Will be triggered just once on cluster coordinator node to perform some generic initialization tasks (Eg. update DB before starting load).
*
* NOTE: This shouldn't be used for the initialization of loader instance itself!
*
* @param session
*/
void init(KeycloakSession session); void init(KeycloakSession session);
/**
* Will be triggered just once on cluster coordinator node to count the number of sessions
*
* @param session
* @return
*/
int getSessionsCount(KeycloakSession session); int getSessionsCount(KeycloakSession session);
/**
* Will be called on all cluster nodes to load the specified page.
*
* @param session
* @param first
* @param max
* @return
*/
boolean loadSessions(KeycloakSession session, int first, int max); boolean loadSessions(KeycloakSession session, int first, int max);
/**
* This will be called on nodes to check if loading is finished. It allows loader to notify that loading is finished for some reason.
*
* @param initializer
* @return
*/
boolean isFinished(BaseCacheInitializer initializer);
/**
* Callback triggered on cluster coordinator once it recognize that all sessions were successfully loaded
*
* @param initializer
*/
void afterAllSessionsLoaded(BaseCacheInitializer initializer);
} }

View file

@ -0,0 +1,51 @@
/*
* 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.models.sessions.infinispan.initializer;
import java.io.Serializable;
import org.infinispan.Cache;
import org.keycloak.models.KeycloakSession;
/**
* This impl is able to run the non-paginatable loader task and hence will be executed just on single node.
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class SingleWorkerCacheInitializer extends BaseCacheInitializer {
private final KeycloakSession session;
public SingleWorkerCacheInitializer(KeycloakSession session, Cache<String, Serializable> workCache, SessionLoader sessionLoader, String stateKeySuffix) {
super(session.getKeycloakSessionFactory(), workCache, sessionLoader, stateKeySuffix, Integer.MAX_VALUE);
this.session = session;
}
@Override
protected void startLoading() {
InitializerState state = getOrCreateInitializerState();
while (!state.isFinished()) {
sessionLoader.loadSessions(session, -1, -1);
state.markSegmentFinished(0);
saveStateToCache(state);
}
// Loader callback after the task is finished
this.sessionLoader.afterAllSessionsLoaded(this);
}
}

View file

@ -1,68 +0,0 @@
/*
* 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.models.sessions.infinispan.mapreduce;
import org.infinispan.distexec.mapreduce.Collector;
import org.infinispan.distexec.mapreduce.Mapper;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import java.io.Serializable;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class SessionMapper implements Mapper<String, SessionEntity, String, Object>, Serializable {
public SessionMapper(String realm) {
this.realm = realm;
}
private enum EmitValue {
KEY, ENTITY
}
private String realm;
private EmitValue emit = EmitValue.ENTITY;
public static SessionMapper create(String realm) {
return new SessionMapper(realm);
}
public SessionMapper emitKey() {
emit = EmitValue.KEY;
return this;
}
@Override
public void map(String key, SessionEntity e, Collector collector) {
if (!realm.equals(e.getRealm())) {
return;
}
switch (emit) {
case KEY:
collector.emit(key, key);
break;
case ENTITY:
collector.emit(key, e);
break;
}
}
}

View file

@ -1,69 +0,0 @@
/*
* 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.models.sessions.infinispan.mapreduce;
import org.infinispan.distexec.mapreduce.Collector;
import org.infinispan.distexec.mapreduce.Mapper;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
import java.io.Serializable;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class UserLoginFailureMapper implements Mapper<LoginFailureKey, LoginFailureEntity, LoginFailureKey, Object>, Serializable {
public UserLoginFailureMapper(String realm) {
this.realm = realm;
}
private enum EmitValue {
KEY, ENTITY
}
private String realm;
private EmitValue emit = EmitValue.ENTITY;
public static UserLoginFailureMapper create(String realm) {
return new UserLoginFailureMapper(realm);
}
public UserLoginFailureMapper emitKey() {
emit = EmitValue.KEY;
return this;
}
@Override
public void map(LoginFailureKey key, LoginFailureEntity e, Collector collector) {
if (!realm.equals(e.getRealm())) {
return;
}
switch (emit) {
case KEY:
collector.emit(key, key);
break;
case ENTITY:
collector.emit(key, e);
break;
}
}
}

View file

@ -1,120 +0,0 @@
/*
* 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.models.sessions.infinispan.mapreduce;
import org.infinispan.distexec.mapreduce.Collector;
import org.infinispan.distexec.mapreduce.Mapper;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import java.io.Serializable;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class UserSessionMapper implements Mapper<String, SessionEntity, String, Object>, Serializable {
public UserSessionMapper(String realm) {
this.realm = realm;
}
private enum EmitValue {
KEY, ENTITY
}
private String realm;
private EmitValue emit = EmitValue.ENTITY;
private String user;
private Integer expired;
private Integer expiredRefresh;
private String brokerSessionId;
private String brokerUserId;
public static UserSessionMapper create(String realm) {
return new UserSessionMapper(realm);
}
public UserSessionMapper emitKey() {
emit = EmitValue.KEY;
return this;
}
public UserSessionMapper user(String user) {
this.user = user;
return this;
}
public UserSessionMapper expired(Integer expired, Integer expiredRefresh) {
this.expired = expired;
this.expiredRefresh = expiredRefresh;
return this;
}
public UserSessionMapper brokerSessionId(String id) {
this.brokerSessionId = id;
return this;
}
public UserSessionMapper brokerUserId(String id) {
this.brokerUserId = id;
return this;
}
@Override
public void map(String key, SessionEntity e, Collector collector) {
if (!(e instanceof UserSessionEntity)) {
return;
}
UserSessionEntity entity = (UserSessionEntity) e;
if (!realm.equals(entity.getRealm())) {
return;
}
if (user != null && !entity.getUser().equals(user)) {
return;
}
if (brokerSessionId != null && !brokerSessionId.equals(entity.getBrokerSessionId())) return;
if (brokerUserId != null && !brokerUserId.equals(entity.getBrokerUserId())) return;
if (expired != null && expiredRefresh != null && entity.getStarted() > expired && entity.getLastSessionRefresh() > expiredRefresh) {
return;
}
if (expired == null && expiredRefresh != null && entity.getLastSessionRefresh() > expiredRefresh) {
return;
}
switch (emit) {
case KEY:
collector.emit(key, key);
break;
case ENTITY:
collector.emit(key, entity);
break;
}
}
}

View file

@ -1,88 +0,0 @@
/*
* 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.models.sessions.infinispan.mapreduce;
import org.infinispan.distexec.mapreduce.Collector;
import org.infinispan.distexec.mapreduce.Mapper;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import java.io.Serializable;
import java.util.Map;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class UserSessionNoteMapper implements Mapper<String, SessionEntity, String, Object>, Serializable {
public UserSessionNoteMapper(String realm) {
this.realm = realm;
}
private enum EmitValue {
KEY, ENTITY
}
private String realm;
private EmitValue emit = EmitValue.ENTITY;
private Map<String, String> notes;
public static UserSessionNoteMapper create(String realm) {
return new UserSessionNoteMapper(realm);
}
public UserSessionNoteMapper emitKey() {
emit = EmitValue.KEY;
return this;
}
public UserSessionNoteMapper notes(Map<String, String> notes) {
this.notes = notes;
return this;
}
@Override
public void map(String key, SessionEntity e, Collector collector) {
if (!(e instanceof UserSessionEntity)) {
return;
}
UserSessionEntity entity = (UserSessionEntity) e;
if (!realm.equals(entity.getRealm())) {
return;
}
for (Map.Entry<String, String> entry : notes.entrySet()) {
String note = entity.getNotes().get(entry.getKey());
if (note == null) return;
if (!note.equals(entry.getValue())) return;
}
switch (emit) {
case KEY:
collector.emit(key, key);
break;
case ENTITY:
collector.emit(key, entity);
break;
}
}
}

View file

@ -0,0 +1,102 @@
/*
* 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.models.sessions.infinispan.remotestore;
import java.util.concurrent.Executor;
import org.infinispan.commons.configuration.ConfiguredBy;
import org.infinispan.filter.KeyFilter;
import org.infinispan.marshall.core.MarshalledEntry;
import org.infinispan.metadata.InternalMetadata;
import org.infinispan.persistence.remote.RemoteStore;
import org.infinispan.persistence.spi.PersistenceException;
import org.jboss.logging.Logger;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@ConfiguredBy(KcRemoteStoreConfiguration.class)
public class KcRemoteStore extends RemoteStore {
protected static final Logger logger = Logger.getLogger(KcRemoteStore.class);
private String cacheName;
@Override
public void start() throws PersistenceException {
super.start();
if (getRemoteCache() == null) {
String cacheName = getConfiguration().remoteCacheName();
throw new IllegalStateException("Remote cache '" + cacheName + "' is not available.");
}
this.cacheName = getRemoteCache().getName();
}
@Override
public MarshalledEntry load(Object key) throws PersistenceException {
logger.debugf("Calling load: '%s' for remote cache '%s'", key, cacheName);
MarshalledEntry entry = super.load(key);
if (entry == null) {
return null;
}
// wrap remote entity
SessionEntity entity = (SessionEntity) entry.getValue();
SessionEntityWrapper entityWrapper = new SessionEntityWrapper(entity);
MarshalledEntry wrappedEntry = marshalledEntry(entry.getKey(), entityWrapper, entry.getMetadata());
logger.debugf("Found entry in load: %s", wrappedEntry.toString());
return wrappedEntry;
}
@Override
public void process(KeyFilter filter, CacheLoaderTask task, Executor executor, boolean fetchValue, boolean fetchMetadata) {
logger.infof("Calling process with filter '%s' on cache '%s'", filter, cacheName);
super.process(filter, task, executor, fetchValue, fetchMetadata);
}
// Don't do anything. Writes handled by KC itself as we need more flexibility
@Override
public void write(MarshalledEntry entry) throws PersistenceException {
}
@Override
public boolean delete(Object key) throws PersistenceException {
logger.debugf("Calling delete for key '%s' on cache '%s'", key, cacheName);
// Optimization - we don't need to know the previous value. Also it's ok to trigger asynchronously
getRemoteCache().removeAsync(key);
return true;
}
protected MarshalledEntry marshalledEntry(Object key, Object value, InternalMetadata metadata) {
return ctx.getMarshalledEntryFactory().newMarshalledEntry(key, value, metadata);
}
}

View file

@ -0,0 +1,40 @@
/*
* 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.models.sessions.infinispan.remotestore;
import org.infinispan.commons.configuration.BuiltBy;
import org.infinispan.commons.configuration.ConfigurationFor;
import org.infinispan.commons.configuration.attributes.AttributeSet;
import org.infinispan.configuration.cache.AsyncStoreConfiguration;
import org.infinispan.configuration.cache.SingletonStoreConfiguration;
import org.infinispan.persistence.remote.configuration.ConnectionPoolConfiguration;
import org.infinispan.persistence.remote.configuration.ExecutorFactoryConfiguration;
import org.infinispan.persistence.remote.configuration.RemoteStoreConfiguration;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@BuiltBy(KcRemoteStoreConfigurationBuilder.class)
@ConfigurationFor(KcRemoteStore.class)
public class KcRemoteStoreConfiguration extends RemoteStoreConfiguration {
public KcRemoteStoreConfiguration(AttributeSet attributes, AsyncStoreConfiguration async, SingletonStoreConfiguration singletonStore,
ExecutorFactoryConfiguration asyncExecutorFactory, ConnectionPoolConfiguration connectionPool) {
super(attributes, async, singletonStore, asyncExecutorFactory, connectionPool);
}
}

View file

@ -0,0 +1,39 @@
/*
* 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.models.sessions.infinispan.remotestore;
import org.infinispan.configuration.cache.PersistenceConfigurationBuilder;
import org.infinispan.persistence.remote.configuration.RemoteStoreConfiguration;
import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class KcRemoteStoreConfigurationBuilder extends RemoteStoreConfigurationBuilder {
public KcRemoteStoreConfigurationBuilder(PersistenceConfigurationBuilder builder) {
super(builder);
}
@Override
public KcRemoteStoreConfiguration create() {
RemoteStoreConfiguration cfg = super.create();
KcRemoteStoreConfiguration cfg2 = new KcRemoteStoreConfiguration(cfg.attributes(), cfg.async(), cfg.singletonStore(), cfg.asyncExecutorFactory(), cfg.connectionPool());
return cfg2;
}
}

View file

@ -0,0 +1,163 @@
/*
* 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.models.sessions.infinispan.remotestore;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.infinispan.client.hotrod.Flag;
import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.client.hotrod.VersionedValue;
import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.changes.SessionUpdateTask;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class RemoteCacheInvoker {
public static final Logger logger = Logger.getLogger(RemoteCacheInvoker.class);
private final Map<String, RemoteCacheContext> remoteCaches = new HashMap<>();
public void addRemoteCache(String cacheName, RemoteCache remoteCache, MaxIdleTimeLoader maxIdleLoader) {
RemoteCacheContext ctx = new RemoteCacheContext(remoteCache, maxIdleLoader);
remoteCaches.put(cacheName, ctx);
}
public Set<String> getRemoteCacheNames() {
return Collections.unmodifiableSet(remoteCaches.keySet());
}
public <S extends SessionEntity> void runTask(KeycloakSession kcSession, RealmModel realm, String cacheName, String key, SessionUpdateTask<S> task, SessionEntityWrapper<S> sessionWrapper) {
RemoteCacheContext context = remoteCaches.get(cacheName);
if (context == null) {
return;
}
S session = sessionWrapper.getEntity();
SessionUpdateTask.CacheOperation operation = task.getOperation(session);
SessionUpdateTask.CrossDCMessageStatus status = task.getCrossDCMessageStatus(sessionWrapper);
if (status == SessionUpdateTask.CrossDCMessageStatus.NOT_NEEDED) {
logger.debugf("Skip writing to remoteCache for entity '%s' of cache '%s' and operation '%s'", key, cacheName, operation.toString());
return;
}
long maxIdleTimeMs = context.maxIdleTimeLoader.getMaxIdleTimeMs(realm);
// Double the timeout to ensure that entry won't expire on remoteCache in case that write of some entities to remoteCache is postponed (eg. userSession.lastSessionRefresh)
maxIdleTimeMs = maxIdleTimeMs * 2;
logger.debugf("Running task '%s' on remote cache '%s' . Key is '%s'", operation, cacheName, key);
runOnRemoteCache(context.remoteCache, maxIdleTimeMs, key, task, session);
}
private <S extends SessionEntity> void runOnRemoteCache(RemoteCache remoteCache, long maxIdleMs, String key, SessionUpdateTask<S> task, S session) {
SessionUpdateTask.CacheOperation operation = task.getOperation(session);
switch (operation) {
case REMOVE:
// REMOVE already handled at remote cache store level
//remoteCache.remove(key);
break;
case ADD:
remoteCache.put(key, session, task.getLifespanMs(), TimeUnit.MILLISECONDS, maxIdleMs, TimeUnit.MILLISECONDS);
break;
case ADD_IF_ABSENT:
SessionEntity existing = (SessionEntity) remoteCache
.withFlags(Flag.FORCE_RETURN_VALUE)
.putIfAbsent(key, session, -1, TimeUnit.MILLISECONDS, maxIdleMs, TimeUnit.MILLISECONDS);
if (existing != null) {
throw new IllegalStateException("There is already existing value in cache for key " + key);
}
break;
case REPLACE:
replace(remoteCache, task.getLifespanMs(), maxIdleMs, key, task);
break;
default:
throw new IllegalStateException("Unsupported state " + operation);
}
}
private <S extends SessionEntity> void replace(RemoteCache remoteCache, long lifespanMs, long maxIdleMs, String key, SessionUpdateTask<S> task) {
boolean replaced = false;
while (!replaced) {
VersionedValue<S> versioned = remoteCache.getVersioned(key);
if (versioned == null) {
logger.warnf("Not found entity to replace for key '%s'", key);
return;
}
S session = versioned.getValue();
// Run task on the remote session
task.runUpdate(session);
if (logger.isDebugEnabled()) {
logger.debugf("Before replaceWithVersion. Written entity: %s", session.toString());
}
replaced = remoteCache.replaceWithVersion(key, session, versioned.getVersion(), lifespanMs, TimeUnit.MILLISECONDS, maxIdleMs, TimeUnit.MILLISECONDS);
if (!replaced) {
logger.debugf("Failed to replace entity '%s' . Will retry again", key);
} else {
if (logger.isDebugEnabled()) {
logger.debugf("Replaced entity in remote cache: %s", session.toString());
}
}
}
}
private class RemoteCacheContext {
private final RemoteCache remoteCache;
private final MaxIdleTimeLoader maxIdleTimeLoader;
public RemoteCacheContext(RemoteCache remoteCache, MaxIdleTimeLoader maxIdleLoader) {
this.remoteCache = remoteCache;
this.maxIdleTimeLoader = maxIdleLoader;
}
}
@FunctionalInterface
public interface MaxIdleTimeLoader {
long getMaxIdleTimeMs(RealmModel realm);
}
}

View file

@ -0,0 +1,182 @@
/*
* 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.models.sessions.infinispan.remotestore;
import org.infinispan.Cache;
import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.client.hotrod.annotation.ClientCacheEntryCreated;
import org.infinispan.client.hotrod.annotation.ClientCacheEntryModified;
import org.infinispan.client.hotrod.annotation.ClientCacheEntryRemoved;
import org.infinispan.client.hotrod.annotation.ClientCacheFailover;
import org.infinispan.client.hotrod.annotation.ClientListener;
import org.infinispan.client.hotrod.event.ClientCacheEntryCreatedEvent;
import org.infinispan.client.hotrod.event.ClientCacheEntryModifiedEvent;
import org.infinispan.client.hotrod.event.ClientCacheEntryRemovedEvent;
import org.infinispan.client.hotrod.event.ClientCacheFailoverEvent;
import org.infinispan.client.hotrod.event.ClientEvent;
import org.infinispan.context.Flag;
import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@ClientListener
public class RemoteCacheSessionListener {
protected static final Logger logger = Logger.getLogger(RemoteCacheSessionListener.class);
private Cache<String, SessionEntityWrapper> cache;
private RemoteCache remoteCache;
private boolean distributed;
private String myAddress;
protected RemoteCacheSessionListener() {
}
protected void init(KeycloakSession session, Cache<String, SessionEntityWrapper> cache, RemoteCache remoteCache) {
this.cache = cache;
this.remoteCache = remoteCache;
this.distributed = InfinispanUtil.isDistributedCache(cache);
if (this.distributed) {
this.myAddress = InfinispanUtil.getMyAddress(session);
} else {
this.myAddress = null;
}
}
@ClientCacheEntryCreated
public void created(ClientCacheEntryCreatedEvent event) {
String key = (String) event.getKey();
if (shouldUpdateLocalCache(event.getType(), key, event.isCommandRetried())) {
// Should load it from remoteStore
cache.get(key);
}
}
@ClientCacheEntryModified
public void updated(ClientCacheEntryModifiedEvent event) {
String key = (String) event.getKey();
if (shouldUpdateLocalCache(event.getType(), key, event.isCommandRetried())) {
replaceRemoteEntityInCache(key);
}
}
private void replaceRemoteEntityInCache(String key) {
// TODO can be optimized and remoteSession sent in the event itself?
SessionEntityWrapper localEntityWrapper = cache.get(key);
SessionEntity remoteSession = (SessionEntity) remoteCache.get(key);
if (logger.isDebugEnabled()) {
logger.debugf("Read session. Entity read from remote cache: %s", remoteSession.toString());
}
SessionEntityWrapper sessionWrapper = remoteSession.mergeRemoteEntityWithLocalEntity(localEntityWrapper);
// We received event from remoteCache, so we won't update it back
cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD, Flag.IGNORE_RETURN_VALUES)
.replace(key, sessionWrapper);
}
@ClientCacheEntryRemoved
public void removed(ClientCacheEntryRemovedEvent event) {
String key = (String) event.getKey();
if (shouldUpdateLocalCache(event.getType(), key, event.isCommandRetried())) {
// We received event from remoteCache, so we won't update it back
cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD, Flag.IGNORE_RETURN_VALUES)
.remove(key);
}
}
@ClientCacheFailover
public void failover(ClientCacheFailoverEvent event) {
logger.infof("Received failover event: " + event.toString());
}
// For distributed caches, ensure that local modification is executed just on owner OR if event.isCommandRetried
protected boolean shouldUpdateLocalCache(ClientEvent.Type type, String key, boolean commandRetried) {
boolean result;
// Case when cache is stopping or stopped already
if (!cache.getStatus().allowInvocations()) {
return false;
}
if (!distributed || commandRetried) {
result = true;
} else {
String keyAddress = InfinispanUtil.getKeyPrimaryOwnerAddress(cache, key);
result = myAddress.equals(keyAddress);
}
logger.debugf("Received event from remote store. Event '%s', key '%s', skip '%b'", type.toString(), key, !result);
return result;
}
@ClientListener(includeCurrentState = true)
public static class FetchInitialStateCacheListener extends RemoteCacheSessionListener {
}
@ClientListener(includeCurrentState = false)
public static class DontFetchInitialStateCacheListener extends RemoteCacheSessionListener {
}
public static RemoteCacheSessionListener createListener(KeycloakSession session, Cache<String, SessionEntityWrapper> cache, RemoteCache remoteCache) {
/*boolean isCoordinator = InfinispanUtil.isCoordinator(cache);
// Just cluster coordinator will fetch userSessions from remote cache.
// In case that coordinator is failover during state fetch, there is slight risk that not all userSessions will be fetched to local cluster. Assume acceptable for now
RemoteCacheSessionListener listener;
if (isCoordinator) {
logger.infof("Will fetch initial state from remote cache for cache '%s'", cache.getName());
listener = new FetchInitialStateCacheListener();
} else {
logger.infof("Won't fetch initial state from remote cache for cache '%s'", cache.getName());
listener = new DontFetchInitialStateCacheListener();
}*/
RemoteCacheSessionListener listener = new RemoteCacheSessionListener();
listener.init(session, cache, remoteCache);
return listener;
}
}

View file

@ -0,0 +1,120 @@
/*
* 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.models.sessions.infinispan.remotestore;
import java.io.Serializable;
import java.util.Map;
import org.infinispan.Cache;
import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.context.Flag;
import org.jboss.logging.Logger;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.initializer.BaseCacheInitializer;
import org.keycloak.models.sessions.infinispan.initializer.OfflinePersistentUserSessionLoader;
import org.keycloak.models.sessions.infinispan.initializer.SessionLoader;
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class RemoteCacheSessionsLoader implements SessionLoader {
private static final Logger log = Logger.getLogger(RemoteCacheSessionsLoader.class);
// Hardcoded limit for now. See if needs to be configurable (or if preloading can be enabled/disabled in configuration)
public static final int LIMIT = 100000;
private final String cacheName;
public RemoteCacheSessionsLoader(String cacheName) {
this.cacheName = cacheName;
}
@Override
public void init(KeycloakSession session) {
}
@Override
public int getSessionsCount(KeycloakSession session) {
RemoteCache remoteCache = InfinispanUtil.getRemoteCache(getCache(session));
return remoteCache.size();
}
@Override
public boolean loadSessions(KeycloakSession session, int first, int max) {
Cache cache = getCache(session);
Cache decoratedCache = cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_LOAD, Flag.SKIP_CACHE_STORE, Flag.IGNORE_RETURN_VALUES);
RemoteCache<?, ?> remoteCache = InfinispanUtil.getRemoteCache(cache);
int size = remoteCache.size();
if (size > LIMIT) {
log.infof("Skip bulk load of '%d' sessions from remote cache '%s'. Sessions will be retrieved lazily", size, cache.getName());
return true;
} else {
log.infof("Will do bulk load of '%d' sessions from remote cache '%s'", size, cache.getName());
}
for (Map.Entry<?, ?> entry : remoteCache.getBulk().entrySet()) {
SessionEntity entity = (SessionEntity) entry.getValue();
SessionEntityWrapper entityWrapper = new SessionEntityWrapper(entity);
decoratedCache.putAsync(entry.getKey(), entityWrapper);
}
return true;
}
private Cache getCache(KeycloakSession session) {
InfinispanConnectionProvider ispn = session.getProvider(InfinispanConnectionProvider.class);
return ispn.getCache(cacheName);
}
@Override
public boolean isFinished(BaseCacheInitializer initializer) {
Cache<String, Serializable> workCache = initializer.getWorkCache();
// Check if persistent sessions were already loaded in this DC. This is possible just for offline sessions ATM
Boolean sessionsLoaded = (Boolean) workCache
.getAdvancedCache().withFlags(Flag.SKIP_CACHE_LOAD, Flag.SKIP_CACHE_STORE)
.get(OfflinePersistentUserSessionLoader.PERSISTENT_SESSIONS_LOADED_IN_CURRENT_DC);
if (cacheName.equals(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME) && sessionsLoaded != null && sessionsLoaded) {
log.debugf("Sessions already loaded in current DC. Skip sessions loading from remote cache '%s'", cacheName);
return true;
} else {
log.debugf("Sessions maybe not yet loaded in current DC. Will load them from remote cache '%s'", cacheName);
return false;
}
}
@Override
public void afterAllSessionsLoaded(BaseCacheInitializer initializer) {
}
}

View file

@ -17,7 +17,7 @@
package org.keycloak.models.sessions.infinispan.stream; package org.keycloak.models.sessions.infinispan.stream;
import org.keycloak.models.sessions.infinispan.UserSessionTimestamp; import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity; import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey; import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity; import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
@ -25,7 +25,6 @@ import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import java.io.Serializable; import java.io.Serializable;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.function.Function; import java.util.function.Function;
/** /**
@ -33,19 +32,19 @@ import java.util.function.Function;
*/ */
public class Mappers { public class Mappers {
public static Function<Map.Entry<String, Optional<UserSessionTimestamp>>, UserSessionTimestamp> userSessionTimestamp() { public static Function<Map.Entry<String, SessionEntityWrapper>, Map.Entry<String, SessionEntity>> unwrap() {
return new UserSessionTimestampMapper(); return new SessionUnwrap();
} }
public static Function<Map.Entry<String, SessionEntity>, String> sessionId() { public static Function<Map.Entry<String, SessionEntityWrapper<UserSessionEntity>>, String> sessionId() {
return new SessionIdMapper(); return new SessionIdMapper();
} }
public static Function<Map.Entry<String, SessionEntity>, SessionEntity> sessionEntity() { public static Function<Map.Entry<String, SessionEntityWrapper>, SessionEntity> sessionEntity() {
return new SessionEntityMapper(); return new SessionEntityMapper();
} }
public static Function<Map.Entry<String, SessionEntity>, UserSessionEntity> userSessionEntity() { public static Function<Map.Entry<String, SessionEntityWrapper<UserSessionEntity>>, UserSessionEntity> userSessionEntity() {
return new UserSessionEntityMapper(); return new UserSessionEntityMapper();
} }
@ -53,32 +52,55 @@ public class Mappers {
return new LoginFailureIdMapper(); return new LoginFailureIdMapper();
} }
private static class UserSessionTimestampMapper implements Function<Map.Entry<String, Optional<org.keycloak.models.sessions.infinispan.UserSessionTimestamp>>, org.keycloak.models.sessions.infinispan.UserSessionTimestamp>, Serializable {
private static class SessionUnwrap implements Function<Map.Entry<String, SessionEntityWrapper>, Map.Entry<String, SessionEntity>>, Serializable {
@Override @Override
public org.keycloak.models.sessions.infinispan.UserSessionTimestamp apply(Map.Entry<String, Optional<org.keycloak.models.sessions.infinispan.UserSessionTimestamp>> e) { public Map.Entry<String, SessionEntity> apply(Map.Entry<String, SessionEntityWrapper> wrapperEntry) {
return e.getValue().get(); return new Map.Entry<String, SessionEntity>() {
@Override
public String getKey() {
return wrapperEntry.getKey();
}
@Override
public SessionEntity getValue() {
return wrapperEntry.getValue().getEntity();
}
@Override
public SessionEntity setValue(SessionEntity value) {
throw new IllegalStateException("Unsupported operation");
}
};
} }
} }
private static class SessionIdMapper implements Function<Map.Entry<String, SessionEntity>, String>, Serializable {
private static class SessionIdMapper implements Function<Map.Entry<String, SessionEntityWrapper<UserSessionEntity>>, String>, Serializable {
@Override @Override
public String apply(Map.Entry<String, SessionEntity> entry) { public String apply(Map.Entry<String, SessionEntityWrapper<UserSessionEntity>> entry) {
return entry.getKey(); return entry.getKey();
} }
} }
private static class SessionEntityMapper implements Function<Map.Entry<String, SessionEntity>, SessionEntity>, Serializable { private static class SessionEntityMapper implements Function<Map.Entry<String, SessionEntityWrapper>, SessionEntity>, Serializable {
@Override @Override
public SessionEntity apply(Map.Entry<String, SessionEntity> entry) { public SessionEntity apply(Map.Entry<String, SessionEntityWrapper> entry) {
return entry.getValue(); return entry.getValue().getEntity();
} }
} }
private static class UserSessionEntityMapper implements Function<Map.Entry<String, SessionEntity>, UserSessionEntity>, Serializable { private static class UserSessionEntityMapper implements Function<Map.Entry<String, SessionEntityWrapper<UserSessionEntity>>, UserSessionEntity>, Serializable {
@Override @Override
public UserSessionEntity apply(Map.Entry<String, SessionEntity> entry) { public UserSessionEntity apply(Map.Entry<String, SessionEntityWrapper<UserSessionEntity>> entry) {
return (UserSessionEntity) entry.getValue(); return entry.getValue().getEntity();
} }
} }
private static class LoginFailureIdMapper implements Function<Map.Entry<LoginFailureKey, LoginFailureEntity>, LoginFailureKey>, Serializable { private static class LoginFailureIdMapper implements Function<Map.Entry<LoginFailureKey, LoginFailureEntity>, LoginFailureKey>, Serializable {

View file

@ -17,6 +17,7 @@
package org.keycloak.models.sessions.infinispan.stream; package org.keycloak.models.sessions.infinispan.stream;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity; import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import java.io.Serializable; import java.io.Serializable;
@ -26,7 +27,7 @@ import java.util.function.Predicate;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
public class SessionPredicate implements Predicate<Map.Entry<String, SessionEntity>>, Serializable { public class SessionPredicate<S extends SessionEntity> implements Predicate<Map.Entry<String, SessionEntityWrapper<S>>>, Serializable {
private String realm; private String realm;
@ -39,8 +40,8 @@ public class SessionPredicate implements Predicate<Map.Entry<String, SessionEnti
} }
@Override @Override
public boolean test(Map.Entry<String, SessionEntity> entry) { public boolean test(Map.Entry<String, SessionEntityWrapper<S>> entry) {
return realm.equals(entry.getValue().getRealm()); return realm.equals(entry.getValue().getEntity().getRealm());
} }
} }

View file

@ -17,6 +17,7 @@
package org.keycloak.models.sessions.infinispan.stream; package org.keycloak.models.sessions.infinispan.stream;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity; import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
@ -27,7 +28,7 @@ import java.util.function.Predicate;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
public class UserSessionPredicate implements Predicate<Map.Entry<String, SessionEntity>>, Serializable { public class UserSessionPredicate implements Predicate<Map.Entry<String, SessionEntityWrapper<UserSessionEntity>>>, Serializable {
private String realm; private String realm;
@ -77,12 +78,8 @@ public class UserSessionPredicate implements Predicate<Map.Entry<String, Session
} }
@Override @Override
public boolean test(Map.Entry<String, SessionEntity> entry) { public boolean test(Map.Entry<String, SessionEntityWrapper<UserSessionEntity>> entry) {
SessionEntity e = entry.getValue(); SessionEntity e = entry.getValue().getEntity();
if (!(e instanceof UserSessionEntity)) {
return false;
}
UserSessionEntity entity = (UserSessionEntity) e; UserSessionEntity entity = (UserSessionEntity) e;

View file

@ -0,0 +1,95 @@
/*
* 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.models.sessions.infinispan.util;
import java.util.Set;
import org.infinispan.Cache;
import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.configuration.cache.CacheMode;
import org.infinispan.distribution.DistributionManager;
import org.infinispan.persistence.manager.PersistenceManager;
import org.infinispan.persistence.remote.RemoteStore;
import org.infinispan.remoting.transport.Address;
import org.infinispan.remoting.transport.Transport;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class InfinispanUtil {
// See if we have RemoteStore (external JDG) configured for cross-Data-Center scenario
public static Set<RemoteStore> getRemoteStores(Cache ispnCache) {
return ispnCache.getAdvancedCache().getComponentRegistry().getComponent(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 boolean isDistributedCache(Cache ispnCache) {
CacheMode cacheMode = ispnCache.getCacheConfiguration().clustering().cacheMode();
return cacheMode.isDistributed();
}
public static String getMyAddress(KeycloakSession session) {
return session.getProvider(InfinispanConnectionProvider.class).getNodeName();
}
public static String getMySite(KeycloakSession session) {
return session.getProvider(InfinispanConnectionProvider.class).getSiteName();
}
/**
*
* @param ispnCache
* @param key
* @return address of the node, who is owner of the specified key in current cluster
*/
public static String getKeyPrimaryOwnerAddress(Cache ispnCache, Object key) {
DistributionManager distManager = ispnCache.getAdvancedCache().getDistributionManager();
if (distManager == null) {
throw new IllegalArgumentException("Cache '" + ispnCache.getName() + "' is not distributed cache");
}
return distManager.getPrimaryLocation(key).toString();
}
/**
*
* @param cache
* @return true if cluster coordinator OR if it's local cache
*/
public static boolean isCoordinator(Cache cache) {
Transport transport = cache.getCacheManager().getTransport();
return transport == null || transport.isCoordinator();
}
}

View file

@ -0,0 +1,164 @@
/*
* 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.models.sessions.infinispan.util;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.infinispan.commons.marshall.Externalizer;
import org.infinispan.commons.marshall.MarshallUtil;
import org.jboss.logging.Logger;
/**
*
* Helper to optimize marshalling/unmarhsalling of some types
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class KeycloakMarshallUtil {
private static final Logger log = Logger.getLogger(KeycloakMarshallUtil.class);
public static final StringExternalizer STRING_EXT = new StringExternalizer();
// MAP
public static <K, V> void writeMap(Map<K, V> map, Externalizer<K> keyExternalizer, Externalizer<V> valueExternalizer, ObjectOutput output) throws IOException {
if (map == null) {
output.writeByte(0);
} else {
output.writeByte(1);
// Copy the map as it can be updated concurrently
Map<K, V> copy = new HashMap<>(map);
//Map<K, V> copy = map;
output.writeInt(copy.size());
for (Map.Entry<K, V> entry : copy.entrySet()) {
keyExternalizer.writeObject(output, entry.getKey());
valueExternalizer.writeObject(output, entry.getValue());
}
}
}
public static <K, V, TYPED_MAP extends Map<K, V>> TYPED_MAP readMap(ObjectInput input,
Externalizer<K> keyExternalizer, Externalizer<V> valueExternalizer,
MarshallUtil.MapBuilder<K, V, TYPED_MAP> mapBuilder) throws IOException, ClassNotFoundException {
byte b = input.readByte();
if (b == 0) {
return null;
} else {
int size = input.readInt();
TYPED_MAP map = mapBuilder.build(size);
for (int i=0 ; i<size ; i++) {
K key = keyExternalizer.readObject(input);
V value = valueExternalizer.readObject(input);
map.put(key, value);
}
return map;
}
}
// COLLECTION
public static <E> void writeCollection(Collection<E> col, Externalizer<E> valueExternalizer, ObjectOutput output) throws IOException {
if (col == null) {
output.writeByte(0);
} else {
output.writeByte(1);
// Copy the collection as it can be updated concurrently
Collection<E> copy = new LinkedList<>(col);
output.writeInt(copy.size());
for (E entry : copy) {
valueExternalizer.writeObject(output, entry);
}
}
}
public static <E, T extends Collection<E>> T readCollection(ObjectInput input, Externalizer<E> valueExternalizer,
MarshallUtil.CollectionBuilder<E, T> colBuilder) throws ClassNotFoundException, IOException {
byte b = input.readByte();
if (b == 0) {
return null;
} else {
int size = input.readInt();
T col = colBuilder.build(size);
for (int i=0 ; i<size ; i++) {
E value = valueExternalizer.readObject(input);
col.add(value);
}
return col;
}
}
public static class ConcurrentHashMapBuilder<K, V> implements MarshallUtil.MapBuilder<K, V, ConcurrentHashMap<K, V>> {
@Override
public ConcurrentHashMap<K, V> build(int size) {
return new ConcurrentHashMap<>(size);
}
}
public static class HashSetBuilder<E> implements MarshallUtil.CollectionBuilder<E, HashSet<E>> {
@Override
public HashSet<E> build(int size) {
return new HashSet<>(size);
}
}
private static class StringExternalizer implements Externalizer<String> {
@Override
public void writeObject(ObjectOutput output, String str) throws IOException {
MarshallUtil.marshallString(str, output);
}
@Override
public String readObject(ObjectInput input) throws IOException, ClassNotFoundException {
return MarshallUtil.unmarshallString(input);
}
}
}

View file

@ -214,7 +214,7 @@ public class ConcurrencyJDGRemoteCacheTest {
} }
} }
private static class EntryInfo { public static class EntryInfo {
AtomicInteger successfulInitializations = new AtomicInteger(0); AtomicInteger successfulInitializations = new AtomicInteger(0);
AtomicInteger successfulListenerWrites = new AtomicInteger(0); AtomicInteger successfulListenerWrites = new AtomicInteger(0);
AtomicInteger th1 = new AtomicInteger(); AtomicInteger th1 = new AtomicInteger();

View file

@ -0,0 +1,378 @@
/*
* 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.cluster.infinispan;
import java.util.Arrays;
import java.util.HashSet;
import java.util.concurrent.atomic.AtomicInteger;
import org.infinispan.Cache;
import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.client.hotrod.VersionedValue;
import org.infinispan.client.hotrod.annotation.ClientCacheEntryCreated;
import org.infinispan.client.hotrod.annotation.ClientCacheEntryModified;
import org.infinispan.client.hotrod.annotation.ClientListener;
import org.infinispan.client.hotrod.event.ClientCacheEntryCreatedEvent;
import org.infinispan.client.hotrod.event.ClientCacheEntryModifiedEvent;
import org.infinispan.configuration.cache.Configuration;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.configuration.global.GlobalConfigurationBuilder;
import org.infinispan.context.Flag;
import org.infinispan.manager.DefaultCacheManager;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.persistence.manager.PersistenceManager;
import org.infinispan.persistence.remote.RemoteStore;
import org.infinispan.persistence.remote.configuration.ExhaustedAction;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import org.keycloak.models.sessions.infinispan.remotestore.KcRemoteStore;
import org.keycloak.models.sessions.infinispan.remotestore.KcRemoteStoreConfigurationBuilder;
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
/**
* Test requires to prepare 2 JDG (or infinispan servers) before it's runned.
* Steps:
* - In JDG1/standalone/configuration/clustered.xml add this: <replicated-cache name="sessions" mode="SYNC" start="EAGER"/>
* - Same in JDG2
* - Run JDG1 with: ./standalone.sh -c clustered.xml
* - Run JDG2 with: ./standalone.sh -c clustered.xml -Djboss.socket.binding.port-offset=100
* - Run this test
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ConcurrencyJDGSessionsCacheTest {
protected static final Logger logger = Logger.getLogger(KcRemoteStore.class);
private static final int ITERATION_PER_WORKER = 1000;
private static final AtomicInteger failedReplaceCounter = new AtomicInteger(0);
private static final AtomicInteger failedReplaceCounter2 = new AtomicInteger(0);
private static final AtomicInteger successfulListenerWrites = new AtomicInteger(0);
private static final AtomicInteger successfulListenerWrites2 = new AtomicInteger(0);
//private static Map<String, EntryInfo> state = new HashMap<>();
public static void main(String[] args) throws Exception {
Cache<String, SessionEntityWrapper<UserSessionEntity>> cache1 = createManager(1).getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
Cache<String, SessionEntityWrapper<UserSessionEntity>> cache2 = createManager(2).getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
// Create initial item
UserSessionEntity session = new UserSessionEntity();
session.setId("123");
session.setRealm("foo");
session.setBrokerSessionId("!23123123");
session.setBrokerUserId(null);
session.setUser("foo");
session.setLoginUsername("foo");
session.setIpAddress("123.44.143.178");
session.setStarted(Time.currentTime());
session.setLastSessionRefresh(Time.currentTime());
AuthenticatedClientSessionEntity clientSession = new AuthenticatedClientSessionEntity();
clientSession.setAuthMethod("saml");
clientSession.setAction("something");
clientSession.setTimestamp(1234);
clientSession.setProtocolMappers(new HashSet<>(Arrays.asList("mapper1", "mapper2")));
clientSession.setRoles(new HashSet<>(Arrays.asList("role1", "role2")));
session.getAuthenticatedClientSessions().put("client1", clientSession);
SessionEntityWrapper<UserSessionEntity> wrappedSession = new SessionEntityWrapper<>(session);
// Some dummy testing of remoteStore behaviour
logger.info("Before put");
cache1
.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL) // will still invoke remoteStore . Just doesn't propagate to cluster
.put("123", wrappedSession);
logger.info("After put");
cache1.replace("123", wrappedSession);
logger.info("After replace");
cache1.get("123");
logger.info("After cache1.get");
cache2.get("123");
logger.info("After cache2.get");
cache1.get("123");
logger.info("After cache1.get - second call");
cache2.get("123");
logger.info("After cache2.get - second call");
cache2.replace("123", wrappedSession);
logger.info("After replace - second call");
cache1.get("123");
logger.info("After cache1.get - third call");
cache2.get("123");
logger.info("After cache2.get - third call");
cache1
.getAdvancedCache().withFlags(Flag.SKIP_CACHE_LOAD)
.entrySet().stream().forEach(e -> {
});
logger.info("After cache1.stream");
// Explicitly call put on remoteCache (KcRemoteCache.write ignores remote writes)
InfinispanUtil.getRemoteCache(cache1).put("123", session);
// Create caches, listeners and finally worker threads
Thread worker1 = createWorker(cache1, 1);
Thread worker2 = createWorker(cache2, 2);
long start = System.currentTimeMillis();
// Start and join workers
worker1.start();
worker2.start();
worker1.join();
worker2.join();
long took = System.currentTimeMillis() - start;
// // Output
// for (Map.Entry<String, EntryInfo> entry : state.entrySet()) {
// System.out.println(entry.getKey() + ":::" + entry.getValue());
// worker1.cache.remove(entry.getKey());
// }
System.out.println("Finished. Took: " + took + " ms. Notes: " + cache1.get("123").getEntity().getNotes().size() +
", successfulListenerWrites: " + successfulListenerWrites.get() + ", successfulListenerWrites2: " + successfulListenerWrites2.get() +
", failedReplaceCounter: " + failedReplaceCounter.get() + ", failedReplaceCounter2: " + failedReplaceCounter2.get() );
// Finish JVM
cache1.getCacheManager().stop();
cache2.getCacheManager().stop();
}
private static Thread createWorker(Cache<String, SessionEntityWrapper<UserSessionEntity>> cache, int threadId) {
System.out.println("Retrieved cache: " + threadId);
RemoteCache remoteCache = InfinispanUtil.getRemoteCache(cache);
remoteCache.keySet();
AtomicInteger counter = threadId ==1 ? successfulListenerWrites : successfulListenerWrites2;
HotRodListener listener = new HotRodListener(cache, remoteCache, counter);
remoteCache.addClientListener(listener);
return new RemoteCacheWorker(remoteCache, threadId);
//return new CacheWorker(cache, threadId);
}
private static EmbeddedCacheManager createManager(int threadId) {
System.setProperty("java.net.preferIPv4Stack", "true");
System.setProperty("jgroups.tcp.port", "53715");
GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder();
boolean clustered = false;
boolean async = false;
boolean allowDuplicateJMXDomains = true;
if (clustered) {
gcb = gcb.clusteredDefault();
gcb.transport().clusterName("test-clustering");
}
gcb.globalJmxStatistics().allowDuplicateDomains(allowDuplicateJMXDomains);
EmbeddedCacheManager cacheManager = new DefaultCacheManager(gcb.build());
Configuration invalidationCacheConfiguration = getCacheBackedByRemoteStore(threadId);
cacheManager.defineConfiguration(InfinispanConnectionProvider.SESSION_CACHE_NAME, invalidationCacheConfiguration);
return cacheManager;
}
private static Configuration getCacheBackedByRemoteStore(int threadId) {
ConfigurationBuilder cacheConfigBuilder = new ConfigurationBuilder();
//int port = threadId==1 ? 11222 : 11322;
int port = 11222;
return cacheConfigBuilder.persistence().addStore(KcRemoteStoreConfigurationBuilder.class)
.fetchPersistentState(false)
.ignoreModifications(false)
.purgeOnStartup(false)
.preload(false)
.shared(true)
.remoteCacheName(InfinispanConnectionProvider.SESSION_CACHE_NAME)
.rawValues(true)
.forceReturnValues(false)
.marshaller(KeycloakHotRodMarshallerFactory.class.getName())
.addServer()
.host("localhost")
.port(port)
.connectionPool()
.maxActive(20)
.exhaustedAction(ExhaustedAction.CREATE_NEW)
.async()
.enabled(false).build();
}
@ClientListener
public static class HotRodListener {
private Cache<String, SessionEntityWrapper<UserSessionEntity>> origCache;
private RemoteCache remoteCache;
private AtomicInteger listenerCount;
public HotRodListener(Cache<String, SessionEntityWrapper<UserSessionEntity>> origCache, RemoteCache remoteCache, AtomicInteger listenerCount) {
this.listenerCount = listenerCount;
this.remoteCache = remoteCache;
this.origCache = origCache;
}
@ClientCacheEntryCreated
public void created(ClientCacheEntryCreatedEvent event) {
String cacheKey = (String) event.getKey();
listenerCount.incrementAndGet();
}
@ClientCacheEntryModified
public void updated(ClientCacheEntryModifiedEvent event) {
String cacheKey = (String) event.getKey();
listenerCount.incrementAndGet();
// TODO: can be optimized
SessionEntity session = (SessionEntity) remoteCache.get(cacheKey);
SessionEntityWrapper sessionWrapper = new SessionEntityWrapper(session);
// TODO: for distributed caches, ensure that it is executed just on owner OR if event.isCommandRetried
origCache
.getAdvancedCache().withFlags(Flag.SKIP_CACHE_LOAD, Flag.SKIP_CACHE_STORE)
.replace(cacheKey, sessionWrapper);
}
}
private static class RemoteCacheWorker extends Thread {
private final RemoteCache<String, UserSessionEntity> cache;
private final int myThreadId;
private RemoteCacheWorker(RemoteCache cache, int myThreadId) {
this.cache = cache;
this.myThreadId = myThreadId;
}
@Override
public void run() {
for (int i=0 ; i<ITERATION_PER_WORKER ; i++) {
String noteKey = "n-" + myThreadId + "-" + i;
boolean replaced = false;
while (!replaced) {
VersionedValue<UserSessionEntity> versioned = cache.getVersioned("123");
UserSessionEntity oldSession = versioned.getValue();
//UserSessionEntity clone = DistributedCacheConcurrentWritesTest.cloneSession(oldSession);
UserSessionEntity clone = oldSession;
clone.getNotes().put(noteKey, "someVal");
//cache.replace("123", clone);
replaced = cacheReplace(versioned, clone);
}
}
}
private boolean cacheReplace(VersionedValue<UserSessionEntity> oldSession, UserSessionEntity newSession) {
try {
boolean replaced = cache.replaceWithVersion("123", newSession, oldSession.getVersion());
//cache.replace("123", newSession);
if (!replaced) {
failedReplaceCounter.incrementAndGet();
//return false;
//System.out.println("Replace failed!!!");
}
return replaced;
} catch (Exception re) {
failedReplaceCounter2.incrementAndGet();
return false;
}
//return replaced;
}
}
/*
// Worker, which operates on "classic" cache and rely on operations delegated to the second cache
private static class CacheWorker extends Thread {
private final Cache<String, SessionEntityWrapper<UserSessionEntity>> cache;
private final int myThreadId;
private CacheWorker(Cache<String, SessionEntityWrapper<UserSessionEntity>> cache, int myThreadId) {
this.cache = cache;
this.myThreadId = myThreadId;
}
@Override
public void run() {
for (int i=0 ; i<ITERATION_PER_WORKER ; i++) {
String noteKey = "n-" + myThreadId + "-" + i;
boolean replaced = false;
while (!replaced) {
VersionedValue<UserSessionEntity> versioned = cache.getVersioned("123");
UserSessionEntity oldSession = versioned.getValue();
//UserSessionEntity clone = DistributedCacheConcurrentWritesTest.cloneSession(oldSession);
UserSessionEntity clone = oldSession;
clone.getNotes().put(noteKey, "someVal");
//cache.replace("123", clone);
replaced = cacheReplace(versioned, clone);
}
}
}
}*/
}

View file

@ -0,0 +1,253 @@
/*
* 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.models.sessions.infinispan.initializer;
import java.util.Arrays;
import java.util.HashSet;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import org.infinispan.Cache;
import org.infinispan.configuration.cache.CacheMode;
import org.infinispan.configuration.cache.Configuration;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.configuration.global.GlobalConfigurationBuilder;
import org.infinispan.manager.DefaultCacheManager;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.remoting.transport.jgroups.JGroupsTransport;
import org.jgroups.JChannel;
import org.junit.Ignore;
import org.keycloak.common.util.Time;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
/**
* Test concurrent writes to distributed cache with usage of atomic replace
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@Ignore
public class DistributedCacheConcurrentWritesTest {
private static final int ITERATION_PER_WORKER = 1000;
private static final AtomicInteger failedReplaceCounter = new AtomicInteger(0);
private static final AtomicInteger failedReplaceCounter2 = new AtomicInteger(0);
public static void main(String[] args) throws Exception {
CacheWrapper<String, UserSessionEntity> cache1 = createCache("node1");
CacheWrapper<String, UserSessionEntity> cache2 = createCache("node2");
// Create initial item
UserSessionEntity session = new UserSessionEntity();
session.setId("123");
session.setRealm("foo");
session.setBrokerSessionId("!23123123");
session.setBrokerUserId(null);
session.setUser("foo");
session.setLoginUsername("foo");
session.setIpAddress("123.44.143.178");
session.setStarted(Time.currentTime());
session.setLastSessionRefresh(Time.currentTime());
AuthenticatedClientSessionEntity clientSession = new AuthenticatedClientSessionEntity();
clientSession.setAuthMethod("saml");
clientSession.setAction("something");
clientSession.setTimestamp(1234);
clientSession.setProtocolMappers(new HashSet<>(Arrays.asList("mapper1", "mapper2")));
clientSession.setRoles(new HashSet<>(Arrays.asList("role1", "role2")));
session.getAuthenticatedClientSessions().put("client1", clientSession);
cache1.put("123", session);
// Create 2 workers for concurrent write and start them
Worker worker1 = new Worker(1, cache1);
Worker worker2 = new Worker(2, cache2);
long start = System.currentTimeMillis();
System.out.println("Started clustering test");
worker1.start();
//worker1.join();
worker2.start();
worker1.join();
worker2.join();
long took = System.currentTimeMillis() - start;
session = cache1.get("123").getEntity();
System.out.println("Took: " + took + " ms. Notes count: " + session.getNotes().size() + ", failedReplaceCounter: " + failedReplaceCounter.get()
+ ", failedReplaceCounter2: " + failedReplaceCounter2.get());
// JGroups statistics
JChannel channel = (JChannel)((JGroupsTransport)cache1.wrappedCache.getAdvancedCache().getRpcManager().getTransport()).getChannel();
System.out.println("Sent MB: " + channel.getSentBytes() / 1000000 + ", sent messages: " + channel.getSentMessages() + ", received MB: " + channel.getReceivedBytes() / 1000000 +
", received messages: " + channel.getReceivedMessages());
// Kill JVM
cache1.getCache().stop();
cache2.getCache().stop();
cache1.getCache().getCacheManager().stop();
cache2.getCache().getCacheManager().stop();
System.out.println("Managers killed");
}
private static class Worker extends Thread {
private final CacheWrapper<String, UserSessionEntity> cache;
private final int threadId;
public Worker(int threadId, CacheWrapper<String, UserSessionEntity> cache) {
this.threadId = threadId;
this.cache = cache;
}
@Override
public void run() {
for (int i=0 ; i<ITERATION_PER_WORKER ; i++) {
String noteKey = "n-" + threadId + "-" + i;
boolean replaced = false;
while (!replaced) {
SessionEntityWrapper<UserSessionEntity> oldWrapped = cache.get("123");
UserSessionEntity oldSession = oldWrapped.getEntity();
//UserSessionEntity clone = DistributedCacheConcurrentWritesTest.cloneSession(oldSession);
UserSessionEntity clone = oldSession;
clone.getNotes().put(noteKey, "someVal");
//cache.replace("123", clone);
replaced = cacheReplace(oldWrapped, clone);
}
}
}
private boolean cacheReplace(SessionEntityWrapper<UserSessionEntity> oldSession, UserSessionEntity newSession) {
try {
boolean replaced = cache.replace("123", oldSession, newSession);
//cache.replace("123", newSession);
if (!replaced) {
failedReplaceCounter.incrementAndGet();
//return false;
//System.out.println("Replace failed!!!");
}
return replaced;
} catch (Exception re) {
failedReplaceCounter2.incrementAndGet();
return false;
}
//return replaced;
}
}
// Session clone
private static UserSessionEntity cloneSession(UserSessionEntity session) {
UserSessionEntity clone = new UserSessionEntity();
clone.setId(session.getId());
clone.setRealm(session.getRealm());
clone.setNotes(new ConcurrentHashMap<>(session.getNotes()));
return clone;
}
// Cache creation utils
public static class CacheWrapper<K, V extends SessionEntity> {
private final Cache<K, SessionEntityWrapper<V>> wrappedCache;
public CacheWrapper(Cache<K, SessionEntityWrapper<V>> wrappedCache) {
this.wrappedCache = wrappedCache;
}
public SessionEntityWrapper<V> get(K key) {
SessionEntityWrapper<V> val = wrappedCache.get(key);
return val;
}
public void put(K key, V newVal) {
SessionEntityWrapper<V> newWrapper = new SessionEntityWrapper<>(newVal);
wrappedCache.put(key, newWrapper);
}
public boolean replace(K key, SessionEntityWrapper<V> oldVal, V newVal) {
SessionEntityWrapper<V> newWrapper = new SessionEntityWrapper<>(newVal);
return wrappedCache.replace(key, oldVal, newWrapper);
}
private Cache<K, SessionEntityWrapper<V>> getCache() {
return wrappedCache;
}
}
public static CacheWrapper<String, UserSessionEntity> createCache(String nodeName) {
EmbeddedCacheManager mgr = createManager(nodeName);
Cache<String, SessionEntityWrapper<UserSessionEntity>> wrapped = mgr.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
return new CacheWrapper<>(wrapped);
}
public static EmbeddedCacheManager createManager(String nodeName) {
System.setProperty("java.net.preferIPv4Stack", "true");
System.setProperty("jgroups.tcp.port", "53715");
GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder();
boolean clustered = true;
boolean async = false;
boolean allowDuplicateJMXDomains = true;
if (clustered) {
gcb = gcb.clusteredDefault();
gcb.transport().clusterName("test-clustering");
gcb.transport().nodeName(nodeName);
}
gcb.globalJmxStatistics().allowDuplicateDomains(allowDuplicateJMXDomains);
EmbeddedCacheManager cacheManager = new DefaultCacheManager(gcb.build());
ConfigurationBuilder distConfigBuilder = new ConfigurationBuilder();
if (clustered) {
distConfigBuilder.clustering().cacheMode(async ? CacheMode.DIST_ASYNC : CacheMode.DIST_SYNC);
distConfigBuilder.clustering().hash().numOwners(1);
// Disable L1 cache
distConfigBuilder.clustering().hash().l1().enabled(false);
}
Configuration distConfig = distConfigBuilder.build();
cacheManager.defineConfiguration(InfinispanConnectionProvider.SESSION_CACHE_NAME, distConfig);
return cacheManager;
}
}

View file

@ -0,0 +1,216 @@
/*
* 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.models.sessions.infinispan.initializer;
import java.util.Arrays;
import java.util.HashSet;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.infinispan.Cache;
import org.infinispan.configuration.cache.CacheMode;
import org.infinispan.configuration.cache.Configuration;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.configuration.cache.VersioningScheme;
import org.infinispan.configuration.global.GlobalConfigurationBuilder;
import org.infinispan.context.Flag;
import org.infinispan.manager.DefaultCacheManager;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.remoting.transport.jgroups.JGroupsTransport;
import org.infinispan.transaction.LockingMode;
import org.infinispan.transaction.lookup.DummyTransactionManagerLookup;
import org.infinispan.util.concurrent.IsolationLevel;
import org.jgroups.JChannel;
import org.junit.Ignore;
import org.keycloak.common.util.Time;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
/**
* Test concurrent writes to distributed cache with usage of write skew
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@Ignore
public class DistributedCacheWriteSkewTest {
private static final int ITERATION_PER_WORKER = 1000;
private static final AtomicInteger failedReplaceCounter = new AtomicInteger(0);
public static void main(String[] args) throws Exception {
Cache<String, UserSessionEntity> cache1 = createManager("node1").getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
Cache<String, UserSessionEntity> cache2 = createManager("node2").getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
// Create initial item
UserSessionEntity session = new UserSessionEntity();
session.setId("123");
session.setRealm("foo");
session.setBrokerSessionId("!23123123");
session.setBrokerUserId(null);
session.setUser("foo");
session.setLoginUsername("foo");
session.setIpAddress("123.44.143.178");
session.setStarted(Time.currentTime());
session.setLastSessionRefresh(Time.currentTime());
AuthenticatedClientSessionEntity clientSession = new AuthenticatedClientSessionEntity();
clientSession.setAuthMethod("saml");
clientSession.setAction("something");
clientSession.setTimestamp(1234);
clientSession.setProtocolMappers(new HashSet<>(Arrays.asList("mapper1", "mapper2")));
clientSession.setRoles(new HashSet<>(Arrays.asList("role1", "role2")));
session.getAuthenticatedClientSessions().put("client1", clientSession);
cache1.put("123", session);
//cache1.replace("123", session);
// Create 2 workers for concurrent write and start them
Worker worker1 = new Worker(1, cache1);
Worker worker2 = new Worker(2, cache2);
long start = System.currentTimeMillis();
System.out.println("Started clustering test");
worker1.start();
//worker1.join();
worker2.start();
worker1.join();
worker2.join();
long took = System.currentTimeMillis() - start;
session = cache1.get("123");
System.out.println("Took: " + took + " ms. Notes count: " + session.getNotes().size() + ", failedReplaceCounter: " + failedReplaceCounter.get());
// JGroups statistics
JChannel channel = (JChannel)((JGroupsTransport)cache1.getAdvancedCache().getRpcManager().getTransport()).getChannel();
System.out.println("Sent MB: " + channel.getSentBytes() / 1000000 + ", sent messages: " + channel.getSentMessages() + ", received MB: " + channel.getReceivedBytes() / 1000000 +
", received messages: " + channel.getReceivedMessages());
// Kill JVM
cache1.stop();
cache2.stop();
cache1.getCacheManager().stop();
cache2.getCacheManager().stop();
System.out.println("Managers killed");
}
private static class Worker extends Thread {
private final Cache<String, UserSessionEntity> cache;
private final int threadId;
public Worker(int threadId, Cache<String, UserSessionEntity> cache) {
this.threadId = threadId;
this.cache = cache;
}
@Override
public void run() {
for (int i=0 ; i<ITERATION_PER_WORKER ; i++) {
String noteKey = "n-" + threadId + "-" + i;
boolean replaced = false;
while (!replaced) {
try {
//cache.startBatch();
UserSessionEntity oldSession = cache.get("123");
//UserSessionEntity clone = DistributedCacheConcurrentWritesTest.cloneSession(oldSession);
UserSessionEntity clone = oldSession;
clone.getNotes().put(noteKey, "someVal");
cache.replace("123", clone);
//cache.getAdvancedCache().withFlags(Flag.FAIL_SILENTLY).endBatch(true);
replaced = true;
} catch (Exception e) {
System.out.println(e);
failedReplaceCounter.incrementAndGet();
}
}
}
}
}
public static EmbeddedCacheManager createManager(String nodeName) {
System.setProperty("java.net.preferIPv4Stack", "true");
System.setProperty("jgroups.tcp.port", "53715");
GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder();
boolean clustered = true;
boolean async = false;
boolean allowDuplicateJMXDomains = true;
if (clustered) {
gcb = gcb.clusteredDefault();
gcb.transport().clusterName("test-clustering");
gcb.transport().nodeName(nodeName);
}
gcb.globalJmxStatistics().allowDuplicateDomains(allowDuplicateJMXDomains);
EmbeddedCacheManager cacheManager = new DefaultCacheManager(gcb.build());
ConfigurationBuilder distConfigBuilder = new ConfigurationBuilder();
if (clustered) {
distConfigBuilder.clustering().cacheMode(async ? CacheMode.DIST_ASYNC : CacheMode.DIST_SYNC);
distConfigBuilder.clustering().hash().numOwners(1);
// Disable L1 cache
distConfigBuilder.clustering().hash().l1().enabled(false);
//distConfigBuilder.storeAsBinary().enable().storeKeysAsBinary(false).storeValuesAsBinary(true);
distConfigBuilder.versioning().enabled(true);
distConfigBuilder.versioning().scheme(VersioningScheme.SIMPLE);
distConfigBuilder.locking().writeSkewCheck(true);
distConfigBuilder.locking().isolationLevel(IsolationLevel.REPEATABLE_READ);
distConfigBuilder.locking().concurrencyLevel(32);
distConfigBuilder.locking().lockAcquisitionTimeout(1000, TimeUnit.SECONDS);
distConfigBuilder.versioning().enabled(true);
distConfigBuilder.versioning().scheme(VersioningScheme.SIMPLE);
// distConfigBuilder.invocationBatching().enable();
//distConfigBuilder.transaction().transactionMode(TransactionMode.TRANSACTIONAL);
distConfigBuilder.transaction().transactionManagerLookup(new DummyTransactionManagerLookup());
distConfigBuilder.transaction().lockingMode(LockingMode.OPTIMISTIC);
}
Configuration distConfig = distConfigBuilder.build();
cacheManager.defineConfiguration(InfinispanConnectionProvider.SESSION_CACHE_NAME, distConfig);
return cacheManager;
}
}

View file

@ -80,6 +80,7 @@
<sun.jaxb.version>2.2.11</sun.jaxb.version> <sun.jaxb.version>2.2.11</sun.jaxb.version>
<sun.xsom.version>20140925</sun.xsom.version> <sun.xsom.version>20140925</sun.xsom.version>
<undertow.version>1.4.11.Final</undertow.version> <undertow.version>1.4.11.Final</undertow.version>
<woodstox.version>5.0.3</woodstox.version>
<xmlsec.version>2.0.5</xmlsec.version> <xmlsec.version>2.0.5</xmlsec.version>
<!-- Authorization Drools Policy Provider --> <!-- Authorization Drools Policy Provider -->
@ -123,6 +124,7 @@
<osgi.bundle.plugin.version>2.3.7</osgi.bundle.plugin.version> <osgi.bundle.plugin.version>2.3.7</osgi.bundle.plugin.version>
<wildfly.plugin.version>1.1.0.Final</wildfly.plugin.version> <wildfly.plugin.version>1.1.0.Final</wildfly.plugin.version>
<nexus.staging.plugin.version>1.6.5</nexus.staging.plugin.version> <nexus.staging.plugin.version>1.6.5</nexus.staging.plugin.version>
<frontend.plugin.version>1.5</frontend.plugin.version>
<!-- Surefire Settings --> <!-- Surefire Settings -->
<surefire.memory.settings>-Xms512m -Xmx2048m -XX:MetaspaceSize=96m -XX:MaxMetaspaceSize=256m</surefire.memory.settings> <surefire.memory.settings>-Xms512m -Xmx2048m -XX:MetaspaceSize=96m -XX:MaxMetaspaceSize=256m</surefire.memory.settings>
@ -1500,6 +1502,11 @@
<artifactId>maven-bundle-plugin</artifactId> <artifactId>maven-bundle-plugin</artifactId>
<version>${osgi.bundle.plugin.version}</version> <version>${osgi.bundle.plugin.version}</version>
</plugin> </plugin>
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>${frontend.plugin.version}</version>
</plugin>
</plugins> </plugins>
</pluginManagement> </pluginManagement>
</build> </build>

Some files were not shown because too many files have changed in this diff Show more