keycloak-uncached

Merge pull request #4391 from mposolda/ispn-clientListeners-bugs KEYCLOAK-4634

8/17/2017 6:01:02 AM

Changes

Details

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 86f6074..a17e724 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
@@ -262,7 +262,14 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
         sessionCacheConfiguration = sessionConfigBuilder.build();
         cacheManager.defineConfiguration(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, sessionCacheConfiguration);
 
-        cacheManager.defineConfiguration(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME, sessionCacheConfigurationBase);
+        if (jdgEnabled) {
+            sessionConfigBuilder = new ConfigurationBuilder();
+            sessionConfigBuilder.read(sessionCacheConfigurationBase);
+            configureRemoteCacheStore(sessionConfigBuilder, async, InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME, KcRemoteStoreConfigurationBuilder.class);
+        }
+        sessionCacheConfiguration = sessionConfigBuilder.build();
+        cacheManager.defineConfiguration(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME, sessionCacheConfiguration);
+
         cacheManager.defineConfiguration(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME, sessionCacheConfigurationBase);
 
         // Retrieve caches to enforce rebalance
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
index 43bb2b3..e5a7a47 100644
--- 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
@@ -33,18 +33,18 @@ import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker;
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
  */
-public class InfinispanChangelogBasedTransaction<S extends SessionEntity> extends AbstractKeycloakTransaction {
+public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> extends AbstractKeycloakTransaction {
 
     public static final Logger logger = Logger.getLogger(InfinispanChangelogBasedTransaction.class);
 
     private final KeycloakSession kcSession;
     private final String cacheName;
-    private final Cache<String, SessionEntityWrapper<S>> cache;
+    private final Cache<K, SessionEntityWrapper<V>> cache;
     private final RemoteCacheInvoker remoteCacheInvoker;
 
-    private final Map<String, SessionUpdatesList<S>> updates = new HashMap<>();
+    private final Map<K, SessionUpdatesList<V>> updates = new HashMap<>();
 
-    public InfinispanChangelogBasedTransaction(KeycloakSession kcSession, String cacheName, Cache<String, SessionEntityWrapper<S>> cache, RemoteCacheInvoker remoteCacheInvoker) {
+    public InfinispanChangelogBasedTransaction(KeycloakSession kcSession, String cacheName, Cache<K, SessionEntityWrapper<V>> cache, RemoteCacheInvoker remoteCacheInvoker) {
         this.kcSession = kcSession;
         this.cacheName = cacheName;
         this.cache = cache;
@@ -52,11 +52,11 @@ public class InfinispanChangelogBasedTransaction<S extends SessionEntity> extend
     }
 
 
-    public void addTask(String key, SessionUpdateTask<S> task) {
-        SessionUpdatesList<S> myUpdates = updates.get(key);
+    public void addTask(K key, SessionUpdateTask<V> task) {
+        SessionUpdatesList<V> myUpdates = updates.get(key);
         if (myUpdates == null) {
             // Lookup entity from cache
-            SessionEntityWrapper<S> wrappedEntity = cache.get(key);
+            SessionEntityWrapper<V> wrappedEntity = cache.get(key);
             if (wrappedEntity == null) {
                 logger.warnf("Not present cache item for key %s", key);
                 return;
@@ -75,14 +75,14 @@ public class InfinispanChangelogBasedTransaction<S extends SessionEntity> extend
 
 
     // Create entity and new version for it
-    public void addTask(String key, SessionUpdateTask<S> task, S entity) {
+    public void addTask(K key, SessionUpdateTask<V> task, V entity) {
         if (entity == null) {
             throw new IllegalArgumentException("Null entity not allowed");
         }
 
         RealmModel realm = kcSession.realms().getRealm(entity.getRealm());
-        SessionEntityWrapper<S> wrappedEntity = new SessionEntityWrapper<>(entity);
-        SessionUpdatesList<S> myUpdates = new SessionUpdatesList<>(realm, wrappedEntity);
+        SessionEntityWrapper<V> wrappedEntity = new SessionEntityWrapper<>(entity);
+        SessionUpdatesList<V> myUpdates = new SessionUpdatesList<>(realm, wrappedEntity);
         updates.put(key, myUpdates);
 
         // Run the update now, so reader in same transaction can see it
@@ -91,19 +91,19 @@ public class InfinispanChangelogBasedTransaction<S extends SessionEntity> extend
     }
 
 
-    public void reloadEntityInCurrentTransaction(RealmModel realm, String key, SessionEntityWrapper<S> entity) {
+    public void reloadEntityInCurrentTransaction(RealmModel realm, K key, SessionEntityWrapper<V> entity) {
         if (entity == null) {
             throw new IllegalArgumentException("Null entity not allowed");
         }
 
-        SessionEntityWrapper<S> latestEntity = cache.get(key);
+        SessionEntityWrapper<V> latestEntity = cache.get(key);
         if (latestEntity == null) {
             return;
         }
 
-        SessionUpdatesList<S> newUpdates = new SessionUpdatesList<>(realm, latestEntity);
+        SessionUpdatesList<V> newUpdates = new SessionUpdatesList<>(realm, latestEntity);
 
-        SessionUpdatesList<S> existingUpdates = updates.get(key);
+        SessionUpdatesList<V> existingUpdates = updates.get(key);
         if (existingUpdates != null) {
             newUpdates.setUpdateTasks(existingUpdates.getUpdateTasks());
         }
@@ -112,10 +112,10 @@ public class InfinispanChangelogBasedTransaction<S extends SessionEntity> extend
     }
 
 
-    public SessionEntityWrapper<S> get(String key) {
-        SessionUpdatesList<S> myUpdates = updates.get(key);
+    public SessionEntityWrapper<V> get(K key) {
+        SessionUpdatesList<V> myUpdates = updates.get(key);
         if (myUpdates == null) {
-            SessionEntityWrapper<S> wrappedEntity = cache.get(key);
+            SessionEntityWrapper<V> wrappedEntity = cache.get(key);
             if (wrappedEntity == null) {
                 return null;
             }
@@ -127,7 +127,7 @@ public class InfinispanChangelogBasedTransaction<S extends SessionEntity> extend
 
             return wrappedEntity;
         } else {
-            S entity = myUpdates.getEntityWrapper().getEntity();
+            V entity = myUpdates.getEntityWrapper().getEntity();
 
             // If entity is scheduled for remove, we don't return it.
             boolean scheduledForRemove = myUpdates.getUpdateTasks().stream().filter((SessionUpdateTask task) -> {
@@ -143,13 +143,13 @@ public class InfinispanChangelogBasedTransaction<S extends SessionEntity> extend
 
     @Override
     protected void commitImpl() {
-        for (Map.Entry<String, SessionUpdatesList<S>> entry : updates.entrySet()) {
-            SessionUpdatesList<S> sessionUpdates = entry.getValue();
-            SessionEntityWrapper<S> sessionWrapper = sessionUpdates.getEntityWrapper();
+        for (Map.Entry<K, SessionUpdatesList<V>> entry : updates.entrySet()) {
+            SessionUpdatesList<V> sessionUpdates = entry.getValue();
+            SessionEntityWrapper<V> sessionWrapper = sessionUpdates.getEntityWrapper();
 
             RealmModel realm = sessionUpdates.getRealm();
 
-            MergedUpdate<S> merged = MergedUpdate.computeUpdate(sessionUpdates.getUpdateTasks(), sessionWrapper);
+            MergedUpdate<V> merged = MergedUpdate.computeUpdate(sessionUpdates.getUpdateTasks(), sessionWrapper);
 
             if (merged != null) {
                 // Now run the operation in our cluster
@@ -162,8 +162,8 @@ public class InfinispanChangelogBasedTransaction<S extends SessionEntity> extend
     }
 
 
-    private void runOperationInCluster(String key, MergedUpdate<S> task,  SessionEntityWrapper<S> sessionWrapper) {
-        S session = sessionWrapper.getEntity();
+    private void runOperationInCluster(K key, MergedUpdate<V> task,  SessionEntityWrapper<V> sessionWrapper) {
+        V session = sessionWrapper.getEntity();
         SessionUpdateTask.CacheOperation operation = task.getOperation(session);
 
         // Don't need to run update of underlying entity. Local updates were already run
@@ -182,9 +182,14 @@ public class InfinispanChangelogBasedTransaction<S extends SessionEntity> extend
                         .put(key, sessionWrapper, task.getLifespanMs(), TimeUnit.MILLISECONDS);
                 break;
             case ADD_IF_ABSENT:
-                SessionEntityWrapper existing = cache.putIfAbsent(key, sessionWrapper);
+                SessionEntityWrapper<V> existing = cache.putIfAbsent(key, sessionWrapper);
                 if (existing != null) {
-                    throw new IllegalStateException("There is already existing value in cache for key " + key);
+                    logger.debugf("Existing entity in cache for key: %s . Will update it", key);
+
+                    // Apply updates on the existing entity and replace it
+                    task.runUpdate(existing.getEntity());
+
+                    replace(key, task, existing);
                 }
                 break;
             case REPLACE:
@@ -197,12 +202,12 @@ public class InfinispanChangelogBasedTransaction<S extends SessionEntity> extend
     }
 
 
-    private void replace(String key, MergedUpdate<S> task, SessionEntityWrapper<S> oldVersionEntity) {
+    private void replace(K key, MergedUpdate<V> task, SessionEntityWrapper<V> oldVersionEntity) {
         boolean replaced = false;
-        S session = oldVersionEntity.getEntity();
+        V session = oldVersionEntity.getEntity();
 
         while (!replaced) {
-            SessionEntityWrapper<S> newVersionEntity = generateNewVersionAndWrapEntity(session, oldVersionEntity.getLocalMetadata());
+            SessionEntityWrapper<V> newVersionEntity = generateNewVersionAndWrapEntity(session, oldVersionEntity.getLocalMetadata());
 
             // Atomic cluster-aware replace
             replaced = cache.replace(key, oldVersionEntity, newVersionEntity);
@@ -235,7 +240,7 @@ public class InfinispanChangelogBasedTransaction<S extends SessionEntity> extend
     protected void rollbackImpl() {
     }
 
-    private SessionEntityWrapper<S> generateNewVersionAndWrapEntity(S entity, Map<String, String> localMetadata) {
+    private SessionEntityWrapper<V> generateNewVersionAndWrapEntity(V entity, Map<String, String> localMetadata) {
         return new SessionEntityWrapper<>(localMetadata, entity);
     }
 
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/LoginFailuresUpdateTask.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/LoginFailuresUpdateTask.java
new file mode 100644
index 0000000..04ed05a
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/LoginFailuresUpdateTask.java
@@ -0,0 +1,36 @@
+/*
+ * 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;
+
+import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public abstract class LoginFailuresUpdateTask implements SessionUpdateTask<LoginFailureEntity> {
+
+    @Override
+    public CacheOperation getOperation(LoginFailureEntity session) {
+        return CacheOperation.REPLACE;
+    }
+
+    @Override
+    public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<LoginFailureEntity> sessionWrapper) {
+        return CrossDCMessageStatus.SYNC;
+    }
+}
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
index 1f24f84..8e0cf0e 100644
--- 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
@@ -32,7 +32,7 @@ class MergedUpdate<S extends SessionEntity> implements SessionUpdateTask<S> {
     private CrossDCMessageStatus crossDCMessageStatus;
 
 
-    public MergedUpdate(CacheOperation operation, CrossDCMessageStatus crossDCMessageStatus) {
+    private MergedUpdate(CacheOperation operation, CrossDCMessageStatus crossDCMessageStatus) {
         this.operation = operation;
         this.crossDCMessageStatus = crossDCMessageStatus;
     }
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
index f9adf9b..25e0df9 100644
--- 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
@@ -60,7 +60,7 @@ public class LastSessionRefreshChecker {
 
         Integer lsrr = sessionWrapper.getLocalMetadataNoteInt(UserSessionEntity.LAST_SESSION_REFRESH_REMOTE);
         if (lsrr == null) {
-            logger.warnf("Not available lsrr note on user session %s.", sessionWrapper.getEntity().getId());
+            logger.debugf("Not available lsrr note on user session %s.", sessionWrapper.getEntity().getId());
             return SessionUpdateTask.CrossDCMessageStatus.SYNC;
         }
 
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
index b79ea16..244a88b 100644
--- 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
@@ -49,7 +49,7 @@ public interface SessionUpdateTask<S extends SessionEntity> {
 
             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());
+                    throw new IllegalStateException("Illegal state. Task already in progress for session " + entity.toString());
                 }
 
                 return this;
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
index 4fd4bbe..1db36a1 100644
--- 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
@@ -17,8 +17,6 @@
 
 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;
 
 /**
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticationSessionEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticationSessionEntity.java
index 0b99225..cdb841e 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticationSessionEntity.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticationSessionEntity.java
@@ -21,6 +21,7 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
+import java.util.TreeSet;
 
 import org.keycloak.sessions.AuthenticationSessionModel;
 
@@ -29,6 +30,8 @@ import org.keycloak.sessions.AuthenticationSessionModel;
  */
 public class AuthenticationSessionEntity extends SessionEntity {
 
+    private String id;
+
     private String clientUuid;
     private String authUserId;
 
@@ -46,6 +49,14 @@ public class AuthenticationSessionEntity extends SessionEntity {
     private Set<String> requiredActions  = new HashSet<>();
     private Map<String, String> userSessionNotes;
 
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
     public String getClientUuid() {
         return clientUuid;
     }
@@ -149,4 +160,26 @@ public class AuthenticationSessionEntity extends SessionEntity {
     public void setAuthNotes(Map<String, String> authNotes) {
         this.authNotes = authNotes;
     }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof AuthenticationSessionEntity)) return false;
+
+        AuthenticationSessionEntity that = (AuthenticationSessionEntity) o;
+
+        if (id != null ? !id.equals(that.id) : that.id != null) return false;
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        return id != null ? id.hashCode() : 0;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("AuthenticationSessionEntity [id=%s, realm=%s, clientUuid=%s ]", getId(), getRealm(), getClientUuid());
+    }
 }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/LoginFailureEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/LoginFailureEntity.java
index d25f58b..5b328ec 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/LoginFailureEntity.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/LoginFailureEntity.java
@@ -17,15 +17,12 @@
 
 package org.keycloak.models.sessions.infinispan.entities;
 
-import java.io.Serializable;
-
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
-public class LoginFailureEntity implements Serializable {
+public class LoginFailureEntity extends SessionEntity {
 
     private String userId;
-    private String realm;
     private int failedLoginNotBefore;
     private int numFailures;
     private long lastFailure;
@@ -39,14 +36,6 @@ public class LoginFailureEntity implements Serializable {
         this.userId = userId;
     }
 
-    public String getRealm() {
-        return realm;
-    }
-
-    public void setRealm(String realm) {
-        this.realm = realm;
-    }
-
     public int getFailedLoginNotBefore() {
         return failedLoginNotBefore;
     }
@@ -85,4 +74,30 @@ public class LoginFailureEntity implements Serializable {
         this.lastFailure = 0;
         this.lastIPFailure = null;
     }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof LoginFailureEntity)) return false;
+
+        LoginFailureEntity that = (LoginFailureEntity) o;
+
+        if (userId != null ? !userId.equals(that.userId) : that.userId != null) return false;
+        if (getRealm() != null ? !getRealm().equals(that.getRealm()) : that.getRealm() != null) return false;
+
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int hashCode = getRealm() != null ? getRealm().hashCode() : 0;
+        hashCode = hashCode * 13 + (userId != null ? userId.hashCode() : 0);
+        return hashCode;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("LoginFailureEntity [ userId=%s, realm=%s, numFailures=%d ]", userId, getRealm(), numFailures);
+    }
 }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/LoginFailureKey.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/LoginFailureKey.java
index c452379..318b1ba 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/LoginFailureKey.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/LoginFailureKey.java
@@ -52,4 +52,9 @@ public class LoginFailureKey implements Serializable {
         return result;
     }
 
+
+    @Override
+    public String toString() {
+        return String.format("LoginFailureKey [ realm=%s. userId=%s ]", realm, userId);
+    }
 }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/SessionEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/SessionEntity.java
index 25ac2a4..37f8e08 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/SessionEntity.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/SessionEntity.java
@@ -26,17 +26,8 @@ import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
  */
 public abstract class SessionEntity implements Serializable {
 
-    private String id;
-
     private String realm;
 
-    public String getId() {
-        return id;
-    }
-
-    public void setId(String id) {
-        this.id = id;
-    }
 
     public String getRealm() {
         return realm;
@@ -46,26 +37,13 @@ public abstract class SessionEntity implements Serializable {
         this.realm = realm;
     }
 
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (!(o instanceof SessionEntity)) return false;
-
-        SessionEntity that = (SessionEntity) o;
-
-        if (id != null ? !id.equals(that.id) : that.id != null) return false;
-
-        return true;
-    }
-
-    @Override
-    public int hashCode() {
-        return id != null ? id.hashCode() : 0;
-    }
-
 
     public SessionEntityWrapper mergeRemoteEntityWithLocalEntity(SessionEntityWrapper localEntityWrapper) {
-        throw new IllegalStateException("Not yet implemented");
+        if (localEntityWrapper == null) {
+            return new SessionEntityWrapper<>(this);
+        } else {
+            return new SessionEntityWrapper<>(localEntityWrapper.getLocalMetadata(), this);
+        }
     };
 
 }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java
index 5d0edb0..8ac85a1 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java
@@ -43,6 +43,8 @@ public class UserSessionEntity extends SessionEntity {
     // 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";
 
+    private String id;
+
     private String user;
 
     private String brokerSessionId;
@@ -62,6 +64,14 @@ public class UserSessionEntity extends SessionEntity {
 
     private UserSessionModel.State state;
 
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
     private Map<String, String> notes = new ConcurrentHashMap<>();
 
     private Map<String, AuthenticatedClientSessionEntity> authenticatedClientSessions  = new ConcurrentHashMap<>();
@@ -163,6 +173,23 @@ public class UserSessionEntity extends SessionEntity {
     }
 
     @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof UserSessionEntity)) return false;
+
+        UserSessionEntity that = (UserSessionEntity) o;
+
+        if (id != null ? !id.equals(that.id) : that.id != null) return false;
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        return id != null ? id.hashCode() : 0;
+    }
+
+    @Override
     public String toString() {
         return String.format("UserSessionEntity [id=%s, realm=%s, lastSessionRefresh=%d, clients=%s]", getId(), getRealm(), getLastSessionRefresh(),
           new TreeSet(this.authenticatedClientSessions.keySet()));
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 1eb58b5..48d1965 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
@@ -42,7 +42,6 @@ import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessi
 import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
 import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
 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;
@@ -76,11 +75,11 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
 
     protected final Cache<String, SessionEntityWrapper<UserSessionEntity>> sessionCache;
     protected final Cache<String, SessionEntityWrapper<UserSessionEntity>> offlineSessionCache;
-    protected final Cache<LoginFailureKey, LoginFailureEntity> loginFailureCache;
+    protected final Cache<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> loginFailureCache;
 
-    protected final InfinispanChangelogBasedTransaction<UserSessionEntity> sessionTx;
-    protected final InfinispanChangelogBasedTransaction<UserSessionEntity> offlineSessionTx;
-    protected final InfinispanKeycloakTransaction tx;
+    protected final InfinispanChangelogBasedTransaction<String, UserSessionEntity> sessionTx;
+    protected final InfinispanChangelogBasedTransaction<String, UserSessionEntity> offlineSessionTx;
+    protected final InfinispanChangelogBasedTransaction<LoginFailureKey, LoginFailureEntity> loginFailuresTx;
 
     protected final SessionEventsSenderTransaction clusterEventsSenderTx;
 
@@ -93,7 +92,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
                                          LastSessionRefreshStore offlineLastSessionRefreshStore,
                                          Cache<String, SessionEntityWrapper<UserSessionEntity>> sessionCache,
                                          Cache<String, SessionEntityWrapper<UserSessionEntity>> offlineSessionCache,
-                                         Cache<LoginFailureKey, LoginFailureEntity> loginFailureCache) {
+                                         Cache<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> loginFailureCache) {
         this.session = session;
 
         this.sessionCache = sessionCache;
@@ -103,24 +102,24 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
         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.loginFailuresTx = new InfinispanChangelogBasedTransaction<>(session, InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME, loginFailureCache, remoteCacheInvoker);
 
         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);
+        session.getTransactionManager().enlistAfterCompletion(loginFailuresTx);
     }
 
     protected Cache<String, SessionEntityWrapper<UserSessionEntity>> getCache(boolean offline) {
         return offline ? offlineSessionCache : sessionCache;
     }
 
-    protected InfinispanChangelogBasedTransaction<UserSessionEntity> getTransaction(boolean offline) {
+    protected InfinispanChangelogBasedTransaction<String, UserSessionEntity> getTransaction(boolean offline) {
         return offline ? offlineSessionTx : sessionTx;
     }
 
@@ -136,7 +135,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
     public AuthenticatedClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession) {
         AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity();
 
-        InfinispanChangelogBasedTransaction<UserSessionEntity> updateTx = getTransaction(false);
+        InfinispanChangelogBasedTransaction<String, UserSessionEntity> updateTx = getTransaction(false);
         AuthenticatedClientSessionAdapter adapter = new AuthenticatedClientSessionAdapter(entity, client, (UserSessionAdapter) userSession, this, updateTx);
         adapter.setUserSession(userSession);
         return adapter;
@@ -202,7 +201,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
     }
 
     private UserSessionEntity getUserSessionEntity(String id, boolean offline) {
-        InfinispanChangelogBasedTransaction<UserSessionEntity> tx = getTransaction(offline);
+        InfinispanChangelogBasedTransaction<String, UserSessionEntity> tx = getTransaction(offline);
         SessionEntityWrapper<UserSessionEntity> entityWrapper = tx.get(id);
         return entityWrapper==null ? null : entityWrapper.getEntity();
     }
@@ -311,7 +310,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
                 UserSessionModel remoteSessionAdapter = wrap(realm, remoteSessionEntity, offline);
                 if (predicate.test(remoteSessionAdapter)) {
 
-                    InfinispanChangelogBasedTransaction<UserSessionEntity> tx = getTransaction(offline);
+                    InfinispanChangelogBasedTransaction<String, UserSessionEntity> tx = getTransaction(offline);
 
                     // Remote entity contains our predicate. Update local cache with the remote entity
                     SessionEntityWrapper<UserSessionEntity> sessionWrapper = remoteSessionEntity.mergeRemoteEntityWithLocalEntity(tx.get(id));
@@ -511,7 +510,14 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
     @Override
     public UserLoginFailureModel getUserLoginFailure(RealmModel realm, String userId) {
         LoginFailureKey key = new LoginFailureKey(realm.getId(), userId);
-        return wrap(key, loginFailureCache.get(key));
+        LoginFailureEntity entity = getLoginFailureEntity(key);
+        return wrap(key, entity);
+    }
+
+    private LoginFailureEntity getLoginFailureEntity(LoginFailureKey key) {
+        InfinispanChangelogBasedTransaction<LoginFailureKey, LoginFailureEntity> tx = getLoginFailuresTx();
+        SessionEntityWrapper<LoginFailureEntity> entityWrapper = tx.get(key);
+        return entityWrapper==null ? null : entityWrapper.getEntity();
     }
 
     @Override
@@ -520,13 +526,53 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
         LoginFailureEntity entity = new LoginFailureEntity();
         entity.setRealm(realm.getId());
         entity.setUserId(userId);
-        tx.put(loginFailureCache, key, entity);
+
+        SessionUpdateTask<LoginFailureEntity> createLoginFailureTask = new SessionUpdateTask<LoginFailureEntity>() {
+
+            @Override
+            public void runUpdate(LoginFailureEntity session) {
+
+            }
+
+            @Override
+            public CacheOperation getOperation(LoginFailureEntity session) {
+                return CacheOperation.ADD_IF_ABSENT;
+            }
+
+            @Override
+            public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<LoginFailureEntity> sessionWrapper) {
+                return CrossDCMessageStatus.SYNC;
+            }
+
+        };
+
+        loginFailuresTx.addTask(key, createLoginFailureTask, entity);
+
         return wrap(key, entity);
     }
 
     @Override
     public void removeUserLoginFailure(RealmModel realm, String userId) {
-        tx.remove(loginFailureCache, new LoginFailureKey(realm.getId(), userId));
+        SessionUpdateTask<LoginFailureEntity> removeTask = new SessionUpdateTask<LoginFailureEntity>() {
+
+            @Override
+            public void runUpdate(LoginFailureEntity entity) {
+
+            }
+
+            @Override
+            public CacheOperation getOperation(LoginFailureEntity entity) {
+                return CacheOperation.REMOVE;
+            }
+
+            @Override
+            public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<LoginFailureEntity> sessionWrapper) {
+                return CrossDCMessageStatus.SYNC;
+            }
+
+        };
+
+        loginFailuresTx.addTask(new LoginFailureKey(realm.getId(), userId), removeTask);
     }
 
     @Override
@@ -543,9 +589,9 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
     private void removeAllLocalUserLoginFailuresEvent(String realmId) {
         FuturesHelper futures = new FuturesHelper();
 
-        Cache<LoginFailureKey, LoginFailureEntity> localCache = CacheDecorators.localCache(loginFailureCache);
+        Cache<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> localCache = CacheDecorators.localCache(loginFailureCache);
 
-        Cache<LoginFailureKey, LoginFailureEntity> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache);
+        Cache<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> localCacheStoreIgnore = CacheDecorators.skipCacheLoaders(localCache);
 
         localCacheStoreIgnore
                 .entrySet()
@@ -593,8 +639,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
         removeUserSessions(realm, user, true);
         removeUserSessions(realm, user, false);
 
-        loginFailureCache.remove(new LoginFailureKey(realm.getId(), user.getUsername()));
-        loginFailureCache.remove(new LoginFailureKey(realm.getId(), user.getEmail()));
+        removeUserLoginFailure(realm, user.getId());
     }
 
     @Override
@@ -602,7 +647,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
     }
 
     protected void removeUserSession(UserSessionEntity sessionEntity, boolean offline) {
-        InfinispanChangelogBasedTransaction<UserSessionEntity> tx = getTransaction(offline);
+        InfinispanChangelogBasedTransaction<String, UserSessionEntity> tx = getTransaction(offline);
 
         SessionUpdateTask<UserSessionEntity> removeTask = new SessionUpdateTask<UserSessionEntity>() {
 
@@ -626,17 +671,17 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
         tx.addTask(sessionEntity.getId(), removeTask);
     }
 
-    InfinispanKeycloakTransaction getTx() {
-        return tx;
+    InfinispanChangelogBasedTransaction<LoginFailureKey, LoginFailureEntity> getLoginFailuresTx() {
+        return loginFailuresTx;
     }
 
     UserSessionAdapter wrap(RealmModel realm, UserSessionEntity entity, boolean offline) {
-        InfinispanChangelogBasedTransaction<UserSessionEntity> tx = getTransaction(offline);
+        InfinispanChangelogBasedTransaction<String, UserSessionEntity> tx = getTransaction(offline);
         return entity != null ? new UserSessionAdapter(session, this, tx, realm, entity, offline) : null;
     }
 
     UserLoginFailureModel wrap(LoginFailureKey key, LoginFailureEntity entity) {
-        return entity != null ? new UserLoginFailureAdapter(this, loginFailureCache, key, entity) : null;
+        return entity != null ? new UserLoginFailureAdapter(this, key, entity) : null;
     }
 
     UserSessionEntity getUserSessionEntity(UserSessionModel userSession, boolean offline) {
@@ -739,7 +784,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
         entity.setLastSessionRefresh(userSession.getLastSessionRefresh());
 
 
-        InfinispanChangelogBasedTransaction<UserSessionEntity> tx = getTransaction(offline);
+        InfinispanChangelogBasedTransaction<String, UserSessionEntity> tx = getTransaction(offline);
 
         SessionUpdateTask importTask = new SessionUpdateTask<UserSessionEntity>() {
 
@@ -775,7 +820,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
 
 
     private AuthenticatedClientSessionAdapter importClientSession(UserSessionAdapter importedUserSession, AuthenticatedClientSessionModel clientSession,
-                                                                  InfinispanChangelogBasedTransaction<UserSessionEntity> updateTx) {
+                                                                  InfinispanChangelogBasedTransaction<String, UserSessionEntity> updateTx) {
         AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity();
 
         entity.setAction(clientSession.getAction());
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 489dd60..df30b4e 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
@@ -85,7 +85,7 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
         InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
         Cache<String, SessionEntityWrapper<UserSessionEntity>> cache = connections.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
         Cache<String, SessionEntityWrapper<UserSessionEntity>> offlineSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME);
-        Cache<LoginFailureKey, LoginFailureEntity> loginFailures = connections.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME);
+        Cache<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> loginFailures = connections.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME);
 
         return new InfinispanUserSessionProvider(session, remoteCacheInvoker, lastSessionRefreshStore, offlineLastSessionRefreshStore, cache, offlineSessionsCache, loginFailures);
     }
@@ -224,6 +224,11 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
         if (offlineSessionsRemoteCache) {
             offlineLastSessionRefreshStore = new LastSessionRefreshStoreFactory().createAndInit(session, offlineSessionsCache, true);
         }
+
+        Cache loginFailuresCache = ispn.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME);
+        boolean loginFailuresRemoteCache = checkRemoteCache(session, loginFailuresCache, (RealmModel realm) -> {
+            return realm.getMaxDeltaTimeSeconds() * 1000;
+        });
     }
 
     private boolean checkRemoteCache(KeycloakSession session, Cache ispnCache, RemoteCacheInvoker.MaxIdleTimeLoader maxIdleLoader) {
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheInvoker.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheInvoker.java
index 89fd215..8891469 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheInvoker.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheInvoker.java
@@ -55,13 +55,13 @@ public class RemoteCacheInvoker {
     }
 
 
-    public <S extends SessionEntity> void runTask(KeycloakSession kcSession, RealmModel realm, String cacheName, String key, SessionUpdateTask<S> task, SessionEntityWrapper<S> sessionWrapper) {
+    public <K, V extends SessionEntity> void runTask(KeycloakSession kcSession, RealmModel realm, String cacheName, K key, SessionUpdateTask<V> task, SessionEntityWrapper<V> sessionWrapper) {
         RemoteCacheContext context = remoteCaches.get(cacheName);
         if (context == null) {
             return;
         }
 
-        S session = sessionWrapper.getEntity();
+        V session = sessionWrapper.getEntity();
 
         SessionUpdateTask.CacheOperation operation = task.getOperation(session);
         SessionUpdateTask.CrossDCMessageStatus status = task.getCrossDCMessageStatus(sessionWrapper);
@@ -82,8 +82,8 @@ public class RemoteCacheInvoker {
     }
 
 
-    private <S extends SessionEntity> void runOnRemoteCache(RemoteCache remoteCache, long maxIdleMs, String key, SessionUpdateTask<S> task, SessionEntityWrapper<S> sessionWrapper) {
-        S session = sessionWrapper.getEntity();
+    private <K, V extends SessionEntity> void runOnRemoteCache(RemoteCache<K, V> remoteCache, long maxIdleMs, K key, SessionUpdateTask<V> task, SessionEntityWrapper<V> sessionWrapper) {
+        V session = sessionWrapper.getEntity();
         SessionUpdateTask.CacheOperation operation = task.getOperation(session);
 
         switch (operation) {
@@ -96,13 +96,16 @@ public class RemoteCacheInvoker {
                 break;
             case ADD_IF_ABSENT:
                 final int currentTime = Time.currentTime();
-                SessionEntity existing = (SessionEntity) remoteCache
+                SessionEntity existing = remoteCache
                         .withFlags(Flag.FORCE_RETURN_VALUE)
                         .putIfAbsent(key, session, -1, TimeUnit.MILLISECONDS, maxIdleMs, TimeUnit.MILLISECONDS);
                 if (existing != null) {
-                    throw new IllegalStateException("There is already existing value in cache for key " + key);
+                    logger.debugf("Existing entity in remote cache for key: %s . Will update it", key);
+
+                    replace(remoteCache, task.getLifespanMs(), maxIdleMs, key, task);
+                } else {
+                    sessionWrapper.putLocalMetadataNoteInt(UserSessionEntity.LAST_SESSION_REFRESH_REMOTE, currentTime);
                 }
-                sessionWrapper.putLocalMetadataNoteInt(UserSessionEntity.LAST_SESSION_REFRESH_REMOTE, currentTime);
                 break;
             case REPLACE:
                 replace(remoteCache, task.getLifespanMs(), maxIdleMs, key, task);
@@ -113,16 +116,16 @@ public class RemoteCacheInvoker {
     }
 
 
-    private <S extends SessionEntity> void replace(RemoteCache remoteCache, long lifespanMs, long maxIdleMs, String key, SessionUpdateTask<S> task) {
+    private <K, V extends SessionEntity> void replace(RemoteCache<K, V> remoteCache, long lifespanMs, long maxIdleMs, K key, SessionUpdateTask<V> task) {
         boolean replaced = false;
         while (!replaced) {
-            VersionedValue<S> versioned = remoteCache.getVersioned(key);
+            VersionedValue<V> versioned = remoteCache.getVersioned(key);
             if (versioned == null) {
                 logger.warnf("Not found entity to replace for key '%s'", key);
                 return;
             }
 
-            S session = versioned.getValue();
+            V session = versioned.getValue();
 
             // Run task on the remote session
             task.runUpdate(session);
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionListener.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionListener.java
index d29e220..b186833 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionListener.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionListener.java
@@ -44,12 +44,12 @@ import org.infinispan.client.hotrod.VersionedValue;
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
  */
 @ClientListener
-public class RemoteCacheSessionListener  {
+public class RemoteCacheSessionListener<K, V extends SessionEntity>  {
 
     protected static final Logger logger = Logger.getLogger(RemoteCacheSessionListener.class);
 
-    private Cache<String, SessionEntityWrapper> cache;
-    private RemoteCache remoteCache;
+    private Cache<K, SessionEntityWrapper<V>> cache;
+    private RemoteCache<K, V> remoteCache;
     private boolean distributed;
     private String myAddress;
 
@@ -58,7 +58,7 @@ public class RemoteCacheSessionListener  {
     }
 
 
-    protected void init(KeycloakSession session, Cache<String, SessionEntityWrapper> cache, RemoteCache remoteCache) {
+    protected void init(KeycloakSession session, Cache<K, SessionEntityWrapper<V>> cache, RemoteCache<K, V> remoteCache) {
         this.cache = cache;
         this.remoteCache = remoteCache;
 
@@ -73,7 +73,7 @@ public class RemoteCacheSessionListener  {
 
     @ClientCacheEntryCreated
     public void created(ClientCacheEntryCreatedEvent event) {
-        String key = (String) event.getKey();
+        K key = (K) event.getKey();
 
         if (shouldUpdateLocalCache(event.getType(), key, event.isCommandRetried())) {
             // Should load it from remoteStore
@@ -84,7 +84,7 @@ public class RemoteCacheSessionListener  {
 
     @ClientCacheEntryModified
     public void updated(ClientCacheEntryModifiedEvent event) {
-        String key = (String) event.getKey();
+        K key = (K) event.getKey();
 
         if (shouldUpdateLocalCache(event.getType(), key, event.isCommandRetried())) {
 
@@ -94,7 +94,7 @@ public class RemoteCacheSessionListener  {
 
     private static final int MAXIMUM_REPLACE_RETRIES = 10;
 
-    private void replaceRemoteEntityInCache(String key, long eventVersion) {
+    private void replaceRemoteEntityInCache(K key, long eventVersion) {
         // TODO can be optimized and remoteSession sent in the event itself?
         boolean replaced = false;
         int replaceRetries = 0;
@@ -102,8 +102,8 @@ public class RemoteCacheSessionListener  {
         do {
             replaceRetries++;
             
-            SessionEntityWrapper localEntityWrapper = cache.get(key);
-            VersionedValue remoteSessionVersioned = remoteCache.getVersioned(key);
+            SessionEntityWrapper<V> localEntityWrapper = cache.get(key);
+            VersionedValue<V> remoteSessionVersioned = remoteCache.getVersioned(key);
             if (remoteSessionVersioned == null || remoteSessionVersioned.getVersion() < eventVersion) {
                 try {
                     logger.debugf("Got replace remote entity event prematurely, will try again. Event version: %d, got: %d",
@@ -120,7 +120,7 @@ public class RemoteCacheSessionListener  {
 
             logger.debugf("Read session%s. Entity read from remote cache: %s", replaceRetries > 1 ? "" : " again", remoteSession);
 
-            SessionEntityWrapper sessionWrapper = remoteSession.mergeRemoteEntityWithLocalEntity(localEntityWrapper);
+            SessionEntityWrapper<V> sessionWrapper = remoteSession.mergeRemoteEntityWithLocalEntity(localEntityWrapper);
 
             // We received event from remoteCache, so we won't update it back
             replaced = cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD, Flag.IGNORE_RETURN_VALUES)
@@ -135,7 +135,7 @@ public class RemoteCacheSessionListener  {
 
     @ClientCacheEntryRemoved
     public void removed(ClientCacheEntryRemovedEvent event) {
-        String key = (String) event.getKey();
+        K key = (K) event.getKey();
 
         if (shouldUpdateLocalCache(event.getType(), key, event.isCommandRetried())) {
             // We received event from remoteCache, so we won't update it back
@@ -152,7 +152,7 @@ public class RemoteCacheSessionListener  {
 
 
     // For distributed caches, ensure that local modification is executed just on owner OR if event.isCommandRetried
-    protected boolean shouldUpdateLocalCache(ClientEvent.Type type, String key, boolean commandRetried) {
+    protected boolean shouldUpdateLocalCache(ClientEvent.Type type, K key, boolean commandRetried) {
         boolean result;
 
         // Case when cache is stopping or stopped already
@@ -184,7 +184,7 @@ public class RemoteCacheSessionListener  {
     }
 
 
-    public static RemoteCacheSessionListener createListener(KeycloakSession session, Cache<String, SessionEntityWrapper> cache, RemoteCache remoteCache) {
+    public static <K, V extends SessionEntity> RemoteCacheSessionListener createListener(KeycloakSession session, Cache<K, SessionEntityWrapper<V>> cache, RemoteCache<K, V> remoteCache) {
         /*boolean isCoordinator = InfinispanUtil.isCoordinator(cache);
 
         // Just cluster coordinator will fetch userSessions from remote cache.
@@ -198,7 +198,7 @@ public class RemoteCacheSessionListener  {
             listener = new DontFetchInitialStateCacheListener();
         }*/
 
-        RemoteCacheSessionListener listener = new RemoteCacheSessionListener();
+        RemoteCacheSessionListener<K, V> listener = new RemoteCacheSessionListener<>();
         listener.init(session, cache, remoteCache);
 
         return listener;
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionsLoader.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionsLoader.java
index 00c133a..789fc16 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionsLoader.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionsLoader.java
@@ -115,14 +115,14 @@ public class RemoteCacheSessionsLoader implements SessionLoader {
 
         for (Map.Entry<byte[], byte[]> entry : remoteObjects.entrySet()) {
             try {
-                String key = (String) marshaller.objectFromByteBuffer(entry.getKey());
+                Object key = marshaller.objectFromByteBuffer(entry.getKey());
                 SessionEntity entity = (SessionEntity) marshaller.objectFromByteBuffer(entry.getValue());
 
                 SessionEntityWrapper entityWrapper = new SessionEntityWrapper(entity);
 
                 decoratedCache.putAsync(key, entityWrapper);
             } catch (Exception e) {
-                log.warnf("Error loading session from remote cache", e);
+                log.warn("Error loading session from remote cache", e);
             }
         }
 
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java
index f75391c..815a54d 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java
@@ -48,7 +48,7 @@ public class Mappers {
         return new UserSessionEntityMapper();
     }
 
-    public static Function<Map.Entry<LoginFailureKey, LoginFailureEntity>, LoginFailureKey> loginFailureId() {
+    public static Function<Map.Entry<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>>, LoginFailureKey> loginFailureId() {
         return new LoginFailureIdMapper();
     }
 
@@ -103,9 +103,9 @@ public class Mappers {
 
     }
 
-    private static class LoginFailureIdMapper implements Function<Map.Entry<LoginFailureKey, LoginFailureEntity>, LoginFailureKey>, Serializable {
+    private static class LoginFailureIdMapper implements Function<Map.Entry<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>>, LoginFailureKey>, Serializable {
         @Override
-        public LoginFailureKey apply(Map.Entry<LoginFailureKey, LoginFailureEntity> entry) {
+        public LoginFailureKey apply(Map.Entry<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> entry) {
             return entry.getKey();
         }
     }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserLoginFailurePredicate.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserLoginFailurePredicate.java
index ae0b28d..4996000 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserLoginFailurePredicate.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserLoginFailurePredicate.java
@@ -17,6 +17,7 @@
 
 package org.keycloak.models.sessions.infinispan.stream;
 
+import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
 import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
 import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
 
@@ -27,7 +28,7 @@ import java.util.function.Predicate;
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
-public class UserLoginFailurePredicate implements Predicate<Map.Entry<LoginFailureKey, LoginFailureEntity>>, Serializable {
+public class UserLoginFailurePredicate implements Predicate<Map.Entry<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>>>, Serializable {
 
     private String realm;
 
@@ -40,8 +41,8 @@ public class UserLoginFailurePredicate implements Predicate<Map.Entry<LoginFailu
     }
 
     @Override
-    public boolean test(Map.Entry<LoginFailureKey, LoginFailureEntity> entry) {
-        LoginFailureEntity e = entry.getValue();
+    public boolean test(Map.Entry<LoginFailureKey, SessionEntityWrapper<LoginFailureEntity>> entry) {
+        LoginFailureEntity e = entry.getValue().getEntity();
         return realm.equals(e.getRealm());
     }
 
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserLoginFailureAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserLoginFailureAdapter.java
index a41777d..0971c26 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserLoginFailureAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserLoginFailureAdapter.java
@@ -17,8 +17,8 @@
 
 package org.keycloak.models.sessions.infinispan;
 
-import org.infinispan.Cache;
 import org.keycloak.models.UserLoginFailureModel;
+import org.keycloak.models.sessions.infinispan.changes.LoginFailuresUpdateTask;
 import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
 import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
 
@@ -28,13 +28,11 @@ import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
 public class UserLoginFailureAdapter implements UserLoginFailureModel {
 
     private InfinispanUserSessionProvider provider;
-    private Cache<LoginFailureKey, LoginFailureEntity> cache;
     private LoginFailureKey key;
     private LoginFailureEntity entity;
 
-    public UserLoginFailureAdapter(InfinispanUserSessionProvider provider, Cache<LoginFailureKey, LoginFailureEntity> cache, LoginFailureKey key, LoginFailureEntity entity) {
+    public UserLoginFailureAdapter(InfinispanUserSessionProvider provider, LoginFailureKey key, LoginFailureEntity entity) {
         this.provider = provider;
-        this.cache = cache;
         this.key = key;
         this.entity = entity;
     }
@@ -51,8 +49,16 @@ public class UserLoginFailureAdapter implements UserLoginFailureModel {
 
     @Override
     public void setFailedLoginNotBefore(int notBefore) {
-        entity.setFailedLoginNotBefore(notBefore);
-        update();
+        LoginFailuresUpdateTask task = new LoginFailuresUpdateTask() {
+
+            @Override
+            public void runUpdate(LoginFailureEntity entity) {
+                entity.setFailedLoginNotBefore(notBefore);
+            }
+
+        };
+
+        update(task);
     }
 
     @Override
@@ -62,14 +68,30 @@ public class UserLoginFailureAdapter implements UserLoginFailureModel {
 
     @Override
     public void incrementFailures() {
-        entity.setNumFailures(getNumFailures() + 1);
-        update();
+        LoginFailuresUpdateTask task = new LoginFailuresUpdateTask() {
+
+            @Override
+            public void runUpdate(LoginFailureEntity entity) {
+                entity.setNumFailures(entity.getNumFailures() + 1);
+            }
+
+        };
+
+        update(task);
     }
 
     @Override
     public void clearFailures() {
-        entity.clearFailures();
-        update();
+        LoginFailuresUpdateTask task = new LoginFailuresUpdateTask() {
+
+            @Override
+            public void runUpdate(LoginFailureEntity entity) {
+                entity.clearFailures();
+            }
+
+        };
+
+        update(task);
     }
 
     @Override
@@ -79,8 +101,16 @@ public class UserLoginFailureAdapter implements UserLoginFailureModel {
 
     @Override
     public void setLastFailure(long lastFailure) {
-        entity.setLastFailure(lastFailure);
-        update();
+        LoginFailuresUpdateTask task = new LoginFailuresUpdateTask() {
+
+            @Override
+            public void runUpdate(LoginFailureEntity entity) {
+                entity.setLastFailure(lastFailure);
+            }
+
+        };
+
+        update(task);
     }
 
     @Override
@@ -90,12 +120,20 @@ public class UserLoginFailureAdapter implements UserLoginFailureModel {
 
     @Override
     public void setLastIPFailure(String ip) {
-        entity.setLastIPFailure(ip);
-        update();
+        LoginFailuresUpdateTask task = new LoginFailuresUpdateTask() {
+
+            @Override
+            public void runUpdate(LoginFailureEntity entity) {
+                entity.setLastIPFailure(ip);
+            }
+
+        };
+
+        update(task);
     }
 
-    void update() {
-        provider.getTx().replace(cache, key, entity);
+    void update(LoginFailuresUpdateTask task) {
+        provider.getLoginFailuresTx().addTask(key, task);
     }
 
 }
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResource.java
index b00f2e4..7fd223f 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResource.java
@@ -146,7 +146,8 @@ public class RealmsAdminResource {
 
             return Response.created(location).build();
         } catch (ModelDuplicateException e) {
-            return ErrorResponse.exists("Realm with same name exists");
+            logger.error("Conflict detected", e);
+            return ErrorResponse.exists("Conflict detected. See logs for details");
         }
     }
 
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java
index 0312cbb..846267f 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java
@@ -462,26 +462,6 @@ public class UserSessionProviderTest {
     }
 
 
-    @Test
-    public void testFailCreateExistingSession() {
-        UserSessionModel userSession = session.sessions().createUserSession("123", realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null);
-
-        // commit
-        resetSession();
-
-
-        try {
-            session.sessions().createUserSession("123", realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null);
-            kc.stopSession(session, true);
-            Assert.fail("Not expected to successfully create duplicated userSession");
-        } catch (IllegalStateException e) {
-            // Expected
-            session = kc.startSession();
-        }
-
-    }
-
-
     private void testAuthenticatedClientSession(AuthenticatedClientSessionModel clientSession, String expectedClientId, String expectedUserSessionId, String expectedAction, int expectedTimestamp) {
         Assert.assertEquals(expectedClientId, clientSession.getClient().getClientId());
         Assert.assertEquals(expectedUserSessionId, clientSession.getUserSession().getId());
@@ -531,6 +511,15 @@ public class UserSessionProviderTest {
 
         resetSession();
 
+        // Add the failure, which already exists
+        failure1 = session.sessions().addUserLoginFailure(realm, "user1");
+        failure1.incrementFailures();
+
+        resetSession();
+
+        failure1 = session.sessions().getUserLoginFailure(realm, "user1");
+        assertEquals(2, failure1.getNumFailures());
+
         failure1 = session.sessions().getUserLoginFailure(realm, "user1");
         failure1.clearFailures();
 
@@ -556,13 +545,15 @@ public class UserSessionProviderTest {
     public void testOnUserRemoved() {
         createSessions();
 
-        session.sessions().addUserLoginFailure(realm, "user1");
-        session.sessions().addUserLoginFailure(realm, "user1@localhost");
-        session.sessions().addUserLoginFailure(realm, "user2");
+        UserModel user1 = session.users().getUserByUsername("user1", realm);
+        UserModel user2 = session.users().getUserByUsername("user2", realm);
+
+        session.sessions().addUserLoginFailure(realm, user1.getId());
+        session.sessions().addUserLoginFailure(realm, user2.getId());
 
         resetSession();
 
-        UserModel user1 = session.users().getUserByUsername("user1", realm);
+        user1 = session.users().getUserByUsername("user1", realm);
         new UserManager(session).removeUser(realm, user1);
 
         resetSession();
@@ -570,9 +561,8 @@ public class UserSessionProviderTest {
         assertTrue(session.sessions().getUserSessions(realm, user1).isEmpty());
         assertFalse(session.sessions().getUserSessions(realm, session.users().getUserByUsername("user2", realm)).isEmpty());
 
-        assertNull(session.sessions().getUserLoginFailure(realm, "user1"));
-        assertNull(session.sessions().getUserLoginFailure(realm, "user1@localhost"));
-        assertNotNull(session.sessions().getUserLoginFailure(realm, "user2"));
+        assertNull(session.sessions().getUserLoginFailure(realm, user1.getId()));
+        assertNotNull(session.sessions().getUserLoginFailure(realm, user2.getId()));
     }
 
     private AuthenticatedClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set<String> roles, Set<String> protocolMappers) {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/BruteForceCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/BruteForceCrossDCTest.java
new file mode 100644
index 0000000..ec572e9
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/BruteForceCrossDCTest.java
@@ -0,0 +1,231 @@
+/*
+ * 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.testsuite.crossdc;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+
+import javax.ws.rs.NotFoundException;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.models.Constants;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserLoginFailureModel;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.Retry;
+import org.keycloak.testsuite.client.KeycloakTestingClient;
+import org.keycloak.testsuite.util.ClientBuilder;
+import org.keycloak.testsuite.util.OAuthClient;
+import org.keycloak.testsuite.util.RealmBuilder;
+import org.keycloak.testsuite.util.UserBuilder;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class BruteForceCrossDCTest extends AbstractAdminCrossDCTest {
+
+    private static final String REALM_NAME = "brute-force-test";
+
+    @Before
+    public void beforeTest() {
+        try {
+            adminClient.realm(REALM_NAME).remove();
+        } catch (NotFoundException ignore) {
+        }
+
+        UserRepresentation user = UserBuilder.create()
+                .id("login-test-1")
+                .username("login-test-1")
+                .email("login-1@test.com")
+                .enabled(true)
+                .password("password")
+                .addRoles(Constants.OFFLINE_ACCESS_ROLE)
+                .build();
+
+        UserRepresentation user2 = UserBuilder.create()
+                .id("login-test-2")
+                .username("login-test-2")
+                .email("login-2@test.com")
+                .enabled(true)
+                .password("password")
+                .addRoles(Constants.OFFLINE_ACCESS_ROLE)
+                .build();
+
+        ClientRepresentation client = ClientBuilder.create()
+                .clientId("test-app")
+                .directAccessGrants()
+                .redirectUris("http://localhost:8180/auth/realms/master/app/*")
+                .addWebOrigin("http://localhost:8180")
+                .secret("password")
+                .build();
+
+        RealmRepresentation realmRep = RealmBuilder.create()
+                .name(REALM_NAME)
+                .user(user)
+                .user(user2)
+                .client(client)
+                .bruteForceProtected(true)
+                .build();
+
+        adminClient.realms().create(realmRep);
+    }
+
+
+    @Test
+    public void testBruteForceWithUserOperations() throws Exception {
+        // Enable 1st DC only
+        enableDcOnLoadBalancer(DC.FIRST);
+        enableDcOnLoadBalancer(DC.SECOND);
+
+        // Clear all
+        adminClient.realms().realm(REALM_NAME).attackDetection().clearAllBruteForce();
+        assertStatistics("After brute force cleared", 0, 0, 0);
+
+        // Create 10 brute force statuses for user1. Assert available on both DC1 and DC2
+        createBruteForceFailures(10, "login-test-1");
+        assertStatistics("After brute force for user1 created", 10, 0, 1);
+
+        // Create 10 brute force statuses for user2. Assert available on both DC1 and DC2createBruteForceFailures(10, "login-test-2");createBruteForceFailures(10, "login-test-2");
+        createBruteForceFailures(10, "login-test-2");
+        assertStatistics("After brute force for user2 created", 10, 10, 2);
+
+        // Remove brute force for user1
+        adminClient.realms().realm(REALM_NAME).attackDetection().clearBruteForceForUser("login-test-1");
+        assertStatistics("After brute force for user1 cleared", 0, 10, 1);
+
+        // Re-add 10 brute force statuses for user1
+        createBruteForceFailures(10, "login-test-1");
+        assertStatistics("After brute force for user1 re-created", 10, 10, 2);
+
+        // Remove user1
+        adminClient.realms().realm(REALM_NAME).users().get("login-test-1").remove();
+        assertStatistics("After user1 removed", 0, 10, 1);
+    }
+
+
+    @Test
+    public void testBruteForceWithRealmOperations() throws Exception {
+        // Enable 1st DC only
+        enableDcOnLoadBalancer(DC.FIRST);
+        enableDcOnLoadBalancer(DC.SECOND);
+
+        // Clear all
+        adminClient.realms().realm(REALM_NAME).attackDetection().clearAllBruteForce();
+        assertStatistics("After brute force cleared", 0, 0, 0);
+
+        // Create 10 brute force statuses for user1 and user2.
+        createBruteForceFailures(10, "login-test-1");
+        createBruteForceFailures(10, "login-test-2");
+        assertStatistics("After brute force for users created", 10, 10, 2);
+
+        // Clear all
+        adminClient.realms().realm(REALM_NAME).attackDetection().clearAllBruteForce();
+        assertStatistics("After brute force cleared for realm", 0, 0, 0);
+
+        // Re-add 10 brute force statuses for users
+        createBruteForceFailures(10, "login-test-1");
+        createBruteForceFailures(10, "login-test-2");
+        assertStatistics("After brute force for users re-created", 10, 10, 2);
+
+        // Remove realm
+        adminClient.realms().realm(REALM_NAME).remove();
+
+        Retry.execute(() -> {
+            int dc0CacheSize = getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME).size();
+            int dc1CacheSize = getTestingClientForStartedNodeInDc(1).testing().cache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME).size();
+            Assert.assertEquals(0, dc0CacheSize);
+            Assert.assertEquals(0, dc1CacheSize);
+        }, 50, 50);
+
+    }
+
+
+    @Test
+    public void testDuplicatedPutIfAbsentOperation() throws Exception {
+        // Enable 1st DC only
+        enableDcOnLoadBalancer(DC.FIRST);
+        enableDcOnLoadBalancer(DC.SECOND);
+
+        // Clear all
+        adminClient.realms().realm(REALM_NAME).attackDetection().clearAllBruteForce();
+        assertStatistics("After brute force cleared", 0, 0, 0);
+
+        // create the entry manually in DC0
+        addUserLoginFailure(getTestingClientForStartedNodeInDc(0));
+        assertStatistics("After create entry1", 1, 0, 1);
+
+        // try to create the entry manually in DC1 (not use real concurrency for now). It should still update the numFailures in existing entry rather then override it
+        addUserLoginFailure(getTestingClientForStartedNodeInDc(1));
+        assertStatistics("After create entry2", 2, 0, 1);
+
+    }
+
+
+    private void assertStatistics(String prefixMessage, int expectedUser1, int expectedUser2, int expectedCacheSize) {
+        Retry.execute(() -> {
+            int dc0user1 = (Integer) getAdminClientForStartedNodeInDc(0).realm(REALM_NAME).attackDetection().bruteForceUserStatus("login-test-1").get("numFailures");
+            int dc1user1 = (Integer) getAdminClientForStartedNodeInDc(1).realm(REALM_NAME).attackDetection().bruteForceUserStatus("login-test-1").get("numFailures");
+            int dc0user2 = (Integer) getAdminClientForStartedNodeInDc(0).realm(REALM_NAME).attackDetection().bruteForceUserStatus("login-test-2").get("numFailures");
+            int dc1user2 = (Integer) getAdminClientForStartedNodeInDc(1).realm(REALM_NAME).attackDetection().bruteForceUserStatus("login-test-2").get("numFailures");
+
+            int dc0CacheSize = getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME).size();
+            int dc1CacheSize = getTestingClientForStartedNodeInDc(1).testing().cache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME).size();
+
+            log.infof("%s: dc0User1=%d, dc0user2=%d, dc1user1=%d, dc1user2=%d, dc0CacheSize=%d, dc1CacheSize=%d", prefixMessage, dc0user1, dc0user2, dc1user1, dc1user2, dc0CacheSize, dc1CacheSize);
+
+            Assert.assertEquals(dc0user1, expectedUser1);
+            Assert.assertEquals(dc0user2, expectedUser2);
+            Assert.assertEquals(dc1user1, expectedUser1);
+            Assert.assertEquals(dc1user2, expectedUser2);
+
+            Assert.assertEquals(expectedCacheSize, dc0CacheSize);
+            Assert.assertEquals(expectedCacheSize, dc1CacheSize);
+        }, 50, 50);
+    }
+
+
+
+
+
+    private void createBruteForceFailures(int count, String username) throws Exception {
+        oauth.realm(REALM_NAME);
+
+        for (int i=0 ; i<count ; i++) {
+            OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", username, "bad-password");
+            Assert.assertNull(response.getAccessToken());
+            Assert.assertNotNull(response.getError());
+        }
+
+    }
+
+
+    // TODO Having this working on Wildfly might be a challenge. Maybe require @Deployment with @TargetsContainer descriptor generated at runtime as we don't know the container qualifier at compile time... Maybe workaround by add endpoint to TestingResourceProvider if needed..
+    private void addUserLoginFailure(KeycloakTestingClient testingClient) throws URISyntaxException, IOException {
+        testingClient.server().run(session -> {
+            RealmModel realm = session.realms().getRealmByName(REALM_NAME);
+            UserLoginFailureModel loginFailure = session.sessions().addUserLoginFailure(realm, "login-test-1");
+            loginFailure.incrementFailures();
+        });
+    }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/manual/SessionsPreloadCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/manual/SessionsPreloadCrossDCTest.java
index a73d283..80949cf 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/manual/SessionsPreloadCrossDCTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/manual/SessionsPreloadCrossDCTest.java
@@ -23,7 +23,9 @@ import java.util.List;
 import org.junit.Test;
 import org.keycloak.OAuth2Constants;
 import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.admin.ApiUtil;
 import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
 import org.keycloak.testsuite.crossdc.AbstractAdminCrossDCTest;
 import org.keycloak.testsuite.crossdc.DC;
@@ -187,6 +189,52 @@ public class SessionsPreloadCrossDCTest extends AbstractAdminCrossDCTest {
     }
 
 
+    @Test
+    public void loginFailuresPreloadTest() throws Exception {
+        // Enable brute force protector
+        RealmRepresentation realmRep = getAdminClientForStartedNodeInDc(0).realms().realm("test").toRepresentation();
+        realmRep.setBruteForceProtected(true);
+        getAdminClientForStartedNodeInDc(0).realms().realm("test").update(realmRep);
+
+        String userId = ApiUtil.findUserByUsername(getAdminClientForStartedNodeInDc(0).realms().realm("test"), "test-user@localhost").getId();
+
+        int loginFailuresBefore = (Integer) getAdminClientForStartedNodeInDc(0).realm("test").attackDetection().bruteForceUserStatus(userId).get("numFailures");
+        log.infof("loginFailuresBefore: %d", loginFailuresBefore);
+
+        // Create initial brute force records
+        for (int i=0 ; i<SESSIONS_COUNT ; i++) {
+            OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", "test-user@localhost", "bad-password");
+            Assert.assertNull(response.getAccessToken());
+            Assert.assertNotNull(response.getError());
+        }
+
+        // Start 2nd DC.
+        containerController.start(getCacheServer(DC.SECOND).getQualifier());
+        startBackendNode(DC.SECOND, 0);
+        enableLoadBalancerNode(DC.SECOND, 0);
+
+        // Ensure loginFailures are loaded in both 1st DC and 2nd DC
+        int size1 = getTestingClientForStartedNodeInDc(0).testing().cache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME).size();
+        int size2 = getTestingClientForStartedNodeInDc(1).testing().cache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME).size();
+        int loginFailures1 = (Integer) getAdminClientForStartedNodeInDc(0).realm("test").attackDetection().bruteForceUserStatus(userId).get("numFailures");
+        int loginFailures2 = (Integer) getAdminClientForStartedNodeInDc(1).realm("test").attackDetection().bruteForceUserStatus(userId).get("numFailures");
+        log.infof("size1: %d, size2: %d, loginFailures1: %d, loginFailures2: %d", size1, size2, loginFailures1, loginFailures2);
+        Assert.assertEquals(size1, 1);
+        Assert.assertEquals(size2, 1);
+        Assert.assertEquals(loginFailures1, loginFailuresBefore + SESSIONS_COUNT);
+        Assert.assertEquals(loginFailures2, loginFailuresBefore + SESSIONS_COUNT);
+
+        // On DC2 sessions were preloaded from from remoteCache
+        Assert.assertTrue(getTestingClientForStartedNodeInDc(1).testing().cache(InfinispanConnectionProvider.WORK_CACHE_NAME).contains("distributed::remoteCacheLoad::loginFailures"));
+
+        // Disable brute force protector
+        realmRep = getAdminClientForStartedNodeInDc(0).realms().realm("test").toRepresentation();
+        realmRep.setBruteForceProtected(true);
+        getAdminClientForStartedNodeInDc(0).realms().realm("test").update(realmRep);
+    }
+
+
+
     private List<OAuthClient.AccessTokenResponse> createInitialSessions(boolean offline) throws Exception {
         if (offline) {
             oauth.scope(OAuth2Constants.OFFLINE_ACCESS);