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