Skip to content

Fix cleanUpExpiredSessions operation in JdbcOperationsSessionRepository #613

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1080,6 +1080,7 @@ However, you can override the default `ConversionService` by providing a Bean na

By default, this implementation uses `SPRING_SESSION` and `SPRING_SESSION_ATTRIBUTES` tables to store sessions.
Note that the table name can be easily customized as already described. In that case the table used to store attributes will be named using the provided table name, suffixed with `_ATTRIBUTES`.
If further customizations are needed, SQL queries used by the repository can be customized using `set*Query` setter methods. In this case you need to manually configure the `sessionRepository` bean.

Due to the differences between the various database vendors, especially when it comes to storing binary data, make sure to use SQL script specific to your database.
Scripts for most major database vendors are packaged as `org/springframework/session/jdbc/schema-\*.sql`, where `*` is the target database type.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

import javax.sql.DataSource;

Expand Down Expand Up @@ -505,9 +506,7 @@ public void findByChangedSecurityPrincipalNameReload() throws Exception {
}

@Test
public void cleanupCleansUpInactiveSessions() {
this.repository.setDefaultMaxInactiveInterval(1800); // 30 minutes

public void cleanupInactiveSessionsUsingRepositoryDefinedInterval() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository
.createSession();

Expand All @@ -517,32 +516,50 @@ public void cleanupCleansUpInactiveSessions() {

this.repository.cleanUpExpiredSessions();

// verify its not cleaned up immediately after creation
assertThat(this.repository.getSession(session.getId())).isNotNull();

long now = System.currentTimeMillis();
long tenMinutesInMilliseconds = 1000 * 60 * 10;
long thirtyMinutesInMilliseconds = 1000 * 60 * 30;

// set the last accessed time to 10 minutes in the past, session should not get
// cleaned up because it has not been inactive for 30 minutes yet
long tenMinutesInThePast = now - tenMinutesInMilliseconds;
session.setLastAccessedTime(tenMinutesInThePast);
session.setLastAccessedTime(now - TimeUnit.MINUTES.toMillis(10));
this.repository.save(session);
this.repository.cleanUpExpiredSessions();

assertThat(this.repository.getSession(session.getId())).isNotNull();

session.setLastAccessedTime(now - TimeUnit.MINUTES.toMillis(30));
this.repository.save(session);
this.repository.cleanUpExpiredSessions();

assertThat(this.repository.getSession(session.getId())).isNull();
}

// gh-580
@Test
public void cleanupInactiveSessionsUsingSessionDefinedInterval() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository
.createSession();
session.setMaxInactiveIntervalInSeconds((int) TimeUnit.MINUTES.toSeconds(45));

this.repository.save(session);

assertThat(this.repository.getSession(session.getId())).isNotNull();

this.repository.cleanUpExpiredSessions();

// session should still exist
assertThat(this.repository.getSession(session.getId())).isNotNull();

// set the last accessed time to 30 minutes in the past, session should get
// cleaned up as it has been inactive for 30 minutes
long thirtyMinutesInThePast = now - thirtyMinutesInMilliseconds;
session.setLastAccessedTime(thirtyMinutesInThePast);
long now = System.currentTimeMillis();

session.setLastAccessedTime(now - TimeUnit.MINUTES.toMillis(40));
this.repository.save(session);
this.repository.cleanUpExpiredSessions();

assertThat(this.repository.getSession(session.getId())).isNotNull();

session.setLastAccessedTime(now - TimeUnit.MINUTES.toMillis(50));
this.repository.save(session);
this.repository.cleanUpExpiredSessions();
// session should have been cleaned up

assertThat(this.repository.getSession(session.getId())).isNull();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -177,7 +176,7 @@ public class JdbcOperationsSessionRepository implements

private static final String DELETE_SESSIONS_BY_LAST_ACCESS_TIME_QUERY =
"DELETE FROM %TABLE_NAME% " +
"WHERE LAST_ACCESS_TIME < ?";
"WHERE LAST_ACCESS_TIME < ? - MAX_INACTIVE_INTERVAL * 1000";

private static final Log logger = LogFactory
.getLog(JdbcOperationsSessionRepository.class);
Expand All @@ -196,6 +195,24 @@ public class JdbcOperationsSessionRepository implements
*/
private String tableName = DEFAULT_TABLE_NAME;

private String createSessionQuery;

private String createSessionAttributeQuery;

private String getSessionQuery;

private String updateSessionQuery;

private String updateSessionAttributeQuery;

private String deleteSessionAttributeQuery;

private String deleteSessionQuery;

private String listSessionsByPrincipalNameQuery;

private String deleteSessionsByLastAccessTimeQuery;

/**
* If non-null, this value is used to override the default value for
* {@link JdbcSession#setMaxInactiveIntervalInSeconds(int)}.
Expand Down Expand Up @@ -229,6 +246,7 @@ public JdbcOperationsSessionRepository(JdbcOperations jdbcOperations,
this.jdbcOperations = jdbcOperations;
this.transactionOperations = createTransactionTemplate(transactionManager);
this.conversionService = createDefaultConversionService();
prepareQueries();
}

/**
Expand All @@ -238,6 +256,88 @@ public JdbcOperationsSessionRepository(JdbcOperations jdbcOperations,
public void setTableName(String tableName) {
Assert.hasText(tableName, "Table name must not be empty");
this.tableName = tableName.trim();
prepareQueries();
}

/**
* Set the custom SQL query used to create the session.
* @param createSessionQuery the SQL query string
*/
public void setCreateSessionQuery(String createSessionQuery) {
Assert.hasText(createSessionQuery, "Query must not be empty");
this.createSessionQuery = createSessionQuery;
}

/**
* Set the custom SQL query used to create the session attribute.
* @param createSessionAttributeQuery the SQL query string
*/
public void setCreateSessionAttributeQuery(String createSessionAttributeQuery) {
Assert.hasText(createSessionAttributeQuery, "Query must not be empty");
this.createSessionAttributeQuery = createSessionAttributeQuery;
}

/**
* Set the custom SQL query used to retrieve the session.
* @param getSessionQuery the SQL query string
*/
public void setGetSessionQuery(String getSessionQuery) {
Assert.hasText(getSessionQuery, "Query must not be empty");
this.getSessionQuery = getSessionQuery;
}

/**
* Set the custom SQL query used to update the session.
* @param updateSessionQuery the SQL query string
*/
public void setUpdateSessionQuery(String updateSessionQuery) {
Assert.hasText(updateSessionQuery, "Query must not be empty");
this.updateSessionQuery = updateSessionQuery;
}

/**
* Set the custom SQL query used to update the session attribute.
* @param updateSessionAttributeQuery the SQL query string
*/
public void setUpdateSessionAttributeQuery(String updateSessionAttributeQuery) {
Assert.hasText(updateSessionAttributeQuery, "Query must not be empty");
this.updateSessionAttributeQuery = updateSessionAttributeQuery;
}

/**
* Set the custom SQL query used to delete the session attribute.
* @param deleteSessionAttributeQuery the SQL query string
*/
public void setDeleteSessionAttributeQuery(String deleteSessionAttributeQuery) {
Assert.hasText(deleteSessionAttributeQuery, "Query must not be empty");
this.deleteSessionAttributeQuery = deleteSessionAttributeQuery;
}

/**
* Set the custom SQL query used to delete the session.
* @param deleteSessionQuery the SQL query string
*/
public void setDeleteSessionQuery(String deleteSessionQuery) {
Assert.hasText(deleteSessionQuery, "Query must not be empty");
this.deleteSessionQuery = deleteSessionQuery;
}

/**
* Set the custom SQL query used to retrieve the sessions by principal name.
* @param listSessionsByPrincipalNameQuery the SQL query string
*/
public void setListSessionsByPrincipalNameQuery(String listSessionsByPrincipalNameQuery) {
Assert.hasText(listSessionsByPrincipalNameQuery, "Query must not be empty");
this.listSessionsByPrincipalNameQuery = listSessionsByPrincipalNameQuery;
}

/**
* Set the custom SQL query used to delete the sessions by last access time.
* @param deleteSessionsByLastAccessTimeQuery the SQL query string
*/
public void setDeleteSessionsByLastAccessTimeQuery(String deleteSessionsByLastAccessTimeQuery) {
Assert.hasText(deleteSessionsByLastAccessTimeQuery, "Query must not be empty");
this.deleteSessionsByLastAccessTimeQuery = deleteSessionsByLastAccessTimeQuery;
}

/**
Expand Down Expand Up @@ -278,7 +378,7 @@ public void save(final JdbcSession session) {

protected void doInTransactionWithoutResult(TransactionStatus status) {
JdbcOperationsSessionRepository.this.jdbcOperations.update(
getQuery(CREATE_SESSION_QUERY),
JdbcOperationsSessionRepository.this.createSessionQuery,
new PreparedStatementSetter() {

public void setValues(PreparedStatement ps) throws SQLException {
Expand All @@ -293,7 +393,7 @@ public void setValues(PreparedStatement ps) throws SQLException {
if (!session.getAttributeNames().isEmpty()) {
final List<String> attributeNames = new ArrayList<String>(session.getAttributeNames());
JdbcOperationsSessionRepository.this.jdbcOperations.batchUpdate(
getQuery(CREATE_SESSION_ATTRIBUTE_QUERY),
JdbcOperationsSessionRepository.this.createSessionAttributeQuery,
new BatchPreparedStatementSetter() {

public void setValues(PreparedStatement ps, int i) throws SQLException {
Expand All @@ -319,7 +419,7 @@ public int getBatchSize() {
protected void doInTransactionWithoutResult(TransactionStatus status) {
if (session.isChanged()) {
JdbcOperationsSessionRepository.this.jdbcOperations.update(
getQuery(UPDATE_SESSION_QUERY),
JdbcOperationsSessionRepository.this.updateSessionQuery,
new PreparedStatementSetter() {

public void setValues(PreparedStatement ps)
Expand All @@ -337,7 +437,7 @@ public void setValues(PreparedStatement ps)
for (final Map.Entry<String, Object> entry : delta.entrySet()) {
if (entry.getValue() == null) {
JdbcOperationsSessionRepository.this.jdbcOperations.update(
getQuery(DELETE_SESSION_ATTRIBUTE_QUERY),
JdbcOperationsSessionRepository.this.deleteSessionAttributeQuery,
new PreparedStatementSetter() {

public void setValues(PreparedStatement ps) throws SQLException {
Expand All @@ -349,7 +449,7 @@ public void setValues(PreparedStatement ps) throws SQLException {
}
else {
int updatedCount = JdbcOperationsSessionRepository.this.jdbcOperations.update(
getQuery(UPDATE_SESSION_ATTRIBUTE_QUERY),
JdbcOperationsSessionRepository.this.updateSessionAttributeQuery,
new PreparedStatementSetter() {

public void setValues(PreparedStatement ps) throws SQLException {
Expand All @@ -361,7 +461,7 @@ public void setValues(PreparedStatement ps) throws SQLException {
});
if (updatedCount == 0) {
JdbcOperationsSessionRepository.this.jdbcOperations.update(
getQuery(CREATE_SESSION_ATTRIBUTE_QUERY),
JdbcOperationsSessionRepository.this.createSessionAttributeQuery,
new PreparedStatementSetter() {

public void setValues(PreparedStatement ps) throws SQLException {
Expand All @@ -387,7 +487,7 @@ public JdbcSession getSession(final String id) {

public ExpiringSession doInTransaction(TransactionStatus status) {
List<ExpiringSession> sessions = JdbcOperationsSessionRepository.this.jdbcOperations.query(
getQuery(GET_SESSION_QUERY),
JdbcOperationsSessionRepository.this.getSessionQuery,
new PreparedStatementSetter() {

public void setValues(PreparedStatement ps) throws SQLException {
Expand Down Expand Up @@ -421,7 +521,7 @@ public void delete(final String id) {

protected void doInTransactionWithoutResult(TransactionStatus status) {
JdbcOperationsSessionRepository.this.jdbcOperations.update(
getQuery(DELETE_SESSION_QUERY), id);
JdbcOperationsSessionRepository.this.deleteSessionQuery, id);
}

});
Expand All @@ -437,7 +537,7 @@ public Map<String, JdbcSession> findByIndexNameAndIndexValue(String indexName,

public List<ExpiringSession> doInTransaction(TransactionStatus status) {
return JdbcOperationsSessionRepository.this.jdbcOperations.query(
getQuery(LIST_SESSIONS_BY_PRINCIPAL_NAME_QUERY),
JdbcOperationsSessionRepository.this.listSessionsByPrincipalNameQuery,
new PreparedStatementSetter() {

public void setValues(PreparedStatement ps) throws SQLException {
Expand All @@ -463,24 +563,12 @@ public void setValues(PreparedStatement ps) throws SQLException {

@Scheduled(cron = "0 * * * * *")
public void cleanUpExpiredSessions() {
long now = System.currentTimeMillis();
long maxInactiveIntervalSeconds = (this.defaultMaxInactiveInterval != null)
? this.defaultMaxInactiveInterval
: MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;

final long sessionsValidFromTime = now - (maxInactiveIntervalSeconds * 1000);

if (logger.isDebugEnabled()) {
logger.debug(
"Cleaning up sessions older than " + new Date(sessionsValidFromTime));
}

int deletedCount = this.transactionOperations.execute(new TransactionCallback<Integer>() {

public Integer doInTransaction(TransactionStatus transactionStatus) {
return JdbcOperationsSessionRepository.this.jdbcOperations.update(
getQuery(DELETE_SESSIONS_BY_LAST_ACCESS_TIME_QUERY),
sessionsValidFromTime);
JdbcOperationsSessionRepository.this.deleteSessionsByLastAccessTimeQuery,
System.currentTimeMillis());
}

});
Expand Down Expand Up @@ -515,10 +603,24 @@ private static GenericConversionService createDefaultConversionService() {
return converter;
}

protected String getQuery(String base) {
private String getQuery(String base) {
return StringUtils.replace(base, "%TABLE_NAME%", this.tableName);
}

private void prepareQueries() {
this.createSessionQuery = getQuery(CREATE_SESSION_QUERY);
this.createSessionAttributeQuery = getQuery(CREATE_SESSION_ATTRIBUTE_QUERY);
this.getSessionQuery = getQuery(GET_SESSION_QUERY);
this.updateSessionQuery = getQuery(UPDATE_SESSION_QUERY);
this.updateSessionAttributeQuery = getQuery(UPDATE_SESSION_ATTRIBUTE_QUERY);
this.deleteSessionAttributeQuery = getQuery(DELETE_SESSION_ATTRIBUTE_QUERY);
this.deleteSessionQuery = getQuery(DELETE_SESSION_QUERY);
this.listSessionsByPrincipalNameQuery =
getQuery(LIST_SESSIONS_BY_PRINCIPAL_NAME_QUERY);
this.deleteSessionsByLastAccessTimeQuery =
getQuery(DELETE_SESSIONS_BY_LAST_ACCESS_TIME_QUERY);
}

private void serialize(PreparedStatement ps, int paramIndex, Object attributeValue)
throws SQLException {
this.lobHandler.getLobCreator().setBlobAsBytes(ps, paramIndex,
Expand Down
Loading