diff --git a/java/org/apache/catalina/session/DataSourceStore.java b/java/org/apache/catalina/session/DataSourceStore.java
index c46aa2b17c68..dcf341640e13 100644
--- a/java/org/apache/catalina/session/DataSourceStore.java
+++ b/java/org/apache/catalina/session/DataSourceStore.java
@@ -50,6 +50,22 @@
*/
public class DataSourceStore extends StoreBase {
+ /**
+ * The DELETE + INSERT saving strategy.
+ *
+ * @see #setSaveStrategy
+ * @see #getSaveStrategy
+ */
+ public static final String SAVE_STRATEGY_DELETE_INSERT = "deleteInsert";
+
+ /**
+ * The SELECT...FOR UPDATE saving strategy.
+ *
+ * @see #setSaveStrategy
+ * @see #getSaveStrategy
+ */
+ public static final String SAVE_STRATEGY_SELECT_FOR_UPDATE = "selectForUpdate";
+
/**
* Context name associated with this Store
*/
@@ -75,6 +91,10 @@ public class DataSourceStore extends StoreBase {
*/
protected DataSource dataSource = null;
+ /**
+ * Which saving strategy to use: deleteInsert, selectForUpdate, etc.
+ */
+ private String saveStrategy = SAVE_STRATEGY_DELETE_INSERT;
// ------------------------------------------------------------ Table & cols
@@ -326,6 +346,31 @@ public void setLocalDataSource(boolean localDataSource) {
this.localDataSource = localDataSource;
}
+ /**
+ * Sets the session-saving strategy to use.
+ *
+ * E.g. {@link #SAVE_STRATEGY_DELETE_INSERT} or {@link #SAVE_STRATEGY_SELECT_FOR_UPDATE}.
+ *
+ * The default is {@link #SAVE_STRATEGY_DELETE_INSERT} for compatibility with all RDMBSs.
+ *
+ * @param saveStrategy The saving strategy to use.
+ */
+ public void setSaveStrategy(String saveStrategy) {
+ this.saveStrategy = saveStrategy;
+ }
+
+ /**
+ * Gets the session-saving strategy to use.
+ *
+ * E.g. {@link #SAVE_STRATEGY_DELETE_INSERT} or {@link #SAVE_STRATEGY_SELECT_FOR_UPDATE}.
+ *
+ * The default is {@link #SAVE_STRATEGY_DELETE_INSERT} for compatibility with all RDMBSs.
+ *
+ * @return The saving strategy to use.
+ */
+ public String getSaveStrategy() {
+ return saveStrategy;
+ }
// --------------------------------------------------------- Public Methods
@@ -596,7 +641,31 @@ public void clear() throws IOException {
*/
@Override
public void save(Session session) throws IOException {
- ByteArrayOutputStream bos = null;
+
+ byte[] sessionBytes;
+ try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ ObjectOutputStream oos =
+ new ObjectOutputStream(new BufferedOutputStream(bos))) {
+
+ ((StandardSession) session).writeObjectData(oos);
+
+ sessionBytes = bos.toByteArray();
+ }
+
+ if(SAVE_STRATEGY_SELECT_FOR_UPDATE.equals(getSaveStrategy())) {
+ saveSelectForUpdate(session, sessionBytes);
+ } else {
+ saveDeleteAndInsert(session, sessionBytes);
+ }
+ }
+
+ /**
+ * Saves a session by DELETEing any existing session and INSERTing a new one.
+ *
+ * @param session The session to be stored.
+ * @throws IOException If an input/output error occurs.
+ */
+ private void saveDeleteAndInsert(Session session, byte[] sessionBytes) throws IOException {
String saveSql = "INSERT INTO " + sessionTable + " ("
+ sessionIdCol + ", " + sessionAppCol + ", "
+ sessionDataCol + ", " + sessionValidCol
@@ -604,6 +673,7 @@ public void save(Session session) throws IOException {
+ sessionLastAccessedCol
+ ") VALUES (?, ?, ?, ?, ?, ?)";
+ int sessionBytesLength = sessionBytes.length;
synchronized (session) {
int numberOfTries = 2;
while (numberOfTries > 0) {
@@ -618,19 +688,12 @@ public void save(Session session) throws IOException {
// * Check if ID exists in database and if so use UPDATE.
remove(session.getIdInternal(), _conn);
- bos = new ByteArrayOutputStream();
- try (ObjectOutputStream oos =
- new ObjectOutputStream(new BufferedOutputStream(bos))) {
- ((StandardSession) session).writeObjectData(oos);
- }
- byte[] obs = bos.toByteArray();
- int size = obs.length;
- try (ByteArrayInputStream bis = new ByteArrayInputStream(obs, 0, size);
- InputStream in = new BufferedInputStream(bis, size);
- PreparedStatement preparedSaveSql = _conn.prepareStatement(saveSql)) {
+ try (ByteArrayInputStream bis = new ByteArrayInputStream(sessionBytes, 0, sessionBytesLength);
+ InputStream in = new BufferedInputStream(bis, sessionBytesLength);
+ PreparedStatement preparedSaveSql = _conn.prepareStatement(saveSql)) {
preparedSaveSql.setString(1, session.getIdInternal());
preparedSaveSql.setString(2, getName());
- preparedSaveSql.setBinaryStream(3, in, size);
+ preparedSaveSql.setBinaryStream(3, in, sessionBytesLength);
preparedSaveSql.setString(4, session.isValid() ? "1" : "0");
preparedSaveSql.setInt(5, session.getMaxInactiveInterval());
preparedSaveSql.setLong(6, session.getLastAccessedTime());
@@ -655,6 +718,139 @@ public void save(Session session) throws IOException {
}
}
+ /**
+ * Saves a session using SELECT ... FOR UPDATE to update any existing session
+ * record or insert a new one.
+ *
+ * This should be more efficient with database resources if it is supported
+ * by the underlying database.
+ *
+ * @param session The session to be stored.
+ * @throws IOException If an input/output error occurs.
+ */
+ private void saveSelectForUpdate(Session session, byte[] sessionBytes) throws IOException {
+ String saveSql = "SELECT " + sessionIdCol
+ + ", " + sessionAppCol
+ + ", " + sessionIdCol
+ + ", " + sessionDataCol
+ + ", " + sessionValidCol
+ + ", " + sessionMaxInactiveCol
+ + ", " + sessionLastAccessedCol
+ + " FROM " + sessionTable
+ + " WHERE " + sessionAppCol + "=?"
+ + " AND " + sessionIdCol + "=? FOR UPDATE"
+ ;
+
+ int sessionBytesLength = sessionBytes.length;
+ synchronized (session) {
+ int numberOfTries = 2;
+ while (numberOfTries > 0) {
+ Connection _conn = getConnection();
+ if (_conn == null) {
+ return;
+ }
+
+ try {
+ try (ByteArrayInputStream bis = new ByteArrayInputStream(sessionBytes, 0, sessionBytesLength);
+ InputStream in = new BufferedInputStream(bis, sessionBytesLength);
+ PreparedStatement preparedSaveSql = _conn.prepareStatement(saveSql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE)) {
+
+ // Store auto-commit state
+ boolean autoCommit = _conn.getAutoCommit();
+
+ ResultSet rs = null;
+ try {
+ if(autoCommit) {
+ _conn.setAutoCommit(false); // BEGIN TRANSACTION
+ }
+
+ preparedSaveSql.setString(1, getName());
+ preparedSaveSql.setString(2, session.getIdInternal());
+
+ rs = preparedSaveSql.executeQuery();
+
+ if(rs.next()) {
+ // Session already exists in the db; update the various fields
+ rs.updateBinaryStream(sessionDataCol, in, sessionBytesLength);
+ rs.updateString(sessionValidCol, session.isValid() ? "1" : "0");
+ rs.updateInt(sessionMaxInactiveCol, session.getMaxInactiveInterval());
+ rs.updateLong(sessionLastAccessedCol, session.getLastAccessedTime());
+
+ rs.updateRow();
+ } else {
+ // Session does not exist. Insert.
+ rs.moveToInsertRow();
+
+ rs.updateString(sessionAppCol, getName());
+ rs.updateString(sessionIdCol, session.getIdInternal());
+ rs.updateBinaryStream(sessionIdCol, in, sessionBytesLength);
+ rs.updateString(sessionValidCol, session.isValid() ? "1" : "0");
+ rs.updateInt(sessionMaxInactiveCol, session.getMaxInactiveInterval());
+ rs.updateLong(sessionLastAccessedCol, session.getLastAccessedTime());
+
+ rs.insertRow();
+ }
+
+ _conn.commit();
+ } catch (SQLException sqle) {
+ manager.getContext().getLogger().error(sm.getString(getStoreName() + ".SQLException", sqle));
+ try {
+ _conn.rollback();
+ } catch (SQLException sqle1) {
+ manager.getContext().getLogger().error(sm.getString(getStoreName() + ".SQLException", sqle1));
+ }
+ } catch (RuntimeException rte) {
+ manager.getContext().getLogger().error(sm.getString(getStoreName() + ".SQLException", rte));
+ try {
+ _conn.rollback();
+ } catch (SQLException sqle1) {
+ manager.getContext().getLogger().error(sm.getString(getStoreName() + ".SQLException", sqle1));
+ }
+
+ throw rte;
+ } catch (Error e) {
+ manager.getContext().getLogger().error(sm.getString(getStoreName() + ".SQLException", e));
+ try {
+ _conn.rollback();
+ } catch (SQLException sqle1) {
+ manager.getContext().getLogger().error(sm.getString(getStoreName() + ".SQLException", sqle1));
+ }
+
+ throw e;
+ } finally {
+ if(null != rs) {
+ try {
+ rs.close();
+ } catch (SQLException sqle) {
+ manager.getContext().getLogger().error(sm.getString(getStoreName() + ".SQLException", sqle));
+ }
+ }
+ if(autoCommit) {
+ // Restore connection auto-commit state
+ _conn.setAutoCommit(autoCommit);
+ }
+ }
+
+ // Break out after the finally block
+ numberOfTries = 0;
+ }
+ } catch (SQLException e) {
+ manager.getContext().getLogger().error(sm.getString(getStoreName() + ".SQLException", e));
+ } catch (IOException e) {
+ // Ignore
+ } finally {
+ release(_conn);
+ }
+ numberOfTries--;
+ }
+ }
+
+ if (manager.getContext().getLogger().isDebugEnabled()) {
+ manager.getContext().getLogger().debug(sm.getString(getStoreName() + ".saving",
+ session.getIdInternal(), sessionTable));
+ }
+ }
+
// --------------------------------------------------------- Protected Methods
diff --git a/webapps/docs/config/manager.xml b/webapps/docs/config/manager.xml
index 1b7e0b9169a6..6acf1c4fe0b6 100644
--- a/webapps/docs/config/manager.xml
+++ b/webapps/docs/config/manager.xml
@@ -468,6 +468,13 @@
false
: use a global DataSource.
The session-saving strategy to use, either deleteInsert
+ or selectForUpdate
. The default is deleteInsert
+ which is compatible with all known RDBMS systems. The selectForUpdate
+ strategy uses SELECT...FOR UPDATE
which may be more efficient
+ if supported by your RDBMS system.
Name of the database column, contained in the specified session table, that contains the Engine, Host, and Web Application Context name in the