diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPTransaction.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPTransaction.java
index 1f2473baab..3cf91b9485 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPTransaction.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPTransaction.java
@@ -18,6 +18,7 @@
package org.keycloak.storage.ldap.mappers;
import org.jboss.logging.Logger;
+import org.keycloak.models.AbstractKeycloakTransaction;
import org.keycloak.models.KeycloakTransaction;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
@@ -25,12 +26,10 @@ import org.keycloak.storage.ldap.idm.model.LDAPObject;
/**
* @author Marek Posolda
*/
-public class LDAPTransaction implements KeycloakTransaction {
+public class LDAPTransaction extends AbstractKeycloakTransaction {
public static final Logger logger = Logger.getLogger(LDAPTransaction.class);
- protected TransactionState state = TransactionState.NOT_STARTED;
-
private final LDAPStorageProvider ldapProvider;
private final LDAPObject ldapUser;
@@ -39,57 +38,21 @@ public class LDAPTransaction implements KeycloakTransaction {
this.ldapUser = ldapUser;
}
- @Override
- public void begin() {
- if (state != TransactionState.NOT_STARTED) {
- throw new IllegalStateException("Transaction already started");
- }
-
- state = TransactionState.STARTED;
- }
@Override
- public void commit() {
- if (state != TransactionState.STARTED) {
- throw new IllegalStateException("Transaction in illegal state for commit: " + state);
- }
-
+ protected void commitImpl() {
if (logger.isTraceEnabled()) {
logger.trace("Transaction commit! Updating LDAP attributes for object " + ldapUser.getDn().toString() + ", attributes: " + ldapUser.getAttributes());
}
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());
- 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
- }
}
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/TxAwareLDAPUserModelDelegate.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/TxAwareLDAPUserModelDelegate.java
index 2bf88f23dd..09f4051e36 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/TxAwareLDAPUserModelDelegate.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/TxAwareLDAPUserModelDelegate.java
@@ -41,7 +41,7 @@ public abstract class TxAwareLDAPUserModelDelegate extends UserModelDelegate {
protected void ensureTransactionStarted() {
LDAPTransaction transaction = provider.getUserManager().getTransaction(getId());
- if (transaction.state == LDAPTransaction.TransactionState.NOT_STARTED) {
+ if (transaction.getState() == LDAPTransaction.TransactionState.NOT_STARTED) {
if (logger.isTraceEnabled()) {
logger.trace("Starting and enlisting transaction for object " + ldapUser.getDn().toString());
}
diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/CrossDCAwareCacheFactory.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/CrossDCAwareCacheFactory.java
index 17795ca213..65ca09d98e 100644
--- a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/CrossDCAwareCacheFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/CrossDCAwareCacheFactory.java
@@ -52,6 +52,12 @@ abstract class CrossDCAwareCacheFactory {
// For cross-DC scenario, we need to return underlying remoteCache for atomic operations to work properly
RemoteStore remoteStore = remoteStores.iterator().next();
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);
}
}
diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProvider.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProvider.java
index 5a4bdb744b..bd23e90133 100644
--- a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProvider.java
@@ -25,6 +25,11 @@ import org.keycloak.cluster.ExecutionResult;
import org.keycloak.common.util.Time;
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;
/**
@@ -43,11 +48,14 @@ public class InfinispanClusterProvider implements ClusterProvider {
private final CrossDCAwareCacheFactory crossDCAwareCacheFactory;
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.clusterStartupTime = clusterStartupTime;
this.crossDCAwareCacheFactory = crossDCAwareCacheFactory;
this.notificationsManager = notificationsManager;
+ this.localExecutor = localExecutor;
}
@@ -85,6 +93,34 @@ public class InfinispanClusterProvider implements ClusterProvider {
}
+ @Override
+ public Future 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 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 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
public void registerListener(String taskKey, ClusterListener task) {
this.notificationsManager.registerListener(taskKey, task);
@@ -92,11 +128,10 @@ public class InfinispanClusterProvider implements ClusterProvider {
@Override
- public void notify(String taskKey, ClusterEvent event, boolean ignoreSender) {
- this.notificationsManager.notify(taskKey, event, ignoreSender);
+ public void notify(String taskKey, ClusterEvent event, boolean ignoreSender, DCNotify dcNotify) {
+ this.notificationsManager.notify(taskKey, event, ignoreSender, dcNotify);
}
-
private LockEntry createLockEntry() {
LockEntry lock = new LockEntry();
lock.setNode(myAddress);
diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProviderFactory.java
index a96621d7b2..330de4fd62 100644
--- a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanClusterProviderFactory.java
@@ -35,12 +35,15 @@ import org.keycloak.common.util.Time;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
import java.io.Serializable;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
import java.util.function.Function;
import java.util.function.Predicate;
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
private CrossDCAwareCacheFactory crossDCAwareCacheFactory;
- private String myAddress;
-
private int clusterStartupTime;
// Just to extract notifications related stuff to separate class
private InfinispanNotificationsManager notificationsManager;
+ private ExecutorService localExecutor = Executors.newCachedThreadPool();
+
@Override
public ClusterProvider create(KeycloakSession 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) {
@@ -83,33 +87,23 @@ public class InfinispanClusterProviderFactory implements ClusterProviderFactory
workCache = ispnConnections.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME);
workCache.getCacheManager().addListener(new ViewChangeListener());
- initMyAddress();
- Set remoteStores = getRemoteStores();
+ // See if we have RemoteStore (external JDG) configured for cross-Data-Center scenario
+ Set remoteStores = InfinispanUtil.getRemoteStores(workCache);
crossDCAwareCacheFactory = CrossDCAwareCacheFactory.getFactory(workCache, remoteStores);
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 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) {
Integer existingClusterStartTime = (Integer) crossDCAwareCacheFactory.getCache().get(InfinispanClusterProvider.CLUSTER_STARTUP_TIME_KEY);
if (existingClusterStartTime != null) {
@@ -201,6 +195,10 @@ public class InfinispanClusterProviderFactory implements ClusterProviderFactory
if (logger.isTraceEnabled()) {
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);
}
}
diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java
index fa73420ebb..998cbeb81f 100644
--- a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java
+++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/InfinispanNotificationsManager.java
@@ -20,31 +20,38 @@ package org.keycloak.cluster.infinispan;
import java.io.Serializable;
import java.util.List;
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 org.infinispan.Cache;
import org.infinispan.client.hotrod.RemoteCache;
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.ClientCacheEntryRemoved;
import org.infinispan.client.hotrod.annotation.ClientListener;
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.ClientEvent;
+import org.infinispan.client.hotrod.event.ClientCacheEntryRemovedEvent;
import org.infinispan.context.Flag;
-import org.infinispan.marshall.core.MarshalledEntry;
import org.infinispan.notifications.Listener;
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.CacheEntryRemoved;
import org.infinispan.notifications.cachelistener.event.CacheEntryCreatedEvent;
+import org.infinispan.notifications.cachelistener.event.CacheEntryExpiredEvent;
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.remoting.transport.Transport;
import org.jboss.logging.Logger;
import org.keycloak.cluster.ClusterEvent;
import org.keycloak.cluster.ClusterListener;
import org.keycloak.cluster.ClusterProvider;
-import org.keycloak.common.util.HostUtils;
import org.keycloak.common.util.MultivaluedHashMap;
/**
@@ -58,20 +65,25 @@ public class InfinispanNotificationsManager {
private final MultivaluedHashMap listeners = new MultivaluedHashMap<>();
+ private final ConcurrentMap taskCallbacks = new ConcurrentHashMap<>();
+
private final Cache workCache;
private final String myAddress;
+ private final String mySite;
- protected InfinispanNotificationsManager(Cache workCache, String myAddress) {
+
+ protected InfinispanNotificationsManager(Cache workCache, String myAddress, String mySite) {
this.workCache = workCache;
this.myAddress = myAddress;
+ this.mySite = mySite;
}
// Create and init manager including all listeners etc
- public static InfinispanNotificationsManager create(Cache workCache, String myAddress, Set remoteStores) {
- InfinispanNotificationsManager manager = new InfinispanNotificationsManager(workCache, myAddress);
+ public static InfinispanNotificationsManager create(Cache workCache, String myAddress, String mySite, Set remoteStores) {
+ 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
if (remoteStores.isEmpty()) {
@@ -85,6 +97,10 @@ public class InfinispanNotificationsManager {
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;
@@ -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();
+ wrappedEvent.setEventKey(taskKey);
wrappedEvent.setDelegateEvent(event);
wrappedEvent.setIgnoreSender(ignoreSender);
+ wrappedEvent.setIgnoreSenderSite(dcNotify == ClusterProvider.DCNotify.ALL_BUT_LOCAL_DC);
wrappedEvent.setSender(myAddress);
+ wrappedEvent.setSenderSite(mySite);
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
- workCache.getAdvancedCache().withFlags(Flag.IGNORE_RETURN_VALUES)
- .put(taskKey, wrappedEvent, 120, TimeUnit.SECONDS);
+ workCache.getAdvancedCache().withFlags(flags)
+ .put(UUID.randomUUID().toString(), wrappedEvent, 120, TimeUnit.SECONDS);
}
@@ -124,6 +158,12 @@ public class InfinispanNotificationsManager {
public void cacheEntryModified(CacheEntryModifiedEvent event) {
eventReceived(event.getKey(), event.getValue());
}
+
+ @CacheEntryRemoved
+ public void cacheEntryRemoved(CacheEntryRemovedEvent event) {
+ taskFinished(event.getKey(), true);
+ }
+
}
@@ -150,6 +190,14 @@ public class InfinispanNotificationsManager {
hotrodEventReceived(key);
}
+
+ @ClientCacheEntryRemoved
+ public void removed(ClientCacheEntryRemovedEvent event) {
+ String key = event.getKey().toString();
+ taskFinished(key, true);
+ }
+
+
private void hotrodEventReceived(String key) {
// TODO: Look at CacheEventConverter stuff to possibly include value in the event and avoid additional remoteCache request
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()) {
- logger.tracef("Received event %s: %s", key, event);
+ logger.tracef("Received event: %s", event);
}
ClusterEvent wrappedEvent = event.getDelegateEvent();
- List myListeners = listeners.get(key);
- if (myListeners != null) {
- for (ClusterListener listener : myListeners) {
- listener.eventReceived(wrappedEvent);
- }
- }
-
- myListeners = listeners.get(ClusterProvider.ALL);
+ List myListeners = listeners.get(eventKey);
if (myListeners != null) {
for (ClusterListener listener : myListeners) {
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();
+ }
+
+ }
}
diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/TaskCallback.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/TaskCallback.java
new file mode 100644
index 0000000000..028d743276
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/TaskCallback.java
@@ -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 Marek Posolda
+ */
+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 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 future) {
+ this.future = future;
+ this.futureAvailableLatch.countDown();
+ }
+
+
+ public Future 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;
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/WrapperClusterEvent.java b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/WrapperClusterEvent.java
index b03dd70c0a..0e58275bcc 100644
--- a/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/WrapperClusterEvent.java
+++ b/model/infinispan/src/main/java/org/keycloak/cluster/infinispan/WrapperClusterEvent.java
@@ -24,10 +24,21 @@ import org.keycloak.cluster.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 ignoreSenderSite;
private ClusterEvent delegateEvent;
+ public String getEventKey() {
+ return eventKey;
+ }
+
+ public void setEventKey(String eventKey) {
+ this.eventKey = eventKey;
+ }
+
public String getSender() {
return sender;
}
@@ -36,6 +47,14 @@ public class WrapperClusterEvent implements ClusterEvent {
this.sender = sender;
}
+ public String getSenderSite() {
+ return senderSite;
+ }
+
+ public void setSenderSite(String senderSite) {
+ this.senderSite = senderSite;
+ }
+
public boolean isIgnoreSender() {
return ignoreSender;
}
@@ -44,6 +63,14 @@ public class WrapperClusterEvent implements ClusterEvent {
this.ignoreSender = ignoreSender;
}
+ public boolean isIgnoreSenderSite() {
+ return ignoreSenderSite;
+ }
+
+ public void setIgnoreSenderSite(boolean ignoreSenderSite) {
+ this.ignoreSenderSite = ignoreSenderSite;
+ }
+
public ClusterEvent getDelegateEvent() {
return delegateEvent;
}
@@ -54,6 +81,6 @@ public class WrapperClusterEvent implements ClusterEvent {
@Override
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());
}
}
diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProvider.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProvider.java
index 71a2ebaf94..d95e4a4bb1 100644
--- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProvider.java
@@ -26,9 +26,13 @@ import org.infinispan.manager.EmbeddedCacheManager;
public class DefaultInfinispanConnectionProvider implements InfinispanConnectionProvider {
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.nodeName = nodeName;
+ this.siteName = siteName;
}
@Override
@@ -36,6 +40,16 @@ public class DefaultInfinispanConnectionProvider implements InfinispanConnection
return cacheManager.getCache(name);
}
+ @Override
+ public String getNodeName() {
+ return nodeName;
+ }
+
+ @Override
+ public String getSiteName() {
+ return siteName;
+ }
+
@Override
public void close() {
}
diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
index a9df0471c1..86f607456f 100755
--- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
@@ -17,6 +17,7 @@
package org.keycloak.connections.infinispan;
+import java.security.SecureRandom;
import java.util.concurrent.TimeUnit;
import org.infinispan.commons.util.FileLookup;
@@ -30,6 +31,7 @@ import org.infinispan.eviction.EvictionType;
import org.infinispan.manager.DefaultCacheManager;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder;
+import org.infinispan.remoting.transport.Transport;
import org.infinispan.remoting.transport.jgroups.JGroupsTransport;
import org.infinispan.transaction.LockingMode;
import org.infinispan.transaction.TransactionMode;
@@ -38,8 +40,12 @@ import org.jboss.logging.Logger;
import org.jgroups.JChannel;
import org.keycloak.Config;
import org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory;
+import org.keycloak.common.util.HostUtils;
import org.keycloak.models.KeycloakSession;
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;
@@ -56,11 +62,15 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
protected boolean containerManaged;
+ private String nodeName;
+
+ private String siteName;
+
@Override
public InfinispanConnectionProvider create(KeycloakSession session) {
lazyInit();
- return new DefaultInfinispanConnectionProvider(cacheManager);
+ return new DefaultInfinispanConnectionProvider(cacheManager, nodeName, siteName);
}
@Override
@@ -96,6 +106,8 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
} else {
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.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);
} catch (Exception e) {
@@ -152,13 +177,27 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
boolean async = config.getBoolean("async", false);
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) {
- String nodeName = config.get("nodeName", System.getProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME));
String jgroupsUdpMcastAddr = config.get("jgroupsUdpMcastAddr", System.getProperty(InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR));
- configureTransport(gcb, nodeName, jgroupsUdpMcastAddr);
+ configureTransport(gcb, nodeName, siteName, jgroupsUdpMcastAddr);
gcb.globalJmxStatistics()
.jmxDomain(InfinispanConnectionProvider.JMX_DOMAIN + "-" + nodeName);
+ } else {
+ if (nodeName == null) {
+ nodeName = generateNodeName();
+ }
}
+
gcb.globalJmxStatistics()
.allowDuplicateDomains(allowDuplicateJMXDomains)
.enable();
@@ -166,6 +205,10 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
cacheManager = new DefaultCacheManager(gcb.build());
containerManaged = false;
+ if (cacheManager.getTransport() != null) {
+ nodeName = cacheManager.getTransport().getAddress().toString();
+ }
+
logger.debug("Started embedded Infinispan cache container");
ConfigurationBuilder modelCacheConfigBuilder = new ConfigurationBuilder();
@@ -198,11 +241,29 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
.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();
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.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
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);
}
- boolean jdgEnabled = config.getBoolean("remoteStoreEnabled", false);
if (jdgEnabled) {
- configureRemoteCacheStore(replicationConfigBuilder, async);
+ configureRemoteCacheStore(replicationConfigBuilder, async, InfinispanConnectionProvider.WORK_CACHE_NAME, RemoteStoreConfigurationBuilder.class);
}
Configuration replicationEvictionCacheConfiguration = replicationConfigBuilder.build();
@@ -267,6 +327,10 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
cacheManager.getCache(InfinispanConnectionProvider.AUTHORIZATION_REVISIONS_CACHE_NAME, true);
}
+ protected String generateNodeName() {
+ return InfinispanConnectionProvider.NODE_PREFIX + new SecureRandom().nextInt(1000000);
+ }
+
private Configuration getRevisionCacheConfig(long maxEntries) {
ConfigurationBuilder cb = new ConfigurationBuilder();
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.
- 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");
Integer jdgPort = config.getInt("remoteStorePort", 11222);
builder.persistence()
.passivation(false)
- .addStore(RemoteStoreConfigurationBuilder.class)
+ .addStore(configBuilderClass)
.fetchPersistentState(false)
.ignoreModifications(false)
.purgeOnStartup(false)
.preload(false)
.shared(true)
- .remoteCacheName(InfinispanConnectionProvider.WORK_CACHE_NAME)
+ .remoteCacheName(cacheName)
.rawValues(true)
.forceReturnValues(false)
.marshaller(KeycloakHotRodMarshallerFactory.class.getName())
@@ -355,7 +419,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
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) {
gcb.transport().defaultTransport();
} else {
@@ -376,6 +440,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
gcb.transport()
.nodeName(nodeName)
+ .siteId(siteName)
.transport(transport)
.globalJmxStatistics()
.jmxDomain(InfinispanConnectionProvider.JMX_DOMAIN + "-" + nodeName)
diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java
index e8cdbf6885..9c3d437de9 100755
--- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java
@@ -55,8 +55,25 @@ public interface InfinispanConnectionProvider extends Provider {
String JBOSS_NODE_NAME = "jboss.node.name";
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";
+ // Constant used as the prefix of the current node if "jboss.node.name" is not configured
+ String NODE_PREFIX = "node_";
+
Cache 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();
+
}
diff --git a/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProvider.java b/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProvider.java
index b5f48cd5bb..52a509ec04 100644
--- a/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProvider.java
@@ -69,7 +69,7 @@ public class InfinispanPublicKeyStorageProvider implements PublicKeyStorageProvi
public void clearCache() {
keys.clear();
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) {
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);
}
}
diff --git a/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProviderFactory.java
index 42a73fca17..e8872a7f3e 100644
--- a/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/keys/infinispan/InfinispanPublicKeyStorageProviderFactory.java
@@ -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 PUBLIC_KEY_STORAGE_INVALIDATION_EVENT = "PUBLIC_KEY_STORAGE_INVALIDATION_EVENT";
+
private volatile Cache keysCache;
private final Map> tasksInProgress = new ConcurrentHashMap<>();
@@ -69,12 +71,10 @@ public class InfinispanPublicKeyStorageProviderFactory implements PublicKeyStora
this.keysCache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.KEYS_CACHE_NAME);
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;
- keysCache.remove(invalidationEvent.getCacheKey());
- }
+ PublicKeyStorageInvalidationEvent invalidationEvent = (PublicKeyStorageInvalidationEvent) event;
+ keysCache.remove(invalidationEvent.getCacheKey());
});
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java
index 4480f7af9a..9a0839fe4f 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CacheManager.java
@@ -198,22 +198,15 @@ public abstract class CacheManager {
}
- public void sendInvalidationEvents(KeycloakSession session, Collection invalidationEvents) {
+ public void sendInvalidationEvents(KeycloakSession session, Collection invalidationEvents, String eventKey) {
ClusterProvider clusterProvider = session.getProvider(ClusterProvider.class);
// Maybe add InvalidationEvent, which will be collection of all invalidationEvents? That will reduce cluster traffic even more.
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) {
Set invalidations = new HashSet<>();
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java
index c2ad8cef40..ef2ce2b0f8 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java
@@ -38,6 +38,7 @@ public class InfinispanCacheRealmProviderFactory implements CacheRealmProviderFa
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_INVALIDATION_EVENTS = "REALM_INVALIDATION_EVENTS";
protected volatile RealmCacheManager realmCache;
@@ -56,12 +57,11 @@ public class InfinispanCacheRealmProviderFactory implements CacheRealmProviderFa
realmCache = new RealmCacheManager(cache, revisions);
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) -> {
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCacheProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCacheProviderFactory.java
index e8c2ba14fb..4d0f445dea 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCacheProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanUserCacheProviderFactory.java
@@ -37,6 +37,7 @@ public class InfinispanUserCacheProviderFactory implements UserCacheProviderFact
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_INVALIDATION_EVENTS = "USER_INVALIDATION_EVENTS";
protected volatile UserCacheManager userCache;
@@ -58,12 +59,10 @@ public class InfinispanUserCacheProviderFactory implements UserCacheProviderFact
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;
- userCache.invalidationEventReceived(invalidationEvent);
- }
+ InvalidationEvent invalidationEvent = (InvalidationEvent) event;
+ userCache.invalidationEventReceived(invalidationEvent);
});
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheManager.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheManager.java
index b01dbabf31..ed5db84720 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheManager.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheManager.java
@@ -95,11 +95,9 @@ public class RealmCacheManager extends CacheManager {
@Override
protected void addInvalidationsFromEvent(InvalidationEvent event, Set invalidations) {
- if (event instanceof RealmCacheInvalidationEvent) {
- invalidations.add(event.getId());
+ invalidations.add(event.getId());
- ((RealmCacheInvalidationEvent) event).addInvalidations(this, invalidations);
- }
+ ((RealmCacheInvalidationEvent) event).addInvalidations(this, invalidations);
}
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java
index 1d7ce64393..fdd5cce38b 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java
@@ -166,7 +166,7 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public void clear() {
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
@@ -298,7 +298,7 @@ public class RealmCacheSession implements CacheRealmProvider {
cache.invalidateObject(id);
}
- cache.sendInvalidationEvents(session, invalidationEvents);
+ cache.sendInvalidationEvents(session, invalidationEvents, InfinispanCacheRealmProviderFactory.REALM_INVALIDATION_EVENTS);
}
private KeycloakTransaction getPrepareTransaction() {
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheManager.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheManager.java
index e9493144e1..9126b2f499 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheManager.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheManager.java
@@ -95,9 +95,7 @@ public class UserCacheManager extends CacheManager {
@Override
protected void addInvalidationsFromEvent(InvalidationEvent event, Set invalidations) {
- if (event instanceof UserCacheInvalidationEvent) {
- ((UserCacheInvalidationEvent) event).addInvalidations(this, invalidations);
- }
+ ((UserCacheInvalidationEvent) event).addInvalidations(this, invalidations);
}
public void invalidateRealmUsers(String realm, Set invalidations) {
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java
index ef19b84b4b..0d971f70b9 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java
@@ -90,7 +90,7 @@ public class UserCacheSession implements UserCache {
public void clear() {
cache.clear();
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() {
@@ -129,7 +129,7 @@ public class UserCacheSession implements UserCache {
cache.invalidateObject(invalidation);
}
- cache.sendInvalidationEvents(session, invalidationEvents);
+ cache.sendInvalidationEvents(session, invalidationEvents, InfinispanUserCacheProviderFactory.USER_INVALIDATION_EVENTS);
}
private KeycloakTransaction getTransaction() {
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/InfinispanCacheStoreFactoryProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/InfinispanCacheStoreFactoryProviderFactory.java
index c74e4fedf7..8a9dd059f9 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/InfinispanCacheStoreFactoryProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/InfinispanCacheStoreFactoryProviderFactory.java
@@ -41,6 +41,7 @@ public class InfinispanCacheStoreFactoryProviderFactory implements CachedStorePr
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_INVALIDATION_EVENTS = "AUTHORIZATION_INVALIDATION_EVENTS";
protected volatile StoreFactoryCacheManager storeCache;
@@ -59,11 +60,11 @@ public class InfinispanCacheStoreFactoryProviderFactory implements CachedStorePr
storeCache = new StoreFactoryCacheManager(cache, revisions);
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
- cluster.registerListener(ClusterProvider.ALL, (ClusterEvent event) -> {
- if (event instanceof InvalidationEvent) {
- InvalidationEvent invalidationEvent = (InvalidationEvent) event;
- storeCache.invalidationEventReceived(invalidationEvent);
- }
+ cluster.registerListener(AUTHORIZATION_INVALIDATION_EVENTS, (ClusterEvent event) -> {
+
+ InvalidationEvent invalidationEvent = (InvalidationEvent) event;
+ storeCache.invalidationEventReceived(invalidationEvent);
+
});
cluster.registerListener(AUTHORIZATION_CLEAR_CACHE_EVENTS, (ClusterEvent event) -> storeCache.clear());
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java
index 10be78d971..a169235643 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java
@@ -216,7 +216,7 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
cache.invalidateObject(id);
}
- cache.sendInvalidationEvents(session, invalidationEvents);
+ cache.sendInvalidationEvents(session, invalidationEvents, InfinispanCacheStoreFactoryProviderFactory.AUTHORIZATION_INVALIDATION_EVENTS);
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java
index 7772bc20f2..be086b8eb0 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java
@@ -22,13 +22,17 @@ import java.util.HashMap;
import java.util.Map;
import java.util.Set;
-import org.infinispan.Cache;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
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.SessionEntity;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
/**
@@ -39,19 +43,20 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
private final AuthenticatedClientSessionEntity entity;
private final ClientModel client;
private final InfinispanUserSessionProvider provider;
- private final Cache cache;
+ private final InfinispanChangelogBasedTransaction updateTx;
private UserSessionAdapter userSession;
- public AuthenticatedClientSessionAdapter(AuthenticatedClientSessionEntity entity, ClientModel client, UserSessionAdapter userSession, InfinispanUserSessionProvider provider, Cache cache) {
+ public AuthenticatedClientSessionAdapter(AuthenticatedClientSessionEntity entity, ClientModel client, UserSessionAdapter userSession,
+ InfinispanUserSessionProvider provider, InfinispanChangelogBasedTransaction updateTx) {
this.provider = provider;
this.entity = entity;
this.client = client;
- this.cache = cache;
+ this.updateTx = updateTx;
this.userSession = userSession;
}
- private void update() {
- provider.getTx().replace(cache, userSession.getEntity().getId(), userSession.getEntity());
+ private void update(UserSessionUpdateTask task) {
+ updateTx.addTask(userSession.getId(), task);
}
@@ -62,15 +67,27 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
// Dettach userSession
if (userSession == null) {
- if (sessionEntity.getAuthenticatedClientSessions() != null) {
- sessionEntity.getAuthenticatedClientSessions().remove(clientUUID);
- update();
- this.userSession = null;
- }
+ UserSessionUpdateTask task = new UserSessionUpdateTask() {
+
+ @Override
+ public void runUpdate(UserSessionEntity sessionEntity) {
+ sessionEntity.getAuthenticatedClientSessions().remove(clientUUID);
+ }
+
+ };
+ update(task);
+ this.userSession = null;
} else {
this.userSession = (UserSessionAdapter) userSession;
- sessionEntity.getAuthenticatedClientSessions().put(clientUUID, entity);
- update();
+ UserSessionUpdateTask task = new UserSessionUpdateTask() {
+
+ @Override
+ public void runUpdate(UserSessionEntity sessionEntity) {
+ sessionEntity.getAuthenticatedClientSessions().put(clientUUID, entity);
+ }
+
+ };
+ update(task);
}
}
@@ -86,8 +103,16 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
@Override
public void setRedirectUri(String uri) {
- entity.setRedirectUri(uri);
- update();
+ UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) {
+
+ @Override
+ protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) {
+ entity.setRedirectUri(uri);
+ }
+
+ };
+
+ update(task);
}
@Override
@@ -112,8 +137,22 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
@Override
public void setTimestamp(int timestamp) {
- entity.setTimestamp(timestamp);
- update();
+ UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) {
+
+ @Override
+ protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) {
+ entity.setTimestamp(timestamp);
+ }
+
+ @Override
+ public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper sessionWrapper) {
+ // We usually update lastSessionRefresh at the same time. That would handle it.
+ return CrossDCMessageStatus.NOT_NEEDED;
+ }
+
+ };
+
+ update(task);
}
@Override
@@ -123,8 +162,16 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
@Override
public void setAction(String action) {
- entity.setAction(action);
- update();
+ UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) {
+
+ @Override
+ protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) {
+ entity.setAction(action);
+ }
+
+ };
+
+ update(task);
}
@Override
@@ -134,8 +181,16 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
@Override
public void setProtocol(String method) {
- entity.setAuthMethod(method);
- update();
+ UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) {
+
+ @Override
+ protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) {
+ entity.setAuthMethod(method);
+ }
+
+ };
+
+ update(task);
}
@Override
@@ -145,8 +200,16 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
@Override
public void setRoles(Set roles) {
- entity.setRoles(roles);
- update();
+ UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) {
+
+ @Override
+ protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) {
+ entity.setRoles(roles); // TODO not thread-safe. But we will remove setRoles anyway...?
+ }
+
+ };
+
+ update(task);
}
@Override
@@ -156,35 +219,54 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
@Override
public void setProtocolMappers(Set protocolMappers) {
- entity.setProtocolMappers(protocolMappers);
- update();
+ UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) {
+
+ @Override
+ protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) {
+ entity.setProtocolMappers(protocolMappers); // TODO not thread-safe. But we will remove setProtocolMappers anyway...?
+ }
+
+ };
+
+ update(task);
}
@Override
public String getNote(String name) {
- return entity.getNotes()==null ? null : entity.getNotes().get(name);
+ return entity.getNotes().get(name);
}
@Override
public void setNote(String name, String value) {
- if (entity.getNotes() == null) {
- entity.setNotes(new HashMap<>());
- }
- entity.getNotes().put(name, value);
- update();
+ UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) {
+
+ @Override
+ protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) {
+ entity.getNotes().put(name, value);
+ }
+
+ };
+
+ update(task);
}
@Override
public void removeNote(String name) {
- if (entity.getNotes() != null) {
- entity.getNotes().remove(name);
- update();
- }
+ UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) {
+
+ @Override
+ protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) {
+ entity.getNotes().remove(name);
+ }
+
+ };
+
+ update(task);
}
@Override
public Map getNotes() {
- if (entity.getNotes() == null || entity.getNotes().isEmpty()) return Collections.emptyMap();
+ if (entity.getNotes().isEmpty()) return Collections.emptyMap();
Map copy = new HashMap<>();
copy.putAll(entity.getNotes());
return copy;
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/LargestResultReducer.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/CacheDecorators.java
similarity index 54%
rename from model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/LargestResultReducer.java
rename to model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/CacheDecorators.java
index dbaf924f05..e9b3288103 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/LargestResultReducer.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/CacheDecorators.java
@@ -15,27 +15,24 @@
* limitations under the License.
*/
-package org.keycloak.models.sessions.infinispan.mapreduce;
+package org.keycloak.models.sessions.infinispan;
-import org.infinispan.distexec.mapreduce.Reducer;
-
-import java.util.Iterator;
+import org.infinispan.AdvancedCache;
+import org.infinispan.Cache;
+import org.infinispan.context.Flag;
/**
- * @author Stian Thorgersen
+ * @author Marek Posolda
*/
-public class LargestResultReducer implements Reducer {
+public class CacheDecorators {
- @Override
- public Integer reduce(String reducedKey, Iterator itr) {
- Integer largest = itr.next();
- while (itr.hasNext()) {
- Integer next = itr.next();
- if (next > largest) {
- largest = next;
- }
- }
- return largest;
+ public static AdvancedCache localCache(Cache cache) {
+ return cache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL);
}
+ public static AdvancedCache skipCacheLoaders(Cache cache) {
+ return cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_LOAD, Flag.SKIP_CACHE_STORE);
+ }
+
+
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProvider.java
index b4689aa743..192c9647c7 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProvider.java
@@ -61,10 +61,6 @@ public class InfinispanActionTokenStoreProvider implements ActionTokenStoreProvi
this.tx.put(actionKeyCache, tokenKey, tokenValue, key.getExpiration() - Time.currentTime(), TimeUnit.SECONDS);
}
- private static String generateActionTokenEventId() {
- return InfinispanActionTokenStoreProviderFactory.ACTION_TOKEN_EVENTS + "/" + UUID.randomUUID();
- }
-
@Override
public ActionTokenValueModel get(ActionTokenKeyModel actionTokenKey) {
if (actionTokenKey == null || actionTokenKey.getUserId() == null || actionTokenKey.getActionId() == null) {
@@ -98,6 +94,6 @@ public class InfinispanActionTokenStoreProvider implements ActionTokenStoreProvi
}
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);
}
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProviderFactory.java
index 95ee903507..e4f3bd0c08 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProviderFactory.java
@@ -70,24 +70,24 @@ public class InfinispanActionTokenStoreProviderFactory implements ActionTokenSto
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
- cluster.registerListener(ClusterProvider.ALL, event -> {
- if (event instanceof RemoveActionTokensSpecificEvent) {
- RemoveActionTokensSpecificEvent e = (RemoveActionTokensSpecificEvent) event;
+ cluster.registerListener(ACTION_TOKEN_EVENTS, event -> {
- LOG.debugf("[%s] Removing token invalidation for user+action: userId=%s, actionId=%s", cacheAddress, e.getUserId(), e.getActionId());
+ RemoveActionTokensSpecificEvent e = (RemoveActionTokensSpecificEvent) event;
- AdvancedCache localCache = cache
- .getAdvancedCache()
- .withFlags(Flag.CACHE_MODE_LOCAL, Flag.SKIP_CACHE_LOAD);
+ LOG.debugf("[%s] Removing token invalidation for user+action: userId=%s, actionId=%s", cacheAddress, e.getUserId(), e.getActionId());
- List toRemove = localCache
- .keySet()
- .stream()
- .filter(k -> Objects.equals(k.getUserId(), e.getUserId()) && Objects.equals(k.getActionId(), e.getActionId()))
- .collect(Collectors.toList());
+ AdvancedCache localCache = cache
+ .getAdvancedCache()
+ .withFlags(Flag.CACHE_MODE_LOCAL, Flag.SKIP_CACHE_LOAD);
+
+ List 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);
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProvider.java
index 5991f98944..be064d4768 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProvider.java
@@ -30,6 +30,9 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.cache.infinispan.events.AuthenticationSessionAuthNoteUpdateEvent;
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.utils.KeycloakModelUtils;
import org.keycloak.models.utils.RealmInfoUtil;
@@ -46,13 +49,17 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
private final KeycloakSession session;
private final Cache cache;
protected final InfinispanKeycloakTransaction tx;
+ protected final SessionEventsSenderTransaction clusterEventsSenderTx;
public InfinispanAuthenticationSessionProvider(KeycloakSession session, Cache cache) {
this.session = session;
this.cache = cache;
this.tx = new InfinispanKeycloakTransaction();
+ this.clusterEventsSenderTx = new SessionEventsSenderTransaction(session);
+
session.getTransactionManager().enlistAfterCompletion(tx);
+ session.getTransactionManager().enlistAfterCompletion(clusterEventsSenderTx);
}
@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)
- Iterator> itr = cache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL)
- .entrySet().stream().filter(AuthenticationSessionPredicate.create(realm.getId()).expired(expired)).iterator();
+ Iterator> itr = CacheDecorators.localCache(cache)
+ .entrySet()
+ .stream()
+ .filter(AuthenticationSessionPredicate.create(realm.getId()).expired(expired))
+ .iterator();
int counter = 0;
while (itr.hasNext()) {
counter++;
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
public void onRealmRemoved(RealmModel realm) {
- Iterator> 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> itr = CacheDecorators.localCache(cache)
+ .entrySet()
+ .stream()
+ .filter(AuthenticationSessionPredicate.create(realmId))
+ .iterator();
+
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
public void onClientRemoved(RealmModel realm, ClientModel client) {
- Iterator> 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> itr = CacheDecorators.localCache(cache)
+ .entrySet()
+ .stream()
+ .filter(AuthenticationSessionPredicate.create(realmId).client(clientUuid))
+ .iterator();
+
while (itr.hasNext()) {
- cache.remove(itr.next().getKey());
+ CacheDecorators.localCache(cache)
+ .remove(itr.next().getKey());
}
}
+
@Override
public void updateNonlocalSessionAuthNotes(String authSessionId, Map authNotesFragment) {
if (authSessionId == null) {
@@ -150,7 +181,8 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
cluster.notify(
InfinispanAuthenticationSessionProviderFactory.AUTHENTICATION_SESSION_EVENTS,
AuthenticationSessionAuthNoteUpdateEvent.create(authSessionId, authNotesFragment),
- true
+ true,
+ ClusterProvider.DCNotify.ALL_BUT_LOCAL_DC
);
}
@@ -159,4 +191,7 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
}
+ public Cache getCache() {
+ return cache;
+ }
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java
index a9589ccca5..04e1dc8c18 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java
@@ -26,6 +26,12 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.cache.infinispan.events.AuthenticationSessionAuthNoteUpdateEvent;
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.AuthenticationSessionProviderFactory;
import java.util.Map;
@@ -42,13 +48,59 @@ public class InfinispanAuthenticationSessionProviderFactory implements Authentic
private volatile Cache authSessionsCache;
+ public static final String PROVIDER_ID = "infinispan";
+
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
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(sessionFactory) {
+
+ @Override
+ protected void eventReceived(KeycloakSession session, InfinispanAuthenticationSessionProvider provider, RealmRemovedSessionEvent sessionEvent) {
+ provider.onRealmRemovedEvent(sessionEvent.getRealmId());
+ }
+
+ });
+
+ cluster.registerListener(CLIENT_REMOVED_AUTHSESSION_EVENT, new AbstractAuthSessionClusterListener(sessionFactory) {
+
+ @Override
+ protected void eventReceived(KeycloakSession session, InfinispanAuthenticationSessionProvider provider, ClientRemovedSessionEvent sessionEvent) {
+ provider.onClientRemovedEvent(sessionEvent.getRealmId(), sessionEvent.getClientUuid());
+ }
+ });
+
+ log.debug("Registered cluster listeners");
+ }
+
+
@Override
public AuthenticationSessionProvider create(KeycloakSession session) {
lazyInit(session);
@@ -98,16 +150,12 @@ public class InfinispanAuthenticationSessionProviderFactory implements Authentic
}
}
- @Override
- public void postInit(KeycloakSessionFactory factory) {
- }
-
@Override
public void close() {
}
@Override
public String getId() {
- return "infinispan";
+ return PROVIDER_ID;
}
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanKeycloakTransaction.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanKeycloakTransaction.java
index 5471184da9..959223c7a2 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanKeycloakTransaction.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanKeycloakTransaction.java
@@ -155,7 +155,7 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
theTaskKey = taskKey + "-" + (i++);
}
- tasks.put(taskKey, () -> clusterProvider.notify(taskKey, event, ignoreSender));
+ tasks.put(taskKey, () -> clusterProvider.notify(taskKey, event, ignoreSender, ClusterProvider.DCNotify.ALL_DCS));
}
public void remove(Cache 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
public V get(Cache cache, K key) {
Object taskKey = getTaskKey(cache, key);
- CacheTask current = tasks.get(taskKey);
+ CacheTask current = tasks.get(taskKey);
if (current != null) {
if (current instanceof CacheTaskWithValue) {
return ((CacheTaskWithValue) current).getValue();
@@ -190,11 +190,11 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
}
}
- public interface CacheTask {
+ public interface CacheTask {
void execute();
}
- public abstract class CacheTaskWithValue implements CacheTask {
+ public abstract class CacheTaskWithValue implements CacheTask {
protected V value;
public CacheTaskWithValue(V value) {
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanStickySessionEncoderProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanStickySessionEncoderProviderFactory.java
index b8e6a7131d..2477b69a53 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanStickySessionEncoderProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanStickySessionEncoderProviderFactory.java
@@ -21,6 +21,7 @@ import org.keycloak.Config;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
import org.keycloak.sessions.StickySessionEncoderProvider;
import org.keycloak.sessions.StickySessionEncoderProviderFactory;
@@ -29,16 +30,22 @@ import org.keycloak.sessions.StickySessionEncoderProviderFactory;
*/
public class InfinispanStickySessionEncoderProviderFactory implements StickySessionEncoderProviderFactory {
- private String myNodeName;
@Override
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);
}
@Override
public void init(Config.Scope config) {
- myNodeName = config.get("nodeName", System.getProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME));
}
@Override
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
index 202a051f14..ced77fb110 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
@@ -18,10 +18,11 @@
package org.keycloak.models.sessions.infinispan;
import org.infinispan.Cache;
-import org.infinispan.CacheStream;
+import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.context.Flag;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
@@ -31,19 +32,27 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider;
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.LoginFailureEntity;
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.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.Mappers;
import org.keycloak.models.sessions.infinispan.stream.SessionPredicate;
import org.keycloak.models.sessions.infinispan.stream.UserLoginFailurePredicate;
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.LinkedList;
import java.util.List;
@@ -51,7 +60,6 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.function.Predicate;
-import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
@@ -62,31 +70,71 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
private static final Logger log = Logger.getLogger(InfinispanUserSessionProvider.class);
protected final KeycloakSession session;
- protected final Cache sessionCache;
- protected final Cache offlineSessionCache;
+
+ protected final Cache> sessionCache;
+ protected final Cache> offlineSessionCache;
protected final Cache loginFailureCache;
+
+ protected final InfinispanChangelogBasedTransaction sessionTx;
+ protected final InfinispanChangelogBasedTransaction offlineSessionTx;
protected final InfinispanKeycloakTransaction tx;
- public InfinispanUserSessionProvider(KeycloakSession session, Cache sessionCache, Cache offlineSessionCache,
+ protected final SessionEventsSenderTransaction clusterEventsSenderTx;
+
+ protected final LastSessionRefreshStore lastSessionRefreshStore;
+ protected final LastSessionRefreshStore offlineLastSessionRefreshStore;
+
+ public InfinispanUserSessionProvider(KeycloakSession session,
+ RemoteCacheInvoker remoteCacheInvoker,
+ LastSessionRefreshStore lastSessionRefreshStore,
+ LastSessionRefreshStore offlineLastSessionRefreshStore,
+ Cache> sessionCache,
+ Cache> offlineSessionCache,
Cache loginFailureCache) {
this.session = session;
+
this.sessionCache = sessionCache;
this.offlineSessionCache = offlineSessionCache;
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.clusterEventsSenderTx = new SessionEventsSenderTransaction(session);
+
+ this.lastSessionRefreshStore = lastSessionRefreshStore;
+ this.offlineLastSessionRefreshStore = offlineLastSessionRefreshStore;
+
session.getTransactionManager().enlistAfterCompletion(tx);
+ session.getTransactionManager().enlistAfterCompletion(clusterEventsSenderTx);
+ session.getTransactionManager().enlistAfterCompletion(sessionTx);
+ session.getTransactionManager().enlistAfterCompletion(offlineSessionTx);
}
- protected Cache getCache(boolean offline) {
+ protected Cache> getCache(boolean offline) {
return offline ? offlineSessionCache : sessionCache;
}
+ protected InfinispanChangelogBasedTransaction getTransaction(boolean offline) {
+ return offline ? offlineSessionTx : sessionTx;
+ }
+
+ protected LastSessionRefreshStore getLastSessionRefreshStore() {
+ return lastSessionRefreshStore;
+ }
+
+ protected LastSessionRefreshStore getOfflineLastSessionRefreshStore() {
+ return offlineLastSessionRefreshStore;
+ }
+
@Override
public AuthenticatedClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession) {
AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity();
- AuthenticatedClientSessionAdapter adapter = new AuthenticatedClientSessionAdapter(entity, client, (UserSessionAdapter) userSession, this, sessionCache);
+ InfinispanChangelogBasedTransaction updateTx = getTransaction(false);
+ AuthenticatedClientSessionAdapter adapter = new AuthenticatedClientSessionAdapter(entity, client, (UserSessionAdapter) userSession, this, updateTx);
adapter.setUserSession(userSession);
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) {
UserSessionEntity entity = new UserSessionEntity();
entity.setId(id);
-
updateSessionEntity(entity, realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, brokerUserId);
- tx.putIfAbsent(sessionCache, id, entity);
+ SessionUpdateTask createSessionTask = new SessionUpdateTask() {
+
+ @Override
+ public void runUpdate(UserSessionEntity session) {
+
+ }
+
+ @Override
+ public CacheOperation getOperation(UserSessionEntity session) {
+ return CacheOperation.ADD_IF_ABSENT;
+ }
+
+ @Override
+ public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper sessionWrapper) {
+ return CrossDCMessageStatus.SYNC;
+ }
+
+ };
+
+ sessionTx.addTask(id, createSessionTask, entity);
return wrap(realm, entity, false);
}
@@ -121,31 +187,43 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
}
+
@Override
public UserSessionModel getUserSession(RealmModel realm, String id) {
return getUserSession(realm, id, false);
}
protected UserSessionAdapter getUserSession(RealmModel realm, String id, boolean offline) {
- Cache cache = getCache(offline);
- UserSessionEntity entity = (UserSessionEntity) tx.get(cache, id); // Chance created in this transaction
-
- if (entity == null) {
- entity = (UserSessionEntity) cache.get(id);
- }
-
+ UserSessionEntity entity = getUserSessionEntity(id, offline);
return wrap(realm, entity, offline);
}
- protected List getUserSessions(RealmModel realm, Predicate> predicate, boolean offline) {
- CacheStream> cacheStream = getCache(offline).entrySet().stream();
- Iterator> itr = cacheStream.filter(predicate).iterator();
- List sessions = new LinkedList<>();
+ private UserSessionEntity getUserSessionEntity(String id, boolean offline) {
+ InfinispanChangelogBasedTransaction tx = getTransaction(offline);
+ SessionEntityWrapper entityWrapper = tx.get(id);
+ return entityWrapper==null ? null : entityWrapper.getEntity();
+ }
+
+
+ protected List getUserSessions(RealmModel realm, Predicate>> predicate, boolean offline) {
+ Cache> cache = getCache(offline);
+
+ cache = CacheDecorators.skipCacheLoaders(cache);
+
+ Stream>> cacheStream = cache.entrySet().stream();
+
+ List resultSessions = new LinkedList<>();
+
+ Iterator itr = cacheStream.filter(predicate)
+ .map(Mappers.userSessionEntity())
+ .iterator();
+
while (itr.hasNext()) {
- UserSessionEntity e = (UserSessionEntity) itr.next().getValue();
- sessions.add(wrap(realm, e, offline));
+ UserSessionEntity userSessionEntity = itr.next();
+ resultSessions.add(wrap(realm, userSessionEntity, offline));
}
- return sessions;
+
+ return resultSessions;
}
@Override
@@ -175,65 +253,90 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
}
protected List getUserSessions(final RealmModel realm, ClientModel client, int firstResult, int maxResults, final boolean offline) {
- final Cache cache = getCache(offline);
+ Cache> cache = getCache(offline);
+
+ cache = CacheDecorators.skipCacheLoaders(cache);
Stream stream = cache.entrySet().stream()
.filter(UserSessionPredicate.create(realm.getId()).client(client.getId()))
.map(Mappers.userSessionEntity())
.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) {
-// stream = stream.skip(firstResult);
-// }
-//
-// if (maxResults > 0) {
-// stream = stream.limit(maxResults);
-// }
-//
-// List 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;
+ if (firstResult > 0) {
+ stream = stream.skip(firstResult);
}
- int count = firstResult + maxResults;
- if (count > 0) {
- stream = stream.limit(count);
+ if (maxResults > 0) {
+ stream = stream.limit(maxResults);
}
- List 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 sessions = new LinkedList<>();
- entities.stream().forEach(new Consumer() {
- @Override
- public void accept(UserSessionEntity userSessionEntity) {
- sessions.add(wrap(realm, userSessionEntity, offline));
- }
- });
+ Iterator itr = stream.iterator();
+
+ while (itr.hasNext()) {
+ UserSessionEntity userSessionEntity = itr.next();
+ sessions.add(wrap(realm, userSessionEntity, offline));
+ }
+
return sessions;
}
+
+ @Override
+ public UserSessionModel getUserSessionWithPredicate(RealmModel realm, String id, boolean offline, Predicate 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> 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 tx = getTransaction(offline);
+
+ // Remote entity contains our predicate. Update local cache with the remote entity
+ SessionEntityWrapper 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
public long getActiveUserSessions(RealmModel realm, ClientModel client) {
return getUserSessionsCount(realm, client, false);
}
protected long getUserSessionsCount(RealmModel realm, ClientModel client, boolean offline) {
- return getCache(offline).entrySet().stream()
+ Cache> cache = getCache(offline);
+ cache = CacheDecorators.skipCacheLoaders(cache);
+
+ return cache.entrySet().stream()
.filter(UserSessionPredicate.create(realm.getId()).client(client.getId()))
.count();
}
@@ -242,7 +345,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
public void removeUserSession(RealmModel realm, UserSessionModel session) {
UserSessionEntity entity = getUserSessionEntity(session, false);
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) {
- Cache cache = getCache(offline);
+ Cache> cache = getCache(offline);
+
+ cache = CacheDecorators.skipCacheLoaders(cache);
+
+ Iterator itr = cache.entrySet().stream().filter(UserSessionPredicate.create(realm.getId()).user(user.getId())).map(Mappers.userSessionEntity()).iterator();
- Iterator itr = cache.entrySet().stream().filter(UserSessionPredicate.create(realm.getId()).user(user.getId())).map(Mappers.sessionEntity()).iterator();
while (itr.hasNext()) {
- UserSessionEntity userSessionEntity = (UserSessionEntity) itr.next();
- removeUserSession(realm, userSessionEntity, offline);
+ UserSessionEntity userSessionEntity = itr.next();
+ removeUserSession(userSessionEntity, offline);
}
}
@@ -273,17 +379,30 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
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)
- Iterator> itr = sessionCache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL)
- .entrySet().stream().filter(UserSessionPredicate.create(realm.getId()).expired(expired, expiredRefresh)).iterator();
+ Cache> localCache = CacheDecorators.localCache(sessionCache);
- int counter = 0;
- while (itr.hasNext()) {
- counter++;
- UserSessionEntity entity = (UserSessionEntity) itr.next().getValue();
- tx.remove(sessionCache, entity.getId());
- }
+ int[] counter = { 0 };
- log.debugf("Removed %d expired user sessions for realm '%s'", counter, realm.getName());
+ Cache> 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() {
+
+ @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) {
@@ -291,38 +410,69 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
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)
+ Cache> localCache = CacheDecorators.localCache(offlineSessionCache);
+
UserSessionPredicate predicate = UserSessionPredicate.create(realm.getId()).expired(null, expiredOffline);
- Iterator> itr = offlineSessionCache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL)
- .entrySet().stream().filter(predicate).iterator();
- int counter = 0;
- while (itr.hasNext()) {
- counter++;
- UserSessionEntity entity = (UserSessionEntity) itr.next().getValue();
- tx.remove(offlineSessionCache, entity.getId());
+ final int[] counter = { 0 };
- persister.removeUserSession(entity.getId(), true);
+ Cache> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache);
- for (String clientUUID : entity.getAuthenticatedClientSessions().keySet()) {
- persister.removeClientSession(entity.getId(), clientUUID, true);
- }
- }
+ // Ignore remoteStore for stream iteration. But we will invoke remoteStore for userSession removal propagate
+ localCacheStoreIgnore
+ .entrySet()
+ .stream()
+ .filter(predicate)
+ .map(Mappers.userSessionEntity())
+ .forEach(new Consumer() {
+
+ @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());
}
@Override
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) {
- Cache cache = getCache(offline);
+ protected void onRemoveUserSessionsEvent(String realmId) {
+ removeLocalUserSessions(realmId, false);
+ }
- Iterator itr = cache.entrySet().stream().filter(SessionPredicate.create(realm.getId())).map(Mappers.sessionId()).iterator();
- while (itr.hasNext()) {
- cache.remove(itr.next());
- }
+ private void removeLocalUserSessions(String realmId, boolean offline) {
+ Cache> cache = getCache(offline);
+ Cache> localCache = CacheDecorators.localCache(cache);
+
+ Cache> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache);
+
+ localCacheStoreIgnore
+ .entrySet()
+ .stream()
+ .filter(SessionPredicate.create(realmId))
+ .map(Mappers.sessionId())
+ .forEach(new Consumer() {
+
+ @Override
+ public void accept(String sessionId) {
+ localCache.remove(sessionId);
+ }
+
+ });
}
@Override
@@ -348,22 +498,48 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
@Override
public void removeAllUserLoginFailures(RealmModel realm) {
- Iterator 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 localCache = CacheDecorators.localCache(loginFailureCache);
+
+ Cache localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache);
+
+ Iterator itr = localCacheStoreIgnore
+ .entrySet()
+ .stream()
+ .filter(UserLoginFailurePredicate.create(realmId))
+ .map(Mappers.loginFailureId())
+ .iterator();
+
while (itr.hasNext()) {
LoginFailureKey key = itr.next();
- tx.remove(loginFailureCache, key);
+ localCache.remove(key);
}
}
@Override
public void onRealmRemoved(RealmModel realm) {
- removeUserSessions(realm, true);
- removeUserSessions(realm, false);
- removeAllUserLoginFailures(realm);
+ clusterEventsSenderTx.addEvent(InfinispanUserSessionProviderFactory.REALM_REMOVED_SESSION_EVENT, RealmRemovedSessionEvent.create(realm.getId()), false);
+ }
+
+ protected void onRealmRemovedEvent(String realmId) {
+ removeLocalUserSessions(realmId, true);
+ removeLocalUserSessions(realmId, false);
+ removeAllLocalUserLoginFailuresEvent(realmId);
}
@Override
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.
}
@@ -380,10 +556,29 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
public void close() {
}
- protected void removeUserSession(RealmModel realm, UserSessionEntity sessionEntity, boolean offline) {
- Cache cache = getCache(offline);
+ protected void removeUserSession(UserSessionEntity sessionEntity, boolean offline) {
+ InfinispanChangelogBasedTransaction tx = getTransaction(offline);
- tx.remove(cache, sessionEntity.getId());
+ SessionUpdateTask removeTask = new SessionUpdateTask() {
+
+ @Override
+ public void runUpdate(UserSessionEntity entity) {
+
+ }
+
+ @Override
+ public CacheOperation getOperation(UserSessionEntity entity) {
+ return CacheOperation.REMOVE;
+ }
+
+ @Override
+ public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper sessionWrapper) {
+ return CrossDCMessageStatus.SYNC;
+ }
+
+ };
+
+ tx.addTask(sessionEntity.getId(), removeTask);
}
InfinispanKeycloakTransaction getTx() {
@@ -391,16 +586,8 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
}
UserSessionAdapter wrap(RealmModel realm, UserSessionEntity entity, boolean offline) {
- Cache cache = getCache(offline);
- return entity != null ? new UserSessionAdapter(session, this, cache, realm, entity, offline) : null;
- }
-
- List wrapUserSessions(RealmModel realm, Collection entities, boolean offline) {
- List models = new LinkedList<>();
- for (UserSessionEntity e : entities) {
- models.add(wrap(realm, e, offline));
- }
- return models;
+ InfinispanChangelogBasedTransaction tx = getTransaction(offline);
+ return entity != null ? new UserSessionAdapter(session, this, tx, realm, entity, offline) : null;
}
UserLoginFailureModel wrap(LoginFailureKey key, LoginFailureEntity entity) {
@@ -411,8 +598,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
if (userSession instanceof UserSessionAdapter) {
return ((UserSessionAdapter) userSession).getEntity();
} else {
- Cache cache = getCache(offline);
- return cache != null ? (UserSessionEntity) cache.get(userSession.getId()) : null;
+ return getUserSessionEntity(userSession.getId(), offline);
}
}
@@ -438,7 +624,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
public void removeOfflineUserSession(RealmModel realm, UserSessionModel userSession) {
UserSessionEntity userSessionEntity = getUserSessionEntity(userSession, true);
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 :
getOfflineUserSession(offlineUserSession.getRealm(), offlineUserSession.getId());
- AuthenticatedClientSessionAdapter offlineClientSession = importClientSession(userSessionAdapter, clientSession);
+ AuthenticatedClientSessionAdapter offlineClientSession = importClientSession(userSessionAdapter, clientSession, getTransaction(true));
// update timestamp to current time
offlineClientSession.setTimestamp(Time.currentTime());
@@ -459,12 +645,18 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
@Override
public List getOfflineUserSessions(RealmModel realm, UserModel user) {
- Iterator> itr = offlineSessionCache.entrySet().stream().filter(UserSessionPredicate.create(realm.getId()).user(user.getId())).iterator();
List userSessions = new LinkedList<>();
- while(itr.hasNext()) {
- UserSessionEntity entity = (UserSessionEntity) itr.next().getValue();
- UserSessionModel userSession = wrap(realm, entity, true);
+ Cache> cache = CacheDecorators.skipCacheLoaders(offlineSessionCache);
+
+ Iterator 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);
}
@@ -492,7 +684,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
entity.setBrokerUserId(userSession.getBrokerUserId());
entity.setIpAddress(userSession.getIpAddress());
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.setRememberMe(userSession.isRememberMe());
entity.setState(userSession.getState());
@@ -502,14 +694,34 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
entity.setLastSessionRefresh(userSession.getLastSessionRefresh());
- Cache cache = getCache(offline);
- tx.put(cache, userSession.getId(), entity);
+ InfinispanChangelogBasedTransaction tx = getTransaction(offline);
+
+ SessionUpdateTask importTask = new SessionUpdateTask() {
+
+ @Override
+ public void runUpdate(UserSessionEntity session) {
+
+ }
+
+ @Override
+ public CacheOperation getOperation(UserSessionEntity session) {
+ return CacheOperation.ADD_IF_ABSENT;
+ }
+
+ @Override
+ public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper sessionWrapper) {
+ return CrossDCMessageStatus.SYNC;
+ }
+
+ };
+ tx.addTask(userSession.getId(), importTask, entity);
+
UserSessionAdapter importedSession = wrap(userSession.getRealm(), entity, offline);
// Handle client sessions
if (importAuthenticatedClientSessions) {
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 updateTx) {
AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity();
entity.setAction(clientSession.getAction());
entity.setAuthMethod(clientSession.getProtocol());
- entity.setNotes(clientSession.getNotes());
+ entity.setNotes(clientSession.getNotes() == null ? new ConcurrentHashMap<>() : clientSession.getNotes());
entity.setProtocolMappers(clientSession.getProtocolMappers());
entity.setRedirectUri(clientSession.getRedirectUri());
entity.setRoles(clientSession.getRoles());
entity.setTimestamp(clientSession.getTimestamp());
+
Map clientSessions = importedUserSession.getEntity().getAuthenticatedClientSessions();
clientSessions.put(clientSession.getClient().getId(), entity);
- importedUserSession.update();
+ SessionUpdateTask importTask = new SessionUpdateTask() {
- return new AuthenticatedClientSessionAdapter(entity, clientSession.getClient(), importedUserSession, this, importedUserSession.getCache());
+ @Override
+ public void runUpdate(UserSessionEntity session) {
+ Map clientSessions = session.getAuthenticatedClientSessions();
+ clientSessions.put(clientSession.getClient().getId(), entity);
+ }
+
+ @Override
+ public CacheOperation getOperation(UserSessionEntity session) {
+ return CacheOperation.REPLACE;
+ }
+
+ @Override
+ public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper sessionWrapper) {
+ return CrossDCMessageStatus.SYNC;
+ }
+
+ };
+ updateTx.addTask(importedUserSession.getId(), importTask);
+
+ return new AuthenticatedClientSessionAdapter(entity, clientSession.getClient(), importedUserSession, this, updateTx);
}
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java
index 663a4b2e7d..110a8124f8 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java
@@ -18,41 +18,76 @@
package org.keycloak.models.sessions.infinispan;
import org.infinispan.Cache;
+import org.infinispan.client.hotrod.RemoteCache;
+import org.infinispan.persistence.remote.RemoteStore;
import org.jboss.logging.Logger;
import org.keycloak.Config;
+import org.keycloak.cluster.ClusterProvider;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakSessionTask;
+import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionProvider;
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.LoginFailureKey;
-import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
-import org.keycloak.models.sessions.infinispan.initializer.InfinispanUserSessionInitializer;
-import org.keycloak.models.sessions.infinispan.initializer.OfflineUserSessionLoader;
+import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
+import org.keycloak.models.sessions.infinispan.events.AbstractUserSessionClusterListener;
+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.PostMigrationEvent;
import org.keycloak.provider.ProviderEvent;
import org.keycloak.provider.ProviderEventListener;
import java.io.Serializable;
+import java.util.Set;
public class InfinispanUserSessionProviderFactory implements UserSessionProviderFactory {
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 RemoteCacheInvoker remoteCacheInvoker;
+ private LastSessionRefreshStore lastSessionRefreshStore;
+ private LastSessionRefreshStore offlineLastSessionRefreshStore;
+
@Override
public InfinispanUserSessionProvider create(KeycloakSession session) {
InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
- Cache cache = connections.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
- Cache offlineSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME);
+ Cache> cache = connections.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
+ Cache> offlineSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME);
Cache 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
@@ -62,18 +97,19 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
@Override
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() {
@Override
public void onEvent(ProviderEvent event) {
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) {
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
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() {
@Override
public void run(KeycloakSession session) {
InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
- Cache cache = connections.getCache(InfinispanConnectionProvider.WORK_CACHE_NAME);
+ Cache 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.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(sessionFactory) {
+
+ @Override
+ protected void eventReceived(KeycloakSession session, InfinispanUserSessionProvider provider, RealmRemovedSessionEvent sessionEvent) {
+ provider.onRealmRemovedEvent(sessionEvent.getRealmId());
+ }
+
+ });
+
+ cluster.registerListener(CLIENT_REMOVED_SESSION_EVENT, new AbstractUserSessionClusterListener(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(sessionFactory) {
+
+ @Override
+ protected void eventReceived(KeycloakSession session, InfinispanUserSessionProvider provider, RemoveUserSessionsEvent sessionEvent) {
+ provider.onRemoveUserSessionsEvent(sessionEvent.getRealmId());
+ }
+
+ });
+
+ cluster.registerListener(REMOVE_ALL_LOGIN_FAILURES_EVENT, new AbstractUserSessionClusterListener(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 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 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
public void close() {
}
@Override
public String getId() {
- return "infinispan";
+ return PROVIDER_ID;
}
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java
index f35dea974a..b8df6052de 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java
@@ -17,15 +17,17 @@
package org.keycloak.models.sessions.infinispan;
-import org.infinispan.Cache;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
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.SessionEntity;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import java.util.Collections;
@@ -33,7 +35,6 @@ import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
/**
* @author Stian Thorgersen
@@ -44,7 +45,7 @@ public class UserSessionAdapter implements UserSessionModel {
private final InfinispanUserSessionProvider provider;
- private final Cache cache;
+ private final InfinispanChangelogBasedTransaction updateTx;
private final RealmModel realm;
@@ -52,11 +53,11 @@ public class UserSessionAdapter implements UserSessionModel {
private final boolean offline;
- public UserSessionAdapter(KeycloakSession session, InfinispanUserSessionProvider provider, Cache cache, RealmModel realm,
+ public UserSessionAdapter(KeycloakSession session, InfinispanUserSessionProvider provider, InfinispanChangelogBasedTransaction updateTx, RealmModel realm,
UserSessionEntity entity, boolean offline) {
this.session = session;
this.provider = provider;
- this.cache = cache;
+ this.updateTx = updateTx;
this.realm = realm;
this.entity = entity;
this.offline = offline;
@@ -74,7 +75,7 @@ public class UserSessionAdapter implements UserSessionModel {
// Check if client still exists
ClientModel client = realm.getClientById(key);
if (client != null) {
- result.put(key, new AuthenticatedClientSessionAdapter(value, client, this, provider, cache));
+ result.put(key, new AuthenticatedClientSessionAdapter(value, client, this, provider, updateTx));
} else {
removedClientUUIDS.add(key);
}
@@ -83,10 +84,18 @@ public class UserSessionAdapter implements UserSessionModel {
// Update user session
if (!removedClientUUIDS.isEmpty()) {
- for (String clientUUID : removedClientUUIDS) {
- entity.getAuthenticatedClientSessions().remove(clientUUID);
- }
- update();
+ UserSessionUpdateTask task = new UserSessionUpdateTask() {
+
+ @Override
+ public void runUpdate(UserSessionEntity entity) {
+ for (String clientUUID : removedClientUUIDS) {
+ entity.getAuthenticatedClientSessions().remove(clientUUID);
+ }
+ }
+
+ };
+
+ update(task);
}
return Collections.unmodifiableMap(result);
@@ -114,12 +123,6 @@ public class UserSessionAdapter implements UserSessionModel {
return session.users().getUserById(entity.getUser(), realm);
}
- @Override
- public void setUser(UserModel user) {
- entity.setUser(user.getId());
- update();
- }
-
@Override
public String getLoginUsername() {
return entity.getLoginUsername();
@@ -148,8 +151,21 @@ public class UserSessionAdapter implements UserSessionModel {
}
public void setLastSessionRefresh(int lastSessionRefresh) {
- entity.setLastSessionRefresh(lastSessionRefresh);
- update();
+ UserSessionUpdateTask task = new UserSessionUpdateTask() {
+
+ @Override
+ public void runUpdate(UserSessionEntity entity) {
+ entity.setLastSessionRefresh(lastSessionRefresh);
+ }
+
+ @Override
+ public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper sessionWrapper) {
+ return new LastSessionRefreshChecker(provider.getLastSessionRefreshStore(), provider.getOfflineLastSessionRefreshStore())
+ .getCrossDCMessageStatus(UserSessionAdapter.this.session, UserSessionAdapter.this.realm, sessionWrapper, offline, lastSessionRefresh);
+ }
+ };
+
+ update(task);
}
@Override
@@ -159,22 +175,36 @@ public class UserSessionAdapter implements UserSessionModel {
@Override
public void setNote(String name, String value) {
- if (value == null) {
- if (entity.getNotes().containsKey(name)) {
- removeNote(name);
+ UserSessionUpdateTask task = new UserSessionUpdateTask() {
+
+ @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
public void removeNote(String name) {
- if (entity.getNotes() != null) {
- entity.getNotes().remove(name);
- update();
- }
+ UserSessionUpdateTask task = new UserSessionUpdateTask() {
+
+ @Override
+ public void runUpdate(UserSessionEntity entity) {
+ entity.getNotes().remove(name);
+ }
+
+ };
+
+ update(task);
}
@Override
@@ -189,19 +219,34 @@ public class UserSessionAdapter implements UserSessionModel {
@Override
public void setState(State state) {
- entity.setState(state);
- update();
+ UserSessionUpdateTask task = new UserSessionUpdateTask() {
+
+ @Override
+ public void runUpdate(UserSessionEntity entity) {
+ entity.setState(state);
+ }
+
+ };
+
+ update(task);
}
@Override
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);
- entity.getNotes().clear();
- entity.getAuthenticatedClientSessions().clear();
+ @Override
+ public void runUpdate(UserSessionEntity entity) {
+ provider.updateSessionEntity(entity, realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, brokerUserId);
- update();
+ entity.setState(null);
+ entity.getNotes().clear();
+ entity.getAuthenticatedClientSessions().clear();
+ }
+
+ };
+
+ update(task);
}
@Override
@@ -222,11 +267,8 @@ public class UserSessionAdapter implements UserSessionModel {
return entity;
}
- void update() {
- provider.getTx().replace(cache, entity.getId(), entity);
+ void update(UserSessionUpdateTask task) {
+ updateTx.addTask(getId(), task);
}
- Cache getCache() {
- return cache;
- }
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/InfinispanChangelogBasedTransaction.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/InfinispanChangelogBasedTransaction.java
new file mode 100644
index 0000000000..d3bcacc122
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/InfinispanChangelogBasedTransaction.java
@@ -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 Marek Posolda
+ */
+public class InfinispanChangelogBasedTransaction extends AbstractKeycloakTransaction {
+
+ public static final Logger logger = Logger.getLogger(InfinispanChangelogBasedTransaction.class);
+
+ private final KeycloakSession kcSession;
+ private final String cacheName;
+ private final Cache> cache;
+ private final RemoteCacheInvoker remoteCacheInvoker;
+
+ private final Map> updates = new HashMap<>();
+
+ public InfinispanChangelogBasedTransaction(KeycloakSession kcSession, String cacheName, Cache> cache, RemoteCacheInvoker remoteCacheInvoker) {
+ this.kcSession = kcSession;
+ this.cacheName = cacheName;
+ this.cache = cache;
+ this.remoteCacheInvoker = remoteCacheInvoker;
+ }
+
+
+ public void addTask(String key, SessionUpdateTask task) {
+ SessionUpdatesList myUpdates = updates.get(key);
+ if (myUpdates == null) {
+ // Lookup entity from cache
+ SessionEntityWrapper 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 task, S entity) {
+ if (entity == null) {
+ throw new IllegalArgumentException("Null entity not allowed");
+ }
+
+ RealmModel realm = kcSession.realms().getRealm(entity.getRealm());
+ SessionEntityWrapper wrappedEntity = new SessionEntityWrapper<>(entity);
+ SessionUpdatesList 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 entity) {
+ if (entity == null) {
+ throw new IllegalArgumentException("Null entity not allowed");
+ }
+
+ SessionEntityWrapper latestEntity = cache.get(key);
+ if (latestEntity == null) {
+ return;
+ }
+
+ SessionUpdatesList newUpdates = new SessionUpdatesList<>(realm, latestEntity);
+
+ SessionUpdatesList existingUpdates = updates.get(key);
+ if (existingUpdates != null) {
+ newUpdates.setUpdateTasks(existingUpdates.getUpdateTasks());
+ }
+
+ updates.put(key, newUpdates);
+ }
+
+
+ public SessionEntityWrapper get(String key) {
+ SessionUpdatesList myUpdates = updates.get(key);
+ if (myUpdates == null) {
+ SessionEntityWrapper 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> entry : updates.entrySet()) {
+ SessionUpdatesList sessionUpdates = entry.getValue();
+ SessionEntityWrapper sessionWrapper = sessionUpdates.getEntityWrapper();
+
+ RealmModel realm = sessionUpdates.getRealm();
+
+ MergedUpdate 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 task, SessionEntityWrapper 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 task, SessionEntityWrapper oldVersionEntity) {
+ boolean replaced = false;
+ S session = oldVersionEntity.getEntity();
+
+ while (!replaced) {
+ SessionEntityWrapper 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 generateNewVersionAndWrapEntity(S entity, Map localMetadata) {
+ return new SessionEntityWrapper<>(localMetadata, entity);
+ }
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/MergedUpdate.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/MergedUpdate.java
new file mode 100644
index 0000000000..695401dca2
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/MergedUpdate.java
@@ -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 Marek Posolda
+ */
+class MergedUpdate implements SessionUpdateTask {
+
+ private List> 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 child : childUpdates) {
+ child.runUpdate(session);
+ }
+ }
+
+ @Override
+ public CacheOperation getOperation(S session) {
+ return operation;
+ }
+
+ @Override
+ public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper sessionWrapper) {
+ return crossDCMessageStatus;
+ }
+
+
+ public static MergedUpdate computeUpdate(List> childUpdates, SessionEntityWrapper sessionWrapper) {
+ if (childUpdates == null || childUpdates.isEmpty()) {
+ return null;
+ }
+
+ MergedUpdate result = null;
+ S session = sessionWrapper.getEntity();
+ for (SessionUpdateTask 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;
+ }
+
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionEntityWrapper.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionEntityWrapper.java
new file mode 100644
index 0000000000..400a1cd0c7
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionEntityWrapper.java
@@ -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 Marek Posolda
+ */
+@SerializeWith(SessionEntityWrapper.ExternalizerImpl.class)
+public class SessionEntityWrapper {
+
+ private UUID version;
+ private final S entity;
+ private final Map localMetadata;
+
+
+ protected SessionEntityWrapper(UUID version, Map 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 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 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 {
+
+
+ @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 localMetadata = MarshallUtil.unmarshallMap(input, new MarshallUtil.MapBuilder>() {
+
+ @Override
+ public Map build(int size) {
+ return new ConcurrentHashMap<>(size);
+ }
+
+ });
+
+ SessionEntity entity = (SessionEntity) input.readObject();
+
+ return new SessionEntityWrapper<>(objVersion, localMetadata, entity);
+ }
+
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionUpdateTask.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionUpdateTask.java
new file mode 100644
index 0000000000..66f88baa05
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionUpdateTask.java
@@ -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 Marek Posolda
+ */
+public interface SessionUpdateTask {
+
+ void runUpdate(S entity);
+
+ CacheOperation getOperation(S entity);
+
+ CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper 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;
+ }
+
+ }
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionUpdatesList.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionUpdatesList.java
new file mode 100644
index 0000000000..136df30b2d
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/SessionUpdatesList.java
@@ -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 Marek Posolda
+ */
+class SessionUpdatesList {
+
+ private final RealmModel realm;
+
+ private final SessionEntityWrapper entityWrapper;
+
+ private List> updateTasks = new LinkedList<>();
+
+ public SessionUpdatesList(RealmModel realm, SessionEntityWrapper entityWrapper) {
+ this.realm = realm;
+ this.entityWrapper = entityWrapper;
+ }
+
+ public RealmModel getRealm() {
+ return realm;
+ }
+
+ public SessionEntityWrapper getEntityWrapper() {
+ return entityWrapper;
+ }
+
+
+ public void add(SessionUpdateTask task) {
+ updateTasks.add(task);
+ }
+
+ public List> getUpdateTasks() {
+ return updateTasks;
+ }
+
+ public void setUpdateTasks(List> updateTasks) {
+ this.updateTasks = updateTasks;
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/UserSessionClientSessionUpdateTask.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/UserSessionClientSessionUpdateTask.java
new file mode 100644
index 0000000000..56e0403143
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/UserSessionClientSessionUpdateTask.java
@@ -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 Marek Posolda
+ */
+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);
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/UserSessionUpdateTask.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/UserSessionUpdateTask.java
new file mode 100644
index 0000000000..4fd4bbebf1
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/UserSessionUpdateTask.java
@@ -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 Marek Posolda
+ */
+public abstract class UserSessionUpdateTask implements SessionUpdateTask {
+
+ @Override
+ public CacheOperation getOperation(UserSessionEntity session) {
+ return CacheOperation.REPLACE;
+ }
+
+ @Override
+ public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper sessionWrapper) {
+ return CrossDCMessageStatus.SYNC;
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshChecker.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshChecker.java
new file mode 100644
index 0000000000..4e349f65ba
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshChecker.java
@@ -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 Marek Posolda
+ */
+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 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;
+ }
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshEvent.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshEvent.java
new file mode 100644
index 0000000000..7d2af5fddf
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshEvent.java
@@ -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 Marek Posolda
+ */
+@SerializeWith(LastSessionRefreshEvent.ExternalizerImpl.class)
+public class LastSessionRefreshEvent implements ClusterEvent {
+
+ private final Map lastSessionRefreshes;
+
+ public LastSessionRefreshEvent(Map lastSessionRefreshes) {
+ this.lastSessionRefreshes = lastSessionRefreshes;
+ }
+
+ public Map getLastSessionRefreshes() {
+ return lastSessionRefreshes;
+ }
+
+
+ public static class ExternalizerImpl implements Externalizer {
+
+
+ @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 map = MarshallUtil.unmarshallMap(input, new MarshallUtil.MapBuilder>() {
+
+ @Override
+ public Map build(int size) {
+ return new HashMap<>(size);
+ }
+
+ });
+
+ LastSessionRefreshEvent event = new LastSessionRefreshEvent(map);
+ return event;
+ }
+
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshListener.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshListener.java
new file mode 100644
index 0000000000..1bc151fb2a
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/sessions/LastSessionRefreshListener.java
@@ -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 Marek Posolda
+ */
+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 cache;
+ private final boolean distributed;
+ private final String myAddress;
+
+ public LastSessionRefreshListener(KeycloakSession session, Cache