This is an automated email from the ASF dual-hosted git repository.

remm pushed a commit to branch 8.5.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/8.5.x by this push:
     new 1fbdf55  Add more optimized DataSource code for session store
1fbdf55 is described below

commit 1fbdf555f954b12b8082dc4c3c07639e4f5a8b97
Author: remm <r...@apache.org>
AuthorDate: Tue Nov 10 12:02:17 2020 +0100

    Add more optimized DataSource code for session store
    
    Remove bottlenecks caused by single JDBC connection code.
    Rename the JDBCStore to DataSourceStore. The JDBCStore is deprecated but
    unchanged. Only document DataSourceStore.
    If the code duplication is too large, I can change DataSourceStore to
    being only an empty extends of JDBCStore.
    Code submitted by Philippe Mouawad.
---
 .../apache/catalina/session/DataSourceStore.java   | 453 +++++++++++++++++++++
 java/org/apache/catalina/session/JDBCStore.java    |   3 +
 .../catalina/session/LocalStrings.properties       |   1 +
 webapps/docs/changelog.xml                         |   6 +
 webapps/docs/config/manager.xml                    |  51 +--
 5 files changed, 474 insertions(+), 40 deletions(-)

diff --git a/java/org/apache/catalina/session/DataSourceStore.java 
b/java/org/apache/catalina/session/DataSourceStore.java
new file mode 100644
index 0000000..778f3ad
--- /dev/null
+++ b/java/org/apache/catalina/session/DataSourceStore.java
@@ -0,0 +1,453 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.catalina.session;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.naming.Context;
+import javax.naming.InitialContext;
+import javax.naming.NamingException;
+import javax.sql.DataSource;
+
+import org.apache.catalina.Globals;
+import org.apache.catalina.Session;
+import org.apache.juli.logging.Log;
+
+/**
+ * Implementation of the {@link org.apache.catalina.Store Store}
+ * interface that stores serialized session objects in a database.
+ * Sessions that are saved are still subject to being expired
+ * based on inactivity.
+ *
+ * @author Bip Thelin
+ */
+@SuppressWarnings("deprecation")
+public class DataSourceStore extends JDBCStore {
+
+    // --------------------------------------------------------- Public Methods
+
+    @Override
+    public String[] expiredKeys() throws IOException {
+        return keys(true);
+    }
+
+    @Override
+    public String[] keys() throws IOException {
+        return keys(false);
+    }
+
+    /**
+     * Return an array containing the session identifiers of all Sessions
+     * currently saved in this Store.  If there are no such Sessions, a
+     * zero-length array is returned.
+     *
+     * @param expiredOnly flag, whether only keys of expired sessions should
+     *        be returned
+     * @return array containing the list of session IDs
+     *
+     * @exception IOException if an input/output error occurred
+     */
+    private String[] keys(boolean expiredOnly) throws IOException {
+        String keys[] = null;
+        int numberOfTries = 2;
+        while (numberOfTries > 0) {
+
+            Connection _conn = getConnection();
+            if (_conn == null) {
+                return new String[0];
+            }
+            try {
+
+                String keysSql = "SELECT " + sessionIdCol + " FROM "
+                        + sessionTable + " WHERE " + sessionAppCol + " = ?";
+                if (expiredOnly) {
+                    keysSql += " AND (" + sessionLastAccessedCol + " + "
+                            + sessionMaxInactiveCol + " * 1000 < ?)";
+                }
+                try (PreparedStatement preparedKeysSql = 
_conn.prepareStatement(keysSql)) {
+                    preparedKeysSql.setString(1, getName());
+                    if (expiredOnly) {
+                        preparedKeysSql.setLong(2, System.currentTimeMillis());
+                    }
+                    try (ResultSet rst = preparedKeysSql.executeQuery()) {
+                        List<String> tmpkeys = new ArrayList<>();
+                        if (rst != null) {
+                            while (rst.next()) {
+                                tmpkeys.add(rst.getString(1));
+                            }
+                        }
+                        keys = tmpkeys.toArray(new String[0]);
+                        // Break out after the finally block
+                        numberOfTries = 0;
+                    }
+                }
+            } catch (SQLException e) {
+                
manager.getContext().getLogger().error(sm.getString(getStoreName() + 
".SQLException", e));
+                keys = new String[0];
+                // Close the connection so that it gets reopened next time
+            } finally {
+                release(_conn);
+            }
+            numberOfTries--;
+        }
+        return keys;
+    }
+
+    /**
+     * Return an integer containing a count of all Sessions
+     * currently saved in this Store.  If there are no Sessions,
+     * <code>0</code> is returned.
+     *
+     * @return the count of all sessions currently saved in this Store
+     *
+     * @exception IOException if an input/output error occurred
+     */
+    @Override
+    public int getSize() throws IOException {
+        int size = 0;
+        String sizeSql = "SELECT COUNT(" + sessionIdCol
+                + ") FROM " + sessionTable + " WHERE "
+                + sessionAppCol + " = ?";
+
+        int numberOfTries = 2;
+        while (numberOfTries > 0) {
+            Connection _conn = getConnection();
+
+            if (_conn == null) {
+                return size;
+            }
+
+            try (PreparedStatement preparedSizeSql = 
_conn.prepareStatement(sizeSql)){
+                preparedSizeSql.setString(1, getName());
+                try (ResultSet rst = preparedSizeSql.executeQuery()) {
+                    if (rst.next()) {
+                        size = rst.getInt(1);
+                    }
+                    // Break out after the finally block
+                    numberOfTries = 0;
+                }
+            } catch (SQLException e) {
+                
manager.getContext().getLogger().error(sm.getString(getStoreName() + 
".SQLException", e));
+            } finally {
+                release(_conn);
+            }
+            numberOfTries--;
+        }
+        return size;
+    }
+
+    /**
+     * Load the Session associated with the id <code>id</code>.
+     * If no such session is found <code>null</code> is returned.
+     *
+     * @param id a value of type <code>String</code>
+     * @return the stored <code>Session</code>
+     * @exception ClassNotFoundException if an error occurs
+     * @exception IOException if an input/output error occurred
+     */
+    @Override
+    public Session load(String id) throws ClassNotFoundException, IOException {
+        StandardSession _session = null;
+        org.apache.catalina.Context context = getManager().getContext();
+        Log contextLog = context.getLogger();
+
+        int numberOfTries = 2;
+        String loadSql = "SELECT " + sessionIdCol + ", "
+                + sessionDataCol + " FROM " + sessionTable
+                + " WHERE " + sessionIdCol + " = ? AND "
+                + sessionAppCol + " = ?";
+        while (numberOfTries > 0) {
+            Connection _conn = getConnection();
+            if (_conn == null) {
+                return null;
+            }
+
+            ClassLoader oldThreadContextCL = 
context.bind(Globals.IS_SECURITY_ENABLED, null);
+
+            try (PreparedStatement preparedLoadSql = 
_conn.prepareStatement(loadSql)){
+                preparedLoadSql.setString(1, id);
+                preparedLoadSql.setString(2, getName());
+                try (ResultSet rst = preparedLoadSql.executeQuery()) {
+                    if (rst.next()) {
+                        try (ObjectInputStream ois =
+                                getObjectInputStream(rst.getBinaryStream(2))) {
+                            if (contextLog.isDebugEnabled()) {
+                                contextLog.debug(sm.getString(
+                                        getStoreName() + ".loading", id, 
sessionTable));
+                            }
+
+                            _session = (StandardSession) 
manager.createEmptySession();
+                            _session.readObjectData(ois);
+                            _session.setManager(manager);
+                        }
+                    } else if (context.getLogger().isDebugEnabled()) {
+                        contextLog.debug(getStoreName() + ": No persisted data 
object found");
+                    }
+                    // Break out after the finally block
+                    numberOfTries = 0;
+                }
+            } catch (SQLException e) {
+                contextLog.error(sm.getString(getStoreName() + 
".SQLException", e));
+            } finally {
+                context.unbind(Globals.IS_SECURITY_ENABLED, 
oldThreadContextCL);
+                release(_conn);
+            }
+            numberOfTries--;
+        }
+        return _session;
+    }
+
+    /**
+     * Remove the Session with the specified session identifier from
+     * this Store, if present.  If no such Session is present, this method
+     * takes no action.
+     *
+     * @param id Session identifier of the Session to be removed
+     *
+     * @exception IOException if an input/output error occurs
+     */
+    @Override
+    public void remove(String id) throws IOException {
+
+        int numberOfTries = 2;
+        while (numberOfTries > 0) {
+            Connection _conn = getConnection();
+
+            if (_conn == null) {
+                return;
+            }
+
+            try {
+                remove(id, _conn);
+                // Break out after the finally block
+                numberOfTries = 0;
+            } catch (SQLException e) {
+                
manager.getContext().getLogger().error(sm.getString(getStoreName() + 
".SQLException", e));
+            } finally {
+                release(_conn);
+            }
+            numberOfTries--;
+        }
+
+        if (manager.getContext().getLogger().isDebugEnabled()) {
+            manager.getContext().getLogger().debug(sm.getString(getStoreName() 
+ ".removing", id, sessionTable));
+        }
+    }
+
+    /**
+     * Remove the Session with the specified session identifier from
+     * this Store, if present.  If no such Session is present, this method
+     * takes no action.
+     *
+     * @param id Session identifier of the Session to be removed
+     * @param _conn open connection to be used
+     * @throws SQLException if an error occurs while talking to the database
+     */
+    private void remove(String id, Connection _conn) throws SQLException {
+        String removeSql = "DELETE FROM " + sessionTable
+                + " WHERE " + sessionIdCol + " = ?  AND "
+                + sessionAppCol + " = ?";
+        try (PreparedStatement preparedRemoveSql = 
_conn.prepareStatement(removeSql)) {
+            preparedRemoveSql.setString(1, id);
+            preparedRemoveSql.setString(2, getName());
+            preparedRemoveSql.execute();
+        }
+    }
+
+    /**
+     * Remove all of the Sessions in this Store.
+     *
+     * @exception IOException if an input/output error occurs
+     */
+    @Override
+    public void clear() throws IOException {
+        String clearSql = "DELETE FROM " + sessionTable
+                + " WHERE " + sessionAppCol + " = ?";
+
+        int numberOfTries = 2;
+        while (numberOfTries > 0) {
+            Connection _conn = getConnection();
+            if (_conn == null) {
+                return;
+            }
+
+            try (PreparedStatement preparedClearSql = 
_conn.prepareStatement(clearSql)){
+                preparedClearSql.setString(1, getName());
+                preparedClearSql.execute();
+                // Break out after the finally block
+                numberOfTries = 0;
+            } catch (SQLException e) {
+                
manager.getContext().getLogger().error(sm.getString(getStoreName() + 
".SQLException", e));
+            } finally {
+                release(_conn);
+            }
+            numberOfTries--;
+        }
+    }
+
+    /**
+     * Save a session to the Store.
+     *
+     * @param session the session to be stored
+     * @exception IOException if an input/output error occurs
+     */
+    @Override
+    public void save(Session session) throws IOException {
+        ByteArrayOutputStream bos = null;
+        String saveSql = "INSERT INTO " + sessionTable + " ("
+                + sessionIdCol + ", " + sessionAppCol + ", "
+                + sessionDataCol + ", " + sessionValidCol
+                + ", " + sessionMaxInactiveCol + ", "
+                + sessionLastAccessedCol
+                + ") VALUES (?, ?, ?, ?, ?, ?)";
+
+        synchronized (session) {
+            int numberOfTries = 2;
+            while (numberOfTries > 0) {
+                Connection _conn = getConnection();
+                if (_conn == null) {
+                    return;
+                }
+
+                try {
+                    // If sessions already exist in DB, remove and insert 
again.
+                    // TODO:
+                    // * 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)) {
+                        preparedSaveSql.setString(1, session.getIdInternal());
+                        preparedSaveSql.setString(2, getName());
+                        preparedSaveSql.setBinaryStream(3, in, size);
+                        preparedSaveSql.setString(4, session.isValid() ? "1" : 
"0");
+                        preparedSaveSql.setInt(5, 
session.getMaxInactiveInterval());
+                        preparedSaveSql.setLong(6, 
session.getLastAccessedTime());
+                        preparedSaveSql.execute();
+                        // 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
+
+    /**
+     * Open (if necessary) and return a database connection for use by
+     * this Store.
+     *
+     * @return database connection ready to use
+     *
+     * @exception SQLException if a database error occurs
+     */
+    protected Connection open() throws SQLException {
+        if (dataSourceName != null && dataSource == null) {
+            org.apache.catalina.Context context = getManager().getContext();
+            ClassLoader oldThreadContextCL = null;
+            if (getLocalDataSource()) {
+                oldThreadContextCL = context.bind(Globals.IS_SECURITY_ENABLED, 
null);
+            }
+
+            Context initCtx;
+            try {
+                initCtx = new InitialContext();
+                Context envCtx = (Context) initCtx.lookup("java:comp/env");
+                this.dataSource = (DataSource) 
envCtx.lookup(this.dataSourceName);
+            } catch (NamingException e) {
+                context.getLogger().error(
+                        sm.getString(getStoreName() + ".wrongDataSource",
+                                this.dataSourceName), e);
+            } finally {
+                if (getLocalDataSource()) {
+                    context.unbind(Globals.IS_SECURITY_ENABLED, 
oldThreadContextCL);
+                }
+            }
+        }
+
+        if (dataSource != null) {
+            return dataSource.getConnection();
+        } else {
+            throw new IllegalStateException(sm.getString(getStoreName() + 
".missingDataSource"));
+        }
+    }
+
+    /**
+     * Close the specified database connection.
+     *
+     * @param dbConnection The connection to be closed
+     */
+    protected void close(Connection dbConnection) {
+
+        // Do nothing if the database connection is already closed
+        if (dbConnection == null)
+            return;
+
+        // Commit if autoCommit is false
+        try {
+            if (!dbConnection.getAutoCommit()) {
+                dbConnection.commit();
+            }
+        } catch (SQLException e) {
+            manager.getContext().getLogger().error(sm.getString(getStoreName() 
+ ".commitSQLException"), e);
+        }
+
+        // Close this database connection, and log any errors
+        try {
+            dbConnection.close();
+        } catch (SQLException e) {
+            manager.getContext().getLogger().error(sm.getString(getStoreName() 
+ ".close", e.toString())); // Just log it here
+        }
+    }
+
+}
diff --git a/java/org/apache/catalina/session/JDBCStore.java 
b/java/org/apache/catalina/session/JDBCStore.java
index 6e16d59..8a4e331 100644
--- a/java/org/apache/catalina/session/JDBCStore.java
+++ b/java/org/apache/catalina/session/JDBCStore.java
@@ -52,7 +52,10 @@ import org.apache.tomcat.util.ExceptionUtils;
  * based on inactivity.
  *
  * @author Bip Thelin
+ * @deprecated Removed in Tomcat 10 and replaced by DataSourceStore
+ *  with removal of legacy JDBC code
  */
+@Deprecated
 public class JDBCStore extends StoreBase {
 
     /**
diff --git a/java/org/apache/catalina/session/LocalStrings.properties 
b/java/org/apache/catalina/session/LocalStrings.properties
index 7676cf4..499e98e 100644
--- a/java/org/apache/catalina/session/LocalStrings.properties
+++ b/java/org/apache/catalina/session/LocalStrings.properties
@@ -22,6 +22,7 @@ JDBCStore.close=Exception closing database connection [{0}]
 JDBCStore.commitSQLException=SQLException committing connection before closing
 JDBCStore.connectError=Cannot connect to database [{0}]
 JDBCStore.loading=Loading Session [{0}] from database [{1}]
+JDBCStore.missingDataSource=No data source available
 JDBCStore.missingDataSourceName=No valid JNDI name was given.
 JDBCStore.removing=Removing Session [{0}] at database [{1}]
 JDBCStore.saving=Saving Session [{0}] to database [{1}]
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 6e85ee4..e182626 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -76,6 +76,12 @@
         <bug>64871</bug>: Log a warning if Tomcat blocks access to a file
         because it uses symlinks. (markt)
       </add>
+      <update>
+        Rename <code>JDBCStore</code> to <code>DataSourceStore</code>
+        and remove bottlenecks for database backed session store. The
+        <code>JDBCStore</code> is deprecated but remains unchanged. Patch
+        submitted by Philippe Mouawad. (remm)
+      </update>
     </changelog>
   </subsection>
   <subsection name="Coyote">
diff --git a/webapps/docs/config/manager.xml b/webapps/docs/config/manager.xml
index d005aad..6d314f9 100644
--- a/webapps/docs/config/manager.xml
+++ b/webapps/docs/config/manager.xml
@@ -401,11 +401,11 @@
   </attributes>
 
 
-  <h5>JDBC Based Store</h5>
+  <h5>Data source Based Store</h5>
 
-  <p>The <em>JDBC Based Store</em> implementation saves swapped out
+  <p>The <em>Data source Based Store</em> implementation saves swapped out
   sessions in individual rows of a preconfigured table in a database
-  that is accessed via a JDBC driver.  With large numbers of swapped out
+  that is accessed via a data sourcer.  With large numbers of swapped out
   sessions, this implementation will exhibit improved performance over
   the File Based Store described above.</p>
 
@@ -419,41 +419,18 @@
       <p>Java class name of the implementation to use.  This class must
       implement the <code>org.apache.catalina.Store</code> interface.  You
       <strong>must</strong> specify
-      <code>org.apache.catalina.session.JDBCStore</code>
+      <code>org.apache.catalina.session.DataSourceStore</code>
       to use this implementation.</p>
     </attribute>
 
-    <attribute name="connectionName" required="true">
-      <p>The user name that will be handed to the configured JDBC driver to
-      establish a connection to the database containing the session table.</p>
-    </attribute>
-
-    <attribute name="connectionPassword" required="true">
-      <p>The password that will be handed to the configured JDBC driver to
-      establish a connection to the database containing the session table.</p>
-    </attribute>
-
-    <attribute name="connectionURL" required="true">
-      <p>The connection URL that will be handed to the configured JDBC
-      driver to establish a connection to the database containing our
-      session table.</p>
-    </attribute>
-
     <attribute name="dataSourceName" required="false">
-      <p>Name of the JNDI resource for a JDBC DataSource-factory. If this 
option
-      is given and a valid JDBC resource can be found, it will be used and any
-      direct configuration of a JDBC connection via <code>connectionURL</code>,
-      <code>connectionName</code>, <code>connectionPassword</code> and
-      <code>driverName</code> will be ignored. Since this code uses prepared
-      statements, you might want to configure pooled prepared statements as
-      shown in <a href="../jndi-resources-howto.html">the JNDI resources
+      <p>Name of the JNDI resource for a JDBC DataSource-factory. Since this
+      code uses prepared statements, you might want to configure pooled
+      prepared statements as shown in
+       <a href="../jndi-resources-howto.html">the JNDI resources
       How-To</a>.</p>
     </attribute>
 
-    <attribute name="driverName" required="true">
-      <p>Java class name of the JDBC driver to be used.</p>
-    </attribute>
-
     <attribute name="localDataSource" required="false">
       <p>This allows the Store to use a DataSource defined for the Context
       rather than a global DataSource. If not specified, the default is
@@ -515,7 +492,7 @@
 
   </attributes>
 
-  <p>Before attempting to use the JDBC Based Store for the first time,
+  <p>Before attempting to use the data source Store for the first time,
   you must create the table that will be used to store swapped out sessions.
   Detailed SQL commands vary depending on the database you are using, but
   a script like this will generally be required:</p>
@@ -531,14 +508,8 @@
 );</source>
 
   <p>Note: The SQL command above does not use the default names for either the
-  table or the columns so the JDBC Store would need to be configured to reflect
-  this.</p>
-
-  <p>In order for the JDBC Based Store to successfully connect to your
-  database, the JDBC driver you configure must be visible to Tomcat's
-  internal class loader.  Generally, that means you must place the JAR
-  file containing this driver into the <code>$CATALINA_HOME/lib</code>
-  directory.</p>
+  table or the columns so the data source Store would need to be configured
+  to reflect this.</p>
 
 </section>
 


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org
For additional commands, e-mail: dev-h...@tomcat.apache.org

Reply via email to