KEYCLOAK-9988 Fix unstable UserSessionPersisterOfflineTest.testExpired. Adding ResetTimeOffsetEvent

This commit is contained in:
mposolda 2019-04-15 22:43:23 +02:00 committed by Hynek Mlnařík
parent 4272495d24
commit 7a671052a3
8 changed files with 86 additions and 11 deletions

View file

@ -58,6 +58,7 @@ import org.keycloak.models.sessions.infinispan.util.InfinispanKeyGenerator;
import org.keycloak.models.sessions.infinispan.util.InfinispanUtil;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.PostMigrationEvent;
import org.keycloak.models.utils.ResetTimeOffsetEvent;
import org.keycloak.provider.ProviderEvent;
import org.keycloak.provider.ProviderEventListener;
@ -133,6 +134,16 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
InfinispanUserSessionProvider provider = (InfinispanUserSessionProvider) userRemovedEvent.getKeycloakSession().getProvider(UserSessionProvider.class, getId());
provider.onUserRemoved(userRemovedEvent.getRealm(), userRemovedEvent.getUser());
} else if (event instanceof ResetTimeOffsetEvent) {
if (persisterLastSessionRefreshStore != null) {
persisterLastSessionRefreshStore.reset();
}
if (lastSessionRefreshStore != null) {
lastSessionRefreshStore.reset();
}
if (offlineLastSessionRefreshStore != null) {
offlineLastSessionRefreshStore.reset();
}
}
}
});

View file

@ -54,7 +54,7 @@ public abstract class AbstractLastSessionRefreshStore {
void checkSendingMessage(KeycloakSession kcSession, int currentTime) {
if (lastSessionRefreshes.size() >= maxCount || lastRun + maxIntervalBetweenMessagesSeconds <= currentTime) {
Map<String, SessionData> refreshesToSend = prepareSendingMessage(currentTime);
Map<String, SessionData> refreshesToSend = prepareSendingMessage();
// Sending message doesn't need to be synchronized
if (refreshesToSend != null) {
@ -65,7 +65,9 @@ public abstract class AbstractLastSessionRefreshStore {
// synchronized manipulation with internal object instances. Will return map if message should be sent. Otherwise return null
private synchronized Map<String, SessionData> prepareSendingMessage(int currentTime) {
private synchronized Map<String, SessionData> prepareSendingMessage() {
// Safer to retrieve currentTime to avoid race conditions during testsuite
int currentTime = Time.currentTime();
if (lastSessionRefreshes.size() >= maxCount || lastRun + maxIntervalBetweenMessagesSeconds <= currentTime) {
// Create new map instance, so that new writers will use that one
Map<String, SessionData> copiedRefreshesToSend = lastSessionRefreshes;
@ -79,6 +81,12 @@ public abstract class AbstractLastSessionRefreshStore {
}
public synchronized void reset() {
lastRun = Time.currentTime();
lastSessionRefreshes = new ConcurrentHashMap<>();
}
/**
* Bulk update the underlying store with all the user sessions, which were refreshed by Keycloak since the last call of this method
*

View file

@ -0,0 +1,28 @@
/*
* 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.utils;
import org.keycloak.provider.ProviderEvent;
/**
* Useful when there is a need for callback when time offset is restarted. Time offset is typically used in testsuite only
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ResetTimeOffsetEvent implements ProviderEvent {
}

View file

@ -47,6 +47,7 @@ import org.keycloak.models.UserProvider;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.sessions.infinispan.changes.sessions.CrossDCLastSessionRefreshStoreFactory;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.ResetTimeOffsetEvent;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.mappers.AudienceProtocolMapper;
import org.keycloak.provider.ProviderFactory;
@ -207,6 +208,12 @@ public class TestingResourceProvider implements RealmResourceProvider {
public Map<String, String> setTimeOffset(Map<String, String> time) {
int offset = Integer.parseInt(time.get("offset"));
Time.setOffset(offset);
// Time offset was restarted
if (offset == 0) {
session.getKeycloakSessionFactory().publish(new ResetTimeOffsetEvent());
}
return getTimeOffset();
}

View file

@ -27,6 +27,7 @@ import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.common.util.Time;
import org.keycloak.models.*;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ResetTimeOffsetEvent;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.managers.ClientManager;
import org.keycloak.services.managers.RealmManager;
@ -239,6 +240,7 @@ public class AuthenticationSessionProviderTest extends AbstractTestRealmKeycloak
} finally {
Time.setOffset(0);
session.getKeycloakSessionFactory().publish(new ResetTimeOffsetEvent());
setAccessCodeLifespan(mainSession, 60, 300, 1800);
}
});

View file

@ -26,6 +26,7 @@ import org.keycloak.common.util.Time;
import org.keycloak.models.*;
import org.keycloak.models.session.UserSessionPersisterProvider;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ResetTimeOffsetEvent;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.managers.ClientManager;
@ -521,6 +522,7 @@ public class UserSessionPersisterProviderTest extends AbstractTestRealmKeycloakT
} finally {
// Cleanup
Time.setOffset(0);
session.getKeycloakSessionFactory().publish(new ResetTimeOffsetEvent());
}
});
}

View file

@ -30,6 +30,7 @@ import org.keycloak.models.*;
import org.keycloak.models.session.UserSessionPersisterProvider;
import org.keycloak.models.sessions.infinispan.changes.sessions.PersisterLastSessionRefreshStoreFactory;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ResetTimeOffsetEvent;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.managers.ClientManager;
@ -438,6 +439,7 @@ public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTes
// Suspend periodic tasks to avoid race-conditions, which may cause missing updates of lastSessionRefresh times to UserSessionPersisterProvider
TimerProvider timer = session.getProvider(TimerProvider.class);
TimerProvider.TimerTaskContext timerTaskCtx = timer.cancelTask(PersisterLastSessionRefreshStoreFactory.DB_LSR_PERIODIC_TASK_NAME);
log.info("Cancelled periodic task " + PersisterLastSessionRefreshStoreFactory.DB_LSR_PERIODIC_TASK_NAME);
try {
AtomicReference<UserSessionModel[]> origSessionsAt = new AtomicReference<>();
@ -474,6 +476,8 @@ public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTes
}
});
log.info("Persisted 3 sessions to UserSessionPersisterProvider");
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionExpired3) -> {
currentSession = sessionExpired3;
realm = currentSession.realms().getRealm("test");
@ -487,22 +491,29 @@ public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTes
Assert.assertEquals(3, persister.getUserSessionsCount(true));
Time.setOffset(300);
log.infof("Set time offset to 300. Time is: %d", Time.currentTime());
// Set lastSessionRefresh to currentSession[0] to 0
session0.setLastSessionRefresh(Time.currentTime());
});
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionExpired4) -> {
currentSession = sessionExpired4;
realm = currentSession.realms().getRealm("test");
UserSessionModel[] origSessions = origSessionsAt.get();
// Increase timeOffset - 20 days
Time.setOffset(1728000);
// Increase timeOffset and update LSR of the session two times - first to 20 days and then to 21 days. At least one of updates
// will propagate to PersisterLastSessionRefreshStore and update DB (Single update is not 100% sure as there is still a
// chance of delayed periodic task to be run in the meantime and causing race-condition, which would mean LSR not updated in the DB)
for (int i=0 ; i<2 ; i++) {
int timeOffset = 1728000 + (i * 86400);
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionExpired4) -> {
currentSession = sessionExpired4;
realm = currentSession.realms().getRealm("test");
UserSessionModel[] origSessions = origSessionsAt.get();
Time.setOffset(timeOffset);
log.infof("Set time offset to %d. Time is: %d", timeOffset, Time.currentTime());
UserSessionModel session0 = currentSession.sessions().getOfflineUserSession(realm, origSessions[0].getId());
session0.setLastSessionRefresh(Time.currentTime());
});
UserSessionModel session0 = currentSession.sessions().getOfflineUserSession(realm, origSessions[0].getId());
session0.setLastSessionRefresh(Time.currentTime());
});
}
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionExpired5) -> {
currentSession = sessionExpired5;
@ -511,6 +522,7 @@ public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTes
// Increase timeOffset - 40 days
Time.setOffset(3456000);
log.infof("Set time offset to 3456000. Time is: %d", Time.currentTime());
// Expire and ensure that all sessions despite session0 were removed
currentSession.sessions().removeExpired(realm);
@ -552,6 +564,7 @@ public class UserSessionProviderOfflineTest extends AbstractTestRealmKeycloakTes
} finally {
Time.setOffset(0);
session.getKeycloakSessionFactory().publish(new ResetTimeOffsetEvent());
timer.schedule(timerTaskCtx.getRunnable(), timerTaskCtx.getIntervalMillis(), PersisterLastSessionRefreshStoreFactory.DB_LSR_PERIODIC_TASK_NAME);
}
}

View file

@ -29,6 +29,7 @@ import org.keycloak.common.util.Time;
import org.keycloak.models.*;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ResetTimeOffsetEvent;
import org.keycloak.models.utils.SessionTimeoutHelper;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.idm.RealmRepresentation;
@ -380,6 +381,7 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest {
}
} finally {
Time.setOffset(0);
session.getKeycloakSessionFactory().publish(new ResetTimeOffsetEvent());
}
}
@ -457,6 +459,7 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest {
} finally {
Time.setOffset(0);
session.getKeycloakSessionFactory().publish(new ResetTimeOffsetEvent());
// restore the original remember-me timeout values in the realm.
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession kcSession) -> {
RealmModel r = kcSession.realms().getRealmByName("test");
@ -481,6 +484,7 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest {
session.sessions().removeUserSession(realm, userSession);
} finally {
Time.setOffset(0);
session.getKeycloakSessionFactory().publish(new ResetTimeOffsetEvent());
}
}